From 3e24bacad08bacac6449db4fe02841589d3906dc Mon Sep 17 00:00:00 2001 From: jv Date: Thu, 26 Mar 2026 08:52:29 -0700 Subject: [PATCH 1/3] Docs: convert README to Markdown, update FAQ/CHANGELOG/config - Replace plain-text README with README.md (Markdown formatting, expanded feature list including Duo Push and multi-session support) - FAQ: fix grammar, expand FreeBSD/OS X answer, add ifconfig hint - CHANGELOG: add v0.8.0 (multi-session) and v0.8.1 (Pulse Secure/Duo) - jvpn.ini: comprehensive documentation overhaul with parameter types, defaults, and examples; add new config options for mult_session, kick, kick_string, duo, token, reconnect, user_agent, recontimeoutcount, recontimeouttimeout - Shell scripts: update shebangs from /bin/sh to /bin/bash - Add sample-script-tunnel.sh demonstrating SSH tunnel setup --- CHANGELOG | 9 +++ FAQ | 37 +++++---- README | 97 ----------------------- README.md | 114 ++++++++++++++++++++++++++ jvpn.ini | 136 +++++++++++++++++++++++--------- scripts/restore-original-dns.sh | 2 +- scripts/sample-script-tunnel.sh | 22 ++++++ scripts/sample-script.sh | 2 +- scripts/stoken.sh | 2 +- 9 files changed, 265 insertions(+), 156 deletions(-) delete mode 100644 README create mode 100644 README.md create mode 100644 scripts/sample-script-tunnel.sh diff --git a/CHANGELOG b/CHANGELOG index 913d003..d168a57 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +*** 0.8.1 (jv) + + Support Pulse Secure / Duo Push + +*** 0.8.0 (jv) + + allow multi-session (if enabled server-side) + # converted README to .md + + added more requirements + +*** 0.7.1 # try to detect valid starting class for tncc.jar + Force hostchecker logging if debug is enabled diff --git a/FAQ b/FAQ index ee3f0ab..3c49b62 100644 --- a/FAQ +++ b/FAQ @@ -1,28 +1,31 @@ -Q: JVPN is not working for me -A: Try to enable debug output. You will find logs in the - ~/.juniper_networks/network_connect/ directory. Also you can try ncui mode. +Q: jvpn is not working for me +A: Try to enable debug output. You will find logs in the + ~/.juniper_networks/network_connect/ directory. Also, you can try ncui mode. -Q: Is it possible to run it non non ARM/MIPS/PPC? -A: No, because it using closing source VPN client binary available only for - i386 platform. +Q: Is it possible to run jvpn on ARM/MIPS/PPC? +A: No, because it using Juniper's closed source VPN client binary available only + for i386 platform (which also works on i686, of course). -Q: Where to send patches? -A: Use "pull request" at https://github.com/samm-git/jvpn/. Or just by mail. +Q: Where should I send patches? +A: Use "pull request" at https://github.com/samm-git/jvpn/. Or just by email. Q: Could you help me with my VPN account, it does not work? -A: Only if you can provide credentials to test. I need them to debug - connection from native GUI and JVPN to identify what is going wrong. - Also you can try to debug yourself. Some tools i used: - * Wireshark/tcpdump - to dump traffic between ncsvc and JAVA GUI +A: Only if you can provide credentials to test. I need them to debug the + connection from native GUI and jvpn to identify what is going wrong. + Also you can try to debug yourself. Some tools I use: + * Wireshark/tcpdump - to dump traffic between ncsvc and Java GUI + * Firefox with Firebug - to see what the "normal" conversation with a browser looks like * strace - to trace VPN processes - * Java decompiler - to decompile source code of GUI, not too much + * Java decompiler - to decompile source code of GUI, not too much interesting, anyway -Q: Is it possible to use jvpn on FreeBSD? -A: No. FreeBSD supports Linux ABI, but ncsvc heavily depends on Linux network +Q: Is it possible to use jvpn on FreeBSD or OS X? +A: Maybe? FreeBSD supports Linux ABI, but ncsvc heavily depends on Linux network stack, including /proc/net/route, /proc/net/tun, etc. It _should_ be possible to emulate all this staff or to use LD_PRELOAD hacks, but it will - require much more efforts. + require much more effort. + + If you get it working, please send a patch. Q: What if I get status=6e and Authentication failed -A: Check, where is your ifconfig binary. It should be at /sbin/ifconfig +A: Check the location of your ifconfig binary. The downloaded binaries insist that it be located at /sbin/ifconfig (so, if it's not, make a symlink). \ No newline at end of file diff --git a/README b/README deleted file mode 100644 index 94e5d1d..0000000 --- a/README +++ /dev/null @@ -1,97 +0,0 @@ -Project status update: i do not have access to the Juniper VPN host anymore, -so project is stalled. Feel free to fork it or use one of the 30+ forks :) - -jvpn.pl - script to connect to the Juniper firewall with enabled HostChecker. - -Features - * Emulates web browser to get authentication data - * Automatically starts juniper client and passing data to it using TCP - connection (it is not possible with command line). - * Able to download Linux client from the Juniper VPN server without browser or - Java. - * Supports launching Host Checker to perform checks on a client host. - * Could protect resolv.conf by setting +i attribute for the connection time - * Works without Java machine on x86 and x86_64 hosts - * Ability to run scripts on connect/disconnect events - * Integration with external password/token providers, including "stoken" RSA - softkey. - -Requirements - * Perl with LWP modules - * openssl binary - * unzip (only for client unpacking) - -Usage -To configure script edit jvpn.ini. If you don`t have installed client - run -jvpn.pl under sudo and it will download and install it automatically. If you -want to run it without sudo - set suid bit on the "ncsvc" binary. -If you have multiply configurations - use --conf switch to define ini file. - -How it works - 1) Script connecting to the VPN web portal with provided user name and password. - 2) Then script handling different authentication scenarios to get DSID value - 3) After getting DSID value script getting md5 fingerprint of the SSL - certificate. - 4) If VPN client is not installed script downloading and unpacking it. - 5) Script starting ncsvc and connecting to daemon (using TCP 127.0.0.1:4242 - socket in ncsvc mode or using "ncui" wrapper in ncui mode). - 6) Script emulates native GUI and passing configuration data to daemon. After - this step VPN should work. - 7) Script can optionally protect resolv.conf from dhcpd or Network Manager by - setting +i flag on it (disabled by default). - 8) On Ctrl+C script sending "Disconnect" command to the daemon and logging out - on the web site. - -Difference between mode=ncui and mode=ncsvc -By default jvpn work in the ncsvc mode. This could be changed in jvpn.ini using -"mode" configuration setting. In ncsvc (default) mode jvpn establishing TCP -socket to nvsvc daemon and trying to establish connection using it protocol. -In "ncui" mode jvpn trying to use main() function libncui.so which later calling -ncsvc. If default mode does not work for you i am recommending to try "ncui" -mode. Please note that to use ncui mode you should have gcc installed. - -Scripting support -It is possible to run user-defined scripts on conncect/disconnect events. To -use this functionality you will need to define script to run in the jvpn.ini -using script= line. Script should be executable file. -List of pre-defined variables and sample route table modification could be found -in scripts/sample-script.sh. - -Different ways to provide password -By default jvpn asks for password on startup. It is also possible to define -password in configuration file or to use external program to provide pass/token. -To store password in configuration use "password=plaintext:mypassword" in ini -file. To use external scripts use "password=helper:scripts/script.sh". -Helper script should provide password to stdout. If it is called second time -(some VPN servers requesting additional tokens) jvpn defines OLDPIN variable -containing first token code. See scripts/stoken.sh for example of "stoken" -integration. - -Host checker support -In version 0.7.0 it is possible to run hostchecker using "hostchecker=1" setting -in jvpn.ini. Host Checker is used to perform checks on endpoint computers that -connect to the VPN device to make sure the endpoints meet certain security -requirements. If hostchecker support is enabled jvpn trying to run tncc.jar using -Java, as web browser applet do. JRE needs to be installed to support this -feature. It is recommended to enable only if you are unable to connect without -host checker running. - -Bugs and debugging -This script is done without any documentation, only using wireshark and -debugger. It is very likely that it has a bugs or will not work correctly for -you. If you need some support - enable debug and send me all information. -Script debug is written to stdout and daemon log is written to the -~/.juniper_networks/network_connect/ncsvc.log file. - -License -The author has placed this work in the Public Domain, thereby relinquishing -all copyrights. Everyone is free to use, modify, republish, sell or give away -this work without prior consent from anybody. - -This software is provided on an "as is" basis, without warranty of any -kind. Use at your own risk! Under no circumstances shall the author(s) or -contributor(s) be liable for damages resulting directly or indirectly from -the use or non-use of this software. - -Author -Alex Samorukov, samm@os2.kiev.ua diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa3227d --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +jvpn +==== +Connect to Juniper Junos Pulse / Pulse Secure VPN (including Duo support) on the command line without Java + +## Features + * Works *without Java* on both x86 and x86_64 hosts + * Emulates web browser to get authentication data + * Supports Duo push two-factor authentication + * Automatically starts juniper client and passes data to it using TCP socket + connection. + * Able to download Linux client from the VPN server without browser or + Java. + * Supports launching Host Checker to perform checks on a client host. + * Option to protect resolv.conf by setting +i attribute for the connection time + * Ability to run scripts on connect/disconnect events + * Integration with external password/token providers, including "stoken" RSA + softkey. + * Ability to kick existing sessions (for example, you forgot to log out of the + VPN on another system and/or your server is set up with connection count limits) + +## Requirements + * Perl with LWP modules (for https) + * openssl binary + * unzip (for client unpacking) + +### Extra requirements for ncui mode (optional): + (note the i686 requirements if your OS is 64-bit) + * gcc + * glibc-devel.i686 + * zlib.i686 + * libgcc.i686 + +## Usage +To configure jvpn.pl, edit jvpn.ini. + +The first run of jvpn.pl (under sudo) will download and install the client automatically. + +If you want to run it without sudo - set the suid bit on the "ncsvc" binary (`chmod u+s ncsvc`). + +If you have multiple configurations - use the --conf switch to define ini file. + +### How the script works + 1. Connects to the VPN web portal with provided user name and password (and PIN/token). + 2. Gets DSID value + 3. Gets md5 fingerprint of the SSL certificate + 4. If VPN client is not installed script downloads and unpacks it. + 5. Starts ncsvc and connects to it (using TCP 127.0.0.1:4242 + socket in ncsvc mode or using "ncui" wrapper in ncui mode). + 6. Script emulates (aka "fakes") native GUI and passes configuration data to daemon. + 7. Script can optionally protect resolv.conf from dhcpd or Network Manager by + setting +i flag on it (disabled by default). + 8. On Ctrl+C script sending "Disconnect" command to the daemon and logs out + by, again, emulating browser interaction. + +### Difference between `mode=ncui` and `mode=ncsvc` +In "ncsvc" (default) mode jvpn establishes a TCP socket connection to nvsvc daemon and tries to establish connection using it protocol. + +In "ncui" mode jvpn tries to use the main() function in libncui.so which later calls ncsvc. Basically, if default mode does not work for you, try ncui mode. + +Please note that to use ncui mode you must have gcc and other stuff (noted above) installed. + +### Scripting support +It is possible to run user-defined scripts on conncect/disconnect events. To +use this functionality you will need to define the script to run in the jvpn.ini +using the `script=` line. That script needs to be executable, of course. + +List of pre-defined variables and sample route table modification can be found +in scripts/sample-script.sh. + +### Different ways to provide password +By default jvpn asks for your password on startup. It is also possible to define +password in configuration file or to use external program to provide it (and +token). + +To store password directly in jvpn.ini, use `password=plaintext:mypassword`. + +If you write a helper script, it should simply print your password to stdout. If it is called a second time (some VPN servers request additional tokens) jvpn will define an "OLDPIN" variable containing first token code. See scripts/stoken.sh for example of "stoken" integration. + +If you need to use an external token (either a key fob or a mobile phone app, +for example), set your password with the `password=xxx` parameter as above, and also set `token=1` in jvpn.ini. You will be prompted to type in the token before the script attempts to connect. + +### Hostchecker support +As of version 0.7.0 it is possible to run hostchecker using the `hostchecker=1` setting +in jvpn.ini. Hostchecker is used to perform checks on endpoint computers that +connect to the VPN device to make sure the endpoints meet certain security +requirements. If hostchecker support is enabled jvpn tries to run tncc.jar using +Java (emulating web browser applet behavior). + +JRE needs to be installed to support this feature. + +Generally, It is recommended to enable this only if you are unable to connect without it. + +### Bugs and debugging +This script was written (and modified) without any official Juniper/Pulse documentation or support, only using wireshark/tcpdump, Firefox (to look at web forms) and a debugger. It is very likely that it has a bugs or will not work correctly for you. + +If you need some support - enable debug and send me as much information as you can. + +Script debug is written to stdout and daemon log is written to the +~/.juniper_networks/network_connect/ncsvc.log file. + +## License +The author has placed this work in the Public Domain, thereby relinquishing +all copyrights. Everyone is free to use, modify, republish, sell or give away +this work without prior consent from anybody. + +This software is provided on an "as is" basis, without warranty of any +kind. Use at your own risk! Under no circumstances shall the author(s) or +contributor(s) be liable for damages resulting directly or indirectly from +the use or non-use of this software. + +## Authors +Original Author: Alex Samorukov + +2015-2016 revisions author: Jeff Vier diff --git a/jvpn.ini b/jvpn.ini index 7b06995..c0a1893 100644 --- a/jvpn.ini +++ b/jvpn.ini @@ -1,55 +1,113 @@ -# vpn host name +# VPN hostname or IP (string) host=vpn.example.com -# -# vpn port (non-default never tested) + +# VPN port (integer) +# Note: (non-default never tested) port=443 -# -# "url" part of vpn login page: https://:/dana-na/auth//login.cgi -# if not set url=url_default + +# "URL" part of VPN login page (string) +# Example: https://:/dana-na/auth//login.cgi +# Default: 'url_default' url=url_default -# -# it is possible to setup a vpn username in the configuration file or ask it interactively -# To prompt for the username every time, use: -# username=interactive -# To setup a username in the configuration file: -# username=myusername + +# Allow multiple sessions (boolean) +# Note: Must be supported by VPN +mult_session=1 + +# VPN username (string) +# Prompt for username every time: +# username=interactive +# Set a static username (less secure): +# username=myusername username=interactive -# -# realm - could be taken from web login form + +# Realm (string) +# Note: Find this in your VPN's web login form realm=Very Secure ID -# -# Certificate validation, disable for self signed certificates + +# User agent sent to VPN server (string) +# Default: user_agent=JVPN/$POSIX::sysname (probably JVPN/Linux) +# Important: setting hostchecker=1 (below) will result in the +# user agent being set to: +# 'Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:23.0) Gecko/20100101 Firefox/23.0' +# to convince the VPN server that JVPN is a "normal" client +# user_agent=JVPN/Linux + +# Kick existing sessions (boolean) +# Note: use this if you are connecting with more computers +# than the number of allowed VPN slots per user. +# Does play nice with mult_session=1 +kick=1 + +# Kick identifier (string) +# Existing session to kick upon encountering a +# conflicting existing VPN connection by your username +# Note: set to '.*' to kick *all* existing sessions by your username +# Default: $user_agent (set above) +#kick_string=Linux + +# Certificate validation (boolean) +# disable for self signed certificates verifycert=1 -# -# set immutable (+i) attribute on /etc/resolv.conf after connect, 1 to enable -# It works only if jvpn script is running with root UID. -# set it if your OS or DHCP client overwriting this file + +# Protect local DNS (boolean) +# Sets immutable (+i) attribute on /etc/resolv.conf after connect +# Note: This works only if jvpn script is running with root UID. +# Set it if your OS or DHCP client wants to overwrite this file +# (and you want to stop it) dnsprotect=0 -# -# Mode could be ncsvc or ncui. Default is ncsvc, use ncui only in case of -# problems. See documentatuion for the details. + +# VPN executable mode (string) +# Set to 'ncsvc' (default) or 'ncui'. +# Use ncui only in case of problems. See README.md for details. mode=ncsvc -# -# scripting support - uncomment next line to run user-defined script on events + +# Scripting support (string) +# run user-defined script on connect or disconnect ## script=scripts/sample-script.sh # -# it is possible to setup password in configuration file, ask it ineractively -# or to use external helpers to provide it. -# Use line -# password=interactive -# to prompt password every time -# To setup password in configuration use -# password=plaintext:mypassword -# format -# If you want to use external program to provide the password use -# password=helper:scripts/password.sh -# format +# Password (string) +# Prompt every time: +# password=interactive +# Plaintext in config (very insecure): +# password=plaintext:mypassword +# Use external script/program to provide your password: +# password=helper:scripts/password.sh password=interactive -# enable host checker support. This will require JRE to run tncc.jar process. -# It is recommended to enable only if your VPN server require this +# Duo support (string) +# Set to '1' to use Duo app key +# Set to 'push' to use Duo Push +duo=push + +# Token support (boolean) +# Ask for two-factor auth token +# Note: if you set password=interactive, you can just do +# password+token yourself. +# Setting token=1 only makes sense with password=plaintext:foobar +# or password=helper:/path/to/script.sh +#token=1 + +# Host checker support (boolean) +# This will require JRE to run tncc.jar process. +# Only enable this if your VPN server requires it (boolean) hostchecker=0 -# debug - set to 1 to enable +# Attempt reconnect after disconnect (boolean) +reconnect=1 + +# Reconnect timeout count (integer) +# How many timed out (missed) reconnects until we assume that you're +# not at the computer and aren't going to ack one? Default: 5 +# Note: only applies if 'reconnect=1' +recontimeoutcount=5 + +# Reconnect timeout timeout (integer) +# Time limit window for recontimeoutcount. Default: 10 +# Note: only applies if 'reconnect=1' +recontimeouttimeout=10 + +# debug (boolean) debug=0 +#end diff --git a/scripts/restore-original-dns.sh b/scripts/restore-original-dns.sh index 12b1f0c..dfe9947 100755 --- a/scripts/restore-original-dns.sh +++ b/scripts/restore-original-dns.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # This script will restore the DNS server settings to the original DNS settings # discarding the DNS servers proposed by the VPN client. diff --git a/scripts/sample-script-tunnel.sh b/scripts/sample-script-tunnel.sh new file mode 100644 index 0000000..febb8c3 --- /dev/null +++ b/scripts/sample-script-tunnel.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# $EVENT - "up" or "down", up is defined after interface comes up, down +# after disconnect +# $MODE - jvpn mode, "ncui" or "ncsvc" +# $DNS1, $DNS2 - DNS servers, only in ncsvc mode +# $IP - local VPN ip address, only in ncsvc mode +# $GATEWAY - VPN gateway, only in ncsvc mode +# $INTERFACE - tun interface used for jvpn, only in ncui mode + +PIDFILE=/tmp/tunnel.pid + +## Kill (whether we're ending or starting up) +if [ -f $PIDFILE ] && [ -s $PIDFILE ] && [ -n "$(pgrep -F $PIDFILE)" ]; then + pkill -F $PIDFILE +fi +echo -n > $PIDFILE ## clean PIDFILE + +## Init tunnel only if we're starting up +if [ -n "$EVENT" ] && [ $EVENT = "up" ]; then + su - jv -c "ssh -N -q user@hostovervpn.domain.private -D 8080 -L1080:127.0.0.1:80 & echo \$!" > $PIDFILE +fi diff --git a/scripts/sample-script.sh b/scripts/sample-script.sh index da9304e..b32f0cc 100755 --- a/scripts/sample-script.sh +++ b/scripts/sample-script.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # this is sample script for the jvpn. It could be used from jvpn if script variable # is defined. jvpn defines some variables which can be used inside: diff --git a/scripts/stoken.sh b/scripts/stoken.sh index d48be5d..3a5d535 100755 --- a/scripts/stoken.sh +++ b/scripts/stoken.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # RSA software token (stoken) password helper # see https://github.com/cernekee/stoken/ for the details From aadc95704f52ffe000a8f814a88d0d87612b1686 Mon Sep 17 00:00:00 2001 From: jv Date: Thu, 26 Mar 2026 09:05:07 -0700 Subject: [PATCH 2/3] Add Duo Push, multi-session, kick, reconnect, and custom user-agent Features added to jvpn.pl: - Duo Push two-factor authentication (duo=push sends push notification, duo=key prompts for Duo authentication key) - Multi-session support (mult_session=1 allows concurrent VPN sessions when the server permits it) - Session kicking (kick=1 with kick_string to forcefully terminate conflicting sessions instead of using the web UI) - Auto-reconnect after disconnect (reconnect=1 with configurable timeout count and window) - Custom user-agent string (defaults to JVPN/$sysname, auto-switches to Firefox UA when hostchecker is enabled) Code changes: - Wrap connection logic in connect_vpn() sub to support reconnection - Add reconnect_vpn() sub with timeout tracking - Reorganize imports alphabetically - Add command-line --kick_string parameter - Add $pulse_nc_dir for Pulse Secure compatibility - Improve debug output with configurable maxlength - Better error messages and "Invalid primary" credential check --- jvpn.pl | 1589 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 876 insertions(+), 713 deletions(-) diff --git a/jvpn.pl b/jvpn.pl index 9935450..6bba317 100755 --- a/jvpn.pl +++ b/jvpn.pl @@ -14,31 +14,34 @@ use strict; use warnings; -use Term::ReadKey; -use IO::Socket::INET; -use IO::Socket::SSL qw(); use Fcntl ':mode'; +use File::Copy; +use File::Path; +use File::Temp; use Getopt::Long; +use HTTP::Cookies; use HTTP::Request::Common; +use IO::Socket::INET; use LWP::UserAgent; -use HTTP::Cookies; -use File::Copy; -use File::Temp; -use File::Path; +use Term::ReadKey; use POSIX; my %Config; my @config_files = ("./jvpn.ini", $ENV{'HOME'}."/.jvpn.ini", "/etc/jvpn/jvpn.ini"); my $config_file = ''; my $show_help = 0; +my $p_kick_string = ''; # find configuration file foreach my $line (@config_files) { - $config_file=$line; - last if -e $config_file; + $config_file=$line; + last if -e $config_file; } # override from command line if specified -GetOptions ("config_file=s" => \$config_file, - "help" => \$show_help); +GetOptions ( + "config_file=s" => \$config_file, + "kick_string=s" => \$p_kick_string, + "help" => \$show_help + ); if($show_help) { print_help(); } # parse configuration @@ -47,8 +50,12 @@ my $dhost=$Config{"host"}; my $dport=$Config{"port"}; my $durl=$Config{"url"}; +my $dmult=$Config{"mult_session"}; +my $kick=$Config{"kick"}; +my $kick_string=$Config{"kick_string"}; my $username=$Config{"username"}; my $realm=$Config{"realm"}; +my $user_agent=$Config{"user_agent"}; my $dnsprotect=$Config{"dnsprotect"}; my $debug=$Config{"debug"}; my $verifycert=$Config{"verifycert"}; @@ -58,64 +65,100 @@ my $workdir=$Config{"workdir"}; my $password=""; my $hostchecker=$Config{"hostchecker"}; +my $reconnect=$Config{"reconnect"}; +my $recontc=$Config{"recontimeoutcount"}; +my $recontt=$Config{"recontimeouttimeout"}; +my $token=$Config{"token"}; +my $duo=$Config{"duo"}; my $tncc_pid = 0; +my $debug_res_maxlength = 0; + +my ($recontry, $reconstart) if ($reconnect); +my $reconresettime = 2 * $recontt if ($reconnect); + my $supportdir = $ENV{"HOME"}."/.juniper_networks"; +my $pulse_nc_dir = $ENV{"HOME"}."/.pulse_secure/network_connect"; my $narport_file = $supportdir."/narport.txt"; +my ($sysname, $nodename, $release, $version, $machine) = POSIX::uname(); + # change directory if (defined $workdir){ - mkpath($workdir) if !-e $workdir; - chdir($workdir); + mkpath($workdir) if !-e $workdir; + chdir($workdir); } # check mode if(defined $mode){ - if($mode !~ /^nc(ui|svc)$/) { - print "Configuration error: mode is set incorrectly ($mode), check jvpn.ini\n"; - exit 1; - } + if($mode !~ /^nc(ui|svc)$/) { + print "Configuration error: mode is set incorrectly ($mode), check jvpn.ini\n"; + exit 1; + } +} +else { + $mode="ncsvc"; } -else { $mode="ncsvc"; } # check password method if(defined $cfgpass){ - if($cfgpass !~ /^(interactive|helper:|plaintext:)/) { - print "Configuration error: password is set incorrectly ($cfgpass), check jvpn.ini\n"; - exit 1; - } + if($cfgpass !~ /^(interactive|helper:|plaintext:)/) { + print "Configuration error: password is set incorrectly ($cfgpass), check jvpn.ini\n"; + exit 1; + } +} +else { + $cfgpass="interactive"; } -else { $cfgpass="interactive"; } # set host checker mode $hostchecker=0 if !defined($mode); + # set default url if needed $durl = "url_default" if (!defined($durl)); +# set user_agent if needed +$user_agent = "JVPN/$sysname" if (!defined($user_agent)); + +# set recon timeout params, if needed +$recontc = 5 if (!defined($recontc)); +$recontt = 10 if (!defined($recontt)); + +# set kick_string if needed +if (defined($p_kick_string)) { ## command line parameter wins + $kick_string = $p_kick_string; +} elsif (!defined($kick_string)) { ## If undefined, default to our user_agent + $kick_string = $user_agent; +} + # checking if we running under root # we need ncsvc to be uid for all modes my $is_setuid = 0; if (-e "./ncsvc") { - my $fmode = (stat("./ncsvc"))[2]; - $is_setuid = ($fmode & S_ISUID) && ((stat("./ncsvc"))[4] == 0); - if(!-x "./ncsvc"){ - print "./ncsvc is not executable, exiting\n"; - exit 1; - } + my $fmode = (stat("./ncsvc"))[2]; + $is_setuid = ($fmode & S_ISUID) && ((stat("./ncsvc"))[4] == 0); + if(!-x "./ncsvc"){ + print "./ncsvc is not executable, exiting\n"; + exit 1; + } } if( $> != 0 && !$is_setuid) { - print "Please, run this script with su/sudo or set suid attribute on $mode \n"; - exit 1; + print "Please, run this script with su/sudo or set suid attribute on $mode \n"; + exit 1; } my $ua = LWP::UserAgent->new; -# on RHEL6 ssl_opts is not exists +# on RHEL6+ ssl_opts does exist if(defined &LWP::UserAgent::ssl_opts) { $ua->ssl_opts('SSL_verify_mode' => IO::Socket::SSL::SSL_VERIFY_NONE); $ua->ssl_opts('verify_hostname' => $verifycert); + if (!$verifycert) { + $ua->ssl_opts('SSL_verify_mode' => '0x00'); + } } $ua->cookie_jar({}); + push @{ $ua->requests_redirectable }, 'POST'; # if Juniper VPN server finds some 'known to be smart' useragent it will try to @@ -126,737 +169,857 @@ $ua->cookie_jar->set_cookie(0,"DSCheckBrowser","java","/",$dhost,$dport,1,1,60*5,0, ()); } else { - $ua->agent('JVPN/Linux'); + $ua->agent($user_agent); } # show LWP traffic dump if debug is enabled if($debug){ - $ua->add_handler("request_send", sub { shift->dump; return }); - $ua->add_handler("response_done", sub { shift->dump; return }); + $ua->add_handler("request_send", sub { shift->dump(maxlength => $debug_res_maxlength); return }); + $ua->add_handler("response_done", sub { shift->dump(maxlength => $debug_res_maxlength); return }); } if (!defined($username) || $username eq "" || $username eq "interactive") { - print "Enter username: "; - $username=read_input(); - print "\n"; + print "Enter username: "; + $username=read_input(); + print "\n"; } -if ($cfgpass eq "interactive") { - print "Enter PIN+password: "; - $password=read_input("password"); - print "\n"; -} -elsif ($cfgpass =~ /^plaintext:(.+)/) { - print "Using user-defined password\n"; - $password=$1; - chomp($password); -} -elsif ($cfgpass =~ /^helper:(.+)/) { - print "Using user-defined script to get the password\n"; - $password=run_pw_helper($1); -} +my ($socket,$client_socket,$data); -my $response_body = ''; - -my $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", - [ btnSubmit => 'Sign In', - password => $password, - realm => $realm, - tz => '60', - username => $username, - ]); - -$response_body=$res->decoded_content; -my $dsid=""; -my $dlast=""; -my $dfirst=""; - -# Looking at the results... -if ($res->is_success) { - print("Transfer went ok\n"); - # next token request - if ($response_body =~ /name="frmDefender"/ || $response_body =~ /name="frmNextToken"/) { - $response_body =~ m/name="key" value="([^"]+)"/; - my $key=$1; - print "The server requires that you enter an additional token ". - "code to verify that your credentials are valid.\n"; - # grid cards. $1 contains grid reference - if ($response_body =~ /Challenge:([^"]+)\./) { - print $1; - print "\n"; - print "Enter challenge response: "; - $password=read_input("password"); - print "\n"; - } - # if password was specified in plaintext we should not use it - # here, it will not work anyway - elsif ($cfgpass eq "interactive" || $cfgpass =~ /^plaintext:/) { - print "To continue, wait for the token code to change and ". - "then enter the new pin and code.\n"; - print "Enter PIN+password: "; - $password=read_input("password"); - print "\n"; - } - elsif ($cfgpass =~ /^helper:(.+)/) { - print "Using user-defined script to get second password\n"; - # set current password to the OLDPIN variable to make - # helper aware that we need a new key - $ENV{'OLDPIN'}=$password; - $password=run_pw_helper($1); - delete $ENV{'OLDPIN'}; - } - $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", - [ Enter => 'secidactionEnter', - password => $password, - key => $key, - ]); - $response_body=$res->decoded_content; - } - if ( $response_body =~ /Invalid username or password/){ - print "Invalid user name or password, exiting \n"; - exit 1; - } - # hostchecker authorization stage - if($hostchecker) { - if(!-e "./tncc.jar") { # download tncc.jar if not exists - print "tncc.jar does not exist, downloading from https://$dhost:$dport/dana-cached/hc/tncc.jar\n"; - my $resdl = $ua->get ("https://$dhost:$dport/dana-cached/hc/tncc.jar",":content_file" => "./tncc.jar"); - if (!$resdl->is_success) { - print "Unable to download tncc.jar, exiting \n"; - exit 1; - } - } - # get state id and check if we on a right page - my $state_id=''; - # sample base https://vpn.com/dana-na/auth/url_default/welcome.cgi?p=preauth&id=state_c63757c951e0050e6d9f22ef13442&signinRealmId=4 - if ( $res->base =~ /[&?]id=(state_[0-9a-f]+)/){ - $state_id=$1; - } - else { - print "Unable to get preauth id from ".$res->base."\n"; - exit 1; - } # now we got preauth, so lets try to start tncc - $tncc_pid = tncc_start($res->decoded_content); - open NARPORT, $narport_file or die $!; - my $narport = ; - chomp $narport; - close NARPORT; - my $narsocket = retry_port($narport); - print "TCP Connection to the tncc.jar process established.\n"; - my $dspreauth=""; - my $cookie=$ua->cookie_jar->as_string; - if ( $cookie =~ /DSPREAUTH=([^;]+)/){ - $dspreauth=$1; - } - # sending DSPREAUTH - print "Sending data to tncc... "; - my $data = "start\nIC=$dhost\nCookie=$dspreauth\nDSSIGNIN=null\n"; - hdump($data) if $debug; - print $narsocket "$data"; - $narsocket->recv($data,2048); - $narsocket->close(); - if(!length($data)) { - print "\nUnable to get data from tncc, exiting"; - exit 1; - } - hdump($data) if $debug; - my @resp_lines = split /\n/, $data; - - if($resp_lines[0]!=200) { - print "\nGot non 200 (".$resp_lines[0].") return code\n"; - exit 1; - } - print "[done]\n"; - $ua->cookie_jar->set_cookie(0,"DSPREAUTH",$resp_lines[2],"/dana-na/",$dhost,$dport,1,1,60*5,0, ()); - $res = $ua->get("https://$dhost:$dport/dana-na/auth/$durl/login.cgi?loginmode=mode_postAuth&postauth=$state_id"); - $response_body=$res->decoded_content; - # send "setcookie" command as native client do - $cookie=$ua->cookie_jar->as_string; - $dspreauth = ""; - if ( $cookie =~ /DSPREAUTH=([^;]+)/){ - $dspreauth=$1; - } - if(length($dspreauth)) { - $narsocket = retry_port($narport); - $data = "setcookie\nCookie=$dspreauth\n"; - hdump($data) if $debug; - print $narsocket "$data"; - $narsocket->close(); - } - } - # active sessions found - if ($response_body =~ /id="DSIDConfirmForm"/) { - $response_body =~ m/name="FormDataStr" value="([^"]+)"/; - print "Active sessions found, reconnecting...\n"; - $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", - [ btnContinue => 'Continue the session', - FormDataStr => $1, - ]); - $response_body=$res->decoded_content; - } - my $cookie=$ua->cookie_jar->as_string; - if ( $cookie =~ /DSID=([a-f\d]+)/){ - $dsid=$1; - } - if ( $cookie =~ /DSFirstAccess=(\d+)/){ - $dfirst=$1; - } - else { - $dfirst=time(); - } - if ( $cookie =~ /DSLastAccess=(\d+)/){ - $dlast=$1; - } - else { - $dlast=time(); - } - - # do not print DSID in normal mode for security reasons - print $debug?"Got DSID=$dsid, dfirst=$dfirst, dlast=$dlast\n":"Got DSID\n"; - if ($dsid eq "") { - print "Unable to get DSID, exiting \n"; - exit 1; - } - -} else { - # Error code, type of error, error message - print("An error happened: ".$res->status_line."\n"); - exit 1; -} +# Trigger the main sub (which is wrapped as a sub so reconnections are possible) +connect_vpn(); -# set int handlers -$SIG{'INT'} = \&INT_handler; # CTRL+C -$SIG{'TERM'} = \&INT_handler; # Kill process -$SIG{'HUP'} = \&INT_handler; # Terminal closed -$SIG{'PIPE'} = \&INT_handler; # Process died - -# flush after every write -$| = 1; - -my $md5hash = ''; -my $crtfile = ''; -my $fh; # should be global or file is unlinked - -if($mode eq "ncsvc") { - $md5hash = <<` SHELL`; - echo | openssl s_client -connect ${dhost}:${dport} 2>/dev/null| \ - openssl x509 -md5 -noout -fingerprint|\ - awk -F\= '{print \$2}'|tr -d \: - exit 0 - SHELL - chop($md5hash); - # changing case - $md5hash =~ tr/A-Z/a-z/; - if($md5hash eq "") { - print "Unable to get md5 hash of certificate, exiting"; - exit 1; - } - print "Certificate fingerprint: [$md5hash]\n"; -} -elsif($mode eq "ncui") { - # we need to fetch certificate - $fh = File::Temp->new(); - $crtfile = $fh->filename; - << ` SHELL`; - echo | openssl s_client -connect ${dhost}:${dport} 2>&1 | \ - sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | \ - openssl x509 -outform der > $crtfile - SHELL - printf("Saved certificate to temporary file: $crtfile\n"); -} +sub connect_vpn { + my $response_body = ''; + my $cont_button = ''; -if (!-e "./$mode") { - $res = $ua->get ("https://$dhost:$dport/dana-cached/nc/ncLinuxApp.jar",':content_file' => './ncLinuxApp.jar'); - print "Client not exists, downloading from https://$dhost:$dport/dana-cached/nc/ncLinuxApp.jar\n"; - if ($res->is_success) { - print "Done, extracting\n"; - system("unzip -o ncLinuxApp.jar ncsvc libncui.so && chmod +x ./ncsvc"); - if($mode eq "ncui") { - if(!-e 'wrapper.c'){ - printf "wrapper.c not found in ".getcwd()."\n"; - printf "Please copy this file from jvpn distro and try again"; - exit 1; - } - printf "Trying to compile 'ncui'. gcc must be installed to make this possible\n"; - system("gcc -m32 -o ncui wrapper.c -ldl -Wall >compile.log 2>&1 && chmod +x ./ncui"); - if (!-e "./ncui") { - printf("Error: Compilation failed, please compile.log\n"); - exit 1; - } - else { - printf("ncui binary compiled\n"); - } - } - } - else { - print "Download failed, exiting\n"; - exit 1; - } -} + if ($cfgpass eq "interactive") { + print "Enter password: "; + $password=read_input("password"); + print "\n"; + } + elsif ($cfgpass =~ /^plaintext:(.+)/) { + print "Using user-defined password\n"; + $password=$1; + chomp($password); + } + elsif ($cfgpass =~ /^helper:(.+)/) { + print "Using user-defined script to get the password\n"; + $password=run_pw_helper($1); + } + + if ($token) { + print "Enter token: "; + $password = $password . read_input(); + print "\n"; + } + my $password2 = ''; + if ($duo eq "push") { + $password2 = "push"; + (! -d $pulse_nc_dir) && mkdir -p $pulse_nc_dir; + } elsif ($duo eq "key") { + print "Enter Duo key: "; + $password2 = read_input(); + (! -d $pulse_nc_dir) && mkdir -p $pulse_nc_dir; + } -my $start_t = time; - -my ($socket,$client_socket); -my $data; -if($mode eq "ncsvc") { - system("./ncsvc >/dev/null 2>/dev/null &"); - # connecting to ncsvc using TCP - $socket = retry_port(4242); - - print "TCP Connection to ncsvc process established.\n"; - - # sending first packet, got it from tcpdump - print "Sending handshake #1 packet... "; - $data = "\0\0\0\0\0\0\0\x64\x01\0\0\0\0\0\0\0\0\0\0\0"; - - hdump($data) if $debug; - print $socket "$data"; - $socket->recv($data,2048); - # XXX - good idea to chek if it valid - print " [done]\n"; - hdump($data) if $debug; - - # second packet from tcpdump - # it contains logging level in the end: - # 0 LogLevel 0, 10 LogLevel 1, 20 LogLevel 2 - # 30 LogLevel 3, 40 LogLevel 4, 50 LogLevel 5 - # We are enabling full log if debug is enabled - $data= "\0\0\0\0\0\0\0\x7c\x01\0\0\0\x01\0\0\0\0\0\0\x10\0\0\0\0\0\x0a\0\0". - "\0\0\0\x04\0\0\0".($debug?"\x32":"\0"); - print "Sending handshake #2 packet... "; - hdump($data) if $debug; - print $socket "$data"; - $socket->recv($data,2048); - # XXX - good idea to chek if it is valid - print " [done]\n"; - hdump($data) if $debug; - my $dsidline="DSSignInURL=/; DSID=$dsid; DSFirstAccess=$dfirst; DSLastAccess=$dlast; path=/; secure"; - # Configuration packet - # XXX - no idea how it works on non default port - $data="\0\0\0\0\0\0\0\x66\x01\0\0\0\x01\0\0\0\0\0\0".pack("C*",(length($dhost)+1) + (length($dsidline)+1) + 57)."\0\xcb\0\0". - "\0".pack("C*",(length($dhost)+1) + (length($dsidline)+1) + 51)."\0\x01\0\0\0".pack("C*",length($dhost)+1). - $dhost. - "\0\0\x02\0\0\0".pack("C*",length($dsidline)+1). - $dsidline. - "\0\0\x0a\0\0\0".pack("C*",length($md5hash)+1). - $md5hash. - "\0"; - print "Sending configuration packet..."; - hdump($data) if $debug; - print $socket "$data"; - $socket->recv($data,2048); - print " [done]\n"; - hdump($data) if $debug; - # checking reply status - my @result = unpack('C*',$data); - my $status = sprintf("%02x",$result[7]); - # 0x6d seems to be "Connect ok" message - # exit on any other values - - if($status ne "6d") { - printf("Status=$status\nAuthentication failed, exiting\n"); - system("./ncsvc -K"); - exit(1); - } - if($> == 0 && $dnsprotect) { - system("chattr +i /etc/resolv.conf"); - } - -} # ncsvc - - -if ($mode eq "ncui"){ - print "Starting ncui, this should bring VPN up.\nPress CTRL+C anytime to terminate connection\n"; - my $childpid; - local $SIG{'CHLD'} = 'IGNORE'; - my @oldlist = get_tap_interfaces(); - my $pid = fork(); - if ($pid == 0) { - my $args = "./ncui\n-p\n\n". - "-h\n$dhost\n". - "-c\nDSSignInURL=/; DSID=$dsid; DSFirstAccess=$dfirst; DSLastAccess=$dlast; path=/; secure\n". - "-f\n$crtfile\n". - ($debug?"-l\n5\n-L\n5\n":""); - $debug && print $args; - open(WRITEME, "|-", "./ncui") or die "Couldn't fork: $!\n"; - print WRITEME $args; - close(WRITEME); - printf("ncui terminated\n"); - exit 0; - } - my $exists = kill 0, $pid; - my $vpnint = get_new_tap_interface(\@oldlist, 15); - if ($vpnint eq '') { - printf("Error: new interface not found, check ncsvc logs\n"); - INT_handler(); - } - printf("Connection established, new interface: $vpnint\n"); - if($exists && $> == 0 && $dnsprotect) { - system("chattr +i /etc/resolv.conf"); - } - if(defined $script && -x $script){ - print "Running user-defined script\n"; - $ENV{'EVENT'}="up"; - $ENV{'MODE'}=$mode; - $ENV{'INTERFACE'}=$vpnint; - system($script); - } - - for (;;) { - $exists = kill SIGCHLD, $pid; - $debug && printf("\nChecking child: exists=$exists, $pid\n"); - # printing RX/TX from /proc/net/dev - my $now = time - $start_t; - open STAT, "/proc/net/dev" or die $!; - while () { - if ($_ =~ m/^\s*${vpnint}:\s*(\d+)(?:\s+\d+){7}\s*(\d+)/) { - print "\r \r"; - printf("Duration: %02d:%02d:%02d Sent: %s\tReceived: %s", - int($now / 3600), int(($now % 3600) / 60), int($now % 60), - format_bytes($2), format_bytes($1)); - } - } - close(STAT); - if(!$exists) { - INT_handler(); - } - sleep 2; - } + my $welcome_cgi="https://$dhost/dana-na/auth/$durl/welcome.cgi"; + my $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", + [ + btnSubmit => 'Sign+In', + password => $password, + 'password#2' => $password2, + realm => $realm, + tz => '-480', + username => $username, + ], + Referer => $welcome_cgi, + ); + + $ua->cookie_jar->extract_cookies( $res ); + + $response_body=$res->decoded_content; + my $dsid=""; + my $dlast=""; + my $dfirst=""; + if ( $response_body =~ /Invalid primary/){ + print "Access denied. Wrong password? Exiting.\n"; + exit 4; + } + + # Looking at the results... + if ($res->is_success) { + print("Initial connection successful\n"); + # next token request + if ($response_body =~ /name="frmDefender"/ || $response_body =~ /name="frmNextToken"/) { + $response_body =~ m/name="key" value="([^"]+)"/; + my $key=$1; + print "The server requires that you enter an additional token ". + "code to verify that your credentials are valid.\n"; + # grid cards. $1 contains grid reference + if ($response_body =~ /Challenge:([^"]+)\./) { + print $1; + print "\n"; + print "Enter challenge response: "; + $password=read_password(); + print "\n"; + } + # if password was specified in plaintext we should not use it + # here, it will not work anyway + elsif ($cfgpass eq "interactive" || $cfgpass =~ /^plaintext:/) { + print "To continue, wait for the token code to change and ". + "then enter your password and new PIN.\n"; + print "Enter password+PIN: "; + $password=read_password(); + print "\n"; + } + elsif ($cfgpass =~ /^helper:(.+)/) { + print "Using user-defined script to get second password\n"; + # set current password to the OLDPIN variable to make + # helper aware that we need a new key + $ENV{'OLDPIN'}=$password; + $password=run_pw_helper($1); + delete $ENV{'OLDPIN'}; + } + $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", + [ + Enter => 'secidactionEnter', + password => $password, + key => $key, + ]); + $response_body=$res->decoded_content; + } + if ( $response_body =~ /Invalid username or password/){ + print "Invalid user name or password, exiting \n"; + exit 1; + } + # hostchecker authorization stage + if($hostchecker) { + if(!-e "./tncc.jar") { # download tncc.jar if not exists + print "tncc.jar does not exist, downloading from https://$dhost:$dport/dana-cached/hc/tncc.jar\n"; + my $resdl = $ua->get ("https://$dhost:$dport/dana-cached/hc/tncc.jar",":content_file" => "./tncc.jar"); + if (!$resdl->is_success) { + print "Unable to download tncc.jar, exiting \n"; + exit 1; + } + } + # get state id and check if we on a right page + my $state_id=''; + # sample base https://vpn.com/dana-na/auth/url_default/welcome.cgi?p=preauth&id=state_c63757c951e0050e6d9f22ef13442&signinRealmId=4 + if ( $res->base =~ /[&?]id=(state_[0-9a-f]+)/){ + $state_id=$1; + } + else { + print "Unable to get preauth id from ".$res->base."\n"; + exit 1; + } + # now we got preauth, so lets try to start tncc + $tncc_pid = tncc_start($res->decoded_content); + open NARPORT, $narport_file or die $!; + my $narport = ; + chomp $narport; + close NARPORT; + my $narsocket = retry_port($narport); + print "TCP Connection to the tncc.jar process established.\n"; + my $dspreauth=""; + my $cookie=$ua->cookie_jar->as_string; + if ( $cookie =~ /DSPREAUTH=([^;]+)/){ + $dspreauth=$1; + } + # sending DSPREAUTH + print "Sending data to tncc... "; + my $data = "start\nIC=$dhost\nCookie=$dspreauth\nDSSIGNIN=null\n"; + hdump($data) if $debug; + print $narsocket "$data"; + $narsocket->recv($data,2048); + $narsocket->close(); + if(!length($data)) { + print "\nUnable to get data from tncc, exiting"; + exit 1; + } + hdump($data) if $debug; + my @resp_lines = split /\n/, $data; + + if($resp_lines[0]!=200) { + print "\nGot non 200 (".$resp_lines[0].") return code\n"; + exit 1; + } + print "[done]\n"; + $ua->cookie_jar->set_cookie(0,"DSPREAUTH",$resp_lines[2],"/dana-na/",$dhost,$dport,1,1,60*5,0, ()); + $res = $ua->get("https://$dhost:$dport/dana-na/auth/$durl/login.cgi?loginmode=mode_postAuth&postauth=$state_id"); + $response_body=$res->decoded_content; + # send "setcookie" command as native client do + $cookie=$ua->cookie_jar->as_string; + $dspreauth = ""; + if ( $cookie =~ /DSPREAUTH=([^;]+)/){ + $dspreauth=$1; + } + if(length($dspreauth)) { + $narsocket = retry_port($narport); + $data = "setcookie\nCookie=$dspreauth\n"; + hdump($data) if $debug; + print $narsocket "$data"; + $narsocket->close(); + } + } + # active sessions found + if ($response_body =~ /id="DSIDConfirmForm"/) { + my ($formdatastr) = ($response_body =~ m/name="FormDataStr" value="([^"]+)"/); + if ($dmult) { + if ($response_body =~ /maximum number of open user sessions allowed/) { + print "Maximum active sessions found...\n"; + if ($kick) { + print "Attempting to kick session '$kick_string'...\n"; + #print "response_body: $response_body\n\n"; + my ($session_id, $ip, $login_time, $idle_time) = ($response_body =~ m/name="postfixSID"\s+value="([^"]+)"\/><\/td>\s+([0-9.]+)<\/td>\s+([^<]+)<\/td>\s+([^<]+)<\/td>\s+[^<]*$kick_string[^<]*<\/td>/); + if ($session_id) { + ($debug) && print "Session_id being killed: $session_id from IP $ip\n"; + print "Session_id being killed: $session_id from IP $ip\n"; + $cont_button =~ m/name="btnContinue" value="([^"]+)"/; + $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", + [ + btnContinue => $cont_button, + postfixSID => $session_id, + FormDataStr => $formdatastr, + ]); + } else { + print "Sorry, didn't find a connected '$kick_string' agent. We have:\n"; + my $valid_kick_string; + while ($response_body =~ m/name="postfixSID"\s+value="[^"]+"\/><\/td>\s+([0-9.]+)<\/td>\s+[^<]+<\/td>\s+[^<]+<\/td>\s+([^<]+)<\/td>/g) { + print "\t$2 @ $1\n"; + $valid_kick_string = $2; + } + print "Try something like this:\n"; + print "\t$0 --kick_string='$valid_kick_string'\n"; + print "Exiting.\n"; + exit 1; + } + } else { + if ($reconnect) { + reconnect_vpn(); + } else { + print "Exiting.\n"; + exit 1; + } + } + } else { + $cont_button =~ m/name="btnContinue" value="([^"]+)"/; + print "Active sessions found, continuing anyway...\n"; + $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", + [ + btnContinue => $cont_button, + FormDataStr => $formdatastr, + ]); + } + } else { + print "Active sessions found, reconnecting...\n"; + $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", + [ + btnContinue => 'Continue the session', + FormDataStr => $formdatastr, + ]); + } + $response_body=$res->decoded_content; + } + my $cookie=$ua->cookie_jar->as_string; + ($debug) && print "Cookies: $cookie\n"; + if ( $cookie =~ /DSID=([a-f\d]+)/){ + $dsid=$1; + } + if ( $cookie =~ /DSFirstAccess=(\d+)/){ + $dfirst=$1; + } + else { + $dfirst=time(); + } + if ( $cookie =~ /DSLastAccess=(\d+)/){ + $dlast=$1; + } + else { + $dlast=time(); + } + + # do not print DSID in normal mode for security reasons + print $debug?"Got DSID=$dsid, dfirst=$dfirst, dlast=$dlast\n":""; + + if ($dsid eq "") { + print "Unable to get DSID, exiting \n"; + exit 1; + } else { + print "Got DSID\n"; + } + + } + else { + # Error code, type of error, error message + print("An error happened: ".$res->status_line."\n"); + exit 1; + } + + # set int handlers + $SIG{'INT'} = \&INT_handler; # CTRL+C + $SIG{'TERM'} = \&INT_handler; # Kill process + $SIG{'HUP'} = \&INT_handler; # Terminal closed + $SIG{'PIPE'} = \&INT_handler; # Process died + + # flush after every write + $| = 1; + + my $md5hash = ''; + my $crtfile = ''; + my $fh; # should be global or file is unlinked + + if($mode eq "ncsvc") { + ($debug) && print "Getting md5hash\n"; + $md5hash = lc <<` SHELL`; + echo | openssl s_client -connect ${dhost}:${dport} 2>/dev/null| \ + openssl x509 -md5 -noout -fingerprint|\ + awk -F\= '{print \$2}'|tr -d \: + exit 0 + SHELL + chop($md5hash); + # changing case + if($md5hash eq "") { + print "Unable to get md5 hash of certificate. Exiting"; + exit 1; + } + print "Certificate fingerprint: [$md5hash]\n"; + } + elsif($mode eq "ncui") { + # we need to fetch certificate + ($debug) && print "Getting md5hash\n"; + $fh = File::Temp->new(); + $crtfile = $fh->filename; + << ` SHELL`; + echo | openssl s_client -connect ${dhost}:${dport} 2>&1 | \ + sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | \ + openssl x509 -outform der > $crtfile + SHELL + printf("Saved certificate to temporary file: $crtfile\n"); + } + + if (!-e "./$mode") { + $res = $ua->get ("https://$dhost:$dport/dana-cached/nc/ncLinuxApp.jar",':content_file' => './ncLinuxApp.jar'); + print "Client jar not found. Downloading from https://$dhost:$dport/dana-cached/nc/ncLinuxApp.jar\n"; + if ($res->is_success) { + print "Done, extracting\n"; + system("unzip -o ncLinuxApp.jar ncsvc libncui.so && chmod +x ./ncsvc"); + if($mode eq "ncui") { + if(!-e 'wrapper.c'){ + printf "wrapper.c not found in ".getcwd()."\n"; + printf "Please copy this file from jvpn distro and try again"; + exit 1; + } + printf "Trying to compile 'ncui'. gcc must be installed to make this possible\n"; + system("gcc -m32 -o ncui wrapper.c -ldl -Wall >compile.log 2>&1 && chmod +x ./ncui"); + if (!-e "./ncui") { + printf("Error: Compilation failed, please see compile.log\n"); + exit 1; + } + else { + printf("ncui binary compiled\n"); + } + } + } + else { + print "Download failed, exiting\n"; + exit 1; + } + } + + my $start_t = time; + if($mode eq "ncsvc") { + print "Starting ncsvc mode\n"; + system("./ncsvc >/dev/null 2>/dev/null &"); + # connecting to ncsvc using TCP + $socket = retry_port(4242); + + print "TCP Connection to ncsvc process established.\n"; + + # sending first packet, got it from tcpdump + print "Sending handshake #1 packet... "; + $data = "\0\0\0\0\0\0\0\x64\x01\0\0\0\0\0\0\0\0\0\0\0"; + + hdump($data) if $debug; + print $socket "$data"; + $socket->recv($data,2048); + # XXX - good idea to check if it valid + print " [done]\n"; + hdump($data) if $debug; + + # second packet from tcpdump + # it contains logging level in the end: + # 0 LogLevel 0, 10 LogLevel 1, 20 LogLevel 2 + # 30 LogLevel 3, 40 LogLevel 4, 50 LogLevel 5 + # We are enabling full log if debug is enabled + $data= "\0\0\0\0\0\0\0\x7c\x01\0\0\0\x01\0\0\0\0\0\0\x10\0\0\0\0\0\x0a\0\0". + "\0\0\0\x04\0\0\0".($debug?"\x32":"\0"); + print "Sending handshake #2 packet... "; + hdump($data) if $debug; + print $socket "$data"; + $socket->recv($data,2048); + # XXX - good idea to check if it is valid + print " [done]\n"; + hdump($data) if $debug; + my $dsidline="DSSignInURL=/; DSID=$dsid; DSFirstAccess=$dfirst; DSLastAccess=$dlast; path=/; secure"; + # Configuration packet + # XXX - no idea how it works on non default port + $data="\0\0\0\0\0\0\0\x66\x01\0\0\0\x01\0\0\0\0\0\0".pack("C*",(length($dhost)+1) + (length($dsidline)+1) + 57)."\0\xcb\0\0". + "\0".pack("C*",(length($dhost)+1) + (length($dsidline)+1) + 51)."\0\x01\0\0\0".pack("C*",length($dhost)+1). + $dhost. + "\0\0\x02\0\0\0".pack("C*",length($dsidline)+1). + $dsidline. + "\0\0\x0a\0\0\0".pack("C*",length($md5hash)+1). + $md5hash. + "\0"; + print "Sending configuration packet..."; + hdump($data) if $debug; + print $socket "$data"; + $socket->recv($data,2048); + print " [done]\n"; + hdump($data) if $debug; + # checking reply status + my @result = unpack('C*',$data); + my $status = sprintf("%02x",$result[7]); + + # 0x6d seems to be "Connect ok" message + # 0x6e seems to be "Connection expired" message + # which also happens if you kick an existing connection + # exit on any other values + + if(($status eq "6e") && ($reconnect == 1)) { + printf("Status=$status\nDisconnected at ". POSIX::strftime("%c", localtime) ."\n"); + reconnect_vpn(); + } + elsif($status ne "6d") { + printf("Status=$status\nAuthentication failed, exiting\n"); + system("./ncsvc -K"); + exit(1); + } + if($> == 0 && $dnsprotect) { + system("chattr +i /etc/resolv.conf"); + } + + } # ncsvc + + if ($mode eq "ncui"){ + print "Starting ncui, this should bring VPN up.\nPress CTRL+C anytime to terminate connection\n"; + my $childpid; + local $SIG{'CHLD'} = 'IGNORE'; + my @oldlist = get_tap_interfaces(); + my $pid = fork(); + if ($pid == 0) { + my $args = "./ncui\n-p\n\n". + "-h\n$dhost\n". + "-c\nDSSignInURL=/; DSID=$dsid; DSFirstAccess=$dfirst; DSLastAccess=$dlast; path=/; secure\n". + "-f\n$crtfile\n". + ($debug?"-l\n5\n-L\n5\n":""); + $debug && print $args; + open(WRITEME, "|-", "./ncui") or die "Couldn't fork: $!\n"; + print WRITEME $args; + close(WRITEME); + printf("ncui terminated\n"); + exit 0; + } + my $exists = kill 0, $pid; + my $vpnint = get_new_tap_interface(\@oldlist, 15); + if ($vpnint eq '') { + printf("Error: new interface not found, check ncsvc logs\n"); + INT_handler(); + } + printf("Connection established, new interface: $vpnint\n"); + if($exists && $> == 0 && $dnsprotect) { + system("chattr +i /etc/resolv.conf"); + } + if(defined $script && -x $script){ + print "Running user-defined script\n"; + $ENV{'EVENT'}="up"; + $ENV{'MODE'}=$mode; + $ENV{'INTERFACE'}=$vpnint; + system($script); + } + + for (;;) { + $exists = kill SIGCHLD, $pid; + $debug && printf("\nChecking child: exists=$exists, $pid\n"); + # printing RX/TX from /proc/net/dev + my $now = time - $start_t; + open STAT, "/proc/net/dev" or die $!; + while () { + if ($_ =~ m/^\s*${vpnint}:\s*(\d+)(?:\s+\d+){7}\s*(\d+)/) { + print "\r \r"; + printf("Duration: %02d:%02d:%02d Sent: %s\tReceived: %s", + int($now / 3600), int(($now % 3600) / 60), int($now % 60), + format_bytes($2), format_bytes($1)); + } + } + close(STAT); + if(!$exists) { + INT_handler(); + } + sleep 2; + } + } + + if($mode eq "ncsvc") { + # information query + $data = "\0\0\0\0\0\0\0\x6a\x01\0\0\0\x01\0\0\0\0\0\0\0"; + hdump($data) if $debug; + print $socket "$data"; + $socket->recv($data,2048); + hdump($data) if $debug; + + if(defined $script && -x $script){ + print "Running user-defined script\n"; + $ENV{'EVENT'}="up"; + $ENV{'MODE'}=$mode; + $ENV{'DNS1'}=inet_ntoa(pack("N",unpack('x[84]N',$data))); + $ENV{'DNS2'}=inet_ntoa(pack("N",unpack('x[94]N',$data))); + $ENV{'IP'}=inet_ntoa(pack("N",unpack('x[48]N',$data))); + $ENV{'GATEWAY'}=inet_ntoa(pack("N",unpack('x[68]N',$data))); + system($script); + } + print "IP: ".inet_ntoa(pack("N",unpack('x[48]N',$data))). + " Gateway: ".inet_ntoa(pack("N",unpack('x[68]N',$data))). + "\nDNS1: ".inet_ntoa(pack("N",unpack('x[84]N',$data))). + " DNS2: ".inet_ntoa(pack("N",unpack('x[94]N',$data))). + "\nConnected to $dhost at ". POSIX::strftime("%c", localtime) .".\nPress CTRL+C to exit.\n"; + # disabling cursor + print "\e[?25l"; + while ( 1 ) { + #stat query + $data="\0\0\0\0\0\0\0\x69\x01\0\0\0\x01\0\0\0\0\0\0\0"; + print "\r \r"; + hdump($data) if $debug; + print $socket "$data"; + $socket->recv($data,2048); + if(!length($data) || !$socket->connected()) { + print "No response from ncsvc, closing connection\n"; + if ($reconnect) { + connect_vpn(); + } else { + INT_handler(); + } + } + hdump($data) if $debug; + my $now = time - $start_t; + # printing RX/TX. This packet also contains encription type, + # compression and transport info, but length seems to be variable + printf("Duration: %02d:%02d:%02d Sent: %s\tReceived: %s", + int($now / 3600), int(($now % 3600) / 60), int($now % 60), + format_bytes(unpack('x[78]N',$data)), format_bytes(unpack('x[68]N',$data))); + sleep(1); + } + + print "Exiting... Connect failed?\n"; + + $socket->close(); + } # mode ncsvc loop } -if($mode eq "ncsvc") { - # information query - $data = "\0\0\0\0\0\0\0\x6a\x01\0\0\0\x01\0\0\0\0\0\0\0"; - hdump($data) if $debug; - print $socket "$data"; - $socket->recv($data,2048); - hdump($data) if $debug; - - if(defined $script && -x $script){ - print "Running user-defined script\n"; - $ENV{'EVENT'}="up"; - $ENV{'MODE'}=$mode; - $ENV{'DNS1'}=inet_ntoa(pack("N",unpack('x[84]N',$data))); - $ENV{'DNS2'}=inet_ntoa(pack("N",unpack('x[94]N',$data))); - $ENV{'IP'}=inet_ntoa(pack("N",unpack('x[48]N',$data))); - $ENV{'GATEWAY'}=inet_ntoa(pack("N",unpack('x[68]N',$data))); - system($script); - } - print "IP: ".inet_ntoa(pack("N",unpack('x[48]N',$data))). - " Gateway: ".inet_ntoa(pack("N",unpack('x[68]N',$data))). - "\nDNS1: ".inet_ntoa(pack("N",unpack('x[84]N',$data))). - " DNS2: ".inet_ntoa(pack("N",unpack('x[94]N',$data))). - "\nConnected to $dhost, press CTRL+C to exit\n"; - # disabling cursor - print "\e[?25l"; - while ( 1 ) { - #stat query - $data="\0\0\0\0\0\0\0\x69\x01\0\0\0\x01\0\0\0\0\0\0\0"; - print "\r \r"; - hdump($data) if $debug; - print $socket "$data"; - $socket->recv($data,2048); - if(!length($data) || !$socket->connected()) { - print "No response from ncsvc, closing connection\n"; - INT_handler(); - } - hdump($data) if $debug; - my $now = time - $start_t; - # printing RX/TX. This packet also contains encription type, - # compression and transport info, but length seems to be variable - printf("Duration: %02d:%02d:%02d Sent: %s\tReceived: %s", - int($now / 3600), int(($now % 3600) / 60), int($now % 60), - format_bytes(unpack('x[78]N',$data)), format_bytes(unpack('x[68]N',$data))); - sleep(1); - } - - print "Exiting... Connect failed?\n"; - - $socket->close(); -} # mode ncsvc loop +sub reconnect_vpn{ + my $reconnow = int time / 60; + $recontry = 0 if (!defined($recontry) or $reconnow - $reconstart gt $reconresettime); + $reconstart = int time / 60 if (!defined($reconstart) or $reconnow - $reconstart gt $reconresettime); + + if ($recontry =~ /^\d+$/ and $recontry ge $recontc and + $reconstart =~ /^\d+$/ and $reconnow-$reconstart ge $recontt) + { + print "Too many reconnection attempts within timeout period. Exiting."; + exit 1; + } + print "Reconnecting.\n"; + $recontry++; + print "Reconnection attempt #$recontry\n"; + my $recontimesofar = int $reconnow - $reconstart; + print "Reconnection timeout counter: $recontimesofar of $recontt minutes elapsed\n"; + sleep 5; + connect_vpn(); +} # for debugging sub hdump { - my $offset = 0; - my(@array,$format); - foreach my $data (unpack("a16"x(length($_[0])/16)."a*",$_[0])) { - my($len)=length($data); - if ($len == 16) { - @array = unpack('N4', $data); - $format="0x%08x (%05d) %08x %08x %08x %08x %s\n"; - } else { - @array = unpack('C*', $data); - $_ = sprintf "%2.2x", $_ for @array; - push(@array, ' ') while $len++ < 16; - $format="0x%08x (%05d)" . - " %s%s%s%s %s%s%s%s %s%s%s%s %s%s%s%s %s\n"; - - } - $data =~ tr/\0-\37\177-\377/./; - printf $format,$offset,$offset,@array,$data; - $offset += 16; - } + my $offset = 0; + my(@array,$format); + foreach my $data (unpack("a16"x(length($_[0])/16)."a*",$_[0])) { + my($len)=length($data); + if ($len == 16) { + @array = unpack('N4', $data); + $format="0x%08x (%05d) %08x %08x %08x %08x %s\n"; + } else { + @array = unpack('C*', $data); + $_ = sprintf "%2.2x", $_ for @array; + push(@array, ' ') while $len++ < 16; + $format="0x%08x (%05d)" . + " %s%s%s%s %s%s%s%s %s%s%s%s %s%s%s%s %s\n"; + + } + $data =~ tr/\0-\37\177-\377/./; + printf $format,$offset,$offset,@array,$data; + $offset += 16; + } } -# handle ctrl+c to logout and kill ncsvc +# handle ctrl+c to logout and kill ncsvc sub INT_handler { - # de-register handlers - $SIG{'INT'} = 'DEFAULT'; - $SIG{'TERM'} = 'DEFAULT'; - $SIG{'HUP'} = 'DEFAULT'; - # re-enabling cursor - print "\e[?25h"; - if($> == 0 && $dnsprotect) { - system("chattr -i /etc/resolv.conf"); - } - if($mode eq "ncsvc" && $socket->connected()){ - print "\nSending disconnect packet\n"; - # disconnect packet - $data="\0\0\0\0\0\0\0\x67\x01\0\0\0\x01\0\0\0\0\0\0\0"; - hdump($data) if $debug; - print $socket "$data"; - $socket->recv($data,2048); - print "Got reply\n"; - # xxx - we are ignoring reply - hdump($data) if $debug; - } - print "Logging out...\n"; - # do logout - $ua -> get ("https://$dhost:$dport/dana-na/auth/logout.cgi"); - print "Killing ncsvc...\n"; - # it is suid, so best is to use own api - system("./ncsvc -K"); - - # checking if resolv.conf correctly restored - if(-f "/etc/jnpr-nc-resolv.conf"){ - print "restoring resolv.conf\n"; - move("/etc/jnpr-nc-resolv.conf","/etc/resolv.conf"); - } - # hostchecker cleanup - if($hostchecker) { - print "Killing tncc.jar...\n"; - kill 'KILL', $tncc_pid if $tncc_pid; - unlink $narport_file if -e $narport_file; - } - if(defined $script && -x $script){ - print "Running user-defined script\n"; - $ENV{'EVENT'}="down"; - $ENV{'MODE'}=$mode; - system($script); - } - print "Exiting\n"; - exit(0); + # de-register handlers + $SIG{'INT'} = 'DEFAULT'; + $SIG{'TERM'} = 'DEFAULT'; + $SIG{'HUP'} = 'DEFAULT'; + # re-enabling cursor + print "\e[?25h"; + if($> == 0 && $dnsprotect) { + system("chattr -i /etc/resolv.conf"); + } + if($mode eq "ncsvc" && $socket->connected()){ + print "\nSending disconnect packet\n"; + # disconnect packet + $data="\0\0\0\0\0\0\0\x67\x01\0\0\0\x01\0\0\0\0\0\0\0"; + hdump($data) if $debug; + print $socket "$data"; + $socket->recv($data,2048); + print "Got reply\n"; + # xxx - we are ignoring reply + hdump($data) if $debug; + } + print "Logging out...\n"; + # do logout + $ua -> get ("https://$dhost:$dport/dana-na/auth/logout.cgi"); + print "Killing ncsvc...\n"; + # it is suid, so best is to use own api + system("./ncsvc -K"); + + # checking if resolv.conf correctly restored + if(-f "/etc/jnpr-nc-resolv.conf"){ + print "restoring resolv.conf\n"; + move("/etc/jnpr-nc-resolv.conf","/etc/resolv.conf"); + } + # hostchecker cleanup + if($hostchecker) { + print "Killing tncc.jar...\n"; + kill 'KILL', $tncc_pid if $tncc_pid; + unlink $narport_file if -e $narport_file; + } + if(defined $script && -x $script){ + print "Running user-defined script\n"; + $ENV{'EVENT'}="down"; + $ENV{'MODE'}=$mode; + system($script); + } + print "Disconnected at ". POSIX::strftime("%c", localtime) .".\n"; + print "Exiting\n"; + exit(0); } sub parse_config_file { - my $Name,my $Value; my $Config; my $File; - - ($File, $Config) = @_; - if (!open (CONFIG, "$File")) { - print "ERROR: Config file not found : $File\n"; - exit(1); - } - while () { - my $config_line=$_; - chomp ($config_line); # Get rid of the trailling \n - $config_line =~ s/^\s*//; # Remove spaces at the start of the line - $config_line =~ s/\s*$//; # Remove spaces at the end of the line - if ( ($config_line !~ /^#/) && ($config_line ne "") ){ # Ignore lines starting with # and blank lines - ($Name, $Value) = split (/=/, $config_line); # Split each line into name value pairs - $$Config{$Name} = $Value; # Create a hash of the name value pairs - } - } - close(CONFIG); + my $Name,my $Value; my $Config; my $File; + + ($File, $Config) = @_; + if (!open (CONFIG, "$File")) { + print "ERROR: Config file not found : $File\n"; + exit(1); + } + while () { + my $config_line=$_; + chomp ($config_line); # Get rid of the trailling \n + $config_line =~ s/^\s*//; # Remove spaces at the start of the line + $config_line =~ s/\s*$//; # Remove spaces at the end of the line + if ( ($config_line !~ /^#/) && ($config_line ne "") ){ # Ignore lines starting with # and blank lines + ($Name, $Value) = split (/=/, $config_line); # Split each line into name value pairs + $$Config{$Name} = $Value; # Create a hash of the name value pairs + } + } + close(CONFIG); } sub run_pw_helper { - my $pw_script=""; - ($pw_script) = @_; - if (-x $pw_script){ - $password=`$pw_script`; - chomp $password - } - return $password; + my $pw_script=""; + ($pw_script) = @_; + if (-x $pw_script){ + $password=`$pw_script`; + chomp $password + } + return $password; } sub tncc_start { - my $body=""; - ($body) = @_; - my @lines = split "\n", $body; - my %params = (); - # read applet params from the page - foreach my $line (@lines) { - if ( $line =~ /NAME="([^"]+)"\s+VALUE="([^"]+)"/){ - $params{ $1 } = $2; - } - } - # enable tncc debug log - if($debug && defined($params{'Parameter0'})){ - $params{'Parameter0'} =~ s/logging=0/logging=1/; - } - # FIXME add some param validation - # create directory for logs if not exists - mkpath($supportdir."/network_connect") if !-e $supportdir."/network_connect"; - # just in case. Should we also kill all tncc.jar processes? - unlink $narport_file; - # users reported at least 2 different class names. - # It is not possible to fetch it from web, because it is hardcoded in hclauncer applet - my @jclasses = ("net.juniper.tnc.NARPlatform.linux.LinuxHttpNAR","net.juniper.tnc.HttpNAR.HttpNAR"); - my $jclass; my $found = ''; - foreach $jclass (@jclasses) { - my $chkpath = $jclass; - $chkpath =~ s/\./\//g; - $chkpath.=".class"; - system("unzip -t ./tncc.jar $chkpath >/dev/null 2>&1"); - $found = $jclass if $? == 0; - last if $? == 0; - } - if($found eq ""){ - print "Unable to find correct start class in the tncc.jar, please report problem to developer\n"; - exit 1; - } - my $pid = fork(); - if ($pid == 0) { - my @cmd = ("java"); - push @cmd, "-classpath", "./tncc.jar"; - push @cmd, $found; # class name, could be different - if($debug) { - push @cmd, "log_level", 10; - } - else { - push @cmd, "log_level", defined($params{'log_level'})?$params{'log_level'}:2; - } - push @cmd, "postRetries", defined($params{'postRetries'})?$params{'postRetries'}:6; - push @cmd, "ivehost", defined($params{'ivehost'})?$params{'ivehost'}:$dhost; - push @cmd, "Parameter0", defined($params{'Parameter0'})?$params{'Parameter0'}:""; - push @cmd, "locale", defined($params{'locale'})?$params{'locale'}:"en"; - push @cmd, "home_dir", $ENV{'HOME'}; - push @cmd, "user_agent", defined($params{'HTTP_USER_AGENT'})?$params{'HTTP_USER_AGENT'}:""; - exec(@cmd); - exit; # should never be reached - } - # wait up to 10 seconds for narport.txt - for(my $i = 0; $i < 10; $i++) { - last if(-e $narport_file); - sleep 1; - } - die("Unable to start tncc.jar process") if !-e $narport_file; - return $pid; + my $body=""; + ($body) = @_; + my @lines = split "\n", $body; + my %params = (); + # read applet params from the page + foreach my $line (@lines) { + if ( $line =~ /NAME="([^"]+)"\s+VALUE="([^"]+)"/){ + $params{ $1 } = $2; + } + } + # enable tncc debug log + if($debug && defined($params{'Parameter0'})){ + $params{'Parameter0'} =~ s/logging=0/logging=1/; + } + # FIXME add some param validation + # create directory for logs if not exists + mkpath($supportdir."/network_connect") if !-e $supportdir."/network_connect"; + # just in case. Should we also kill all tncc.jar processes? + unlink $narport_file; + # users reported at least 2 different class names. + # It is not possible to fetch it from web, because it is hardcoded in hclauncer applet + my @jclasses = ("net.juniper.tnc.NARPlatform.linux.LinuxHttpNAR","net.juniper.tnc.HttpNAR.HttpNAR"); + my $jclass; my $found = ''; + foreach $jclass (@jclasses) { + my $chkpath = $jclass; + $chkpath =~ s/\./\//g; + $chkpath.=".class"; + system("unzip -t ./tncc.jar $chkpath >/dev/null 2>&1"); + $found = $jclass if $? == 0; + last if $? == 0; + } + if($found eq ""){ + print "Unable to find correct start class in the tncc.jar, please report problem to developer\n"; + exit 1; + } + my $pid = fork(); + if ($pid == 0) { + my @cmd = ("java"); + push @cmd, "-classpath", "./tncc.jar"; + push @cmd, $found; # class name, could be different + if($debug) { + push @cmd, "log_level", 10; + } + else { + push @cmd, "log_level", defined($params{'log_level'})?$params{'log_level'}:2; + } + push @cmd, "postRetries", defined($params{'postRetries'})?$params{'postRetries'}:6; + push @cmd, "ivehost", defined($params{'ivehost'})?$params{'ivehost'}:$dhost; + push @cmd, "Parameter0", defined($params{'Parameter0'})?$params{'Parameter0'}:""; + push @cmd, "locale", defined($params{'locale'})?$params{'locale'}:"en"; + push @cmd, "home_dir", $ENV{'HOME'}; + push @cmd, "user_agent", defined($params{'HTTP_USER_AGENT'})?$params{'HTTP_USER_AGENT'}:""; + exec(@cmd); + exit; # should never be reached + } + # wait up to 10 seconds for narport.txt + for(my $i = 0; $i < 10; $i++) { + last if(-e $narport_file); + sleep 1; + } + die("Unable to start tncc.jar process") if !-e $narport_file; + return $pid; } sub retry_port { - my $port = shift; - - my $retry = 10; - while ( $retry-- ) { - my $socket = IO::Socket::INET->new( - Proto => 'tcp', - PeerAddr => '127.0.0.1', - PeerPort => $port, - ); - return $socket if $socket; - sleep 1; - } - die "Error connecting to 127.0.0.1:$port : $!"; + my $port = shift; + + my $retry = 10; + while ( $retry-- ) { + my $socket = IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1', + PeerPort => $port, + ); + return $socket if $socket; + sleep 1; + } + die "Error connecting to 127.0.0.1:$port : $!"; } sub read_input { - my $param = shift; - my $is_passwd = 0; - my $input = ""; - my $pkey=""; - # Print '*' instead of the real characters when "password" is provided as argument - if (defined $param && $param eq "password") { - $is_passwd = 1; - } - # Start reading the keys - ReadMode(4); # Disable the control keys - while(ord($pkey = ReadKey(0)) != 10) - # This will continue until the Enter key is pressed (decimal value of 10) - { - # For all value of ord($key) see http://www.asciitable.com/ - if(ord($pkey) == 127 || ord($pkey) == 8) { - # DEL/Backspace was pressed - # 1. Remove the last char from the password - # 2. move the cursor back by one, print a blank character, move the cursor back by one - if (length($input)) { - print "\b \b"; - } - chop($input); - } elsif(ord($pkey) < 32) { - # Do nothing with these control characters - } else { - $input = $input.$pkey; - if ($is_passwd == 1) { - print "*"; - } else { - print $pkey; - } - } - } - ReadMode(0); # Reset the terminal once we are done - return $input; + my $param = shift; + my $is_passwd = 0; + my $input = ""; + my $pkey=""; + # Print '*' instead of the real characters when "password" is provided as argument + if (defined $param && $param eq "password") { + $is_passwd = 1; + } + # Start reading the keys + ReadMode(4); # Disable the control keys + while(ord($pkey = ReadKey(0)) != 10) + # This will continue until the Enter key is pressed (decimal value of 10) + { + # For all value of ord($key) see http://www.asciitable.com/ + if(ord($pkey) == 127 || ord($pkey) == 8) { + # DEL/Backspace was pressed + # 1. Remove the last char from the password + # 2. move the cursor back by one, print a blank character, move the cursor back by one + if (length($input)) { + print "\b \b"; + } + chop($input); + } elsif(ord($pkey) < 32) { + # Do nothing with these control characters + } else { + $input = $input.$pkey; + if ($is_passwd == 1) { + print "*"; + } else { + print $pkey; + } + } + } + ReadMode(0); # Reset the terminal once we are done + return $input; } sub print_help { - print "Usage: $0 [--config ] [-h]\n". - "\t-c, --config configuration file, default jvpn.ini\n". - "\t-h, --help print this text\n". - "Report jvpn bugs to samm\@os2.kiev.ua\n"; - exit 0; + print "Usage: $0 [--config ] [-h]\n". + "\t-c, --config configuration file, default jvpn.ini\n". + "\t-h, --help print this text\n". + "Report jvpn bugs to samm\@os2.kiev.ua\n"; + exit 0; } # i don`t want CPAN hell sub format_bytes { - my ($size) = @_; - - if ($size > 1099511627776) # TiB: 1024 GiB - { - return sprintf("%.2f TiB", $size / 1099511627776); - } - elsif ($size > 1073741824) # GiB: 1024 MiB - { - return sprintf("%.2f GiB", $size / 1073741824); - } - elsif ($size > 1048576) # MiB: 1024 KiB - { - return sprintf("%.2f MiB", $size / 1048576); - } - elsif ($size > 1024) # KiB: 1024 B - { - return sprintf("%.2f KiB", $size / 1024); - } - else # B - { - return sprintf("%.2f B", $size); - } + my ($size) = @_; + + if ($size > 1099511627776) # TiB: 1024 GiB + { + return sprintf("%.2f TiB", $size / 1099511627776); + } + elsif ($size > 1073741824) # GiB: 1024 MiB + { + return sprintf("%.2f GiB", $size / 1073741824); + } + elsif ($size > 1048576) # MiB: 1024 KiB + { + return sprintf("%.2f MiB", $size / 1048576); + } + elsif ($size > 1024) # KiB: 1024 B + { + return sprintf("%.2f KiB", $size / 1024); + } + else # B + { + return sprintf("%.2f B", $size); + } } sub get_tap_interfaces { - my @intlist; - open FILE, "/proc/net/dev" or die $!; - while (my $line = ){ - if($line =~ /^\s*(tun[0-9]+):/) { - push(@intlist, $1); - } - } - return @intlist; + my @intlist; + open FILE, "/proc/net/dev" or die $!; + while (my $line = ){ + if($line =~ /^\s*(tun[0-9]+):/) { + push(@intlist, $1); + } + } + return @intlist; } sub get_new_tap_interface { - my (@newints, $i); - my ($oldint, $timeout) = @_; - for($i = 0; $i < $timeout; $i++) { - @newints = get_tap_interfaces(); - foreach my $tunint (@newints) { - if ( !grep { $_ eq $tunint} @$oldint ) { - return $tunint; - } - } - sleep(1); - } - return ''; + my (@newints, $i); + my ($oldint, $timeout) = @_; + for($i = 0; $i < $timeout; $i++) { + @newints = get_tap_interfaces(); + foreach my $tunint (@newints) { + if ( !grep { $_ eq $tunint} @$oldint ) { + return $tunint; + } + } + sleep(1); + } + return ''; } From 80b572a7a97a77449f669c9ea063339bd12bf3d8 Mon Sep 17 00:00:00 2001 From: jv Date: Thu, 26 Mar 2026 09:38:31 -0700 Subject: [PATCH 3/3] Fix bugs: SSL import, mkdir syntax, cont_button extraction, duplicate print - Add missing 'use IO::Socket::SSL qw()' import and use the SSL_VERIFY_NONE constant instead of magic string '0x00' (aligns with upstream PR #33) - Replace shell 'mkdir -p' syntax with Perl mkpath() in Duo push/key paths (2 occurrences) - Extract cont_button from response_body instead of matching against empty string (regex always failed, leaving cont_button empty in subsequent POST requests) - Remove duplicate debug-gated print identical to the unconditional print on the next line --- jvpn.pl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jvpn.pl b/jvpn.pl index 6bba317..ef3af92 100755 --- a/jvpn.pl +++ b/jvpn.pl @@ -22,6 +22,7 @@ use HTTP::Cookies; use HTTP::Request::Common; use IO::Socket::INET; +use IO::Socket::SSL qw(); use LWP::UserAgent; use Term::ReadKey; use POSIX; @@ -151,10 +152,9 @@ my $ua = LWP::UserAgent->new; # on RHEL6+ ssl_opts does exist if(defined &LWP::UserAgent::ssl_opts) { - $ua->ssl_opts('SSL_verify_mode' => IO::Socket::SSL::SSL_VERIFY_NONE); $ua->ssl_opts('verify_hostname' => $verifycert); if (!$verifycert) { - $ua->ssl_opts('SSL_verify_mode' => '0x00'); + $ua->ssl_opts('SSL_verify_mode' => IO::Socket::SSL::SSL_VERIFY_NONE); } } $ua->cookie_jar({}); @@ -216,11 +216,11 @@ sub connect_vpn { my $password2 = ''; if ($duo eq "push") { $password2 = "push"; - (! -d $pulse_nc_dir) && mkdir -p $pulse_nc_dir; + mkpath($pulse_nc_dir) if (! -d $pulse_nc_dir); } elsif ($duo eq "key") { print "Enter Duo key: "; $password2 = read_input(); - (! -d $pulse_nc_dir) && mkdir -p $pulse_nc_dir; + mkpath($pulse_nc_dir) if (! -d $pulse_nc_dir); } my $welcome_cgi="https://$dhost/dana-na/auth/$durl/welcome.cgi"; @@ -373,9 +373,9 @@ sub connect_vpn { #print "response_body: $response_body\n\n"; my ($session_id, $ip, $login_time, $idle_time) = ($response_body =~ m/name="postfixSID"\s+value="([^"]+)"\/><\/td>\s+([0-9.]+)<\/td>\s+([^<]+)<\/td>\s+([^<]+)<\/td>\s+[^<]*$kick_string[^<]*<\/td>/); if ($session_id) { - ($debug) && print "Session_id being killed: $session_id from IP $ip\n"; print "Session_id being killed: $session_id from IP $ip\n"; - $cont_button =~ m/name="btnContinue" value="([^"]+)"/; + ($cont_button) = ($response_body =~ m/name="btnContinue" value="([^"]+)"/); + $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", [ btnContinue => $cont_button, @@ -403,7 +403,7 @@ sub connect_vpn { } } } else { - $cont_button =~ m/name="btnContinue" value="([^"]+)"/; + ($cont_button) = ($response_body =~ m/name="btnContinue" value="([^"]+)"/); print "Active sessions found, continuing anyway...\n"; $res = $ua->post("https://$dhost:$dport/dana-na/auth/$durl/login.cgi", [