Skip to content

Instantly share code, notes, and snippets.

@mildsunrise
Last active April 18, 2024 12:35
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mildsunrise/501037e2c29ace3edc406c77551b9990 to your computer and use it in GitHub Desktop.
Save mildsunrise/501037e2c29ace3edc406c77551b9990 to your computer and use it in GitHub Desktop.
Wrapper that simplifies SSH tunnels

ssh-from

ssh-from simplifies common usage of SSH tunnels.
It instructs SSH to start a SOCKS proxy, and spawns a (local) shell that uses this proxy.

$ ssh-from my-host
$ curl http://foo/bar  # the request is tunnelled through my-host

Installation

Install dependencies (tsocks is optional):

sudo apt install python3 lsof tsocks

Download the script somewhere in your PATH and make it executable:

sudo curl -fL https://gist.github.com/mildsunrise/501037e2c29ace3edc406c77551b9990/raw/ssh-from.py -o /usr/local/bin/ssh-from
sudo chmod +x /usr/local/bin/ssh-from

Usage

ssh-from works by starting ssh in the background (instructing it to create a SOCKS proxy) and starting a shell with *_proxy variables set to use that proxy. Commands that have proxy support (such as curl) will pipe their connections through the specified host.

ssh nesting support

It's recommended to install this helper program as well, which adds proxy support to ssh (and thus ssh-from). This lets you chain hops easily:

$ ssh-from hop1
$ ssh-from hop2
$ ssh target

tsocks support

Even for commands that do not have proxy support, you can still run them with tsocks to have their connections go through the proxy.

ssh-from takes care of creating a configuration file for tsocks (and setting it on $TSOCKS_CONF_FILE), so all you have to do is run the command prefixed with tsocks:

$ ssh-from hop
$ tsocks <command>

Limitations: because tsocks works by patching libc functions (socket, connect, etc.) it doesn't play well with IPv6/dual-stack, and resolutions do not happen at the remote host.

#!/usr/bin/env python3
# Dependencies: lsof
import sys, os, signal, subprocess, socket, random, atexit, tempfile
addr = "127.0.0.1"
ssh_args = sys.argv[1:]
# Find free port
tsock = socket.socket()
while True:
port = random.randint(8192, 65535)
try:
tsock.bind((addr, port))
break
except socket.error:
pass
# Start SSH proxy
tsock.close() # race, but whatever
ssh = subprocess.Popen(["ssh", "-fND", "%s:%d" % (addr, port)] + ssh_args)
while True:
try:
r = ssh.wait()
break
except KeyboardInterrupt:
ssh.send_signal(signal.SIGINT)
if r != 0: exit(r)
print("\nConnected ({}).".format(" ".join(ssh_args)))
# Modify env
proxy = "socks5://%s:%d" % (addr, port)
for var in ["http_proxy", "https_proxy", "ftp_proxy", "rsync_proxy", "ssh_proxy", "all_proxy"]:
os.putenv(var, proxy)
os.putenv(var.upper(), proxy)
os.putenv("no_proxy", "")
os.putenv("ssh_from_orig", " ".join(ssh_args))
# Create tsocks config file, just in case
tsconf = tempfile.NamedTemporaryFile(suffix=".conf", delete=False)
tsconf.write("server = {}\nserver_port = {}\nserver_type = 5\n".format(addr, port).encode('ascii'))
tsconf.close()
os.putenv("TSOCKS_CONF_FILE", tsconf.name)
# Terminate ssh proxy at exit
def cleanup():
out = subprocess.check_output(["lsof", "-iTCP@%s:%d" % (addr, port), "-sTCP:LISTEN", "-Fp"])
pid = int({ o[0]: o[1:] for o in out.decode('ascii').splitlines() }["p"])
os.kill(pid, signal.SIGTERM)
orig = os.getenv("ssh_from_orig")
if orig: print("Back at ({}).".format(orig))
else: print("All SSH proxies terminated.")
os.remove(tsconf.name)
atexit.register(cleanup)
# Run shell
sh = subprocess.Popen([os.environ["SHELL"] or "sh"])
while True:
try:
exit(sh.wait())
except KeyboardInterrupt:
sh.send_signal(signal.SIGINT)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment