Skip to content
Snippets Groups Projects
awit-ssh 27.7 KiB
Newer Older
Nigel Kukard's avatar
Nigel Kukard committed
#!/usr/bin/perl
# awit-ssh - SSH initiator which searches LDAP for host details
# Copyright (c) 2016-2019, AllWorldIT
Nigel Kukard's avatar
Nigel Kukard committed
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

Nigel Kukard's avatar
Nigel Kukard committed


=encoding utf8

=head1 NAME

awit-ssh - LDAP lookup utility for SSH hosts.

=head1 SYNOPSIS

	awit-ssh [--libvirt-vnc HOST:PORT] [--knock HOST:PORT] HOST[:PORT]
	awit-ssh --rsync -- [USER@]HOST:path DEST
Nigel Kukard's avatar
Nigel Kukard committed

=cut

=head1 DESCRIPTION

awit-ssh perl script that automates connecting to a server via ssh by looking up the user and port information from a LDAP
database.

=cut



Nigel Kukard's avatar
Nigel Kukard committed
use strict;
use warnings;

Nigel Kukard's avatar
Nigel Kukard committed

use Term::ANSIColor;
Nigel Kukard's avatar
Nigel Kukard committed
use Getopt::Long;
Robert Spencer's avatar
Robert Spencer committed
use Net::DBus qw(:typing);
# Check Config::IniFiles
if (!eval {require Config::IniFiles; 1;}) {
	print STDERR "You're missing Config::IniFiles, try 'apt-get install libconfig-inifiles-perl'\n";
	exit 1;
}
Nigel Kukard's avatar
Nigel Kukard committed
# Check IO::Socket::INET6
if (!eval {require IO::Socket::INET6; 1;}) {
	print STDERR "You're missing IO::Socket::INET6, try 'apt-get install libio-socket-inet6-perl'\n";
	exit 1;
}
# Check Net::LDAP
if (!eval {require Net::LDAP; 1;}) {
	print STDERR "You're missing Net::LDAP, try 'apt-get install libnet-ldap-perl'\n";
	exit 1;
}
# Check IO::Prompt
if (!eval {require IO::Prompt; 1;}) {
	print STDERR "You're missing IO::Prompt, try 'apt-get install libio-prompt-perl'\n";
Nigel Kukard's avatar
Nigel Kukard committed
	exit 1;
}
## no critic (BuiltinFunctions::ProhibitStringyEval)
eval qq(
	use IO::Prompt qw(prompt);
);
## use critic
use Digest::SHA qw( sha1_hex );
use User::pwent;
Nigel Kukard's avatar
Nigel Kukard committed


Robert Spencer's avatar
Robert Spencer committed
my $NAME = "AWIT-SSH-Client";
our $VERSION = "0.8.11";
Nigel Kukard's avatar
Nigel Kukard committed

print(STDERR "$NAME v$VERSION - Copyright (c) 2016-2019, AllWorldIT\n\n");
Nigel Kukard's avatar
Nigel Kukard committed


Nigel Kukard's avatar
Nigel Kukard committed
=head1 OPTIONS

C<awit-ssh> provides the below commandline options...

=head2 --help|?

	Display this help information.

=head2 --version

	Display version information.

Nigel Kukard's avatar
Nigel Kukard committed
=head2 --debug

	Enable debug output.

=head2 --forward-agent

	Forward the ssh-agent socket.

Nigel Kukard's avatar
Nigel Kukard committed
=head2 --knock <HOST:PORT>

	Knock on HOST:PORT to gain access.

=head2 --rsync <[USER@]HOST:/path> <DEST>
Nigel Kukard's avatar
Nigel Kukard committed

	Use rsync to rsync data from remote server to DEST. This can be specified either way around.

=head2 --libvirt-vnc <HOST:PORT>

	Connect to remote VNC server HOST:PORT.

=cut
Nigel Kukard's avatar
Nigel Kukard committed

# Grab options
my %optctl = ();
GetOptions(\%optctl,
	"help|?",
	"version",

	"debug",

	# TODO: Improve globbing before adding it to displayHelp
	"globbing",

	"forward-agent",

Nigel Kukard's avatar
Nigel Kukard committed
	"knock=s",

	"rsync",

	"libvirt-vnc=s",
Nigel Kukard's avatar
Nigel Kukard committed
) or exit 1;

