diff --git a/client.py b/client.py index 05225a5..419e339 100644 --- a/client.py +++ b/client.py @@ -13,17 +13,40 @@ def original_dst(sock): return (ip,port) -def iptables_setup(port, subnets): - subnets_str = ['%s/%d' % (ip,width) for ip,width in subnets] - argv = (['sudo', sys.argv[0]] + - ['-v'] * (helpers.verbose or 0) + - ['--iptables', str(port)] + subnets_str) - rv = subprocess.call(argv) - if rv != 0: - raise Fatal('%r returned %d' % (argv, rv)) +class IPTables: + def __init__(self, port, subnets): + self.port = port + self.subnets = subnets + subnets_str = ['%s/%d' % (ip,width) for ip,width in subnets] + self.argv = (['sudo', sys.argv[0]] + + ['-v'] * (helpers.verbose or 0) + + ['--iptables', str(port)] + subnets_str) + self.p = subprocess.Popen(self.argv, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + line = self.p.stdout.readline() + self.check() + if line != 'READY\n': + raise Fatal('%r expected READY, got %r' % (self.argv, line)) + + def check(self): + rv = self.p.poll() + if rv: + raise Fatal('%r returned %d' % (self.argv, rv)) + + def start(self): + self.p.stdin.write('GO\n') + self.p.stdin.flush() + + def done(self): + self.p.stdin.close() + self.p.stdout.close() + rv = self.p.wait() + if rv: + raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) -def _main(listener, listenport, use_server, remotename, subnets): +def _main(listener, ipt, use_server, remotename): handlers = [] if use_server: if helpers.verbose >= 1: @@ -47,7 +70,7 @@ def _main(listener, listenport, use_server, remotename, subnets): # we definitely want to do this *after* starting ssh, or we might end # up intercepting the ssh connection! - iptables_setup(listenport, subnets) + ipt.start() def onaccept(): sock,srcip = listener.accept() @@ -118,7 +141,9 @@ def main(listenip, use_server, remotename, subnets): listenip = listener.getsockname() debug1('Listening on %r.\n' % (listenip,)) + ipt = IPTables(listenip[1], subnets) + try: - return _main(listener, listenip[1], use_server, remotename, subnets) + return _main(listener, ipt, use_server, remotename) finally: - iptables_setup(listenip[1], []) + ipt.done() diff --git a/iptables.py b/iptables.py index 8afdb81..77c4ecf 100644 --- a/iptables.py +++ b/iptables.py @@ -22,24 +22,7 @@ def ipt(*args): raise Exception('%r returned %d' % (argv, rv)) -# 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"). -# -# sshuttle 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 -# supercede it in the transproxy list, at least, so the leftover iptables -# chains are mostly harmless. -def main(port, subnets): - assert(port > 0) - assert(port <= 65535) - +def do_it(port, subnets): chain = 'sshuttle-%s' % port # basic cleanup/setup of chains @@ -63,4 +46,45 @@ def main(port, subnets): '--to-ports', str(port), '-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops ) - subnets_str = ['%s/%d' % (ip,width) for ip,width in 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"). +# +# 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 +# supercede it in the transproxy list, at least, so the leftover iptables +# chains are mostly harmless. +def main(port, subnets): + assert(port > 0) + assert(port <= 65535) + + debug1('iptables manager ready.\n') + sys.stdout.write('READY\n') + sys.stdout.flush() + + # ctrl-c shouldn't be passed along to me. When the main sshuttle dies, + # I'll die automatically. + os.setsid() + + # we wait until we get some input before creating the rules. That way, + # sshuttle can launch us as early as possible (and get sudo password + # authentication as early in the startup process as possible). + sys.stdin.readline() + try: + do_it(port, subnets) + + # Now we wait until EOF or any other kind of exception. We need + # to stay running so that we don't need a *second* password + # authentication at shutdown time - that cleanup is important! + while sys.stdin.readline(): + pass + finally: + do_it(port, []) diff --git a/main.py b/main.py index 1e4756b..e683ca6 100755 --- a/main.py +++ b/main.py @@ -83,5 +83,6 @@ except Fatal, e: log('fatal: %s\n' % e) sys.exit(99) except KeyboardInterrupt: - log('\nKeyboard interrupt: exiting.\n') + log('\n') + log('Keyboard interrupt: exiting.\n') sys.exit(1) diff --git a/ssh.py b/ssh.py index d778245..a60999f 100644 --- a/ssh.py +++ b/ssh.py @@ -32,7 +32,8 @@ def connect(rhost): os.setsid() s1a,s1b = os.dup(s1.fileno()), os.dup(s1.fileno()) s1.close() - 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) os.close(s1a) os.close(s1b) return p, s2