Compare commits

..

3 Commits

Author SHA1 Message Date
8fe3592be3 Don't require the remote server to have sshuttle installed.
Instead, grab our source code, send it over the link, and have python eval
it and then start the server.py main() function.

Strangely, there's now *less* horrible stuff in ssh.py, because we no longer
have to munge around with the PATH environment variable.  And this
significantly reduces the setup required to get sshuttle going.

Based on a suggestion from Wayne Scott.
2010-05-04 23:42:36 -04:00
ba19d9c72d Rename iptables->firewall.
Since we "almost" support ipfw on MacOS (which I guess might mean FreeBSD
too), the name should be a bit more generic.
2010-05-04 22:06:27 -04:00
096bbcc576 Client "almost" works on MacOS and maybe FreeBSD.
Basic forwarding now works on MacOS, assuming you set up ipfw correctly
(ha ha).  I wasted a few hours today trying to figure this out, and I'm *so
very close*, but unfortunately it just didn't work.  Think you can figure it
out?

Related changes:
- don't die if iptables is unavailable
- BSD uses getsockname() instead of SO_ORIGINAL_DST
- non-blocking connect() returns EISCONN once it's connected
- you can't setsockopt IP_TTL more than once
2010-05-04 22:06:22 -04:00
8 changed files with 225 additions and 80 deletions

View File

@ -36,12 +36,18 @@ Prerequisites
- sudo, su, or logged in as root on your client machine. - sudo, su, or logged in as root on your client machine.
(The server doesn't need admin access.) (The server doesn't need admin access.)
- Linux+iptables on your client machine, including at - If you use Linux on your client machine:
iptables installed on the client, including at
least the iptables DNAT, REDIRECT, and ttl modules. least the iptables DNAT, REDIRECT, and ttl modules.
This is available by default on most Linux distributions. These are installed by default on most Linux distributions.
(The server doesn't need iptables and doesn't need to be (The server doesn't need iptables and doesn't need to be
Linux.) Linux.)
- If you use MacOS or BSD on your client machine:
Your kernel needs to be compiled with IPFIREWALL_FORWARD
(MacOS has this by default) and you need to have ipfw
available. (The server doesn't need to be MacOS or BSD.)
This is how you use it: This is how you use it:
----------------------- -----------------------
@ -58,6 +64,11 @@ That's it! Now your local machine can access the remote network as if you
were right there! And if your "client" machine is a router, everyone on were right there! And if your "client" machine is a router, everyone on
your local network can make connections to your remote network. your local network can make connections to your remote network.
You don't need to install sshuttle on the remote server;
the remote server just needs to have python available.
sshuttle will automatically upload and run its source code
to the remote python interpreter.
This creates a transparent proxy server on your local machine for all IP This creates a transparent proxy server on your local machine for all IP
addresses that match 0.0.0.0/0. (You can use more specific IP addresses if addresses that match 0.0.0.0/0. (You can use more specific IP addresses if
you want; use any number of IP addresses or subnets to change which you want; use any number of IP addresses or subnets to change which

26
assembler.py Normal file
View File

@ -0,0 +1,26 @@
import sys, zlib
z = zlib.decompressobj()
mainmod = sys.modules[__name__]
while 1:
name = sys.stdin.readline().strip()
if name:
nbytes = int(sys.stdin.readline())
if verbosity >= 2:
sys.stderr.write('remote assembling %r (%d bytes)\n'
% (name, nbytes))
content = z.decompress(sys.stdin.read(nbytes))
exec compile(content, name, "exec")
# FIXME: this crushes everything into a single module namespace,
# then makes each of the module names point at this one. Gross.
assert(name.endswith('.py'))
modname = name[:-3]
mainmod.__dict__[modname] = mainmod
else:
break
verbose = verbosity
sys.stderr.flush()
sys.stdout.flush()
main()

View File

@ -4,23 +4,29 @@ from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import * from helpers import *
def original_dst(sock): def original_dst(sock):
SO_ORIGINAL_DST = 80 try:
SOCKADDR_MIN = 16 SO_ORIGINAL_DST = 80
sockaddr_in = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, SOCKADDR_MIN) SOCKADDR_MIN = 16
(proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) sockaddr_in = sock.getsockopt(socket.SOL_IP,
assert(socket.htons(proto) == socket.AF_INET) SO_ORIGINAL_DST, SOCKADDR_MIN)
ip = '%d.%d.%d.%d' % (a,b,c,d) (proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
return (ip,port) assert(socket.htons(proto) == socket.AF_INET)
ip = '%d.%d.%d.%d' % (a,b,c,d)
return (ip,port)
except socket.error, e:
if e.args[0] == errno.ENOPROTOOPT:
return sock.getsockname()
raise
class IPTables: class FirewallClient:
def __init__(self, port, subnets): def __init__(self, port, subnets):
self.port = port self.port = port
self.subnets = subnets self.subnets = subnets
subnets_str = ['%s/%d' % (ip,width) for ip,width in subnets] subnets_str = ['%s/%d' % (ip,width) for ip,width in subnets]
argvbase = ([sys.argv[0]] + argvbase = ([sys.argv[0]] +
['-v'] * (helpers.verbose or 0) + ['-v'] * (helpers.verbose or 0) +
['--iptables', str(port)] + subnets_str) ['--firewall', str(port)] + subnets_str)
argv_tries = [ argv_tries = [
['sudo'] + argvbase, ['sudo'] + argvbase,
['su', '-c', ' '.join(argvbase)], ['su', '-c', ' '.join(argvbase)],
@ -47,7 +53,7 @@ class IPTables:
s1.close() s1.close()
self.pfile = s2.makefile('wb+') self.pfile = s2.makefile('wb+')
if e: if e:
log('Spawning iptables: %r\n' % self.argv) log('Spawning firewall manager: %r\n' % self.argv)
raise Fatal(e) raise Fatal(e)
line = self.pfile.readline() line = self.pfile.readline()
self.check() self.check()
@ -74,7 +80,7 @@ class IPTables:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
def _main(listener, ipt, use_server, remotename): def _main(listener, fw, use_server, remotename):
handlers = [] handlers = []
if use_server: if use_server:
if helpers.verbose >= 1: if helpers.verbose >= 1:
@ -98,14 +104,14 @@ def _main(listener, ipt, use_server, remotename):
# we definitely want to do this *after* starting ssh, or we might end # we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection! # up intercepting the ssh connection!
ipt.start() fw.start()
def onaccept(): def onaccept():
sock,srcip = listener.accept() sock,srcip = listener.accept()
dstip = original_dst(sock) dstip = original_dst(sock)
debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1], debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1],
dstip[0],dstip[1])) dstip[0],dstip[1]))
if dstip == sock.getsockname(): if dstip == listener.getsockname():
debug1("-- ignored: that's my address!\n") debug1("-- ignored: that's my address!\n")
sock.close() sock.close()
return return
@ -150,7 +156,7 @@ def main(listenip, use_server, remotename, subnets):
if listenip[1]: if listenip[1]:
ports = [listenip[1]] ports = [listenip[1]]
else: else:
ports = xrange(12300,65536) ports = xrange(12300,9000,-1)
last_e = None last_e = None
bound = False bound = False
debug2('Binding:') debug2('Binding:')
@ -170,9 +176,9 @@ def main(listenip, use_server, remotename, subnets):
listenip = listener.getsockname() listenip = listener.getsockname()
debug1('Listening on %r.\n' % (listenip,)) debug1('Listening on %r.\n' % (listenip,))
ipt = IPTables(listenip[1], subnets) fw = FirewallClient(listenip[1], subnets)
try: try:
return _main(listener, ipt, use_server, remotename) return _main(listener, fw, use_server, remotename)
finally: finally:
ipt.done() fw.done()

View File

@ -3,7 +3,7 @@ import helpers
from helpers import * from helpers import *
def chain_exists(name): def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL'] argv = ['iptables', '-t', 'nat', '-nL']
p = subprocess.Popen(argv, stdout = subprocess.PIPE) p = subprocess.Popen(argv, stdout = subprocess.PIPE)
for line in p.stdout: for line in p.stdout:
@ -22,11 +22,16 @@ def ipt(*args):
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
def do_it(port, subnets): # We name the chain based on the transproxy port number so that it's possible
# to run multiple copies of sshuttle at the same time. Of course, the
# multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
def do_iptables(port, subnets):
chain = 'sshuttle-%s' % port chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains # basic cleanup/setup of chains
if chain_exists(chain): if ipt_chain_exists(chain):
ipt('-D', 'OUTPUT', '-j', chain) ipt('-D', 'OUTPUT', '-j', chain)
ipt('-D', 'PREROUTING', '-j', chain) ipt('-D', 'PREROUTING', '-j', chain)
ipt('-F', chain) ipt('-F', chain)
@ -48,33 +53,113 @@ def do_it(port, subnets):
) )
# This is some iptables voodoo for setting up the Linux kernel's transparent def ipfw_rule_exists(n):
# proxying stuff. If subnets is empty, we just delete our sshuttle chain; argv = ['ipfw', 'list']
# otherwise we delete it, then make it from scratch. p = subprocess.Popen(argv, stdout = subprocess.PIPE)
for line in p.stdout:
if line.startswith('%05d ' % n):
if line[5:].find('ipttl 42') < 0:
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
return True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def sysctl_get(name):
argv = ['sysctl', '-n', name]
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
line = p.stdout.readline()
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
if not line:
raise Fatal('%r returned no data' % (argv,))
assert(line[-1] == '\n')
return line[:-1]
def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv, stdout = open('/dev/null', 'w'))
_oldctls = []
def sysctl_set(name, val):
oldval = sysctl_get(name)
if str(val) != str(oldval):
_oldctls.append((name, oldval))
return _sysctl_set(name, val)
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def do_ipfw(port, subnets):
sport = str(port)
# cleanup any existing rules
if ipfw_rule_exists(port):
ipfw('del', sport)
while _oldctls:
(name,oldval) = _oldctls.pop()
_sysctl_set(name, oldval)
if subnets:
sysctl_set('net.inet.ip.fw.enable', 1)
sysctl_set('net.inet.ip.forwarding', 1)
# create new subnet entries
for snet,swidth in subnets:
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
'log', 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth),
'not', 'ipttl', '42')
def program_exists(name):
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
for p in paths:
fn = '%s/%s' % (p, name)
if os.path.exists(fn):
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
# This is some voodoo for setting up the kernel's transparent
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
# otherwise we delete it, then make them from scratch.
# #
# We name the chain based on the transproxy port number so that it's possible # This code is supposed to clean up after itself by deleting its rules on
# to run multiple copies of sshuttle at the same time. Of course, the
# multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
#
# This code is supposed to clean up after itself by deleting extra chains on
# exit. In case that fails, it's not the end of the world; future runs will # exit. In case that fails, it's not the end of the world; future runs will
# supercede it in the transproxy list, at least, so the leftover iptables # supercede it in the transproxy list, at least, so the leftover rules
# chains are mostly harmless. # are hopefully harmless.
def main(port, subnets): def main(port, subnets):
assert(port > 0) assert(port > 0)
assert(port <= 65535) assert(port <= 65535)
if os.getuid() != 0: if os.getuid() != 0:
raise Fatal('you must be root (or enable su/sudo) to set up iptables') raise Fatal('you must be root (or enable su/sudo) to set the firewall')
if program_exists('ipfw'):
do_it = do_ipfw
elif program_exists('iptables'):
do_it = do_iptables
else:
raise Fatal("can't find either ipfw or iptables; check your PATH")
# because of limitations of the 'su' command, the *real* stdin/stdout # because of limitations of the 'su' command, the *real* stdin/stdout
# are both attached to stdout initially. Clone stdout into stdin so we # are both attached to stdout initially. Clone stdout into stdin so we
# can read from it. # can read from it.
os.dup2(1, 0) os.dup2(1, 0)
debug1('iptables manager ready.\n') debug1('firewall manager ready.\n')
sys.stdout.write('READY\n') sys.stdout.write('READY\n')
sys.stdout.flush() sys.stdout.flush()
@ -89,10 +174,10 @@ def main(port, subnets):
if not line: if not line:
return # parent died; nothing to do return # parent died; nothing to do
if line != 'GO\n': if line != 'GO\n':
raise Fatal('iptables: expected GO but got %r' % line) raise Fatal('firewall: expected GO but got %r' % line)
try: try:
if line: if line:
debug1('iptables manager: starting transproxy.\n') debug1('firewall manager: starting transproxy.\n')
do_it(port, subnets) do_it(port, subnets)
sys.stdout.write('STARTED\n') sys.stdout.write('STARTED\n')
@ -111,7 +196,7 @@ def main(port, subnets):
finally: finally:
try: try:
debug1('iptables manager: undoing changes.\n') debug1('firewall manager: undoing changes.\n')
except: except:
pass pass
do_it(port, []) do_it(port, [])

10
main.py
View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys, os, re import sys, os, re
import helpers, options, client, server, iptables import helpers, options, client, server, firewall
from helpers import * from helpers import *
@ -46,7 +46,7 @@ def parse_ipport(s):
optspec = """ optspec = """
sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...> sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] <subnets...>
sshuttle --iptables <port> <subnets...> sshuttle --firewall <port> <subnets...>
sshuttle --server sshuttle --server
-- --
l,listen= transproxy to this ip address and port number [default=0] l,listen= transproxy to this ip address and port number [default=0]
@ -54,7 +54,7 @@ r,remote= ssh hostname (and optional username) of remote sshuttle server
v,verbose increase debug message verbosity v,verbose increase debug message verbosity
noserver don't use a separate server process (mostly for debugging) noserver don't use a separate server process (mostly for debugging)
server [internal use only] server [internal use only]
iptables [internal use only] firewall [internal use only]
""" """
o = options.Options('sshuttle', optspec) o = options.Options('sshuttle', optspec)
(opt, flags, extra) = o.parse(sys.argv[1:]) (opt, flags, extra) = o.parse(sys.argv[1:])
@ -64,10 +64,10 @@ helpers.verbose = opt.verbose
try: try:
if opt.server: if opt.server:
sys.exit(server.main()) sys.exit(server.main())
elif opt.iptables: elif opt.firewall:
if len(extra) < 1: if len(extra) < 1:
o.fatal('at least one argument expected') o.fatal('at least one argument expected')
sys.exit(iptables.main(int(extra[0]), sys.exit(firewall.main(int(extra[0]),
parse_subnets(extra[1:]))) parse_subnets(extra[1:])))
else: else:
if len(extra) < 1: if len(extra) < 1:

View File

