diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8dcd398a3a5e7e4fc7b3b36bc392c07ed4ee1040..e5acb6570f10061d56c305304f26e6f1d0b20dd6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,7 @@ code-quality: - apt-get update - apt-get dist-upgrade -y - apt-get install -y libperl-critic-perl make - - perlcritic --gentle "$CI_PROJECT_DIR" + - perlcritic --stern "$CI_PROJECT_DIR" make-test: stage: tests diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000000000000000000000000000000000000..c4c963947f99ac5be5120d91dc51e36966c61cb0 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,6 @@ +awit-ssh +LICENSE +Makefile.PL +MANIFEST This list of files +README.md +TODO diff --git a/Makefile.PL b/Makefile.PL index c22d1862fe96d66a00210e08a9d65b084b326f80..90482695f9930cda9d5f74c26ca3d26d45dc030c 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -21,7 +21,7 @@ use warnings; use ExtUtils::MakeMaker; WriteMakefile( - NAME => 'AWIT-SSH-Client', + NAME => 'AWITSSHClient', VERSION_FROM => "awit-ssh", EXE_FILES => [qw( awit-ssh )], diff --git a/awit-ssh b/awit-ssh index 43eb39196d3717f5209c3f54af7af7097b0be732..4cd22d80a0e55f62f89e90f5a307e2b08e7cdd45 100755 --- a/awit-ssh +++ b/awit-ssh @@ -44,12 +44,13 @@ if (!eval {require IO::Prompt; 1;}) { exit 1; } +use Digest::SHA qw( sha1_hex ); use IO::Prompt qw( prompt ); use User::pwent; my $NAME = "AWIT-SSH-Client"; -our $VERSION = "0.4.0"; +our $VERSION = "0.5.0"; print(STDERR "$NAME v$VERSION - Copyright (c) 2016, AllWorldIT\n\n"); @@ -91,8 +92,6 @@ if (defined(my $knock = $optctl{'knock'})) { } - - # Variables we may set below my $loginUsername; @@ -110,7 +109,11 @@ if (defined($loginHost)) { logger('ERROR',color('magenta')."No hostname provided".color('reset')); exit 1; } +# Make sure we save the hostname +my $realLoginHost = $loginHost; +# Port forwarding/bouncing +my ($forwardHost,$forwardPort,$forwardUsername); my %iniSetup; @@ -122,11 +125,9 @@ if (! -f $configFile) { tie %iniSetup, 'Config::IniFiles'; $iniSetup{server} = {}; - print STDERR "Your LDAP URI : "; - chomp($iniSetup{server}{uri} = ); + $iniSetup{server}{uri} = prompt("Your LDAP URI : "); $iniSetup{server}{uri} =~ s/^uri=//; - print STDERR "Your LDAP Base : "; - chomp($iniSetup{server}{base} = ); + $iniSetup{server}{base} = prompt("Your LDAP Base : "); $iniSetup{server}{base} =~ s/^base=//; tied(%iniSetup)->WriteConfig($configFile) or die "Could not write settings to new configuration file."; untie %iniSetup; @@ -168,8 +169,7 @@ my $pwent = getpwnam($ENV{'USER'}); (my $username) = split(/,/,$pwent->gecos); if (!defined($username) || $username eq "") { print STDERR "WARNING: Cannot determine your name, set your gecos field.\n\n"; - print STDERR "Your LDAP CN : "; - $username = ; + $username = prompt("Your LDAP CN : "); } else { print STDERR "Your LDAP CN : $username (passwd->gecos)\n"; } @@ -276,6 +276,7 @@ if ($ldapNumResults < 1) { print STDERR "Exiting...\n"; exit 3; } + print STDERR "\n"; $menuSelection--; $ldapEntry = $ldapResults[$menuSelection]; } @@ -297,6 +298,20 @@ if ($ldapEntry) { $knockPort //= $ldapLoginKnockPort; } + # Check if we need to set the port forwarding info + if (my $ldapLoginForwardHost = $ldapEntry->get_value('awitLoginForwardHost')) { + logger('INFO'," - Forward host ".color('green')."%s".color('reset')." (awitLoginForwardHost)",$ldapLoginForwardHost); + $forwardHost //= $ldapLoginForwardHost; + } + if (my $ldapLoginForwardPort = $ldapEntry->get_value('awitLoginForwardPort')) { + logger('INFO'," - Forward port ".color('green')."%s".color('reset')." (awitLoginForwardPort)",$ldapLoginForwardPort); + $forwardPort //= $ldapLoginForwardPort; + } + if (my $ldapLoginForwardUsername = $ldapEntry->get_value('awitLoginForwardUsername')) { + logger('INFO'," - Forward user ".color('green')."%s".color('reset')." (awitLoginForwardUsername)",$ldapLoginForwardUsername); + $forwardUsername //= $ldapLoginForwardUsername; + } + # Check if we need to set the host $loginHost = $ldapEntryName; if (my $ldapLoginHost = $ldapEntry->get_value('awitLoginHost')) { @@ -312,7 +327,7 @@ if ($ldapEntry) { # Check if we need to set the username if (my $ldapLoginUsername = $ldapEntry->get_value('awitLoginUsername')) { - logger('INFO'," - Username ".color('green')."%s".color('reset')." (awitLoginUsername)",$ldapLoginUsername); + logger('INFO'," - User ".color('green')."%s".color('reset')." (awitLoginUsername)",$ldapLoginUsername); $loginUsername //= $ldapLoginUsername; } @@ -338,11 +353,18 @@ if ($ldapEntry) { } +# If we have kwalletObject and kwalletHandle defined, store the password as we've given awit-ssh-client permission to access +# kwallet +if (defined($kwalletObject) && defined($kwalletHandle)) { + $kwalletObject->writePassword($kwalletHandle,"ldap","password",$password,$NAME); +} + + # Check if we need to do port knocking if (defined($knockHost)) { # Make sure we have a port knocking port if (!defined($knockPort)) { - logger('ERROR',color('bold red')."No port knocking port defined!".color('reset')), + logger('ERROR',color('bold red')."No port knocking port defined!".color('reset')); exit 1; } @@ -357,36 +379,22 @@ if (defined($knockHost)) { ); # We should get a failure of "Connection refused", if not ERR if (defined($sock) || $! ne "Connection refused") { - logger('ERROR',color('bold red')."Port knocking failed!".color('reset')), + logger('ERROR',color('bold red')."Port knocking failed!".color('reset')); exit 1; } - logger('INFO',"Port knocking success!"), + logger('INFO',"Port knocking success!"); print STDERR "\n"; } -# TODO for forwarding - - - my @sshArgs = (); if (defined($pkcsProvider) && $pkcsProvider ne "") { push(@sshArgs,'-I',$pkcsProvider); - logger('NOTICE',color('blue')."Enabling smartcard/token authentication.".color('reset')), + logger('NOTICE',color('blue')."Enabling smartcard/token authentication.".color('reset')); print STDERR "\n"; } -# Check if we have a port defined, if so specify it -if (defined($loginPort)) { - push(@sshArgs,'-p',$loginPort); -} - -# Check if we have a different username defined to login as -if (defined($loginUsername)) { - push(@sshArgs,'-l',$loginUsername); -} - # Only push the config file override to SSH if the config file exists in the users homedir\ if (-f (my $sshConfigFile = $ENV{"HOME"}.'/.ssh/config')) { push(@sshArgs,'-F',$sshConfigFile); @@ -400,44 +408,171 @@ if (defined($needDSS)) { push(@sshArgs,'-o','HostKeyAlgorithms=+ssh-dss'); } -logger('NOTICE',"Connecting to host '".color('green')."$loginHost".color('reset')."'" . - (defined($loginPort) ? " on port '".color('green')."$loginPort".color('reset')."'" : "") . "...\n\n\n"); +# Try our key only, we should never need to fall back to password +push(@sshArgs,'-o','PreferredAuthentications=publickey'); +push(@sshArgs,'-o','StrictHostKeyChecking=ask'); + +# Use TCP keepalive +push(@sshArgs,'-o','TCPKeepAlive=yes'); +push(@sshArgs,'-o','ServerAliveInterval=5'); +push(@sshArgs,'-o','ServerAliveCountMax=24'); # 120s + +# Timeout for our connect +push(@sshArgs,'-o','ConnectTimeout=30'); + +# Fail if we cannot forward ports +push(@sshArgs,'-o','ExitOnForwardFailure=yes'); -# Fixup environment -$ENV{'LANG'} = "en_US.UTF-8"; + + +# Fixup environment before we start to run SSH +local $ENV{'LANG'} = "en_US.UTF-8"; delete($ENV{'LC_ALL'}); delete($ENV{'LC_TIME'}); delete($ENV{'LC_CTYPE'}); -# If we have kwalletObject and kwalletHandle defined, store the password as we've given awit-ssh-client permission to access -# kwallet -if (defined($kwalletObject) && defined($kwalletHandle)) { - $kwalletObject->writePassword($kwalletHandle,"ldap","password",$password,$NAME); -} +# Setup TMPDIR, we prefer XDG_RUNTIME_DIR as its protected in /run/user/$UID/ +my $TMPDIR = $ENV{'XDG_RUNTIME_DIR'} // $ENV{'TMPDIR'} // '/tmp'; + + +# Setup our forward port name +our $forwardSocket; +# Forward child PID +our $forwardChild; + +# Check if we're forwarding, we need to work a few things out... +if (defined($forwardHost)) { + + logger('NOTICE',"Forwarding '".color('green').$realLoginHost.color('reset')."' via host '".color('green').$loginHost. + color('reset')."'" .(defined($loginPort) ? " on port '".color('green')."$loginPort".color('reset')."'" : "") . + "...\n\n\n"); + + # Default to port 22 if the login port is not defined + my $destPort = $forwardPort // 22; + # Add forward socket name + $forwardSocket = "$TMPDIR/awit-ssh-forward-".sha1_hex("$forwardHost:$destPort $$").".sock"; -exec('/usr/bin/ssh', - # Try our key only, we should never need to fall back to password - '-o','PreferredAuthentications=publickey', - '-o','StrictHostKeyChecking=ask', - # Use TCP keepalive - '-o','TCPKeepAlive=yes', - '-o','ServerAliveInterval=5', - '-o','ServerAliveCountMax=24', # 120s - # Timeout for our connect - '-o','ConnectTimeout=30', + # Build up our forwarding process args into this... + my @forwardArgs = (); + + # Add on port we're forwarding + push(@forwardArgs,'-L',"$forwardSocket:$forwardHost:$forwardPort"); + + # Check if we have a port defined, if so specify it + if (defined($loginPort)) { + push(@forwardArgs,'-p',$loginPort); + } + + # Check if we have a different username defined to login as + if (defined($loginUsername)) { + push(@forwardArgs,'-l',$loginUsername); + } + + # Explicitly disable control master for the main forwarding process + push(@forwardArgs,'-o','ControlMaster=no'); + + # Fork off child to establish the main connection + $forwardChild = fork(); + if (!$forwardChild) { + + # Exec ssh + if (!exec('/usr/bin/ssh', + @sshArgs, + @forwardArgs, + # Use basic compression + '-o','Compression=yes', + '-o','CompressionLevel=1', + # All we're doing here is forwarding the port... + '-N', + $loginHost + )) { + + logger('ERROR',color('magenta')."Forwarding SSH process failed to start".color('reset')); + + exit 1; + } + } +}; + + +# Install signal handlers to cleanup if we get a TERM or INT +local $SIG{TERM} = local $SIG{INT} = \&cleanup; + + +# Check if we're forwarding to a socket... +if (defined($forwardSocket)) { + + # Loop waiting for the socket to be created + my $delay = 30; + while (! -e $forwardSocket && $delay > 0) { + $delay--; + sleep 1; + } + + if ($delay) { + + # Check if we need to specify the username + push(@sshArgs,'-l',$forwardUsername) if (defined($forwardUsername)); + + logger('NOTICE',"Connecting to host '".color('green')."$forwardHost".color('reset')."'" . + (defined($forwardPort) ? " on port '".color('green')."$forwardPort".color('reset')."'" : "") . "...\n\n\n"); + + # Fire up ssh + system('/usr/bin/ssh', + @sshArgs, + # Override where we connecting to + '-o',"ProxyCommand=nc -U $forwardSocket", + # Explicitly disable control master + '-o','ControlMaster=no', + $realLoginHost + ); + + + # Unlink socket and unset it to designate we exited normally + unlink($forwardSocket); + undef($forwardSocket); + + } else { + logger('ERROR',color('magenta')."Socket not connected, aborting!".color('reset')); + } + + +# Normal SSH connection +} else { + + logger('NOTICE',"Connecting to host '".color('green')."$loginHost".color('reset')."'" . + (defined($loginPort) ? " on port '".color('green')."$loginPort".color('reset')."'" : "") . "...\n\n\n"); + + # Make sure we get asked for control master connections... + push(@sshArgs,'-o','ControlMaster=autoask'); + push(@sshArgs,'-o',"ControlPath=$TMPDIR/awit-ssh-master-%C"); + + # Check if we have a different username defined to login as + if (defined($loginUsername)) { + push(@sshArgs,'-l',$loginUsername); + } + + # Check if we have a port defined, if so specify it + if (defined($loginPort)) { + push(@sshArgs,'-p',$loginPort); + } + + system('/usr/bin/ssh', + @sshArgs, # Use basic compression '-o','Compression=yes', '-o','CompressionLevel=1', - # Fail if we cannot forward ports - '-o','ExitOnForwardFailure=yes', - '-o','ControlMaster=autoask', - '-o','ControlPath=~/.ssh/awit-ssh-master-%C', - @sshArgs, $loginHost -); + ); + +} + + +cleanup(); + exit 0; @@ -449,6 +584,28 @@ exit 0; +# Cleanup function +sub cleanup +{ + # Kill the child + if ($forwardChild && kill(-1,$forwardChild)) { + kill('TERM',$forwardChild); + # Wait for it to die + waitpid($forwardChild,-1); + } + + # Unlink the socket + if ($forwardSocket) { + unlink($forwardSocket); + print STDERR "\nExiting...\n"; + exit 1; + } + + exit 0; +} + + + # Log something sub logger {