diff options
author | Avery Pennarun <apenwarr@gmail.com> | 2010-05-04 18:24:43 -0400 |
---|---|---|
committer | Avery Pennarun <apenwarr@gmail.com> | 2010-05-04 22:06:22 -0400 |
commit | 096bbcc57641445f0b3bbf4f463b9e6909bd2601 (patch) | |
tree | 9a7df73b2f44613bfbdc6b4f07a79b175a32c63b | |
parent | 7bd0efd57bdfd4e809a335af7d1eea184b1d6721 (diff) |
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
-rw-r--r-- | client.py | 24 | ||||
-rw-r--r-- | iptables.py | 125 | ||||
-rw-r--r-- | ssnet.py | 6 |
3 files changed, 125 insertions, 30 deletions
@@ -4,13 +4,19 @@ from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from helpers import * def original_dst(sock): - SO_ORIGINAL_DST = 80 - SOCKADDR_MIN = 16 - sockaddr_in = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, SOCKADDR_MIN) - (proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) - assert(socket.htons(proto) == socket.AF_INET) - ip = '%d.%d.%d.%d' % (a,b,c,d) - return (ip,port) + try: + SO_ORIGINAL_DST = 80 + SOCKADDR_MIN = 16 + sockaddr_in = sock.getsockopt(socket.SOL_IP, + SO_ORIGINAL_DST, SOCKADDR_MIN) + (proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) + 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: @@ -105,7 +111,7 @@ def _main(listener, ipt, use_server, remotename): dstip = original_dst(sock) debug1('Accept: %r:%r -> %r:%r.\n' % (srcip[0],srcip[1], dstip[0],dstip[1])) - if dstip == sock.getsockname(): + if dstip == listener.getsockname(): debug1("-- ignored: that's my address!\n") sock.close() return @@ -150,7 +156,7 @@ def main(listenip, use_server, remotename, subnets): if listenip[1]: ports = [listenip[1]] else: - ports = xrange(12300,65536) + ports = xrange(12300,9000,-1) last_e = None bound = False debug2('Binding:') diff --git a/iptables.py b/iptables.py index 6ad216f..b4bef1f 100644 --- a/iptables.py +++ b/iptables.py @@ -3,7 +3,7 @@ import helpers from helpers import * -def chain_exists(name): +def ipt_chain_exists(name): argv = ['iptables', '-t', 'nat', '-nL'] p = subprocess.Popen(argv, stdout = subprocess.PIPE) for line in p.stdout: @@ -22,11 +22,16 @@ def ipt(*args): 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 # basic cleanup/setup of chains - if chain_exists(chain): + if ipt_chain_exists(chain): ipt('-D', 'OUTPUT', '-j', chain) ipt('-D', 'PREROUTING', '-j', 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 -# proxying stuff. If subnets is empty, we just delete our sshuttle chain; -# otherwise we delete it, then make it from scratch. -# -# 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 ipfw_rule_exists(n): + argv = ['ipfw', 'list'] + 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. # -# This code is supposed to clean up after itself by deleting extra chains on +# This code is supposed to clean up after itself by deleting its rules on # 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 -# chains are mostly harmless. +# supercede it in the transproxy list, at least, so the leftover rules +# are hopefully harmless. def main(port, subnets): assert(port > 0) assert(port <= 65535) 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 # are both attached to stdout initially. Clone stdout into stdin so we # can read from it. os.dup2(1, 0) - debug1('iptables manager ready.\n') + debug1('firewall manager ready.\n') sys.stdout.write('READY\n') sys.stdout.flush() @@ -89,10 +174,10 @@ def main(port, subnets): if not line: return # parent died; nothing to do if line != 'GO\n': - raise Fatal('iptables: expected GO but got %r' % line) + raise Fatal('firewall: expected GO but got %r' % line) try: if line: - debug1('iptables manager: starting transproxy.\n') + debug1('firewall manager: starting transproxy.\n') do_it(port, subnets) sys.stdout.write('STARTED\n') @@ -111,7 +196,7 @@ def main(port, subnets): finally: try: - debug1('iptables manager: undoing changes.\n') + debug1('firewall manager: undoing changes.\n') except: pass do_it(port, []) @@ -71,14 +71,17 @@ class SockWrapper: def try_connect(self): if not self.connect_to: return # already connected - self.rsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) self.rsock.setblocking(False) try: self.rsock.connect(self.connect_to) + # connected successfully (Linux) self.connect_to = None except socket.error, e: if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]: 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]: # a "normal" kind of error self.connect_to = None @@ -387,6 +390,7 @@ class MuxWrapper(SockWrapper): def connect_dst(ip, port): debug2('Connecting to %s:%d\n' % (ip, port)) outsock = socket.socket() + outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) return SockWrapper(outsock, outsock, connect_to = (ip,port), peername = '%s:%d' % (ip,port)) |