Script to create and manage SSH tunnels and open Remote Desktop sessions

Script simplifies connecting to a Windows host via RDP (Terminal Services) through a Linux host using a SSH tunnel.

Let's say I've got a Windows box inside a corporate firewall and I'd like to connect to it via RDP (Terminal Services), but port 3389 is not open to the Internet so I can't access it. Let's say I don't want to mess with VPN and have a box inside the corporate firewall which I can SSH into. In this scenario, I can RDP to the Windows box from the Internet via a SSH tunnel.

This, in itself, isn't particularly special, but here's a script that simplifies the whole process:

  • Allows to supply a password on the command line to save the trouble of typing
  • Can be called repeatedly to establish simultaneous connections to multiple Windows hosts
  • Keeps track of local tunnel port usage
  • Creates new ports for new connections
  • Frees up unused ports by killing tunnels for RDP connections as they're disconnected

It calls rdesktop for the RDP part, which provides a slew of useful features:

  • Mounts local Linux directories on the remote Windows host (can also mount local CD/DVD and printers)
  • Allows copy/paste
  • Can startup a particular application in Windows. For instance to launch just Internet Explorer upon login, add to rdesktop opts:
    -s C:\Progra~1\Intern~1\iexplore.exe
  • Can emulate various keyboard layouts
  • And probably more..

Anyway, here's the script:

#!/bin/bash

# some defaults
rdp_user=
rdp_pass="-"
rdp_host=
rdp_port=3389
rdesktop_opts="-z -g 1280x960 -a 16 -r disk:home=$HOME -k en-us"
#rdesktop_opts="-z -g 1280x960 -a 16 -r disk:home=$HOME -k en-us -s C:\Progra~1\Intern~1\iexplore.exe"

usage() 
{ 
 echo "Usage:"
 echo "$(basename $0) -r rdp_host -u rdp_user -p rdp_pass -s sshuser@sshhost"
 echo " "
 echo "Connect to a Windows host via RDP through a Linux host using a SSH tunnel."
 echo ""
 echo "Required parameters:"
 echo "    -r, --rdp-host     FQDN hostname of the Windows machine (e.g. winhost.example.com)."
 echo "    -u, --rdp-user     Username on the Windows host (e.g. juser)."
 echo "    -s, --ssh-info     SSH user and host info (e.g. sshuser@sshhost.example.com)."
 echo ""
 echo "Optional parameters:"
 echo "    -p, --rdp-pass     Password of the Windows user."
 echo "    -h, --help         This help message."
 echo ""
}

err_exit() { echo -e 1>&2; exit 1; }

if [ -z "$1" ]; then usage; exit 1; fi

while [ "$1" ]; do
  case "$1" in
	-r|--rdp-host)
        shift
	    rdp_host="$1"
	    ;;
	-u|--rdp-user)
        shift
	    rdp_user="$1"
	    ;;
	-p|--rdp-pass)
        shift
	    rdp_pass="$1"
	    ;;
	-s|--ssh-info)
        shift
	    ssh_info="$1"
	    ;;
	-h|--help)
        usage
        exit 0
	    ;;
    -*)
        echo "$(basename $0): invalid option $1" >&2
        echo "see --help for usage"
        exit 1
        ;;
    *)
        break
        ;;
  esac
  shift
done

if [ "$rdp_host" == "" ]; then
    echo "Error: --rdp-host is a required parameter"
    exit 1
fi

if [ "$rdp_user" == "" ]; then
    echo "Error: --rdp-user is a required parameter"
    exit 1
fi

if [ "$ssh_info" == "" ]; then
    echo "Error: --ssh-info is a required parameter"
    exit 1
fi

lastport=$(netstat -tapn | grep -Eo "526[0-9]{2}" | sort -u | tail -1)

if [ "$lastport" == "" ]; then
    nextport="52600"
  else
    nextport=$(($lastport+1))
fi

ssh -fNL $nextport:$rdp_host:$rdp_port $ssh_info || err_exit
rdesktop $rdesktop_opts -u $rdp_user -p $rdp_pass -T $rdp_host localhost:$nextport && pkill -f $nextport || err_exit &

Lends to other uses. For example, programatically provisioned a bunch of Windows virtual machines and had to connect to each one to change computer name. Ideally, something like this would be scripted, but even in the absence of that, used the following little script to establish a connection, logon without password prompts, do what I gotta do, then move on to the next box. Not completely automated, but better than nothing and definitely saved me some time.

for n in $(seq 1 20); do
 rdpssh -r winhost$n.example.com -u winuser -p winpass -s sshuser@sshhost.example.com
 echo "Connected to winhost$n.example.com"
 read -p "Press any key to go on to the next machine.."
done

Once in a while, the script won't clean up after itself and not kill the tunnel it created. Typically because rdesktop didn't exit cleanly. I could add some additional code to correct for that, but it happens rarely enough (usually just when I'm testing various rdesktop parameters), where a manual cleanup method should suffice.

To find all tunnels in the scripts port range (526xx):

ps -eo pid,command |  grep -E "526[0-9]{2}:"
15939 ssh -fNL 52602:foo20.example.com:3389 sshuser@sshhost.example.com
15957 ssh -fNL 52603:foo20.example.com:3389 sshuser@sshhost.example.com
16093 ssh -fNL 52601:bar18.example.com:3389 sshuser@sshhost.example.com
16096 ssh -fNL 52602:foo20.example.com:3389 sshuser@sshhost.example.com
16099 ssh -fNL 52607:bar18.example.com:3389 sshuser@sshhost.example.com

To kill all tunnels in the scripts port range (526xx) with "pkill":

pkill -f "526[0-9]{2}:"

It's always a good idea to test your search criteria first to ensure you're killing the right programs! Use "pgrep" as it's syntax is identical to "pkill", but instead of killing PIDs it displays them:

pgrep -f "526[0-9]{2}:"
15939
15957
16093
16096
16099

Let's say you don't want to kill all tunnels, but just the ones for bar18.example.com:

ps -eo pid,command | grep "bar18.example.com"
16093 ssh -fNL 52601:bar18.example.com:3389 sshuser@sshhost.example.com
16099 ssh -fNL 52607:bar18.example.com:3389 sshuser@sshhost.example.com
pgrep -f "bar18.example.com"
16093
16099
pkill -f "bar18.example.com"

Now they're gone:

ps -eo pid,command |  grep -E "526[0-9]{2}"
15939 ssh -fNL 52602:foo20.example.com:3389 sshuser@sshhost.example.com
15957 ssh -fNL 52603:foo20.example.com:3389 sshuser@sshhost.example.com
16096 ssh -fNL 52602:foo20.example.com:3389 sshuser@sshhost.example.com

Leave a comment

NOTE: Enclose quotes in <blockquote></blockquote>. Enclose code in <pre lang="LANG"></pre> (where LANG is one of these).