# Check for help
if (defined($optctl{'help'})) {
	displayHelp();
	exit 0;
}

# Check for version
if (defined($optctl{'version'})) {
	displayVersion();
	exit 0;
}

Nigel Kukard's avatar
Nigel Kukard committed
# Check if we using rsync instead of SSH
my $useRsync;
Nigel Kukard's avatar
Nigel Kukard committed
my @rsyncParams;
if (defined(my $rsyncHost = $optctl{'rsync'})) {
	$useRsync = $rsyncHost;
}

# Check if we using libvirt vnc instead of SSH
my $libvirtVNC;
if (defined(my $vmName = $optctl{'libvirt-vnc'})) {
	if (! -x '/usr/bin/ssvncviewer') {
		logger('ERROR',color('magenta')."To use --libvirt-vnc you need to install ssvncviewer. Hint: apt-get install ssvnc".
				color('reset'));
		exit 1;
	}
	$libvirtVNC = $vmName;
}

# Check if we should be doing port knocking
my ($knockHost,$knockPort);
if (defined(my $knock = $optctl{'knock'})) {
	# If so, split off the host and the port
	($knockHost,$knockPort) = split(':',$knock);
	if (!defined($knockPort)) {
		logger('ERROR',color('magenta')."Port knock specifications should be in the format of HOST:PORT".color('reset'));
		exit 1;
	}
}


# Check for option combinations
if (defined($useRsync) && defined($libvirtVNC)) {
	logger('ERROR',color('magenta')."Options --rsync and --libvirt-monitor cannot be used together".color('reset'));
	exit 1;
}


# Variables we may set below
my $loginUsername;

Nigel Kukard's avatar
Nigel Kukard committed
my $hostSpec;
if (defined($useRsync)) {
Nigel Kukard's avatar
Nigel Kukard committed
	foreach my $param (@ARGV) {
		# Look for the : param
		if ($param =~ /:/) {
Nigel Kukard's avatar
Nigel Kukard committed
			# Assing hostSpec to the first part of the tag
			($hostSpec) = split(/:/,$param);
			push(@rsyncParams,$param);
Nigel Kukard's avatar
Nigel Kukard committed
		# Else just add it
		} else {
			push(@rsyncParams,$param);
		}
	}
	# Make sure we got a hostSpec
	if (!defined($hostSpec)) {
		logger('ERROR',color('magenta')."awit-ssh --rsync needs a <HOST:/path> parameter to be specified on the command line".
Nigel Kukard's avatar
Nigel Kukard committed
				color('reset'));
		exit 1;
	}

} else {
	$hostSpec = shift(@ARGV) // "";
}
my ($loginHost,$loginPort) = split(':',$hostSpec);
if (defined($loginHost)) {
	# Suck in username if specified
Nigel Kukard's avatar
Nigel Kukard committed
	my ($userBit,$hostBit) = split('@',$loginHost);
	if (defined($hostBit)) {
		$loginUsername = $userBit;
		$loginHost = $hostBit;
	}
} else {
	logger('ERROR',color('magenta')."No hostname provided".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed
# Make sure we save the hostname
my $realLoginHost = $loginHost;
# Port forwarding/bouncing
my ($forwardHost,$forwardPort,$forwardUsername,@forwardPortExtra);
Nigel Kukard's avatar
Nigel Kukard committed
=head1 CONFIG FILE

The following options are read from C<~/.config/awit-ssh.conf> (or C<~/.awit-ssh.conf> for legacy installations), each set
of options is organized in an inifile [section].
my $configFile;

# Legacy configuration file location
if (-f $ENV{"HOME"}.'/.awit-ssh.conf') {
	$configFile = $ENV{"HOME"}.'/.awit-ssh.conf';
}

# XDG Base Directory configuration file location
if (-f $ENV{"HOME"}.'/.config/awit-ssh.conf') {
	$configFile = $ENV{"HOME"}.'/.config/awit-ssh.conf';
}

# Check for config and read
if (!defined($configFile)) {
	$configFile = $ENV{"HOME"}.'/.config/awit-ssh.conf';

	print STDERR "No configuration file found. Please answer the questions below to generate it.\n\n";

	tie %iniSetup, 'Config::IniFiles';
Nigel Kukard's avatar
Nigel Kukard committed

=head2 [server]

LDAP server options.

=head3 B<uri>=LDAP_URI

Set the LDAP server URI, for example C<ldaps://example.com>.

=head3 B<base>=BASE_DN

Set the LDAP server base DN to use, for example C<dc=example,dc=com>.

=cut

	$iniSetup{server} = {};
	$iniSetup{server}{uri} = prompt("Your LDAP URI     : ", '-tty');
	$iniSetup{server}{uri} =~ s/^uri=//;
	$iniSetup{server}{base} = prompt("Your LDAP Base    : ", '-tty');
	$iniSetup{server}{base} =~ s/^base=//;
	tied(%iniSetup)->WriteConfig($configFile) or die "Could not write settings to new configuration file.";
	untie %iniSetup;
} else {
	# Flag, so we don't echo the same output lower down immediately after a setup.
	$iniSetup{server} = 'installed';
}
my $config = Config::IniFiles->new(-file => $configFile);

my $ldapURI = $config->val("server","uri");
if (!defined($ldapURI) || $ldapURI eq "") {
	logger('ERROR',color('magenta')."Server URI not defined in config file".color('reset'));
	exit 1;
}

my $ldapBase = $config->val("server","base");
if (!defined($ldapBase) || $ldapBase eq "") {
	logger('ERROR',color('magenta')."Server base DN not defined in config file".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed

=head2 [pkcs11]

Options for integration into PKCS11, this allows you to login to servers users a smartcard.

=head3 B<provider>=PKCS11_PROVIDER

Libary path of the PKCS11 provider. For example I</usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so>.

=cut

my $pkcsProvider = $config->val("pkcs11","provider");
if (defined($pkcsProvider) && $pkcsProvider ne "") {
	if (! -f $pkcsProvider) {
		logger('ERROR',color('magenta')."PKCS11 provider '$pkcsProvider' does not exist".color('reset'));
		exit 1;
	}
}

if (%iniSetup) {
	print STDERR "LDAP server URI   : $ldapURI\n";
	print STDERR "LDAP server base  : $ldapBase\n";
}
# Try get name automatically
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";
	$username = prompt("Your LDAP CN      : ", '-tty');
} else {
	print STDERR "Your LDAP CN      : $username (passwd->gecos)\n";
Nigel Kukard's avatar
Nigel Kukard committed

# Sort out LDAP password

my $password;
# Lets try kwallet
my ($kwalletObject,$kwalletHandle);
# IF removed, lets rather run this in its own scope...
{
	my $dbus = Net::DBus->find();

	# Grab the kwallet service off DBus
	my $kwalletService;
	eval {
Nigel Kukard's avatar
Nigel Kukard committed
		$kwalletService = $dbus->get_service('org.kde.kwalletd5');
	if (!defined($kwalletService)) {
		logger('WARNING',color('magenta')."Kwallet not found on DBus".color('reset'));
		goto KWALLET_END;
	}
Nigel Kukard's avatar
Nigel Kukard committed
	$kwalletObject = $kwalletService->get_object('/modules/kwalletd5','org.kde.KWallet');
	# Grab a handle to the network wallet
	my $networkWalletName = $kwalletObject->networkWallet();
	$kwalletHandle = $kwalletObject->open($networkWalletName,0,$NAME);
	$password = $kwalletObject->readPassword($kwalletHandle,"ldap","password",$NAME);
# If kwallet returned nothing, try read from terminal
Robert Spencer's avatar
Robert Spencer committed
if (!defined($password) || $password eq "") {
	$password = prompt("Your LDAP Password: ", '-echo' => "*", '-tty');
Nigel Kukard's avatar
Nigel Kukard committed
print STDERR "\n";


my $ldap = Net::LDAP->new($ldapURI,
Nigel Kukard's avatar
Nigel Kukard committed
#	'debug' => 15
);
if (!defined($ldap)) {
	logger('ERROR',color('magenta')."Failed to setup LDAP object '%s'".color('reset'),$@);
Nigel Kukard's avatar
Nigel Kukard committed
	exit 2;
}

# Bind
my $mesg = $ldap->bind("cn=$username,ou=Users,$ldapBase",password => $password);
Nigel Kukard's avatar
Nigel Kukard committed

# Search
$mesg = $ldap->search(
	base => "ou=Servers,$ldapBase",
	filter => "(|(cn=$loginHost)(awitLoginHostAlias=$loginHost))",
Nigel Kukard's avatar
Nigel Kukard committed
);
# Check for error
if (my $mesgCode = $mesg->code()) {
	if ($mesgCode eq "No such object") {
		logger('ERROR',color('magenta')."LDAP returned '%s', this more than likely means a Username/Password error or your BaseDN is wrong.".color('reset'),$mesgCode);
		logger('ERROR',color('magenta')."LDAP returned '%s'".color('reset'),$mesgCode);
Nigel Kukard's avatar
Nigel Kukard committed
	exit 2;
}


Nigel Kukard's avatar
Nigel Kukard committed


# If no matches
my @ldapResults = $mesg->entries();
my $ldapNumResults = @ldapResults;
my $ldapEntry;
if ($ldapNumResults < 1) {
	logger('NOTICE',color('bold red')."No LDAP results, using the hostname provided on the commandline".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed

} elsif ($ldapNumResults == 1) {
	$ldapEntry = $ldapResults[0];

} elsif ($ldapNumResults > 1) {
	logger('WARNING',color('red')."Found multiple entries!".color('reset'));
	print STDERR "\n";
	foreach my $key (sort(keys %{$mesg->as_struct()})) {
		logger('MENU '.$counter,"  ".color('red')."%s".color('reset'),$key);
		$counter++;
Nigel Kukard's avatar
Nigel Kukard committed
	}
	my $menuSelection = prompt("Your selection [1-$ldapNumResults,q]: ",
		'-onechar',
		'-require' => {
			"Invalid Value - Your selection [1-$ldapNumResults,q]: " => sub {
				my $val = $_;
				return (
						# Check if is numeric and its within range
						$val =~ /^\d$/ &&
						$val > 0 &&
						$val <= $ldapNumResults
					) || (
						# Else our only other option we accept is 'q'
						$val eq "q"
					);
			}
		}
	);
	if ($menuSelection eq "q") {
Nigel Kukard's avatar
Nigel Kukard committed
		print STDERR "\nExiting...\n";
Nigel Kukard's avatar
Nigel Kukard committed
	print STDERR "\n";
	$menuSelection = $menuSelection->{'value'} - 1;
	$ldapEntry = $ldapResults[$menuSelection];
Nigel Kukard's avatar
Nigel Kukard committed
}

print STDERR "\n";

# Check if we got a result and modify our connection details
if ($ldapEntry) {
	my $ldapEntryName = $ldapEntry->get_value('cn');
	logger('INFO',"Found server entry '".color('green')."$ldapEntryName".color('reset')."'");
Nigel Kukard's avatar
Nigel Kukard committed

Nigel Kukard's avatar
Nigel Kukard committed
	# TODO: Ability to select between mulitple awitLoginHost's separated by ,'s

	# Check if we need to set the port knocking host & port
	if (my $ldapLoginKnockHost = $ldapEntry->get_value('awitLoginKnockHost')) {
		logger('INFO',"  - Knock host ".color('green')."%s".color('reset')." (awitLoginKnockHost)",$ldapLoginKnockHost);
		$knockHost //= $ldapLoginKnockHost;
	}
	if (my $ldapLoginKnockPort = $ldapEntry->get_value('awitLoginKnockPort')) {
		logger('INFO',"  - Knock port ".color('green')."%s".color('reset')." (awitLoginKnockPort)",$ldapLoginKnockPort);
		$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;
		$forwardPort = 22;
	}
	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;
	}

	if (my $ldapLoginForwardPortExtra = $ldapEntry->get_value('awitLoginForwardPortExtra', 'asref' => 1)) {
		my @tmpList = ();
		# Check if this is an array ref or not
		if (ref($ldapLoginForwardPortExtra) eq "ARRAY") {
			@tmpList = @{$ldapLoginForwardPortExtra};
		} else {
			@tmpList = ($ldapLoginForwardPortExtra);
		}
		# Output all the port forwards
		foreach my $item (@tmpList) {
			my ($localPort,$destHost,$destPort) = split(/:/,$item);
			# Check localPort
			if (!defined($localPort) || $localPort < 8000) {
				logger('WARNING',"  - Forward port extra ".color('red')."%s".color('reset').
						" (awitLoginForwardPortExtra) is INVALID, localPort check failed",$item);
				goto PFEC1;
			}
			# Check destHost
			if (!defined($destHost)) {
				logger('WARNING',"  - Forward port extra ".color('red')."%s".color('reset').
						" (awitLoginForwardPortExtra) is INVALID, destHost check failed",$item);
				goto PFEC1;
			}
			# Check destPort
			if (!defined($destPort) || $destPort < 1) {
				logger('WARNING',"  - Forward port extra ".color('red')."%s".color('reset').
						" (awitLoginForwardPortExtra) is INVALID, destPort check failed",$item);
				goto PFEC1;
			}
			# Add port forwarding to our list
			push(@forwardPortExtra,{'localPort' => $localPort, 'destHost' => $destHost, 'destPort' => $destPort});

			logger('INFO',"  - Forward port extra ".color('green')."%s".color('reset')." (awitLoginForwardPortExtra)",$item);
PFEC1:
		}
	}

Nigel Kukard's avatar
Nigel Kukard committed
	# Check if we need to set the host
	if (defined($optctl{'globbing'})) {
		$loginHost = $ldapEntryName;
	}
Nigel Kukard's avatar
Nigel Kukard committed
	if (my $ldapLoginHost = $ldapEntry->get_value('awitLoginHost')) {
		logger('INFO',"  - Host ".color('green')."%s".color('reset')." (awitLoginHost)",$ldapLoginHost);
		$loginHost = $ldapLoginHost;
Nigel Kukard's avatar
Nigel Kukard committed
	}

	# Check if we need to set the port
	if (my $ldapLoginPort = $ldapEntry->get_value('awitLoginPort')) {
		logger('INFO',"  - Port ".color('green')."%s".color('reset')." (awitLoginPort)",$ldapLoginPort);
		$loginPort = $ldapLoginPort;
Nigel Kukard's avatar
Nigel Kukard committed
	}

	# Check if we need to set the username
	if (my $ldapLoginUsername = $ldapEntry->get_value('awitLoginUsername')) {
Nigel Kukard's avatar
Nigel Kukard committed
		logger('INFO',"  - User ".color('green')."%s".color('reset')." (awitLoginUsername)",$ldapLoginUsername);
		$loginUsername //= $ldapLoginUsername;
Nigel Kukard's avatar
Nigel Kukard committed
	# Check if we have a description
	if (my $ldapDescription = $ldapEntry->get_value('description')) {
		logger('INFO',"Description");
		foreach my $line (split(/\n/,$ldapDescription)) {
			logger('INFO',"    ".color('green')."%s".color('reset'),$line);
Nigel Kukard's avatar
Nigel Kukard committed
		}
		# Hack'ish ... look if the description mentions dss is required...
		if ($ldapDescription =~ /needs ssh-dss/i) {
			$needDSS = 1;
		}
Nigel Kukard's avatar
Nigel Kukard committed
	}

	# Check if we have a wiki page
	if (my $ldapLoginWikiPage = $ldapEntry->get_value('awitLoginWikiPage')) {
Nigel Kukard's avatar
Nigel Kukard committed
		logger('INFO',"Wiki Page");
		logger('INFO',"    ".color('green')."%s".color('reset'),$ldapLoginWikiPage);
Nigel Kukard's avatar
Nigel Kukard committed
	}

	print STDERR "\n";
}


# 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)) {
Nigel Kukard's avatar
Nigel Kukard committed
		logger('ERROR',color('bold red')."No port knocking port defined!".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed

	logger('NOTICE',"Port knocking '".color('green')."%s".color('reset')."' on port '".color('green')."%s".color('reset')."'...",
			$knockHost,$knockPort);
	# Do the port knock
	my $sock = IO::Socket::INET6->new(
		PeerAddr => $knockHost,
		PeerPort => $knockPort,
		Proto => 'tcp',
		Timeout => 3
	);
	# We should get a failure of "Connection refused", if not ERR
	if (defined($sock) || $! ne "Connection refused") {
Nigel Kukard's avatar
Nigel Kukard committed
		logger('ERROR',color('bold red')."Port knocking failed!".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed
	logger('INFO',"Port knocking success!");
	print STDERR "\n";
}
Nigel Kukard's avatar
Nigel Kukard committed


my @sshArgs = ();
my @sshArgsPortForwards = ();
Nigel Kukard's avatar
Nigel Kukard committed

if (defined($pkcsProvider) && $pkcsProvider ne "") {
	push(@sshArgs,'-I',$pkcsProvider);
Nigel Kukard's avatar
Nigel Kukard committed
	logger('NOTICE',color('blue')."Enabling smartcard/token authentication.".color('reset'));
	print STDERR "\n";
Nigel Kukard's avatar
Nigel Kukard committed
# If we're in debug mode set ssh to be verbose
if (defined($optctl{'debug'})) {
	push(@sshArgs,'-v');
}

# Notify user we'll be forwarding his authentication agent
if (defined($optctl{'forward-agent'})) {
	logger('NOTICE',color('red')."Forwarding authentication agent!".color('reset'));
	print STDERR "\n";
}

# 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);
}

# If the server is ancient, we need to enable DSS
if (defined($needDSS)) {
	logger('WARNING',color('red')."Host needs ssh-dss".color('reset'));
	push(@sshArgs,'-o','PubkeyAcceptedKeyTypes=+ssh-dss');
	push(@sshArgs,'-o','HostbasedKeyTypes=+ssh-dss');
	push(@sshArgs,'-o','HostKeyAlgorithms=+ssh-dss');
}

# 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');

# Check if we're doing port forwarding...
foreach my $item (@forwardPortExtra) {
	push(@sshArgsPortForwards,'-L',sprintf('%s:%s:%s',$item->{'localPort'},$item->{'destHost'},$item->{'destPort'}));

	logger('NOTICE',color('magenta')."Forwarding port '".color('reset').$item->{'localPort'}.color('magenta').
			"' on localhost to '".color('reset').$item->{'destHost'}.color('magenta')."' port '" .color('reset').
			$item->{'destPort'}.color('magenta')."'\n");
}


# 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'});


# Setup TMPDIR, we prefer XDG_RUNTIME_DIR as its protected in /run/user/$UID/
my $TMPDIR = $ENV{'XDG_RUNTIME_DIR'} // $ENV{'TMPDIR'} // '/tmp';


# Sockets we may use
our $forwardSocket;
our $libvirtSocket;
# Children PID's we may create
our $forwardChild;
our $libvirtChild;

# 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";

	# Build up our forwarding process args into this...
	my @forwardArgs = ();

Nigel Kukard's avatar
Nigel Kukard committed
	# TODO: Allow the use of multiple forwarded ports by separating them with ,'s
	# The first port will be assumed as the SSH port, all other ports will be forwarded via TCP/IP and reported in terminal

	# 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',
			# 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");

Nigel Kukard's avatar
Nigel Kukard committed
		# Check what operation we're doing
		if (defined($useRsync)) {
Nigel Kukard's avatar
Nigel Kukard committed
			# Build SSH command
			my $sshCmd = join(' ','/usr/bin/ssh',
				@sshArgs,
				# Override where we connecting to
				'-o',"ProxyCommand='nc -U $forwardSocket'",
Nigel Kukard's avatar
Nigel Kukard committed
				# Explicitly disable control master
				'-o','ControlMaster=no',
			);
			# Run rsync
			system('/usr/bin/rsync',
Nigel Kukard's avatar
Nigel Kukard committed
				'-e',$sshCmd,
				@rsyncParams
			);

		} else {
			# Fire up SSH
			system('/usr/bin/ssh',
				@sshArgs,
				@sshArgsPortForwards,
Nigel Kukard's avatar
Nigel Kukard committed
				# 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')."Forward 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);
	}

	# Check if we're doing rsync...
	if (defined($useRsync)) {
Nigel Kukard's avatar
Nigel Kukard committed
		# Build SSH command
		my $sshCmd = join(' ','/usr/bin/ssh',
			@sshArgs,
			# Use basic compression
			'-o','Compression=yes',
		);
		# Run rsync
		system('/usr/bin/rsync',
Nigel Kukard's avatar
Nigel Kukard committed
			'-e',$sshCmd,
			@rsyncParams
		);

	# Check if we're doing libvirt monitor port forwarding...
	} elsif (defined($libvirtVNC)) {
		# Split off host and port
		my ($vncHost,$vncDisplay) = split(':',$libvirtVNC);
		if (!defined($vncHost) || $vncHost ne "127.0.0.1" || !defined($vncDisplay)) {
				logger('ERROR',color('magenta')."Libvirt VNC socket looks invalid '%s'".color('reset'),$libvirtVNC);
				exit 1;
		}
		# VNC port is the display plus 5900
		my $vncPort = 5900 + $vncDisplay;

		# Add forward socket name
		$libvirtSocket = "$TMPDIR/awit-ssh-vnc-".sha1_hex("$loginHost:".($loginPort // 22).":$libvirtVNC $$").".sock";

		my @libvirtArgs = ();

		# Add on port we're forwarding
		push(@libvirtArgs,'-L',"$libvirtSocket:$vncHost:$vncPort");

		# Fork off child to establish the main connection
		$libvirtChild = fork();
		if (!$libvirtChild) {
			# Don't use signals for this child
			undef($SIG{'TERM'});
			undef($SIG{'INT'});

			# Exec ssh
			if (!exec('/usr/bin/ssh',
				@sshArgs,
				'-o','ControlMaster=no',
				@libvirtArgs,
				# Use basic compression
				'-o','Compression=yes',
				# All we're doing here is forwarding the port...
				'-N',
				$loginHost
			)) {

				logger('ERROR',color('magenta')."Libvirt VNC unix forwarding SSH process failed to start".color('reset'));

				exit 1;
			}
		}

		# Loop waiting for the socket to be created
		my $delay = 30;
		while (! -e $libvirtSocket && $delay > 0) {
			$delay--;
			sleep 1;
		}

		# If we still have timeout ticks left, then we connected, hopefully successfully
		if ($delay) {
			system('/usr/bin/ssvncviewer',
Nigel Kukard's avatar
Nigel Kukard committed
#					'-encodings','copyrect tight hextile zlib corre rre raw',
#					'-encodings','tight hextile zlib corre rre raw',
#					'-quality','7',
					# This is handled by ssh
Nigel Kukard's avatar
Nigel Kukard committed
#					'-compresslevel','0',
#					'-16bpp',
					$libvirtSocket);
		} else {
			logger('ERROR',color('magenta')."Libvirt socket not connected, aborting!".color('reset'));
		}


Nigel Kukard's avatar
Nigel Kukard committed
	# Normal SSH
	} else {

		# Check if we're forwarding our agent
		if ($optctl{'forward-agent'}) {
			# FIXME - check if our keys expire
			push(@sshArgs,'-A');
		}

Nigel Kukard's avatar
Nigel Kukard committed
		system('/usr/bin/ssh',
			@sshArgs,
			@sshArgsPortForwards,
Nigel Kukard's avatar
Nigel Kukard committed
			# Use basic compression
			'-o','Compression=yes',
			$loginHost
		);
	}
Nigel Kukard's avatar
Nigel Kukard committed

Nigel Kukard's avatar
Nigel Kukard committed
exit 0;



#
# Internal Functions
#

# Cleanup function
sub cleanup
{
	# Kill the children
	if ($forwardChild && kill(0,$forwardChild)) {
		kill('TERM',$forwardChild);
		# Wait for it to die
		waitpid($forwardChild,-1);
	}
	if ($libvirtChild && kill(0,$libvirtChild)) {
		kill('TERM',$libvirtChild);
		# Wait for it to die
		waitpid($libvirtChild,-1);
	}
	# Unlink sockets
	if ($forwardSocket) {
		unlink($forwardSocket);
	}
	if ($libvirtSocket) {
		unlink($libvirtSocket);
	}
	# Check if we exiting abnormally...
	if ($forwardSocket || $libvirtSocket) {
		print STDERR "\nExiting...\n";
		exit 1;
	}

	exit 0;
}



Nigel Kukard's avatar
Nigel Kukard committed
# Log something
sub logger
{
	my ($level,$arg1,@args) = @_;


Nigel Kukard's avatar
Nigel Kukard committed
	printf(STDERR '%-7s: '.$arg1.color('reset')."\n",$level,@args);
Nigel Kukard's avatar
Nigel Kukard committed
}


# Display version
sub displayVersion
{
	print("Version: $VERSION\n");
Nigel Kukard's avatar
Nigel Kukard committed
}


# Display usage
sub displayHelp
{
	print(STDERR<<EOF);
Usage: $0 <options> [USER@]HOST
       $0 <options> --knock HOST:PORT [USER@]HOST
       $0 <options> --libvirt-vnc HOST:PORT [USER@]HOST
       $0 <options> --rsync -- <rsync options> [USER@]HOST:/path/file.name /tmp
Nigel Kukard's avatar
Nigel Kukard committed

    General Options:
      --help                             What you're seeing now.
      --version                          Display version.
    Agent Fowarding:
      --forward-agent                    Forward SSH agent socket.

    Port Knocking: