Skip to content

Instantly share code, notes, and snippets.

@adgaudio
Last active December 15, 2015 07:18
Show Gist options
  • Save adgaudio/5221975 to your computer and use it in GitHub Desktop.
Save adgaudio/5221975 to your computer and use it in GitHub Desktop.
SSH tunnel through a gateway to another machine. I know there are plenty of implementations, but none I found just worked and returned a "localhost:port" string like this does. I've been using this successfully for several months with no problems. However, I have noticed that sometimes, --encrypted is required (this may have something to do with…
"""SSH tunnel through a gateway to another machine.
USAGE:
python ./ssh_tunnel.py -h
or
>>> import ssh_tunnel
>>> ssh_tunnel.main('gateway_username', 'dest_host_addr')
"""
import argparse
import atexit
import fabric.api as f
import random
import shlex
import socket
import subprocess
import sys
GATEWAY_IP = 'My gateway ip address'
GATEWAY_USER = 'ssh username'
# TODO: randomize suggested_bridge_port. if I do that, also need to add a
# wildcard=suggested_bridge_port in ssh_tunnel_encrypted
def kill_cmd(remote_cmd, wildcard=None, user=None):
"""kills process(es) by searching for given string"""
return "%s | xargs --no-run-if-empty kill" % \
find_pid_for(remote_cmd, wildcard=wildcard, user=user)
def _grep_cmd(cmd, user=None):
if not user:
user = r'$(id -un)'
return (r"ps -o pid,command -u {user} | grep -E '{cmd}' | grep -v grep"
).format(user=user, cmd=cmd)
def find_pid_for(cmd, wildcard=None, user=None):
if wildcard:
cmd = cmd.replace(str(wildcard), '.*')
return (r"{_grep_cmd} | awk '{{print $1}}' | xargs").format(
cmd=cmd, user=user, _grep_cmd=_grep_cmd(cmd, user))
def find_forwarded_port_for(cmd, wildcard, user=None):
wildcard = str(wildcard)
cmd = cmd.replace(wildcard, '.*')
return r"{_grep_cmd} | sed -r 's/.*L (.*):localhost:.*/\1/g'".format(
cmd=cmd, user=user, _grep_cmd=_grep_cmd(cmd, user))
def ssh_tunnel_encrypted(ns, atexit_managers, atexit_kwargs):
#local_port, bridge_user, bridge_host, suggested_bridge_port,
#bridge_tunnel_user, dest_host, atexit_args, atexit_kwargs):
"""Set up a fully encrypted ssh tunnel by proxying through a gateway.
Returns a localhost:local_port string giving access to the tunnel.
"""
cmd1 = ("ssh -A -o ConnectTimeout=1 -o BatchMode=yes"
" -fNL {ns.suggested_bridge_port}:localhost:{ns.dest_port} {ns.dest_host}"
).format(ns=ns)
cmd1_pids = f.run(find_pid_for(
cmd1, wildcard=ns.suggested_bridge_port, user=ns.bridge_tunnel_user))
if not cmd1_pids or not ns.reuse_existing:
# print 'Creating tunnel between bridge host and dest host'
_cmd1 = 'sudo -u %s %s' % (ns.bridge_tunnel_user, cmd1)
print _cmd1
f.run(_cmd1)
elif ns.reuse_existing:
_cmd = 'sudo -u %s %s' % (
ns.bridge_tunnel_user,
find_forwarded_port_for(cmd1, wildcard=ns.suggested_bridge_port,
user=ns.bridge_tunnel_user))
ns.suggested_bridge_port = f.run(_cmd)
cmd2 = ('ssh -A -o ConnectTimeout=1 -o BatchMode=yes'
' -fNL {ns.local_port}:localhost:{ns.suggested_bridge_port}'
' {ns.bridge_user}@{ns.bridge_host}').format(ns=ns)
cmd2_pids = f.local(find_pid_for(cmd2, wildcard=ns.local_port),
capture=True).strip()
if not cmd2_pids:
# print 'Creating tunnel between localhost and bridge host'
print cmd2
f.local(cmd2)
_local_port = f.local(
find_forwarded_port_for(cmd2, wildcard=ns.local_port),
capture=True)
if not ns.keepalive:
atexit.register(
f.with_settings(
shell='sudo -u %s /bin/bash -c' % ns.bridge_tunnel_user,
*[x() for x in atexit_managers],
**atexit_kwargs)(f.execute),
f.run,
kill_cmd(cmd1, user=ns.bridge_tunnel_user),
)
if not ns.keepalivelocal:
atexit.register(
f.with_settings(
*[x() for x in atexit_managers], **atexit_kwargs)(f.local),
kill_cmd(cmd2), capture=True)
return 'localhost:%d' % int(_local_port)
def ssh_tunnel_unencrypted(ns):
"""Set up an ssh tunnel by proxying through a gateway.
The connection between gateway and the destination node is unencrypted"""
# ssh -vANf -p suggested_bridge_port -L local_port:dest_host:dest_port bridge_user@bridge_host
cmd = ('ssh -vAN -o ExitOnForwardFailure=yes'
' -L {local_port}:{dest_host}:{dest_port}'
' {bridge_user}@{bridge_host}').format(**ns.__dict__)
print cmd
process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if not ns.keepalivelocal or not ns.keepalive:
atexit.register(process.kill)
return 'localhost:%d' % ns.local_port
def get_local_port():
"""Let OS find a free port we can bind to"""
sock = socket.socket()
sock.bind(('localhost', 0))
return sock.getsockname()[1]
def get_random_port():
"""Let bridge OS find free port we can bind to"""
return random.randint(1025, 65535)
def build_arg_parser():
argument_defaults = (
('bridge_user', dict(help="login to bridge as user")),
('dest_host', {}),
('--bridge_tunnel_user', dict(default=GATEWAY_USER, help=(
"The tunnel between bridge and dest "
"will be owned by this user. Useful when "
"you need to login as one user and run "
"the bridge-dest tunnel as another"))),
('--local_port', dict(default=get_local_port())),
('--bridge_host', dict(default=GATEWAY_IP, help=' ')),
('--suggested_bridge_port', dict(default=get_random_port(),
help=('If remote ssh session already'
' exists, use that port so we'
' dont create duplicate'
' connections'))),
('--dest_port', dict(default=22, help=' ')),
('--timeout', dict(default=3, help=' ')),
('--encrypted', dict(action='store_true', help=' ')),
('--debug', dict(action='store_true', help=' ')),
('--keepalive', dict(action='store_true',
help="Don't kill tunnel between bridge and dest")),
('--keepalivelocal', dict(action='store_true',
help="Don't kill tunnel between localhost and bridge")),
('--reuse_existing', dict(action='store_true',
help='Use existing bridge tunnel if it exists.')),
)
parser = argparse.ArgumentParser(
description='Create an ssh tunnel by proxying through a bridge node',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
for k, v in argument_defaults:
parser.add_argument('%s' % k, **v)
return parser
def main(bridge_user, dest_host, **kwargs):
p = build_arg_parser()
ns = p.parse_args([bridge_user, dest_host])
ns.__dict__.update(kwargs)
if ns.debug:
managers = ()
else:
managers = (lambda: f.hide('running', 'stdout', 'stderr', 'debug'),)
env_kwargs = dict(user=ns.bridge_user, hosts=[ns.bridge_host])
with f.settings(*[x() for x in managers], **env_kwargs):
if not ns.encrypted:
rv = f.execute(ssh_tunnel_unencrypted, ns)
else:
rv = f.execute(ssh_tunnel_encrypted, ns, managers, env_kwargs)
return rv.values()[0]
if __name__ == '__main__':
p = build_arg_parser()
ns = p.parse_args(sys.argv[1:])
rv = main(**ns.__dict__)
print rv
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment