Last active
December 15, 2015 07:18
-
-
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""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