@ -1,18 +1,20 @@
import struct, socket, select import struct, socket, select
import ssnet, helpers if not globals().get('skip_imports'):
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper import ssnet, helpers
from helpers import * from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from helpers import *
def main(): def main():
# synchronization header
sys.stdout.write('SSHUTTLE0001')
sys.stdout.flush()
if helpers.verbose >= 1: if helpers.verbose >= 1:
helpers.logprefix = ' s: ' helpers.logprefix = ' s: '
else: else:
helpers.logprefix = 'server: ' helpers.logprefix = 'server: '
# synchronization header
sys.stdout.write('SSHUTTLE0001')
sys.stdout.flush()
handlers = [] handlers = []
mux = Mux(socket.fromfd(sys.stdin.fileno(), mux = Mux(socket.fromfd(sys.stdin.fileno(),
socket.AF_INET, socket.SOCK_STREAM), socket.AF_INET, socket.SOCK_STREAM),

64
ssh.py
View File

@ -1,7 +1,20 @@
import sys, os, re, subprocess, socket import sys, os, re, subprocess, socket, zlib
import helpers import helpers
from helpers import * from helpers import *
def readfile(name):
basedir = os.path.dirname(os.path.abspath(sys.argv[0]))
fullname = os.path.join(basedir, name)
return open(fullname, 'rb').read()
def empackage(z, filename):
content = z.compress(readfile(filename))
content += z.flush(zlib.Z_SYNC_FLUSH)
return '%s\n%d\n%s' % (filename,len(content), content)
def connect(rhostport): def connect(rhostport):
main_exe = sys.argv[0] main_exe = sys.argv[0]
l = (rhostport or '').split(':', 1) l = (rhostport or '').split(':', 1)
@ -9,45 +22,42 @@ def connect(rhostport):
portl = [] portl = []
if len(l) > 1: if len(l) > 1:
portl = ['-p', str(int(l[1]))] portl = ['-p', str(int(l[1]))]
nicedir = os.path.split(os.path.abspath(main_exe))[0]
nicedir = re.sub(r':', "_", nicedir)
myhome = os.path.expanduser('~') + '/'
if nicedir.startswith(myhome):
nicedir2 = nicedir[len(myhome):]
else:
nicedir2 = nicedir
if rhost == '-': if rhost == '-':
rhost = None rhost = None
z = zlib.compressobj(1)
content = readfile('assembler.py')
content2 = (empackage(z, 'helpers.py') +
empackage(z, 'ssnet.py') +
empackage(z, 'server.py') +
"\n")
pyscript = r"""
import sys;
skip_imports=1;
verbosity=%d;
exec compile(sys.stdin.read(%d), "assembler.py", "exec")
""" % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
if not rhost: if not rhost:
argv = ['sshuttle', '--server'] + ['-v']*(helpers.verbose or 0) argv = ['python', '-c', pyscript]
else: else:
# WARNING: shell quoting security holes are possible here, so we argv = ['ssh'] + portl + [rhost, '--', "python -c '%s'" % pyscript]
# have to be super careful. We have to use 'sh -c' because
# csh-derived shells can't handle PATH= notation. We can't
# set PATH in advance, because ssh probably replaces it. We
# can't exec *safely* using argv, because *both* ssh and 'sh -c'
# allow shellquoting. So we end up having to double-shellquote
# stuff here.
escapedir = re.sub(r'([^\w/])', r'\\\\\\\1', nicedir)
escapedir2 = re.sub(r'([^\w/])', r'\\\\\\\1', nicedir2)
cmd = r"""
sh -c PATH=%s:'$HOME'/%s:'$PATH exec sshuttle --server%s'
""" % (escapedir, escapedir2,
' -v' * (helpers.verbose or 0))
argv = ['ssh'] + portl + [rhost, '--', cmd.strip()]
debug2('executing: %r\n' % argv)
(s1,s2) = socket.socketpair() (s1,s2) = socket.socketpair()
def setup(): def setup():
# runs in the child process # runs in the child process
s2.close() s2.close()
if not rhost:
os.environ['PATH'] = ':'.join([nicedir,
os.environ.get('PATH', '')])
os.setsid() os.setsid()
s1a,s1b = os.dup(s1.fileno()), os.dup(s1.fileno()) s1a,s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
s1.close() s1.close()
debug2('executing: %r\n' % argv)
p = subprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, p = subprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
close_fds=True) close_fds=True)
os.close(s1a) os.close(s1a)
os.close(s1b) os.close(s1b)
s2.sendall(content)
s2.sendall(content2)
return p, s2 return p, s2

View File

@ -1,5 +1,6 @@
import struct, socket, errno, select import struct, socket, errno, select
from helpers import * if not globals().get('skip_imports'):
from helpers import *
HDR_LEN = 8 HDR_LEN = 8
@ -71,14 +72,17 @@ class SockWrapper:
def try_connect(self): def try_connect(self):
if not self.connect_to: if not self.connect_to:
return # already connected return # already connected
self.rsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
self.rsock.setblocking(False) self.rsock.setblocking(False)
try: try:
self.rsock.connect(self.connect_to) self.rsock.connect(self.connect_to)
# connected successfully (Linux)
self.connect_to = None self.connect_to = None
except socket.error, e: except socket.error, e:
if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]: if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]:
pass # not connected yet pass # not connected yet
elif e.args[0] == errno.EISCONN:
# connected successfully (BSD)
self.connect_to = None
elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT]: elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT]:
# a "normal" kind of error # a "normal" kind of error
self.connect_to = None self.connect_to = None
@ -387,6 +391,7 @@ class MuxWrapper(SockWrapper):
def connect_dst(ip, port): def connect_dst(ip, port):
debug2('Connecting to %s:%d\n' % (ip, port)) debug2('Connecting to %s:%d\n' % (ip, port))
outsock = socket.socket() outsock = socket.socket()
outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
return SockWrapper(outsock, outsock, return SockWrapper(outsock, outsock,
connect_to = (ip,port), connect_to = (ip,port),
peername = '%s:%d' % (ip,port)) peername = '%s:%d' % (ip,port))