From 31534ba2a8aace0fc75248c311cc48ee9f339e5d Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 12 May 2011 15:23:47 +1000 Subject: [PATCH 01/90] Initial version. Add tproxy support. Also see http://groups.google.com/group/sshuttle/browse_thread/thread/e54d0916b79c1e4b. --- client.py | 39 ++++++++++++++++++-------- firewall.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++------ main.py | 56 +++++++++++++++++++++++++++++++------ server.py | 4 +-- ssnet.py | 7 +++-- 5 files changed, 153 insertions(+), 32 deletions(-) diff --git a/client.py b/client.py index 449a75a..1d78b63 100644 --- a/client.py +++ b/client.py @@ -12,6 +12,8 @@ def got_signal(signum, frame): _pidname = None +IP_TRANSPARENT = 19 + def check_daemon(pidfile): global _pidname _pidname = os.path.abspath(pidfile) @@ -96,15 +98,16 @@ def original_dst(sock): class FirewallClient: - def __init__(self, port, subnets_include, subnets_exclude, dnsport): + def __init__(self, port, subnets_include, subnets_exclude, dnsport, ipv6): self.port = port self.auto_nets = [] self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude self.dnsport = dnsport + self.ipv6 = ipv6 argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] + ['-v'] * (helpers.verbose or 0) + - ['--firewall', str(port), str(dnsport)]) + ['--firewall', str(port), str(dnsport), str(ipv6 or '0')]) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ @@ -176,7 +179,7 @@ def done(self): def _main(listener, fw, ssh_cmd, remotename, python, latency_control, - dnslistener, seed_hosts, auto_nets, + dnslistener, ipv6, seed_hosts, auto_nets, syslog, daemon): handlers = [] if helpers.verbose >= 1: @@ -188,7 +191,7 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control, try: (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python, stderr=ssyslog._p and ssyslog._p.stdin, - options=dict(latency_control=latency_control)) + options=dict(latency_control=latency_control, ipv6=ipv6)) except socket.error, e: if e.args[0] == errno.EPIPE: raise Fatal("failed to establish ssh session (1)") @@ -272,7 +275,10 @@ def onaccept(): return else: raise - dstip = original_dst(sock) + if ipv6: + dstip = sock.getsockname(); + else: + dstip = original_dst(sock) debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], dstip[0],dstip[1])) if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]): @@ -284,7 +290,7 @@ def onaccept(): log('warning: too many open channels. Discarded connection.\n') sock.close() return - mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip) + mux.send(chan, ssnet.CMD_CONNECT, '%s,%r' % (dstip[0], dstip[1])) outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) handlers.append(Handler([listener], onaccept)) @@ -329,7 +335,7 @@ def ondns(): def main(listenip, ssh_cmd, remotename, python, latency_control, dns, - seed_hosts, auto_nets, + ipv6, seed_hosts, auto_nets, subnets_include, subnets_exclude, syslog, daemon, pidfile): if syslog: ssyslog.start_syslog() @@ -350,9 +356,19 @@ def main(listenip, ssh_cmd, remotename, python, latency_control, dns, debug2('Binding:') for port in ports: debug2(' %d' % port) - listener = socket.socket() + if ipv6: + listener = socket.socket(socket.AF_INET6) + else: + listener = socket.socket(socket.AF_INET) listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + except socket.error, e: + last_e = e + if ipv6: + dnslistener = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + else: + dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: listener.bind((listenip[0], port)) @@ -377,12 +393,13 @@ def main(listenip, ssh_cmd, remotename, python, latency_control, dns, dnsport = 0 dnslistener = None - fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport) + fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport, ipv6) try: return _main(listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, - seed_hosts, auto_nets, syslog, daemon) + ipv6, seed_hosts, auto_nets, syslog, + daemon) finally: try: if daemon: diff --git a/firewall.py b/firewall.py index 4fd8c79..84112ea 100644 --- a/firewall.py +++ b/firewall.py @@ -14,8 +14,14 @@ def nonfatal(func, *args): log('error: %s\n' % e) -def ipt_chain_exists(name): - argv = ['iptables', '-t', 'nat', '-nL'] +def ipt_chain_exists(name, ipv6): + if ipv6: + table = 'mangle' + cmd = 'ip6tables' + else: + table = 'nat' + cmd = 'iptables' + argv = [cmd, '-t', table, '-nL'] p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) for line in p.stdout: if line.startswith('Chain %s ' % name): @@ -32,6 +38,13 @@ def ipt(*args): if rv: raise Fatal('%r returned %d' % (argv, rv)) +def ipt6(*args): + argv = ['ip6tables', '-t', 'mangle'] + list(args) + debug1('>> %s\n' % ' '.join(argv)) + rv = ssubprocess.call(argv) + if rv: + raise Fatal('%r returned %d' % (argv, rv)) + _no_ttl_module = False def ipt_ttl(*args): @@ -59,11 +72,11 @@ def ipt_ttl(*args): # 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, dnsport, subnets): +def do_iptables(port, dnsport, ipv6, subnets): chain = 'sshuttle-%s' % port # basic cleanup/setup of chains - if ipt_chain_exists(chain): + if ipt_chain_exists(chain, ipv6): nonfatal(ipt, '-D', 'OUTPUT', '-j', chain) nonfatal(ipt, '-D', 'PREROUTING', '-j', chain) nonfatal(ipt, '-F', chain) @@ -101,6 +114,51 @@ def do_iptables(port, dnsport, subnets): '--dport', '53', '--to-ports', str(dnsport)) +def do_ip6tables(port, dnsport, ipv6, subnets): + mark_chain = 'sshuttle-m-%s' % port + tproxy_chain = 'sshuttle-t-%s' % port + + # basic cleanup/setup of chains + if ipt_chain_exists(mark_chain, ipv6): + ipt6('-D', 'OUTPUT', '-j', mark_chain) + ipt6('-F', mark_chain) + ipt6('-X', mark_chain) + + if ipt_chain_exists(tproxy_chain, ipv6): + ipt6('-D', 'PREROUTING', '-j', tproxy_chain) + ipt6('-F', tproxy_chain) + ipt6('-X', tproxy_chain) + + if subnets or dnsport: + ipt6('-N', mark_chain) + ipt6('-F', mark_chain) + ipt6('-N', tproxy_chain) + ipt6('-F', tproxy_chain) + ipt6('-I', 'OUTPUT', '1', '-j', mark_chain) + ipt6('-I', 'PREROUTING', '1', '-j', tproxy_chain) + ipt6('-A', tproxy_chain, '-m', 'socket', '-j', 'RETURN', + '-m', 'tcp', '-p', 'tcp') + + #ipt6('-A', mark_chain, '-o', 'lo', '-j', 'RETURN') + + if subnets: + for swidth,sexclude,snet in sorted(subnets, reverse=True): + if sexclude: + ipt6('-A', mark_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'tcp', '-p', 'tcp') + ipt6('-A', tproxy_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'tcp', '-p', 'tcp') + else: + ipt6('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'tcp', '-p', 'tcp') + ipt6('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'tcp', '-p', 'tcp', + '--on-port', str(port)) + def ipfw_rule_exists(n): argv = ['ipfw', 'list'] @@ -207,7 +265,7 @@ def ipfw(*args): raise Fatal('%r returned %d' % (argv, rv)) -def do_ipfw(port, dnsport, subnets): +def do_ipfw(port, dnsport, ipv6, subnets): sport = str(port) xsport = str(port+1) @@ -369,7 +427,7 @@ def restore_etc_hosts(port): # 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 rules # are hopefully harmless. -def main(port, dnsport, syslog): +def main(port, dnsport, ipv6, syslog): assert(port > 0) assert(port <= 65535) assert(dnsport >= 0) @@ -381,7 +439,10 @@ def main(port, dnsport, syslog): if program_exists('ipfw'): do_it = do_ipfw elif program_exists('iptables'): - do_it = do_iptables + if ipv6: + do_it = do_ip6tables + else: + do_it = do_iptables else: raise Fatal("can't find either ipfw or iptables; check your PATH") @@ -427,7 +488,7 @@ def main(port, dnsport, syslog): try: if line: debug1('firewall manager: starting transproxy.\n') - do_wait = do_it(port, dnsport, subnets) + do_wait = do_it(port, dnsport, ipv6, subnets) sys.stdout.write('STARTED\n') try: @@ -456,5 +517,5 @@ def main(port, dnsport, syslog): debug1('firewall manager: undoing changes.\n') except: pass - do_it(port, 0, []) + do_it(port, 0, ipv6, []) restore_etc_hosts(port) diff --git a/main.py b/main.py index 1cf00af..1963227 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,23 @@ def parse_subnets(subnets_str): subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width)) return subnets +# list of: +# 1:2::3/64 or just 1:2::3 +def parse_subnets6(subnets_str): + subnets = [] + for s in subnets_str: + m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s) + if not m: + raise Fatal('%r is not a valid IP subnet format' % s) + (net,width) = m.groups() + if width == None: + width = 128 + else: + width = int(width) + if width > 128: + raise Fatal('*/%d is greater than the maximum of 128' % width) + subnets.append((net, width)) + return subnets # 1.2.3.4:567 or just 1.2.3.4 or just 567 def parse_ipport(s): @@ -43,6 +60,16 @@ def parse_ipport(s): a = b = c = d = 0 return ('%d.%d.%d.%d' % (a,b,c,d), port) +# [1:2::3]:456 or [1:2::3] or 456 + +def parse_ipport6(s): + s = str(s) + m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s) + if not m: + raise Fatal('%s is not a valid IP:port format' % s) + (ip,port) = m.groups() + (ip,port) = (ip or '::', int(port or 0)) + return (ip, port) optspec = """ sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] @@ -50,10 +77,11 @@ def parse_ipport(s): sshuttle --firewall sshuttle --hostwatch -- -l,listen= transproxy to this ip address and port number [127.0.0.1:0] +l,listen= transproxy to this ip address and port number H,auto-hosts scan for remote hostnames and update local /etc/hosts N,auto-nets automatically determine subnets to route dns capture local DNS requests and forward to the remote DNS server +6,ipv6 mode for dealing with IPv6 python= path to python interpreter on the remote server r,remote= ssh hostname (and optional username) of remote sshuttle server x,exclude= exclude this subnet (can be used more than once) @@ -84,18 +112,23 @@ def parse_ipport(s): if len(extra) != 0: o.fatal('no arguments expected') server.latency_control = opt.latency_control + server.ipv6 = opt.ipv6 sys.exit(server.main()) elif opt.firewall: - if len(extra) != 2: - o.fatal('exactly two arguments expected') - sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog)) + if len(extra) != 3: + o.fatal('exactly three arguments expected') + sys.exit(firewall.main(int(extra[0]), int(extra[1]), + int(extra[2]), opt.syslog)) elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: if len(extra) < 1 and not opt.auto_nets: o.fatal('at least one subnet (or -N) expected') includes = extra - excludes = ['127.0.0.0/8'] + if not opt.ipv6: + excludes = ['127.0.0.0/8'] + else: + excludes = [] #FIXME for k,v in flags: if k in ('-x','--exclude'): excludes.append(v) @@ -110,16 +143,23 @@ def parse_ipport(s): sh = [] else: sh = None - sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'), + if opt.ipv6: + ipport = parse_ipport6(opt.listen or '[::]:0') + do_parse_subnets = parse_subnets6 + else: + ipport = parse_ipport(opt.listen or '127.0.0.1:0') + do_parse_subnets = parse_subnets + sys.exit(client.main(ipport, opt.ssh_cmd, remotename, opt.python, opt.latency_control, opt.dns, + opt.ipv6, sh, opt.auto_nets, - parse_subnets(includes), - parse_subnets(excludes), + do_parse_subnets(includes), + do_parse_subnets(excludes), opt.syslog, opt.daemon, opt.pidfile)) except Fatal, e: log('fatal: %s\n' % e) diff --git a/server.py b/server.py index e1b327d..5c6a35d 100644 --- a/server.py +++ b/server.py @@ -166,7 +166,7 @@ def main(): else: helpers.logprefix = 'server: ' debug1('latency control setting = %r\n' % latency_control) - + debug1('IPv6 mode = %r\n' % ipv6) routes = list(list_routes()) debug1('available routes:\n') for r in routes: @@ -215,7 +215,7 @@ def got_host_req(data): def new_channel(channel, data): (dstip,dstport) = data.split(',', 1) dstport = int(dstport) - outwrap = ssnet.connect_dst(dstip,dstport) + outwrap = ssnet.connect_dst(dstip,dstport,ipv6) handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) mux.new_channel = new_channel diff --git a/ssnet.py b/ssnet.py index b6d73c2..7f4ef36 100644 --- a/ssnet.py +++ b/ssnet.py @@ -511,9 +511,12 @@ def got_packet(self, cmd, data): % (cmd, len(data))) -def connect_dst(ip, port): +def connect_dst(ip, port, ipv6): debug2('Connecting to %s:%d\n' % (ip, port)) - outsock = socket.socket() + if ipv6: + outsock = socket.socket(socket.AF_INET6) + else: + outsock = socket.socket(socket.AF_INET) outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) return SockWrapper(outsock, outsock, connect_to = (ip,port), From 2b68ea62d9989370a63bf3c1f30525c3337a8ca6 Mon Sep 17 00:00:00 2001 From: Brian May Date: Wed, 18 May 2011 14:37:17 +1000 Subject: [PATCH 02/90] Support IPv4 and IPv6 at same time. --- client.py | 153 ++++++++++++++++++++++++++++++++++++---------------- firewall.py | 137 +++++++++++++++++++++++++++------------------- helpers.py | 2 +- main.py | 99 +++++++++++++++++----------------- server.py | 3 +- ssh.py | 7 --- ssnet.py | 9 ++-- 7 files changed, 248 insertions(+), 162 deletions(-) diff --git a/client.py b/client.py index 1d78b63..36ee9ad 100644 --- a/client.py +++ b/client.py @@ -97,17 +97,67 @@ def original_dst(sock): raise +class independent_listener: + + def __init__(self, type=socket.SOCK_STREAM, proto=0): + self.v6 = socket.socket(socket.AF_INET6, type, proto) + self.v4 = socket.socket(socket.AF_INET, type, proto) + + def setsockopt(self, level, optname, value): + if self.v6: + self.v6.setsockopt(level, optname, value) + if self.v4: + self.v4.setsockopt(level, optname, value) + + def add_handler(self, handlers, handler): + if self.v6: + handlers.append(Handler([self.v6], lambda: handler(self.v6))) + if self.v4: + handlers.append(Handler([self.v4], lambda: handler(self.v4))) + + def listen(self, backlog): + if self.v6: + self.v6.listen(backlog) + if self.v4: + try: + self.v4.listen(backlog) + except socket.error, e: + # on some systems v4 bind will fail if the v6 suceeded, + # in this case the v6 socket will receive v4 too. + if e.errno == errno.EADDRINUSE and self.v6: + self.v4 = None + else: + raise e + + def bind(self, address_v4, address_v6): + if address_v6 and self.v6: + self.v6.bind(address_v6) + else: + self.v6 = None + if address_v4 and self.v4: + self.v4.bind(address_v4) + else: + self.v4 = None + + def print_listening(self, what): + if self.v6: + listenip = self.v6.getsockname() + debug1('%s listening on %r.\n' % (what, listenip, )) + if self.v4: + listenip = self.v4.getsockname() + debug1('%s listening on %r.\n' % (what, listenip, )) + class FirewallClient: - def __init__(self, port, subnets_include, subnets_exclude, dnsport, ipv6): + def __init__(self, port, subnets_include, subnets_exclude, dnsport, tproxy): self.port = port self.auto_nets = [] self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude self.dnsport = dnsport - self.ipv6 = ipv6 + self.tproxy = tproxy argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] + ['-v'] * (helpers.verbose or 0) + - ['--firewall', str(port), str(dnsport), str(ipv6 or '0')]) + ['--firewall', str(port), str(dnsport), str(tproxy or 0)]) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ @@ -154,10 +204,10 @@ def check(self): def start(self): self.pfile.write('ROUTES\n') - for (ip,width) in self.subnets_include+self.auto_nets: - self.pfile.write('%d,0,%s\n' % (width, ip)) - for (ip,width) in self.subnets_exclude: - self.pfile.write('%d,1,%s\n' % (width, ip)) + for (family,ip,width) in self.subnets_include+self.auto_nets: + self.pfile.write('%d,%d,0,%s\n' % (family, width, ip)) + for (family,ip,width) in self.subnets_exclude: + self.pfile.write('%d,%d,1,%s\n' % (family, width, ip)) self.pfile.write('GO\n') self.pfile.flush() line = self.pfile.readline() @@ -179,7 +229,7 @@ def done(self): def _main(listener, fw, ssh_cmd, remotename, python, latency_control, - dnslistener, ipv6, seed_hosts, auto_nets, + dnslistener, tproxy, seed_hosts, auto_nets, syslog, daemon): handlers = [] if helpers.verbose >= 1: @@ -191,7 +241,7 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control, try: (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python, stderr=ssyslog._p and ssyslog._p.stdin, - options=dict(latency_control=latency_control, ipv6=ipv6)) + options=dict(latency_control=latency_control, tproxy=tproxy)) except socket.error, e: if e.args[0] == errno.EPIPE: raise Fatal("failed to establish ssh session (1)") @@ -258,30 +308,30 @@ def onhostlist(hostlist): fw.sethostip(name, ip) mux.got_host_list = onhostlist - def onaccept(): + def onaccept(listener_sock): global _extra_fd try: - sock,srcip = listener.accept() + sock,srcip = listener_sock.accept() except socket.error, e: if e.args[0] in [errno.EMFILE, errno.ENFILE]: debug1('Rejected incoming connection: too many open files!\n') # free up an fd so we can eat the connection os.close(_extra_fd) try: - sock,srcip = listener.accept() + sock,srcip = listener_sock.accept() sock.close() finally: _extra_fd = os.open('/dev/null', os.O_RDONLY) return else: raise - if ipv6: + if tproxy: dstip = sock.getsockname(); else: dstip = original_dst(sock) debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], dstip[0],dstip[1])) - if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]): + if dstip[1] == sock.getsockname()[1] and islocal(dstip[0]): debug1("-- ignored: that's my address!\n") sock.close() return @@ -293,23 +343,23 @@ def onaccept(): mux.send(chan, ssnet.CMD_CONNECT, '%s,%r' % (dstip[0], dstip[1])) outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) - handlers.append(Handler([listener], onaccept)) + listener.add_handler(handlers, onaccept) dnsreqs = {} def dns_done(chan, data): - peer,timeout = dnsreqs.get(chan) or (None,None) + peer,sock,timeout = dnsreqs.get(chan) or (None,None) debug3('dns_done: channel=%r peer=%r\n' % (chan, peer)) if peer: del dnsreqs[chan] debug3('doing sendto %r\n' % (peer,)) - dnslistener.sendto(data, peer) - def ondns(): - pkt,peer = dnslistener.recvfrom(4096) + sock.sendto(data, peer) + def ondns(listener_sock): + pkt,peer = listener_sock.recvfrom(4096) now = time.time() if pkt: debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt))) chan = mux.next_channel() - dnsreqs[chan] = peer,now+30 + dnsreqs[chan] = peer,listener_sock,now+30 mux.send(chan, ssnet.CMD_DNS_REQ, pkt) mux.channels[chan] = lambda cmd,data: dns_done(chan,data) for chan,(peer,timeout) in dnsreqs.items(): @@ -317,7 +367,7 @@ def ondns(): del dnsreqs[chan] debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) if dnslistener: - handlers.append(Handler([dnslistener], ondns)) + dnslistener.add_handler(handlers, ondsn) if seed_hosts != None: debug1('seed_hosts: %r\n' % seed_hosts) @@ -334,8 +384,9 @@ def ondns(): mux.callback() -def main(listenip, ssh_cmd, remotename, python, latency_control, dns, - ipv6, seed_hosts, auto_nets, +def main(listenip_v6, listenip_v4, + ssh_cmd, remotename, python, latency_control, dns, + tproxy, seed_hosts, auto_nets, subnets_include, subnets_exclude, syslog, daemon, pidfile): if syslog: ssyslog.start_syslog() @@ -347,58 +398,70 @@ def main(listenip, ssh_cmd, remotename, python, latency_control, dns, return 5 debug1('Starting sshuttle proxy.\n') - if listenip[1]: + if listenip_v4[1]: ports = [listenip[1]] else: ports = xrange(12300,9000,-1) last_e = None + redirectport = None bound = False debug2('Binding:') for port in ports: debug2(' %d' % port) - if ipv6: - listener = socket.socket(socket.AF_INET6) - else: - listener = socket.socket(socket.AF_INET) + listener = independent_listener() listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: + + if tproxy: listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - except socket.error, e: - last_e = e - if ipv6: - dnslistener = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) - else: - dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + dnslistener = independent_listener(socket.SOCK_DGRAM) dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: - listener.bind((listenip[0], port)) - dnslistener.bind((listenip[0], port)) + if listenip_v6: + lv6 = (listenip_v6[0],port) + else: + lv6 = None + if listenip_v4: + lv4 = (listenip_v4[0],port) + else: + lv4 = None + listener.bind(lv4, lv6) + dnslistener.bind(lv4, lv6) + redirectport = port bound = True break + except Fatal, e: + raise e except socket.error, e: - last_e = e + if e.errno == errno.EADDRNOTAVAIL: + last_e = e + else: + raise e debug2('\n') if not bound: assert(last_e) raise last_e listener.listen(10) - listenip = listener.getsockname() - debug1('Listening on %r.\n' % (listenip,)) + listener.print_listening("Redirector") if dns: - dnsip = dnslistener.getsockname() - debug1('DNS listening on %r.\n' % (dnsip,)) - dnsport = dnsip[1] + dnslistener.print_listening("DNS") + if dnslistener.v6: + dnsip = dnslistener.v6.getsockname() + dnsport = dnsip[1] + else: + dnsip = dnslistener.v4.getsockname() + dnsport = dnsip[1] else: dnsport = 0 dnslistener = None - fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport, ipv6) + fw = FirewallClient(redirectport, subnets_include, subnets_exclude, dnsport, tproxy) try: return _main(listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, - ipv6, seed_hosts, auto_nets, syslog, + tproxy, seed_hosts, auto_nets, syslog, daemon) finally: try: diff --git a/firewall.py b/firewall.py index 84112ea..7c3f36a 100644 --- a/firewall.py +++ b/firewall.py @@ -14,13 +14,15 @@ def nonfatal(func, *args): log('error: %s\n' % e) -def ipt_chain_exists(name, ipv6): - if ipv6: +def ipt_chain_exists(name, family): + if family == socket.AF_INET6: table = 'mangle' cmd = 'ip6tables' - else: + elif family == socket.AF_INET: table = 'nat' cmd = 'iptables' + else: + raise Fatal("Unsupported socket type '%s'"%family) argv = [cmd, '-t', table, '-nL'] p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) for line in p.stdout: @@ -31,7 +33,7 @@ def ipt_chain_exists(name, ipv6): raise Fatal('%r returned %d' % (argv, rv)) -def ipt(*args): +def ipt4(*args): argv = ['iptables', '-t', 'nat'] + list(args) debug1('>> %s\n' % ' '.join(argv)) rv = ssubprocess.call(argv) @@ -45,9 +47,17 @@ def ipt6(*args): if rv: raise Fatal('%r returned %d' % (argv, rv)) +def ipt(family, *args): + if family == socket.AF_INET6: + ipt6(*args) + elif family == socket.AF_INET: + ipt4(*args) + else: + raise Fatal("Unsupported socket type '%s'"%family) + _no_ttl_module = False -def ipt_ttl(*args): +def ipt_ttl(family, *args): global _no_ttl_module if not _no_ttl_module: # we avoid infinite loops by generating server-side connections @@ -55,15 +65,15 @@ def ipt_ttl(*args): # connections, in case client == server. try: argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42'] - ipt(*argsplus) + ipt(family, *argsplus) except Fatal: - ipt(*args) + ipt(family, *args) # we only get here if the non-ttl attempt succeeds log('sshuttle: warning: your iptables is missing ' 'the ttl module.\n') _no_ttl_module = True else: - ipt(*args) + ipt(family, *args) @@ -72,21 +82,25 @@ def ipt_ttl(*args): # 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, dnsport, ipv6, subnets): +def do_iptables_nat(port, dnsport, family, subnets): + # only ipv4 supported with NAT + if family != socket.AF_INET: + return + chain = 'sshuttle-%s' % port # basic cleanup/setup of chains - if ipt_chain_exists(chain, ipv6): - nonfatal(ipt, '-D', 'OUTPUT', '-j', chain) - nonfatal(ipt, '-D', 'PREROUTING', '-j', chain) - nonfatal(ipt, '-F', chain) - ipt('-X', chain) + if ipt_chain_exists(chain, family): + nonfatal(ipt, family, '-D', 'OUTPUT', '-j', chain) + nonfatal(ipt, family, '-D', 'PREROUTING', '-j', chain) + nonfatal(ipt, family, '-F', chain) + ipt(family, '-X', chain) if subnets or dnsport: - ipt('-N', chain) - ipt('-F', chain) - ipt('-I', 'OUTPUT', '1', '-j', chain) - ipt('-I', 'PREROUTING', '1', '-j', chain) + ipt(family, '-N', chain) + ipt(family, '-F', chain) + ipt(family, '-I', 'OUTPUT', '1', '-j', chain) + ipt(family, '-I', 'PREROUTING', '1', '-j', chain) if subnets: # create new subnet entries. Note that we're sorting in a very @@ -94,13 +108,15 @@ def do_iptables(port, dnsport, ipv6, subnets): # to least-specific, and at any given level of specificity, we want # excludes to come first. That's why the columns are in such a non- # intuitive order. - for swidth,sexclude,snet in sorted(subnets, reverse=True): - if sexclude: - ipt('-A', chain, '-j', 'RETURN', + for f,swidth,sexclude,snet in sorted(subnets, reverse=True): + if f != family: + pass + elif sexclude: + ipt(family, '-A', chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp') else: - ipt_ttl('-A', chain, '-j', 'REDIRECT', + ipt_ttl(family, '-A', chain, '-j', 'REDIRECT', '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp', '--to-ports', str(port)) @@ -108,53 +124,58 @@ def do_iptables(port, dnsport, ipv6, subnets): if dnsport: nslist = resolvconf_nameservers() for ip in nslist: - ipt_ttl('-A', chain, '-j', 'REDIRECT', + ipt_ttl(family, '-A', chain, '-j', 'REDIRECT', '--dest', '%s/32' % ip, '-p', 'udp', '--dport', '53', '--to-ports', str(dnsport)) -def do_ip6tables(port, dnsport, ipv6, subnets): +def do_ip6tables_tproxy(port, dnsport, family, subnets): + if family not in [socket.AF_INET, socket.AF_INET6]: + return + mark_chain = 'sshuttle-m-%s' % port tproxy_chain = 'sshuttle-t-%s' % port # basic cleanup/setup of chains - if ipt_chain_exists(mark_chain, ipv6): - ipt6('-D', 'OUTPUT', '-j', mark_chain) - ipt6('-F', mark_chain) - ipt6('-X', mark_chain) + if ipt_chain_exists(mark_chain, family): + ipt(family, '-D', 'OUTPUT', '-j', mark_chain) + ipt(family, '-F', mark_chain) + ipt(family, '-X', mark_chain) - if ipt_chain_exists(tproxy_chain, ipv6): - ipt6('-D', 'PREROUTING', '-j', tproxy_chain) - ipt6('-F', tproxy_chain) - ipt6('-X', tproxy_chain) + if ipt_chain_exists(tproxy_chain, family): + ipt(family, '-D', 'PREROUTING', '-j', tproxy_chain) + ipt(family, '-F', tproxy_chain) + ipt(family, '-X', tproxy_chain) if subnets or dnsport: - ipt6('-N', mark_chain) - ipt6('-F', mark_chain) - ipt6('-N', tproxy_chain) - ipt6('-F', tproxy_chain) - ipt6('-I', 'OUTPUT', '1', '-j', mark_chain) - ipt6('-I', 'PREROUTING', '1', '-j', tproxy_chain) - ipt6('-A', tproxy_chain, '-m', 'socket', '-j', 'RETURN', + ipt(family, '-N', mark_chain) + ipt(family, '-F', mark_chain) + ipt(family, '-N', tproxy_chain) + ipt(family, '-F', tproxy_chain) + ipt(family, '-I', 'OUTPUT', '1', '-j', mark_chain) + ipt(family, '-I', 'PREROUTING', '1', '-j', tproxy_chain) + ipt(family, '-A', tproxy_chain, '-m', 'socket', '-j', 'RETURN', '-m', 'tcp', '-p', 'tcp') - #ipt6('-A', mark_chain, '-o', 'lo', '-j', 'RETURN') + #ipt(family, '-A', mark_chain, '-o', 'lo', '-j', 'RETURN') if subnets: - for swidth,sexclude,snet in sorted(subnets, reverse=True): - if sexclude: - ipt6('-A', mark_chain, '-j', 'RETURN', + for f,swidth,sexclude,snet in sorted(subnets, reverse=True): + if f != family: + pass + elif sexclude: + ipt(family, '-A', mark_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') - ipt6('-A', tproxy_chain, '-j', 'RETURN', + ipt(family, '-A', tproxy_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') else: - ipt6('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + ipt(family, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') - ipt6('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', + ipt(family, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp', '--on-port', str(port)) @@ -265,7 +286,11 @@ def ipfw(*args): raise Fatal('%r returned %d' % (argv, rv)) -def do_ipfw(port, dnsport, ipv6, subnets): +def do_ipfw(port, dnsport, family, subnets): + # IPv6 support TODO + if family != socket.AF_INET: + return + sport = str(port) xsport = str(port+1) @@ -427,7 +452,7 @@ def restore_etc_hosts(port): # 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 rules # are hopefully harmless. -def main(port, dnsport, ipv6, syslog): +def main(port, dnsport, tproxy, syslog): assert(port > 0) assert(port <= 65535) assert(dnsport >= 0) @@ -439,10 +464,10 @@ def main(port, dnsport, ipv6, syslog): if program_exists('ipfw'): do_it = do_ipfw elif program_exists('iptables'): - if ipv6: - do_it = do_ip6tables + if tproxy: + do_it = do_ip6tables_tproxy else: - do_it = do_iptables + do_it = do_iptables_nat else: raise Fatal("can't find either ipfw or iptables; check your PATH") @@ -480,15 +505,16 @@ def main(port, dnsport, ipv6, syslog): elif line == 'GO\n': break try: - (width,exclude,ip) = line.strip().split(',', 2) + (family,width,exclude,ip) = line.strip().split(',', 3) except: raise Fatal('firewall: expected route or GO but got %r' % line) - subnets.append((int(width), bool(int(exclude)), ip)) + subnets.append((int(family), int(width), bool(int(exclude)), ip)) try: if line: debug1('firewall manager: starting transproxy.\n') - do_wait = do_it(port, dnsport, ipv6, subnets) + do_wait = do_it(port, dnsport, socket.AF_INET6, subnets) + do_wait = do_it(port, dnsport, socket.AF_INET, subnets) sys.stdout.write('STARTED\n') try: @@ -517,5 +543,6 @@ def main(port, dnsport, ipv6, syslog): debug1('firewall manager: undoing changes.\n') except: pass - do_it(port, 0, ipv6, []) + do_it(port, 0, socket.AF_INET6, []) + do_it(port, 0, socket.AF_INET, []) restore_etc_hosts(port) diff --git a/helpers.py b/helpers.py index c169d0c..af49788 100644 --- a/helpers.py +++ b/helpers.py @@ -1,4 +1,4 @@ -import sys, os, socket +import sys, os, socket, errno logprefix = '' verbose = 0 diff --git a/main.py b/main.py index 1963227..13c7f6b 100644 --- a/main.py +++ b/main.py @@ -1,50 +1,53 @@ -import sys, os, re +import sys, os, re, socket import helpers, options, client, server, firewall, hostwatch import compat.ssubprocess as ssubprocess from helpers import * -# list of: -# 1.2.3.4/5 or just 1.2.3.4 -def parse_subnets(subnets_str): - subnets = [] - for s in subnets_str: - m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s) - if not m: - raise Fatal('%r is not a valid IP subnet format' % s) - (a,b,c,d,width) = m.groups() - (a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0)) - if width == None: - width = 32 - else: - width = int(width) - if a > 255 or b > 255 or c > 255 or d > 255: - raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d)) - if width > 32: - raise Fatal('*/%d is greater than the maximum of 32' % width) - subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width)) - return subnets +def parse_subnet4(s): + m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s) + if not m: + raise Fatal('%r is not a valid IP subnet format' % s) + (a,b,c,d,width) = m.groups() + (a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0)) + if width == None: + width = 32 + else: + width = int(width) + if a > 255 or b > 255 or c > 255 or d > 255: + raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d)) + if width > 32: + raise Fatal('*/%d is greater than the maximum of 32' % width) + return(socket.AF_INET, '%d.%d.%d.%d' % (a,b,c,d), width) + +def parse_subnet6(s): + m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s) + if not m: + raise Fatal('%r is not a valid IP subnet format' % s) + (net,width) = m.groups() + if width == None: + width = 128 + else: + width = int(width) + if width > 128: + raise Fatal('*/%d is greater than the maximum of 128' % width) + return(socket.AF_INET6, net, width) # list of: +# 1.2.3.4/5 or just 1.2.3.4 # 1:2::3/64 or just 1:2::3 -def parse_subnets6(subnets_str): +def parse_subnets(subnets_str): subnets = [] for s in subnets_str: - m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s) - if not m: - raise Fatal('%r is not a valid IP subnet format' % s) - (net,width) = m.groups() - if width == None: - width = 128 + if ':' in s: + subnet = parse_subnet6(s) else: - width = int(width) - if width > 128: - raise Fatal('*/%d is greater than the maximum of 128' % width) - subnets.append((net, width)) + subnet = parse_subnet4(s) + subnets.append(subnet) return subnets # 1.2.3.4:567 or just 1.2.3.4 or just 567 -def parse_ipport(s): +def parse_ipport4(s): s = str(s) m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s) if not m: @@ -81,7 +84,7 @@ def parse_ipport6(s): H,auto-hosts scan for remote hostnames and update local /etc/hosts N,auto-nets automatically determine subnets to route dns capture local DNS requests and forward to the remote DNS server -6,ipv6 mode for dealing with IPv6 +tproxy tproxy support python= path to python interpreter on the remote server r,remote= ssh hostname (and optional username) of remote sshuttle server x,exclude= exclude this subnet (can be used more than once) @@ -112,7 +115,7 @@ def parse_ipport6(s): if len(extra) != 0: o.fatal('no arguments expected') server.latency_control = opt.latency_control - server.ipv6 = opt.ipv6 + server.tproxy = opt.tproxy sys.exit(server.main()) elif opt.firewall: if len(extra) != 3: @@ -125,10 +128,7 @@ def parse_ipport6(s): if len(extra) < 1 and not opt.auto_nets: o.fatal('at least one subnet (or -N) expected') includes = extra - if not opt.ipv6: - excludes = ['127.0.0.0/8'] - else: - excludes = [] #FIXME + excludes = ['127.0.0.0/8'] for k,v in flags: if k in ('-x','--exclude'): excludes.append(v) @@ -143,23 +143,26 @@ def parse_ipport6(s): sh = [] else: sh = None - if opt.ipv6: - ipport = parse_ipport6(opt.listen or '[::]:0') - do_parse_subnets = parse_subnets6 + if not opt.listen: + ipport_v6 = parse_ipport6('[::]:0') + ipport_v4 = parse_ipport4('127.0.0.1:0') + elif ':' in opt.listen: + ipport_v6 = parse_ipport6(opt.listen or '[::]:0') + ipport_v4 = None else: - ipport = parse_ipport(opt.listen or '127.0.0.1:0') - do_parse_subnets = parse_subnets - sys.exit(client.main(ipport, + ipport_v6 = None + ipport_v4 = parse_ipport4(opt.listen or '127.0.0.1:0') + sys.exit(client.main(ipport_v6, ipport_v4, opt.ssh_cmd, remotename, opt.python, opt.latency_control, opt.dns, - opt.ipv6, + opt.tproxy, sh, opt.auto_nets, - do_parse_subnets(includes), - do_parse_subnets(excludes), + parse_subnets(includes), + parse_subnets(excludes), opt.syslog, opt.daemon, opt.pidfile)) except Fatal, e: log('fatal: %s\n' % e) diff --git a/server.py b/server.py index 5c6a35d..3e6c7a4 100644 --- a/server.py +++ b/server.py @@ -166,7 +166,6 @@ def main(): else: helpers.logprefix = 'server: ' debug1('latency control setting = %r\n' % latency_control) - debug1('IPv6 mode = %r\n' % ipv6) routes = list(list_routes()) debug1('available routes:\n') for r in routes: @@ -215,7 +214,7 @@ def got_host_req(data): def new_channel(channel, data): (dstip,dstport) = data.split(',', 1) dstport = int(dstport) - outwrap = ssnet.connect_dst(dstip,dstport,ipv6) + outwrap = ssnet.connect_dst(dstip,dstport) handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) mux.new_channel = new_channel diff --git a/ssh.py b/ssh.py index c4bf06e..85d96b4 100644 --- a/ssh.py +++ b/ssh.py @@ -27,9 +27,7 @@ def connect(ssh_cmd, rhostport, python, stderr, options): main_exe = sys.argv[0] portl = [] - rhostIsIPv6 = False if (rhostport or '').count(':') > 1: - rhostIsIPv6 = True if rhostport.count(']') or rhostport.count('['): result = rhostport.split(']') rhost = result[0].strip('[') @@ -48,10 +46,6 @@ def connect(ssh_cmd, rhostport, python, stderr, options): if rhost == '-': rhost = None - ipv6flag = [] - if rhostIsIPv6: - ipv6flag = ['-6'] - z = zlib.compressobj(1) content = readfile('assembler.py') optdata = ''.join("%s=%r\n" % (k,v) for (k,v) in options.items()) @@ -88,7 +82,6 @@ def connect(ssh_cmd, rhostport, python, stderr, options): "exec \"$P\" -c '%s'") % pyscript argv = (sshl + portl + - ipv6flag + [rhost, '--', pycmd]) (s1,s2) = socket.socketpair() def setup(): diff --git a/ssnet.py b/ssnet.py index 7f4ef36..50a99d8 100644 --- a/ssnet.py +++ b/ssnet.py @@ -511,12 +511,13 @@ def got_packet(self, cmd, data): % (cmd, len(data))) -def connect_dst(ip, port, ipv6): +def connect_dst(ip, port): debug2('Connecting to %s:%d\n' % (ip, port)) - if ipv6: - outsock = socket.socket(socket.AF_INET6) + if ':' in ip: + family = socket.AF_INET6 else: - outsock = socket.socket(socket.AF_INET) + family = socket.AF_INET + outsock = socket.socket(family) outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) return SockWrapper(outsock, outsock, connect_to = (ip,port), From 0ffbb9db06b78a85f18151e2ac039011daab8d34 Mon Sep 17 00:00:00 2001 From: Brian May Date: Wed, 18 May 2011 15:22:06 +1000 Subject: [PATCH 03/90] iptables table name varies depending on nat or tproxy, not ipv4 or ipv6. --- firewall.py | 68 +++++++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/firewall.py b/firewall.py index 7c3f36a..b7f289c 100644 --- a/firewall.py +++ b/firewall.py @@ -33,15 +33,15 @@ def ipt_chain_exists(name, family): raise Fatal('%r returned %d' % (argv, rv)) -def ipt4(*args): - argv = ['iptables', '-t', 'nat'] + list(args) +def ipt4(table, *args): + argv = ['iptables', '-t', table] + list(args) debug1('>> %s\n' % ' '.join(argv)) rv = ssubprocess.call(argv) if rv: raise Fatal('%r returned %d' % (argv, rv)) -def ipt6(*args): - argv = ['ip6tables', '-t', 'mangle'] + list(args) +def ipt6(table, *args): + argv = ['ip6tables', '-t', table] + list(args) debug1('>> %s\n' % ' '.join(argv)) rv = ssubprocess.call(argv) if rv: @@ -87,20 +87,21 @@ def do_iptables_nat(port, dnsport, family, subnets): if family != socket.AF_INET: return + table = "nat" chain = 'sshuttle-%s' % port # basic cleanup/setup of chains if ipt_chain_exists(chain, family): - nonfatal(ipt, family, '-D', 'OUTPUT', '-j', chain) - nonfatal(ipt, family, '-D', 'PREROUTING', '-j', chain) - nonfatal(ipt, family, '-F', chain) - ipt(family, '-X', chain) + nonfatal(ipt, family, table, '-D', 'OUTPUT', '-j', chain) + nonfatal(ipt, family, table, '-D', 'PREROUTING', '-j', chain) + nonfatal(ipt, family, table, '-F', chain) + ipt(family, table, '-X', chain) if subnets or dnsport: - ipt(family, '-N', chain) - ipt(family, '-F', chain) - ipt(family, '-I', 'OUTPUT', '1', '-j', chain) - ipt(family, '-I', 'PREROUTING', '1', '-j', chain) + ipt(family, table, '-N', chain) + ipt(family, table, '-F', chain) + ipt(family, table, '-I', 'OUTPUT', '1', '-j', chain) + ipt(family, table, '-I', 'PREROUTING', '1', '-j', chain) if subnets: # create new subnet entries. Note that we're sorting in a very @@ -112,11 +113,11 @@ def do_iptables_nat(port, dnsport, family, subnets): if f != family: pass elif sexclude: - ipt(family, '-A', chain, '-j', 'RETURN', + ipt(family, table, '-A', chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp') else: - ipt_ttl(family, '-A', chain, '-j', 'REDIRECT', + ipt_ttl(family, table, '-A', chain, '-j', 'REDIRECT', '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp', '--to-ports', str(port)) @@ -124,7 +125,7 @@ def do_iptables_nat(port, dnsport, family, subnets): if dnsport: nslist = resolvconf_nameservers() for ip in nslist: - ipt_ttl(family, '-A', chain, '-j', 'REDIRECT', + ipt_ttl(family, table, '-A', chain, '-j', 'REDIRECT', '--dest', '%s/32' % ip, '-p', 'udp', '--dport', '53', @@ -134,48 +135,49 @@ def do_ip6tables_tproxy(port, dnsport, family, subnets): if family not in [socket.AF_INET, socket.AF_INET6]: return + table = "nat" mark_chain = 'sshuttle-m-%s' % port tproxy_chain = 'sshuttle-t-%s' % port # basic cleanup/setup of chains if ipt_chain_exists(mark_chain, family): - ipt(family, '-D', 'OUTPUT', '-j', mark_chain) - ipt(family, '-F', mark_chain) - ipt(family, '-X', mark_chain) + ipt(family, table, '-D', 'OUTPUT', '-j', mark_chain) + ipt(family, table, '-F', mark_chain) + ipt(family, table, '-X', mark_chain) if ipt_chain_exists(tproxy_chain, family): - ipt(family, '-D', 'PREROUTING', '-j', tproxy_chain) - ipt(family, '-F', tproxy_chain) - ipt(family, '-X', tproxy_chain) + ipt(family, table, '-D', 'PREROUTING', '-j', tproxy_chain) + ipt(family, table, '-F', tproxy_chain) + ipt(family, table, '-X', tproxy_chain) if subnets or dnsport: - ipt(family, '-N', mark_chain) - ipt(family, '-F', mark_chain) - ipt(family, '-N', tproxy_chain) - ipt(family, '-F', tproxy_chain) - ipt(family, '-I', 'OUTPUT', '1', '-j', mark_chain) - ipt(family, '-I', 'PREROUTING', '1', '-j', tproxy_chain) - ipt(family, '-A', tproxy_chain, '-m', 'socket', '-j', 'RETURN', + ipt(family, table, '-N', mark_chain) + ipt(family, table, '-F', mark_chain) + ipt(family, table, '-N', tproxy_chain) + ipt(family, table, '-F', tproxy_chain) + ipt(family, table, '-I', 'OUTPUT', '1', '-j', mark_chain) + ipt(family, table, '-I', 'PREROUTING', '1', '-j', tproxy_chain) + ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', 'RETURN', '-m', 'tcp', '-p', 'tcp') - #ipt(family, '-A', mark_chain, '-o', 'lo', '-j', 'RETURN') + #ipt(family, table, '-A', mark_chain, '-o', 'lo', '-j', 'RETURN') if subnets: for f,swidth,sexclude,snet in sorted(subnets, reverse=True): if f != family: pass elif sexclude: - ipt(family, '-A', mark_chain, '-j', 'RETURN', + ipt(family, table, '-A', mark_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') - ipt(family, '-A', tproxy_chain, '-j', 'RETURN', + ipt(family, table, '-A', tproxy_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') else: - ipt(family, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') - ipt(family, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', + ipt(family, table, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp', '--on-port', str(port)) From eadfaa80256cbb1d58933f1855ed374dc308e95e Mon Sep 17 00:00:00 2001 From: Brian May Date: Wed, 18 May 2011 15:24:56 +1000 Subject: [PATCH 04/90] Got table name wrong in previous commit. --- firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firewall.py b/firewall.py index b7f289c..300fe2b 100644 --- a/firewall.py +++ b/firewall.py @@ -135,7 +135,7 @@ def do_ip6tables_tproxy(port, dnsport, family, subnets): if family not in [socket.AF_INET, socket.AF_INET6]: return - table = "nat" + table = "mangle" mark_chain = 'sshuttle-m-%s' % port tproxy_chain = 'sshuttle-t-%s' % port From 6a4b9ec210113cb1b5c6b0f07a09fcd69a7bf760 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 09:30:07 +1000 Subject: [PATCH 05/90] Fix islocal function, now works with IPv6. --- client.py | 2 +- helpers.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client.py b/client.py index 36ee9ad..7f59ae5 100644 --- a/client.py +++ b/client.py @@ -331,7 +331,7 @@ def onaccept(listener_sock): dstip = original_dst(sock) debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], dstip[0],dstip[1])) - if dstip[1] == sock.getsockname()[1] and islocal(dstip[0]): + if dstip[1] == sock.getsockname()[1] and islocal(dstip[0],sock.family): debug1("-- ignored: that's my address!\n") sock.close() return diff --git a/helpers.py b/helpers.py index af49788..b65c5b6 100644 --- a/helpers.py +++ b/helpers.py @@ -58,8 +58,8 @@ def resolvconf_random_nameserver(): return '127.0.0.1' -def islocal(ip): - sock = socket.socket() +def islocal(ip,family): + sock = socket.socket(family) try: try: sock.bind((ip, 0)) From 39bdff0e46f8ed1c3769752e439c14ca09eef427 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 09:30:24 +1000 Subject: [PATCH 06/90] Fix firewall cleanup. --- firewall.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/firewall.py b/firewall.py index 300fe2b..73b9b74 100644 --- a/firewall.py +++ b/firewall.py @@ -14,12 +14,10 @@ def nonfatal(func, *args): log('error: %s\n' % e) -def ipt_chain_exists(name, family): +def ipt_table_exists(name, family, table): if family == socket.AF_INET6: - table = 'mangle' cmd = 'ip6tables' elif family == socket.AF_INET: - table = 'nat' cmd = 'iptables' else: raise Fatal("Unsupported socket type '%s'"%family) @@ -91,7 +89,7 @@ def do_iptables_nat(port, dnsport, family, subnets): chain = 'sshuttle-%s' % port # basic cleanup/setup of chains - if ipt_chain_exists(chain, family): + if ipt_table_exists(chain, family, table): nonfatal(ipt, family, table, '-D', 'OUTPUT', '-j', chain) nonfatal(ipt, family, table, '-D', 'PREROUTING', '-j', chain) nonfatal(ipt, family, table, '-F', chain) @@ -131,7 +129,7 @@ def do_iptables_nat(port, dnsport, family, subnets): '--dport', '53', '--to-ports', str(dnsport)) -def do_ip6tables_tproxy(port, dnsport, family, subnets): +def do_iptables_tproxy(port, dnsport, family, subnets): if family not in [socket.AF_INET, socket.AF_INET6]: return @@ -140,12 +138,12 @@ def do_ip6tables_tproxy(port, dnsport, family, subnets): tproxy_chain = 'sshuttle-t-%s' % port # basic cleanup/setup of chains - if ipt_chain_exists(mark_chain, family): + if ipt_table_exists(mark_chain, family, table): ipt(family, table, '-D', 'OUTPUT', '-j', mark_chain) ipt(family, table, '-F', mark_chain) ipt(family, table, '-X', mark_chain) - if ipt_chain_exists(tproxy_chain, family): + if ipt_table_exists(tproxy_chain, family, table): ipt(family, table, '-D', 'PREROUTING', '-j', tproxy_chain) ipt(family, table, '-F', tproxy_chain) ipt(family, table, '-X', tproxy_chain) @@ -467,7 +465,7 @@ def main(port, dnsport, tproxy, syslog): do_it = do_ipfw elif program_exists('iptables'): if tproxy: - do_it = do_ip6tables_tproxy + do_it = do_iptables_tproxy else: do_it = do_iptables_nat else: From fd58960dc3b0e6f7896c6ac9766737a50c425ec2 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 09:53:14 +1000 Subject: [PATCH 07/90] Fix sorting. Excluded routes must be processed first. --- firewall.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/firewall.py b/firewall.py index 73b9b74..e92aed7 100644 --- a/firewall.py +++ b/firewall.py @@ -107,7 +107,7 @@ def do_iptables_nat(port, dnsport, family, subnets): # to least-specific, and at any given level of specificity, we want # excludes to come first. That's why the columns are in such a non- # intuitive order. - for f,swidth,sexclude,snet in sorted(subnets, reverse=True): + for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[2], reverse=True): if f != family: pass elif sexclude: @@ -161,7 +161,7 @@ def do_iptables_tproxy(port, dnsport, family, subnets): #ipt(family, table, '-A', mark_chain, '-o', 'lo', '-j', 'RETURN') if subnets: - for f,swidth,sexclude,snet in sorted(subnets, reverse=True): + for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[2], reverse=True): if f != family: pass elif sexclude: @@ -323,7 +323,7 @@ def do_ipfw(port, dnsport, family, subnets): if subnets: # create new subnet entries - for swidth,sexclude,snet in sorted(subnets, reverse=True): + for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[2], reverse=True): if sexclude: ipfw('add', sport, 'skipto', xsport, 'log', 'tcp', From ae552bf0fd994731997c4c6f27f43dc94cc46e1f Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 11:02:15 +1000 Subject: [PATCH 08/90] Fix IPv4 only test. --- ssnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssnet.py b/ssnet.py index 50a99d8..dfebe44 100644 --- a/ssnet.py +++ b/ssnet.py @@ -128,7 +128,7 @@ def try_connect(self): return # already connected self.rsock.setblocking(False) debug3('%r: trying connect to %r\n' % (self, self.connect_to)) - if socket.inet_aton(self.connect_to[0])[0] == '\0': + if self.rsock.family==socket.AF_INET and socket.inet_pton(self.rsock.family, self.connect_to[0])[0] == '\0': self.seterr(Exception("Can't connect to %r: " "IP address starts with zero\n" % (self.connect_to,))) From 10247a0147cf57ff2c6fbfd3118c8d5d0e8b3ef5 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 11:10:04 +1000 Subject: [PATCH 09/90] Fix stupid typo introduced by 2b68ea62d9989370a63bf3c1f30525c3337a8ca6. --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index 7f59ae5..417623a 100644 --- a/client.py +++ b/client.py @@ -367,7 +367,7 @@ def ondns(listener_sock): del dnsreqs[chan] debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) if dnslistener: - dnslistener.add_handler(handlers, ondsn) + dnslistener.add_handler(handlers, ondns) if seed_hosts != None: debug1('seed_hosts: %r\n' % seed_hosts) From edabb923dddcd6c56de41e9c3cca04c5440c5f2b Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 11:39:32 +1000 Subject: [PATCH 10/90] New function to guess address family from IP address. --- helpers.py | 5 +++++ ssnet.py | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/helpers.py b/helpers.py index b65c5b6..4b684fa 100644 --- a/helpers.py +++ b/helpers.py @@ -73,3 +73,8 @@ def islocal(ip,family): return True # it's a local IP, or there would have been an error +def guess_address_family(ip): + if ':' in ip: + return socket.AF_INET6 + else: + return socket.AF_INET diff --git a/ssnet.py b/ssnet.py index dfebe44..002df94 100644 --- a/ssnet.py +++ b/ssnet.py @@ -513,10 +513,7 @@ def got_packet(self, cmd, data): def connect_dst(ip, port): debug2('Connecting to %s:%d\n' % (ip, port)) - if ':' in ip: - family = socket.AF_INET6 - else: - family = socket.AF_INET + family = guess_address_family(ip) outsock = socket.socket(family) outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) return SockWrapper(outsock, outsock, From 86d71313343cd4d4f09c96c9305bca1cf10bfd30 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 11:40:37 +1000 Subject: [PATCH 11/90] Fix error in DNS processing. --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index 417623a..37c4070 100644 --- a/client.py +++ b/client.py @@ -362,7 +362,7 @@ def ondns(listener_sock): dnsreqs[chan] = peer,listener_sock,now+30 mux.send(chan, ssnet.CMD_DNS_REQ, pkt) mux.channels[chan] = lambda cmd,data: dns_done(chan,data) - for chan,(peer,timeout) in dnsreqs.items(): + for chan,(peer,sock,timeout) in dnsreqs.items(): if timeout < now: del dnsreqs[chan] debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) From cccce91fc8efbbef2888773cc94c0dd756ac69df Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 11:41:14 +1000 Subject: [PATCH 12/90] Fix handling of IPv4/IPv6 DNS servers. --- firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firewall.py b/firewall.py index e92aed7..48fa947 100644 --- a/firewall.py +++ b/firewall.py @@ -122,7 +122,7 @@ def do_iptables_nat(port, dnsport, family, subnets): if dnsport: nslist = resolvconf_nameservers() - for ip in nslist: + for ip in filter(lambda i: guess_address_family(i)==family, nslist): ipt_ttl(family, table, '-A', chain, '-j', 'REDIRECT', '--dest', '%s/32' % ip, '-p', 'udp', From 63cc20edbd1ac78b2ac119cb107e86272b98959c Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 11:42:22 +1000 Subject: [PATCH 13/90] Prepare for UDP handling. DNS port unlinked from redirector port. --- client.py | 77 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/client.py b/client.py index 37c4070..25c185a 100644 --- a/client.py +++ b/client.py @@ -228,7 +228,7 @@ def done(self): raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) -def _main(listener, fw, ssh_cmd, remotename, python, latency_control, +def _main(tcp_listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, tproxy, seed_hosts, auto_nets, syslog, daemon): handlers = [] @@ -343,7 +343,7 @@ def onaccept(listener_sock): mux.send(chan, ssnet.CMD_CONNECT, '%s,%r' % (dstip[0], dstip[1])) outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) - listener.add_handler(handlers, onaccept) + tcp_listener.add_handler(handlers, onaccept) dnsreqs = {} def dns_done(chan, data): @@ -405,28 +405,26 @@ def main(listenip_v6, listenip_v4, last_e = None redirectport = None bound = False - debug2('Binding:') + debug2('Binding redirector:') for port in ports: debug2(' %d' % port) - listener = independent_listener() - listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + tcp_listener = independent_listener() + tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if tproxy: - listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + + if listenip_v6: + lv6 = (listenip_v6[0],port) + else: + lv6 = None + if listenip_v4: + lv4 = (listenip_v4[0],port) + else: + lv4 = None - dnslistener = independent_listener(socket.SOCK_DGRAM) - dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: - if listenip_v6: - lv6 = (listenip_v6[0],port) - else: - lv6 = None - if listenip_v4: - lv4 = (listenip_v4[0],port) - else: - lv4 = None - listener.bind(lv4, lv6) - dnslistener.bind(lv4, lv6) + tcp_listener.bind(lv4, lv6) redirectport = port bound = True break @@ -441,17 +439,42 @@ def main(listenip_v6, listenip_v4, if not bound: assert(last_e) raise last_e - listener.listen(10) - listener.print_listening("Redirector") + tcp_listener.listen(10) + tcp_listener.print_listening("Redirector") + bound = False if dns: + debug2('Binding DNS:') + ports = xrange(12300,9000,-1) + for port in ports: + debug2(' %d' % port) + dnslistener = independent_listener(socket.SOCK_DGRAM) + dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + if listenip_v6: + lv6 = (listenip_v6[0],port) + else: + lv6 = None + if listenip_v4: + lv4 = (listenip_v4[0],port) + else: + lv4 = None + + try: + dnslistener.bind( lv4, lv6 ) + dnsport = port + bound = True + break + except socket.error, e: + if e.errno == errno.EADDRNOTAVAIL: + last_e = e + else: + raise e + debug2('\n') dnslistener.print_listening("DNS") - if dnslistener.v6: - dnsip = dnslistener.v6.getsockname() - dnsport = dnsip[1] - else: - dnsip = dnslistener.v4.getsockname() - dnsport = dnsip[1] + if not bound: + assert(last_e) + raise last_e else: dnsport = 0 dnslistener = None @@ -459,7 +482,7 @@ def main(listenip_v6, listenip_v4, fw = FirewallClient(redirectport, subnets_include, subnets_exclude, dnsport, tproxy) try: - return _main(listener, fw, ssh_cmd, remotename, + return _main(tcp_listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, tproxy, seed_hosts, auto_nets, syslog, daemon) From c2fd92eea8d78fce361ae9660b79438081609abe Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 11:51:17 +1000 Subject: [PATCH 14/90] Another DNS bug fixed. --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index 25c185a..8900409 100644 --- a/client.py +++ b/client.py @@ -347,7 +347,7 @@ def onaccept(listener_sock): dnsreqs = {} def dns_done(chan, data): - peer,sock,timeout = dnsreqs.get(chan) or (None,None) + peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) debug3('dns_done: channel=%r peer=%r\n' % (chan, peer)) if peer: del dnsreqs[chan] From 6d276ab582e02c6685f2ce53e0523dbf00a165e0 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 14:11:18 +1000 Subject: [PATCH 15/90] Change subnet sort order to how it was previously. --- firewall.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/firewall.py b/firewall.py index 48fa947..980761d 100644 --- a/firewall.py +++ b/firewall.py @@ -107,7 +107,7 @@ def do_iptables_nat(port, dnsport, family, subnets): # to least-specific, and at any given level of specificity, we want # excludes to come first. That's why the columns are in such a non- # intuitive order. - for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[2], reverse=True): + for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True): if f != family: pass elif sexclude: @@ -161,7 +161,7 @@ def do_iptables_tproxy(port, dnsport, family, subnets): #ipt(family, table, '-A', mark_chain, '-o', 'lo', '-j', 'RETURN') if subnets: - for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[2], reverse=True): + for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True): if f != family: pass elif sexclude: @@ -323,7 +323,7 @@ def do_ipfw(port, dnsport, family, subnets): if subnets: # create new subnet entries - for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[2], reverse=True): + for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True): if sexclude: ipfw('add', sport, 'skipto', xsport, 'log', 'tcp', From 996011008498106136c1c84800e0543147613740 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 14:32:21 +1000 Subject: [PATCH 16/90] Revert brain dead function rename introduced in 39bdff0e46f8ed1c3769752e439c14ca09eef427. --- firewall.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/firewall.py b/firewall.py index 980761d..76a544f 100644 --- a/firewall.py +++ b/firewall.py @@ -14,7 +14,7 @@ def nonfatal(func, *args): log('error: %s\n' % e) -def ipt_table_exists(name, family, table): +def ipt_chain_exists(name, family, table): if family == socket.AF_INET6: cmd = 'ip6tables' elif family == socket.AF_INET: @@ -89,7 +89,7 @@ def do_iptables_nat(port, dnsport, family, subnets): chain = 'sshuttle-%s' % port # basic cleanup/setup of chains - if ipt_table_exists(chain, family, table): + if ipt_chain_exists(chain, family, table): nonfatal(ipt, family, table, '-D', 'OUTPUT', '-j', chain) nonfatal(ipt, family, table, '-D', 'PREROUTING', '-j', chain) nonfatal(ipt, family, table, '-F', chain) @@ -138,12 +138,12 @@ def do_iptables_tproxy(port, dnsport, family, subnets): tproxy_chain = 'sshuttle-t-%s' % port # basic cleanup/setup of chains - if ipt_table_exists(mark_chain, family, table): + if ipt_chain_exists(mark_chain, family, table): ipt(family, table, '-D', 'OUTPUT', '-j', mark_chain) ipt(family, table, '-F', mark_chain) ipt(family, table, '-X', mark_chain) - if ipt_table_exists(tproxy_chain, family, table): + if ipt_chain_exists(tproxy_chain, family, table): ipt(family, table, '-D', 'PREROUTING', '-j', tproxy_chain) ipt(family, table, '-F', tproxy_chain) ipt(family, table, '-X', tproxy_chain) From b6e6911bdd0bb1c8b0a1a958c6a636555bc42dfe Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 14:34:19 +1000 Subject: [PATCH 17/90] Change order of parameters to make consistant with other functions. --- firewall.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/firewall.py b/firewall.py index 76a544f..1f03678 100644 --- a/firewall.py +++ b/firewall.py @@ -14,7 +14,7 @@ def nonfatal(func, *args): log('error: %s\n' % e) -def ipt_chain_exists(name, family, table): +def ipt_chain_exists(family, table, name): if family == socket.AF_INET6: cmd = 'ip6tables' elif family == socket.AF_INET: @@ -89,7 +89,7 @@ def do_iptables_nat(port, dnsport, family, subnets): chain = 'sshuttle-%s' % port # basic cleanup/setup of chains - if ipt_chain_exists(chain, family, table): + if ipt_chain_exists(family, table, chain): nonfatal(ipt, family, table, '-D', 'OUTPUT', '-j', chain) nonfatal(ipt, family, table, '-D', 'PREROUTING', '-j', chain) nonfatal(ipt, family, table, '-F', chain) @@ -138,12 +138,12 @@ def do_iptables_tproxy(port, dnsport, family, subnets): tproxy_chain = 'sshuttle-t-%s' % port # basic cleanup/setup of chains - if ipt_chain_exists(mark_chain, family, table): + if ipt_chain_exists(family, table, mark_chain): ipt(family, table, '-D', 'OUTPUT', '-j', mark_chain) ipt(family, table, '-F', mark_chain) ipt(family, table, '-X', mark_chain) - if ipt_chain_exists(tproxy_chain, family, table): + if ipt_chain_exists(family, table, tproxy_chain): ipt(family, table, '-D', 'PREROUTING', '-j', tproxy_chain) ipt(family, table, '-F', tproxy_chain) ipt(family, table, '-X', tproxy_chain) From acce8849f36577c7875da26dd44e7b92e3fb9ea5 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 19 May 2011 15:14:24 +1000 Subject: [PATCH 18/90] Process packets from external interfaces correctly with tproxy. --- firewall.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/firewall.py b/firewall.py index 1f03678..dc6bf4b 100644 --- a/firewall.py +++ b/firewall.py @@ -136,6 +136,7 @@ def do_iptables_tproxy(port, dnsport, family, subnets): table = "mangle" mark_chain = 'sshuttle-m-%s' % port tproxy_chain = 'sshuttle-t-%s' % port + divert_chain = 'sshuttle-d-%s' % port # basic cleanup/setup of chains if ipt_chain_exists(family, table, mark_chain): @@ -148,14 +149,22 @@ def do_iptables_tproxy(port, dnsport, family, subnets): ipt(family, table, '-F', tproxy_chain) ipt(family, table, '-X', tproxy_chain) + if ipt_chain_exists(family, table, divert_chain): + ipt(family, table, '-F', divert_chain) + ipt(family, table, '-X', divert_chain) + if subnets or dnsport: ipt(family, table, '-N', mark_chain) ipt(family, table, '-F', mark_chain) + ipt(family, table, '-N', divert_chain) + ipt(family, table, '-F', divert_chain) ipt(family, table, '-N', tproxy_chain) ipt(family, table, '-F', tproxy_chain) ipt(family, table, '-I', 'OUTPUT', '1', '-j', mark_chain) ipt(family, table, '-I', 'PREROUTING', '1', '-j', tproxy_chain) - ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', 'RETURN', + ipt(family, table, '-A', divert_chain, '-j', 'MARK', '--set-mark', '1') + ipt(family, table, '-A', divert_chain, '-j', 'ACCEPT') + ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'tcp', '-p', 'tcp') #ipt(family, table, '-A', mark_chain, '-o', 'lo', '-j', 'RETURN') From c90b58f96c14884c7b48b5bb177cba66210e65aa Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 10:07:21 +1000 Subject: [PATCH 19/90] Listen on localhost only, by default. --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 13c7f6b..9dcfe8a 100644 --- a/main.py +++ b/main.py @@ -144,10 +144,10 @@ def parse_ipport6(s): else: sh = None if not opt.listen: - ipport_v6 = parse_ipport6('[::]:0') + ipport_v6 = parse_ipport6('[::1]:0') ipport_v4 = parse_ipport4('127.0.0.1:0') elif ':' in opt.listen: - ipport_v6 = parse_ipport6(opt.listen or '[::]:0') + ipport_v6 = parse_ipport6(opt.listen or '[::1]:0') ipport_v4 = None else: ipport_v6 = None From 6d418306ddc77c44568f1cba0f2824bb8fb4a3ae Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 11:14:37 +1000 Subject: [PATCH 20/90] Fix various ipv4/ipv6 issues. --- client.py | 34 ++++++++++++++++++++++------------ firewall.py | 38 ++++++++++++++++++++++++++------------ main.py | 18 +++++++++++------- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/client.py b/client.py index 8900409..65fd3b7 100644 --- a/client.py +++ b/client.py @@ -148,16 +148,16 @@ def print_listening(self, what): debug1('%s listening on %r.\n' % (what, listenip, )) class FirewallClient: - def __init__(self, port, subnets_include, subnets_exclude, dnsport, tproxy): - self.port = port + def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy): self.auto_nets = [] self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude - self.dnsport = dnsport self.tproxy = tproxy argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] + ['-v'] * (helpers.verbose or 0) + - ['--firewall', str(port), str(dnsport), str(tproxy or 0)]) + ['--firewall', str(port_v6), str(port_v4), + str(dnsport_v6), str(dnsport_v4), + str(tproxy or 0)]) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ @@ -398,12 +398,15 @@ def main(listenip_v6, listenip_v4, return 5 debug1('Starting sshuttle proxy.\n') - if listenip_v4[1]: - ports = [listenip[1]] + if listenip_v6: + ports = [listenip_v6[1]] + elif listenip_v4: + ports = [listenip_v4[1]] else: ports = xrange(12300,9000,-1) last_e = None - redirectport = None + redirectport_v6 = 0 + redirectport_v4 = 0 bound = False debug2('Binding redirector:') for port in ports: @@ -416,16 +419,19 @@ def main(listenip_v6, listenip_v4, if listenip_v6: lv6 = (listenip_v6[0],port) + redirectport_v6 = port else: lv6 = None + redirectport_v6 = 0 if listenip_v4: lv4 = (listenip_v4[0],port) + redirectport_v4 = port else: lv4 = None + redirectport_v4 = 0 try: tcp_listener.bind(lv4, lv6) - redirectport = port bound = True break except Fatal, e: @@ -453,16 +459,19 @@ def main(listenip_v6, listenip_v4, if listenip_v6: lv6 = (listenip_v6[0],port) + dnsport_v6 = port else: lv6 = None + dnsport_v6 = 0 if listenip_v4: lv4 = (listenip_v4[0],port) + dnsport_v4 = port else: lv4 = None + dnsport_v4 = 0 try: dnslistener.bind( lv4, lv6 ) - dnsport = port bound = True break except socket.error, e: @@ -476,13 +485,14 @@ def main(listenip_v6, listenip_v4, assert(last_e) raise last_e else: - dnsport = 0 + dnsport_v6 = 0 + dnsport_v4 = 0 dnslistener = None - fw = FirewallClient(redirectport, subnets_include, subnets_exclude, dnsport, tproxy) + fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy) try: - return _main(tcp_listener, fw, ssh_cmd, remotename, + return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, tproxy, seed_hosts, auto_nets, syslog, daemon) diff --git a/firewall.py b/firewall.py index dc6bf4b..2fea0e7 100644 --- a/firewall.py +++ b/firewall.py @@ -84,6 +84,11 @@ def do_iptables_nat(port, dnsport, family, subnets): # only ipv4 supported with NAT if family != socket.AF_INET: return + if not port: + subnets = None + port = dnsport + if not port: + return table = "nat" chain = 'sshuttle-%s' % port @@ -132,6 +137,11 @@ def do_iptables_nat(port, dnsport, family, subnets): def do_iptables_tproxy(port, dnsport, family, subnets): if family not in [socket.AF_INET, socket.AF_INET6]: return + if not port: + subnets = None + port = dnsport + if not port: + return table = "mangle" mark_chain = 'sshuttle-m-%s' % port @@ -153,7 +163,7 @@ def do_iptables_tproxy(port, dnsport, family, subnets): ipt(family, table, '-F', divert_chain) ipt(family, table, '-X', divert_chain) - if subnets or dnsport: + if subnets: ipt(family, table, '-N', mark_chain) ipt(family, table, '-F', mark_chain) ipt(family, table, '-N', divert_chain) @@ -461,11 +471,15 @@ def restore_etc_hosts(port): # 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 rules # are hopefully harmless. -def main(port, dnsport, tproxy, syslog): - assert(port > 0) - assert(port <= 65535) - assert(dnsport >= 0) - assert(dnsport <= 65535) +def main(port_v6, port_v4, dnsport_v6, dnsport_v4, tproxy, syslog): + assert(port_v6 >= 0) + assert(port_v6 <= 65535) + assert(port_v4 >= 0) + assert(port_v4 <= 65535) + assert(dnsport_v6 >= 0) + assert(dnsport_v6 <= 65535) + assert(dnsport_v4 >= 0) + assert(dnsport_v4 <= 65535) if os.getuid() != 0: raise Fatal('you must be root (or enable su/sudo) to set the firewall') @@ -522,8 +536,8 @@ def main(port, dnsport, tproxy, syslog): try: if line: debug1('firewall manager: starting transproxy.\n') - do_wait = do_it(port, dnsport, socket.AF_INET6, subnets) - do_wait = do_it(port, dnsport, socket.AF_INET, subnets) + do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, subnets) + do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets) sys.stdout.write('STARTED\n') try: @@ -542,7 +556,7 @@ def main(port, dnsport, tproxy, syslog): if line.startswith('HOST '): (name,ip) = line[5:].strip().split(',', 1) hostmap[name] = ip - rewrite_etc_hosts(port) + rewrite_etc_hosts(port_v6 or port_v4) elif line: raise Fatal('expected EOF, got %r' % line) else: @@ -552,6 +566,6 @@ def main(port, dnsport, tproxy, syslog): debug1('firewall manager: undoing changes.\n') except: pass - do_it(port, 0, socket.AF_INET6, []) - do_it(port, 0, socket.AF_INET, []) - restore_etc_hosts(port) + do_it(port_v6, 0, socket.AF_INET6, []) + do_it(port_v4, 0, socket.AF_INET, []) + restore_etc_hosts(port_v6 or port_v4) diff --git a/main.py b/main.py index 9dcfe8a..5c1661f 100644 --- a/main.py +++ b/main.py @@ -118,10 +118,11 @@ def parse_ipport6(s): server.tproxy = opt.tproxy sys.exit(server.main()) elif opt.firewall: - if len(extra) != 3: + if len(extra) != 5: o.fatal('exactly three arguments expected') - sys.exit(firewall.main(int(extra[0]), int(extra[1]), - int(extra[2]), opt.syslog)) + sys.exit(firewall.main(int(extra[0]), int(extra[1]), + int(extra[2]), int(extra[3]), + int(extra[4]), opt.syslog)) elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: @@ -146,12 +147,15 @@ def parse_ipport6(s): if not opt.listen: ipport_v6 = parse_ipport6('[::1]:0') ipport_v4 = parse_ipport4('127.0.0.1:0') - elif ':' in opt.listen: - ipport_v6 = parse_ipport6(opt.listen or '[::1]:0') - ipport_v4 = None else: ipport_v6 = None - ipport_v4 = parse_ipport4(opt.listen or '127.0.0.1:0') + ipport_v4 = None + list = opt.listen.split(",") + for ip in list: + if ':' in ip: + ipport_v6 = parse_ipport6(ip) + else: + ipport_v4 = parse_ipport4(ip) sys.exit(client.main(ipport_v6, ipport_v4, opt.ssh_cmd, remotename, From f1b61850d2d23a0a7f7e45013489569b8fe79241 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 11:34:18 +1000 Subject: [PATCH 21/90] Fix IPv6 address detection. --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 5c1661f..fa4a6c2 100644 --- a/main.py +++ b/main.py @@ -152,7 +152,7 @@ def parse_ipport6(s): ipport_v4 = None list = opt.listen.split(",") for ip in list: - if ':' in ip: + if '[' in ip and ']' in ip: ipport_v6 = parse_ipport6(ip) else: ipport_v4 = parse_ipport4(ip) From 32d5b5cfbb39dda20360bab489882f79969c792e Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 11:35:07 +1000 Subject: [PATCH 22/90] Fix port detection. --- client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client.py b/client.py index 65fd3b7..78eff0c 100644 --- a/client.py +++ b/client.py @@ -398,9 +398,9 @@ def main(listenip_v6, listenip_v4, return 5 debug1('Starting sshuttle proxy.\n') - if listenip_v6: + if listenip_v6 and listenip_v6[1]: ports = [listenip_v6[1]] - elif listenip_v4: + elif listenip_v4 and listenip_v4[1]: ports = [listenip_v4[1]] else: ports = xrange(12300,9000,-1) From 0489be512e562bee76ed134c2ddd5b87ef5491d6 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 19:27:46 +1000 Subject: [PATCH 23/90] Change order of bind parameters for consistancy. IPv6 comes first. --- client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client.py b/client.py index 78eff0c..4b9d6c2 100644 --- a/client.py +++ b/client.py @@ -129,7 +129,7 @@ def listen(self, backlog): else: raise e - def bind(self, address_v4, address_v6): + def bind(self, address_v6, address_v4): if address_v6 and self.v6: self.v6.bind(address_v6) else: @@ -431,7 +431,7 @@ def main(listenip_v6, listenip_v4, redirectport_v4 = 0 try: - tcp_listener.bind(lv4, lv6) + tcp_listener.bind(lv6, lv4) bound = True break except Fatal, e: @@ -471,7 +471,7 @@ def main(listenip_v6, listenip_v4, dnsport_v4 = 0 try: - dnslistener.bind( lv4, lv6 ) + dnslistener.bind( lv6, lv4 ) bound = True break except socket.error, e: From 72bc745953c2b23fd8dbdde5deb0a2a94515fd82 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 19:31:54 +1000 Subject: [PATCH 24/90] Use ports, if any specified on command line. Don't assume IPv6 port and IPv4 are the same. --- client.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/client.py b/client.py index 4b9d6c2..ec0706e 100644 --- a/client.py +++ b/client.py @@ -398,12 +398,14 @@ def main(listenip_v6, listenip_v4, return 5 debug1('Starting sshuttle proxy.\n') - if listenip_v6 and listenip_v6[1]: - ports = [listenip_v6[1]] - elif listenip_v4 and listenip_v4[1]: - ports = [listenip_v4[1]] + if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]: + # if both ports given, no need to search for a spare port + ports = [ 0, ] else: + # if at least one port missing, we have to search ports = xrange(12300,9000,-1) + + # search for free ports and try to bind last_e = None redirectport_v6 = 0 redirectport_v4 = 0 @@ -417,13 +419,20 @@ def main(listenip_v6, listenip_v4, if tproxy: tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - if listenip_v6: + if listenip_v6 and listenip_v6[1]: + lv6 = listenip_v6 + redirectport_v6 = lv6[1] + elif listenip_v6: lv6 = (listenip_v6[0],port) redirectport_v6 = port else: lv6 = None redirectport_v6 = 0 - if listenip_v4: + + if listenip_v4 and listenip_v4[1]: + lv4 = listenip_v4 + redirectport_v4 = lv4[1] + elif listenip_v4: lv4 = (listenip_v4[0],port) redirectport_v4 = port else: @@ -434,8 +443,6 @@ def main(listenip_v6, listenip_v4, tcp_listener.bind(lv6, lv4) bound = True break - except Fatal, e: - raise e except socket.error, e: if e.errno == errno.EADDRNOTAVAIL: last_e = e @@ -450,6 +457,7 @@ def main(listenip_v6, listenip_v4, bound = False if dns: + # search for spare port for DNS debug2('Binding DNS:') ports = xrange(12300,9000,-1) for port in ports: From d1f4a47424e8da1b145bafdad9943d9c7ac91550 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 19:34:58 +1000 Subject: [PATCH 25/90] Move code to expire connections into seperate function. --- client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client.py b/client.py index ec0706e..1935f1f 100644 --- a/client.py +++ b/client.py @@ -308,6 +308,12 @@ def onhostlist(hostlist): fw.sethostip(name, ip) mux.got_host_list = onhostlist + dnsreqs = {} + def expire_connections(now): + for chan,(peer,sock,timeout) in dnsreqs.items(): + if timeout < now: + del dnsreqs[chan] + def onaccept(listener_sock): global _extra_fd try: @@ -343,9 +349,9 @@ def onaccept(listener_sock): mux.send(chan, ssnet.CMD_CONNECT, '%s,%r' % (dstip[0], dstip[1])) outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) + expire_connections(time.time()) tcp_listener.add_handler(handlers, onaccept) - dnsreqs = {} def dns_done(chan, data): peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) debug3('dns_done: channel=%r peer=%r\n' % (chan, peer)) @@ -362,9 +368,7 @@ def ondns(listener_sock): dnsreqs[chan] = peer,listener_sock,now+30 mux.send(chan, ssnet.CMD_DNS_REQ, pkt) mux.channels[chan] = lambda cmd,data: dns_done(chan,data) - for chan,(peer,sock,timeout) in dnsreqs.items(): - if timeout < now: - del dnsreqs[chan] + expire_connections(now) debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) if dnslistener: dnslistener.add_handler(handlers, ondns) From c0e9f96d24b649086aa5424ed35fa1174f5f2829 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 19:36:52 +1000 Subject: [PATCH 26/90] Minor changes in preparation for UDP support. --- client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client.py b/client.py index 1935f1f..14c9490 100644 --- a/client.py +++ b/client.py @@ -314,7 +314,7 @@ def expire_connections(now): if timeout < now: del dnsreqs[chan] - def onaccept(listener_sock): + def onaccept_tcp(listener_sock): global _extra_fd try: sock,srcip = listener_sock.accept() @@ -350,7 +350,7 @@ def onaccept(listener_sock): outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) expire_connections(time.time()) - tcp_listener.add_handler(handlers, onaccept) + tcp_listener.add_handler(handlers, onaccept_tcp) def dns_done(chan, data): peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) @@ -457,7 +457,7 @@ def main(listenip_v6, listenip_v4, assert(last_e) raise last_e tcp_listener.listen(10) - tcp_listener.print_listening("Redirector") + tcp_listener.print_listening("TCP redirector") bound = False if dns: From 0cb65df1e0f5d24aa4f8e8089777c9a00067b7b6 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 19:38:17 +1000 Subject: [PATCH 27/90] Seems silly converting native to network byte ordering and back again. Fixed. --- client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client.py b/client.py index 14c9490..c665fb2 100644 --- a/client.py +++ b/client.py @@ -87,8 +87,9 @@ def original_dst(sock): 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) + (proto, port, a,b,c,d) = struct.unpack('=HHBBBB', sockaddr_in[:8]) + port = socket.htons(port) + assert(proto == socket.AF_INET) ip = '%d.%d.%d.%d' % (a,b,c,d) return (ip,port) except socket.error, e: From f8d536c07e807b65fc29015c2384d457e0a82a52 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 20:04:08 +1000 Subject: [PATCH 28/90] Delete mux channel after DNS timeout or response received. --- client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client.py b/client.py index c665fb2..14d50a2 100644 --- a/client.py +++ b/client.py @@ -313,6 +313,7 @@ def onhostlist(hostlist): def expire_connections(now): for chan,(peer,sock,timeout) in dnsreqs.items(): if timeout < now: + del mux.channels[chan] del dnsreqs[chan] def onaccept_tcp(listener_sock): @@ -357,6 +358,7 @@ def dns_done(chan, data): peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) debug3('dns_done: channel=%r peer=%r\n' % (chan, peer)) if peer: + del mux.channels[chan] del dnsreqs[chan] debug3('doing sendto %r\n' % (peer,)) sock.sendto(data, peer) From 58cebf1122b4839cc8201bf62ccd0ec4f624e6a7 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 20:07:15 +1000 Subject: [PATCH 29/90] No point listening on IPv6 port if not using tproxy. --- main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index fa4a6c2..c24e4f6 100644 --- a/main.py +++ b/main.py @@ -145,14 +145,17 @@ def parse_ipport6(s): else: sh = None if not opt.listen: - ipport_v6 = parse_ipport6('[::1]:0') + if opt.tproxy: + ipport_v6 = parse_ipport6('[::1]:0') + else: + ipport_v6 = None ipport_v4 = parse_ipport4('127.0.0.1:0') else: ipport_v6 = None ipport_v4 = None list = opt.listen.split(",") for ip in list: - if '[' in ip and ']' in ip: + if '[' in ip and ']' in ip and opt.tproxy: ipport_v6 = parse_ipport6(ip) else: ipport_v4 = parse_ipport4(ip) From aba81472f6d5399301a13b96dc81a060d7e7a7de Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 20:51:47 +1000 Subject: [PATCH 30/90] Explicit close of DNS port after timeout period. --- server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server.py b/server.py index 3e6c7a4..0f2ead2 100644 --- a/server.py +++ b/server.py @@ -243,4 +243,5 @@ def dns_req(channel, data): for channel,h in dnshandlers.items(): if h.timeout < now or not h.ok: del dnshandlers[channel] + h.sock.close() h.ok = False From 8bedb24e9d23f5bb0f10b3bed343d096c0f34368 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 20:57:05 +1000 Subject: [PATCH 31/90] Add FIXME commment. --- server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server.py b/server.py index 0f2ead2..9ce223b 100644 --- a/server.py +++ b/server.py @@ -108,6 +108,7 @@ def __init__(self): class DnsProxy(Handler): def __init__(self, mux, chan, request): + # FIXME! IPv4 specific sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) Handler.__init__(self, [sock]) self.timeout = time.time()+30 From 07219cb867b336463531ea440fb480c0ffc06031 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sat, 21 May 2011 20:58:32 +1000 Subject: [PATCH 32/90] TProxy UDP support. Requires PyXAPI for recvmsg(). --- client.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++-- firewall.py | 15 ++++++++++ server.py | 57 ++++++++++++++++++++++++++++++++++++++ ssnet.py | 11 ++++++++ stresstest.py | 0 5 files changed, 157 insertions(+), 2 deletions(-) mode change 100755 => 100644 stresstest.py diff --git a/client.py b/client.py index 14d50a2..9cf52d8 100644 --- a/client.py +++ b/client.py @@ -1,8 +1,9 @@ -import struct, socket, select, errno, re, signal, time +import struct, select, errno, re, signal, time import compat.ssubprocess as ssubprocess import helpers, ssnet, ssh, ssyslog from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from helpers import * +import socket_ext as socket _extra_fd = os.open('/dev/null', os.O_RDONLY) @@ -13,6 +14,8 @@ def got_signal(signum, frame): _pidname = None IP_TRANSPARENT = 19 +IP_RECVORIGDSTADDR = 20 +IP_ORIGDSTADDR = 20 def check_daemon(pidfile): global _pidname @@ -229,7 +232,7 @@ def done(self): raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) -def _main(tcp_listener, fw, ssh_cmd, remotename, python, latency_control, +def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, tproxy, seed_hosts, auto_nets, syslog, daemon): handlers = [] @@ -310,11 +313,17 @@ def onhostlist(hostlist): mux.got_host_list = onhostlist dnsreqs = {} + udp_by_src = {} def expire_connections(now): for chan,(peer,sock,timeout) in dnsreqs.items(): if timeout < now: del mux.channels[chan] del dnsreqs[chan] + for src,(chan,timeout) in udp_by_src.items(): + if timeout < now: + mux.send(chan, ssnet.CMD_UDP_CLOSE, None) + del mux.channels[chan] + del udp_by_src[src] def onaccept_tcp(listener_sock): global _extra_fd @@ -354,6 +363,62 @@ def onaccept_tcp(listener_sock): expire_connections(time.time()) tcp_listener.add_handler(handlers, onaccept_tcp) + def udp_done(chan, data, family, dstip): + (src,srcport,data) = data.split(",",2) + srcip = (src,int(srcport)) + debug3('doing send from %r port %d to %r port %d\n' % (srcip[0],srcip[1],dstip[0],dstip[1],)) + + sock = socket.socket(family, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + sock.bind(srcip) + sock.sendto(data, dstip) + sock.close() + + def onaccept_udp(listener_sock): + srcip, data, adata, flags = listener_sock.recvmsg((4096,),socket.CMSG_SPACE(24)) + now = time.time() + dstip = None + family = None + print "a", srcip, data, adata, flags + for a in adata: + if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR: + print "b",a.cmsg_level, a.cmsg_type + family,port = struct.unpack('=HH', a.cmsg_data[0:4]) + port = socket.htons(port) + print "c", family, port, socket.AF_INET, socket.AF_INET6 + if family == socket.AF_INET6: + print "IPV6" + start = 8 + length = 16 + elif family == socket.AF_INET: + print "IPV4" + print struct.unpack("=BBBBBBBBBBBBBBBB",a.cmsg_data) + start = 4 + length = 4 + else: + raise Fatal("Unsupported socket type '%s'"%family) + ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) + dstip = (ip, port) + break + if not dstip: + debug1("-- ignored: couldn't determine destination IP address\n") + return + if srcip in udp_by_src: + chan,timeout = udp_by_src[srcip] + else: + chan = mux.next_channel() + mux.channels[chan] = lambda cmd,data: udp_done(chan,data,family,dstip=srcip) + mux.send(chan, ssnet.CMD_UDP_OPEN, family) + udp_by_src[srcip] = chan,now+30 + + hdr = "%s,%r,"%(dstip[0], dstip[1]) + mux.send(chan, ssnet.CMD_UDP_DATA, hdr+data[0]) + + expire_connections(now) + debug3('Remaining UDP connections: %d\n' % len(udp_by_src)) + udp_listener.add_handler(handlers, onaccept_udp) + def dns_done(chan, data): peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) debug3('dns_done: channel=%r peer=%r\n' % (chan, peer)) @@ -423,8 +488,13 @@ def main(listenip_v6, listenip_v4, tcp_listener = independent_listener() tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + udp_listener = independent_listener(socket.SOCK_DGRAM) + udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if tproxy: tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + udp_listener.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) if listenip_v6 and listenip_v6[1]: lv6 = listenip_v6 @@ -448,6 +518,7 @@ def main(listenip_v6, listenip_v4, try: tcp_listener.bind(lv6, lv4) + udp_listener.bind(lv6, lv4) bound = True break except socket.error, e: @@ -461,6 +532,7 @@ def main(listenip_v6, listenip_v4, raise last_e tcp_listener.listen(10) tcp_listener.print_listening("TCP redirector") + udp_listener.print_listening("UDP redirector") bound = False if dns: diff --git a/firewall.py b/firewall.py index 2fea0e7..fd0dd33 100644 --- a/firewall.py +++ b/firewall.py @@ -176,6 +176,8 @@ def do_iptables_tproxy(port, dnsport, family, subnets): ipt(family, table, '-A', divert_chain, '-j', 'ACCEPT') ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'tcp', '-p', 'tcp') + ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, + '-m', 'udp', '-p', 'udp') #ipt(family, table, '-A', mark_chain, '-o', 'lo', '-j', 'RETURN') @@ -187,17 +189,30 @@ def do_iptables_tproxy(port, dnsport, family, subnets): ipt(family, table, '-A', mark_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') + ipt(family, table, '-A', mark_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'udp', '-p', 'udp') ipt(family, table, '-A', tproxy_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') + ipt(family, table, '-A', tproxy_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'udp', '-p', 'udp') else: ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') + ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'udp', '-p', 'udp') ipt(family, table, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp', '--on-port', str(port)) + ipt(family, table, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'udp', '-p', 'udp', + '--on-port', str(port)) def ipfw_rule_exists(n): diff --git a/server.py b/server.py index 9ce223b..964a7fa 100644 --- a/server.py +++ b/server.py @@ -161,6 +161,34 @@ def callback(self): self.ok = False +class UdpProxy(Handler): + def __init__(self, mux, chan, family): + sock = socket.socket(family, socket.SOCK_DGRAM) + Handler.__init__(self, [sock]) + self.timeout = time.time()+30 + self.mux = mux + self.chan = chan + self.sock = sock + self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) + + def send(self, dstip, data): + debug2('UDP: sending to %r port %d\n' % dstip) + try: + self.sock.sendto(data,dstip) + except socket.error, e: + log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e)) + return + + def callback(self): + try: + data,peer = self.sock.recvfrom(4096) + except socket.error, e: + log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e)) + return + debug2('UDP response: %d bytes\n' % len(data)) + hdr = "%s,%r,"%(peer[0], peer[1]) + self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr+data) + def main(): if helpers.verbose >= 1: helpers.logprefix = ' s: ' @@ -227,6 +255,35 @@ def dns_req(channel, data): dnshandlers[channel] = h mux.got_dns_req = dns_req + udphandlers = {} + def udp_req(channel, cmd, data): + debug2('Incoming UDP request.\n') + if cmd == ssnet.CMD_UDP_DATA: + (dstip,dstport,data) = data.split(",",2) + dstport = int(dstport) + debug2('Incoming UDP request. %r %d.\n'%(dstip,dstport)) + h = udphandlers[channel] + h.send((dstip,dstport),data) + elif cmd == ssnet.CMD_UDP_CLOSE: + h = udphandlers[channel] + h.sock.close() + del mux.channels[channel] + del udphandlers[channel] + + def udp_open(channel, data): + debug2('Incoming UDP open.\n') + family = int(data[0]) + debug2('Incoming UDP open. %d.\n'%family) + mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data) + if channel in udphandlers: + raise Fatal('UDP connection channel %d already open'%channel) + else: + h = UdpProxy(mux, channel, family) + handlers.append(h) + udphandlers[channel] = h + mux.got_udp_open = udp_open + + while mux.ok: if hw.pid: assert(hw.pid > 0) diff --git a/ssnet.py b/ssnet.py index 002df94..d89a44f 100644 --- a/ssnet.py +++ b/ssnet.py @@ -25,6 +25,9 @@ CMD_HOST_LIST = 0x4209 CMD_DNS_REQ = 0x420a CMD_DNS_RESPONSE = 0x420b +CMD_UDP_OPEN = 0x420c +CMD_UDP_DATA = 0x420d +CMD_UDP_CLOSE = 0x420e cmd_to_name = { CMD_EXIT: 'EXIT', @@ -39,6 +42,9 @@ CMD_HOST_LIST: 'HOST_LIST', CMD_DNS_REQ: 'DNS_REQ', CMD_DNS_RESPONSE: 'DNS_RESPONSE', + CMD_UDP_OPEN: 'UDP_OPEN', + CMD_UDP_DATA: 'UDP_DATA', + CMD_UDP_CLOSE: 'UDP_CLOSE', } @@ -306,6 +312,7 @@ def __init__(self, rsock, wsock): self.rsock = rsock self.wsock = wsock self.new_channel = self.got_dns_req = self.got_routes = None + self.got_udp_open = self.got_udp_data = self.got_udp_close = None self.got_host_req = self.got_host_list = None self.channels = {} self.chani = 0 @@ -371,6 +378,10 @@ def got_packet(self, channel, cmd, data): assert(not self.channels.get(channel)) if self.got_dns_req: self.got_dns_req(channel, data) + elif cmd == CMD_UDP_OPEN: + assert(not self.channels.get(channel)) + if self.got_udp_open: + self.got_udp_open(channel, data) elif cmd == CMD_ROUTES: if self.got_routes: self.got_routes(data) diff --git a/stresstest.py b/stresstest.py old mode 100755 new mode 100644 From c82147aba68fcba684a873aad76a9bdb40f4741a Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 22 May 2011 10:10:53 +1000 Subject: [PATCH 33/90] Fix IPv6 over UDP. --- client.py | 32 +++++++++++++++++++++++--------- server.py | 6 ++++-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/client.py b/client.py index 9cf52d8..4f3f177 100644 --- a/client.py +++ b/client.py @@ -14,8 +14,11 @@ def got_signal(signum, frame): _pidname = None IP_TRANSPARENT = 19 -IP_RECVORIGDSTADDR = 20 IP_ORIGDSTADDR = 20 +IP_RECVORIGDSTADDR = IP_ORIGDSTADDR +SOL_IPV6 = 41 +IPV6_ORIGDSTADDR = 74 +IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR def check_daemon(pidfile): global _pidname @@ -382,16 +385,12 @@ def onaccept_udp(listener_sock): family = None print "a", srcip, data, adata, flags for a in adata: + print "b",a.cmsg_level, a.cmsg_type if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR: - print "b",a.cmsg_level, a.cmsg_type family,port = struct.unpack('=HH', a.cmsg_data[0:4]) port = socket.htons(port) - print "c", family, port, socket.AF_INET, socket.AF_INET6 - if family == socket.AF_INET6: - print "IPV6" - start = 8 - length = 16 - elif family == socket.AF_INET: + print "c4", family, port, socket.AF_INET, socket.AF_INET6 + if family == socket.AF_INET: print "IPV4" print struct.unpack("=BBBBBBBBBBBBBBBB",a.cmsg_data) start = 4 @@ -401,6 +400,20 @@ def onaccept_udp(listener_sock): ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) dstip = (ip, port) break + elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR: + family,port = struct.unpack('=HH', a.cmsg_data[0:4]) + port = socket.htons(port) + print "c6", family, port, socket.AF_INET, socket.AF_INET6 + if family == socket.AF_INET6: + print "IPV6" + start = 8 + length = 16 + else: + raise Fatal("Unsupported socket type '%s'"%family) + ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) + dstip = (ip, port) + print dstip + break if not dstip: debug1("-- ignored: couldn't determine destination IP address\n") return @@ -494,7 +507,8 @@ def main(listenip_v6, listenip_v4, if tproxy: tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - udp_listener.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) + udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) + udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) if listenip_v6 and listenip_v6[1]: lv6 = listenip_v6 diff --git a/server.py b/server.py index 964a7fa..7d6ce89 100644 --- a/server.py +++ b/server.py @@ -118,6 +118,7 @@ def __init__(self, mux, chan, request): self.peer = None self.request = request self.sock = sock + # FIXME! IPv4 specific self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) self.try_send() @@ -169,7 +170,8 @@ def __init__(self, mux, chan, family): self.mux = mux self.chan = chan self.sock = sock - self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) + if family == socket.AF_INET: + self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) def send(self, dstip, data): debug2('UDP: sending to %r port %d\n' % dstip) @@ -272,7 +274,7 @@ def udp_req(channel, cmd, data): def udp_open(channel, data): debug2('Incoming UDP open.\n') - family = int(data[0]) + family = int(data) debug2('Incoming UDP open. %d.\n'%family) mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data) if channel in udphandlers: From 46d26d0e0d5e531c0c0c3aa811af84cfc07f497b Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 22 May 2011 10:22:02 +1000 Subject: [PATCH 34/90] Whoops. This change shouldn't have got through here. --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index 14d50a2..93ba817 100644 --- a/client.py +++ b/client.py @@ -507,7 +507,7 @@ def main(listenip_v6, listenip_v4, fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy) try: - return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, + return _main(tcp_listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, tproxy, seed_hosts, auto_nets, syslog, daemon) From d881ed3320d19405e4f750be90e8909f1bd52f3c Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 22 May 2011 10:22:39 +1000 Subject: [PATCH 35/90] Don't guess the address family for TCP connections, pass it through. --- client.py | 2 +- server.py | 5 +++-- ssnet.py | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client.py b/client.py index 93ba817..f5be4e0 100644 --- a/client.py +++ b/client.py @@ -348,7 +348,7 @@ def onaccept_tcp(listener_sock): log('warning: too many open channels. Discarded connection.\n') sock.close() return - mux.send(chan, ssnet.CMD_CONNECT, '%s,%r' % (dstip[0], dstip[1])) + mux.send(chan, ssnet.CMD_CONNECT, '%d,%s,%r' % (sock.family, dstip[0], dstip[1])) outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) expire_connections(time.time()) diff --git a/server.py b/server.py index 9ce223b..b82f9a5 100644 --- a/server.py +++ b/server.py @@ -213,9 +213,10 @@ def got_host_req(data): mux.got_host_req = got_host_req def new_channel(channel, data): - (dstip,dstport) = data.split(',', 1) + (family,dstip,dstport) = data.split(',', 2) + family = int(family) dstport = int(dstport) - outwrap = ssnet.connect_dst(dstip,dstport) + outwrap = ssnet.connect_dst(family, dstip, dstport) handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) mux.new_channel = new_channel diff --git a/ssnet.py b/ssnet.py index 002df94..148980e 100644 --- a/ssnet.py +++ b/ssnet.py @@ -511,9 +511,8 @@ def got_packet(self, cmd, data): % (cmd, len(data))) -def connect_dst(ip, port): +def connect_dst(family, ip, port): debug2('Connecting to %s:%d\n' % (ip, port)) - family = guess_address_family(ip) outsock = socket.socket(family) outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) return SockWrapper(outsock, outsock, From ad57904fd64127c115021d94ef49ebb9bfc661c0 Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 22 May 2011 10:27:33 +1000 Subject: [PATCH 36/90] Make it clear same commands are TCP specific. --- client.py | 2 +- ssnet.py | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/client.py b/client.py index f5be4e0..c57e0ed 100644 --- a/client.py +++ b/client.py @@ -348,7 +348,7 @@ def onaccept_tcp(listener_sock): log('warning: too many open channels. Discarded connection.\n') sock.close() return - mux.send(chan, ssnet.CMD_CONNECT, '%d,%s,%r' % (sock.family, dstip[0], dstip[1])) + mux.send(chan, ssnet.CMD_TCP_CONNECT, '%d,%s,%r' % (sock.family, dstip[0], dstip[1])) outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) expire_connections(time.time()) diff --git a/ssnet.py b/ssnet.py index 148980e..b0f1239 100644 --- a/ssnet.py +++ b/ssnet.py @@ -16,10 +16,10 @@ CMD_EXIT = 0x4200 CMD_PING = 0x4201 CMD_PONG = 0x4202 -CMD_CONNECT = 0x4203 -CMD_STOP_SENDING = 0x4204 -CMD_EOF = 0x4205 -CMD_DATA = 0x4206 +CMD_TCP_CONNECT = 0x4203 +CMD_TCP_STOP_SENDING = 0x4204 +CMD_TCP_EOF = 0x4205 +CMD_TCP_DATA = 0x4206 CMD_ROUTES = 0x4207 CMD_HOST_REQ = 0x4208 CMD_HOST_LIST = 0x4209 @@ -30,10 +30,10 @@ CMD_EXIT: 'EXIT', CMD_PING: 'PING', CMD_PONG: 'PONG', - CMD_CONNECT: 'CONNECT', - CMD_STOP_SENDING: 'STOP_SENDING', - CMD_EOF: 'EOF', - CMD_DATA: 'DATA', + CMD_TCP_CONNECT: 'TCP_CONNECT', + CMD_TCP_STOP_SENDING: 'TCP_STOP_SENDING', + CMD_TCP_EOF: 'TCP_EOF', + CMD_TCP_DATA: 'TCP_DATA', CMD_ROUTES: 'ROUTES', CMD_HOST_REQ: 'HOST_REQ', CMD_HOST_LIST: 'HOST_LIST', @@ -363,7 +363,7 @@ def got_packet(self, channel, cmd, data): self.fullness = 0 elif cmd == CMD_EXIT: self.ok = False - elif cmd == CMD_CONNECT: + elif cmd == CMD_TCP_CONNECT: assert(not self.channels.get(channel)) if self.new_channel: self.new_channel(channel, data) @@ -467,13 +467,13 @@ def __repr__(self): def noread(self): if not self.shut_read: self.shut_read = True - self.mux.send(self.channel, CMD_STOP_SENDING, '') + self.mux.send(self.channel, CMD_TCP_STOP_SENDING, '') self.maybe_close() def nowrite(self): if not self.shut_write: self.shut_write = True - self.mux.send(self.channel, CMD_EOF, '') + self.mux.send(self.channel, CMD_TCP_EOF, '') self.maybe_close() def maybe_close(self): @@ -490,7 +490,7 @@ def uwrite(self, buf): return 0 # too much already enqueued if len(buf) > 2048: buf = buf[:2048] - self.mux.send(self.channel, CMD_DATA, buf) + self.mux.send(self.channel, CMD_TCP_DATA, buf) return len(buf) def uread(self): @@ -500,11 +500,11 @@ def uread(self): return None # no data available right now def got_packet(self, cmd, data): - if cmd == CMD_EOF: + if cmd == CMD_TCP_EOF: self.noread() - elif cmd == CMD_STOP_SENDING: + elif cmd == CMD_TCP_STOP_SENDING: self.nowrite() - elif cmd == CMD_DATA: + elif cmd == CMD_TCP_DATA: self.buf.append(data) else: raise Exception('unknown command %d (%d bytes)' From 1064f091f1e6927759ed546c151401027e0b7ecf Mon Sep 17 00:00:00 2001 From: Brian May Date: Sun, 22 May 2011 10:30:04 +1000 Subject: [PATCH 37/90] Redo this required change. --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index 4ba36b4..fcadc85 100644 --- a/client.py +++ b/client.py @@ -593,7 +593,7 @@ def main(listenip_v6, listenip_v4, fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy) try: - return _main(tcp_listener, fw, ssh_cmd, remotename, + return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, tproxy, seed_hosts, auto_nets, syslog, daemon) From f092e5e378e73e2e360828b071950eae1e56acab Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 23 May 2011 13:23:01 +1000 Subject: [PATCH 38/90] Improved DNS debugging. --- client.py | 5 +++-- server.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client.py b/client.py index c57e0ed..30876c9 100644 --- a/client.py +++ b/client.py @@ -313,8 +313,10 @@ def onhostlist(hostlist): def expire_connections(now): for chan,(peer,sock,timeout) in dnsreqs.items(): if timeout < now: + debug3('expiring dnsreqs channel=%d peer=%r\n' % (chan, peer)) del mux.channels[chan] del dnsreqs[chan] + debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) def onaccept_tcp(listener_sock): global _extra_fd @@ -356,7 +358,7 @@ def onaccept_tcp(listener_sock): def dns_done(chan, data): peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) - debug3('dns_done: channel=%r peer=%r\n' % (chan, peer)) + debug3('dns_done: channel=%d peer=%r\n' % (chan, peer)) if peer: del mux.channels[chan] del dnsreqs[chan] @@ -372,7 +374,6 @@ def ondns(listener_sock): mux.send(chan, ssnet.CMD_DNS_REQ, pkt) mux.channels[chan] = lambda cmd,data: dns_done(chan,data) expire_connections(now) - debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) if dnslistener: dnslistener.add_handler(handlers, ondns) diff --git a/server.py b/server.py index b82f9a5..0f69c1c 100644 --- a/server.py +++ b/server.py @@ -222,7 +222,7 @@ def new_channel(channel, data): dnshandlers = {} def dns_req(channel, data): - debug2('Incoming DNS request.\n') + debug2('Incoming DNS request channel=%d.\n' % channel) h = DnsProxy(mux, channel, data) handlers.append(h) dnshandlers[channel] = h @@ -244,6 +244,7 @@ def dns_req(channel, data): now = time.time() for channel,h in dnshandlers.items(): if h.timeout < now or not h.ok: + debug3('expiring dnsreqs channel=%d\n' % channel) del dnshandlers[channel] h.sock.close() h.ok = False From d730a5dcd8706bff7a5a6e82054efd8aba7e01ff Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 23 May 2011 14:58:28 +1000 Subject: [PATCH 39/90] Change debug messages. --- client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client.py b/client.py index 5f42cb3..3c7db23 100644 --- a/client.py +++ b/client.py @@ -326,9 +326,11 @@ def expire_connections(now): debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) for src,(chan,timeout) in udp_by_src.items(): if timeout < now: + debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer)) mux.send(chan, ssnet.CMD_UDP_CLOSE, None) del mux.channels[chan] del udp_by_src[src] + debug3('Remaining UDP channels: %d\n' % len(udp_by_src)) def onaccept_tcp(listener_sock): global _extra_fd @@ -431,7 +433,6 @@ def onaccept_udp(listener_sock): mux.send(chan, ssnet.CMD_UDP_DATA, hdr+data[0]) expire_connections(now) - debug3('Remaining UDP connections: %d\n' % len(udp_by_src)) udp_listener.add_handler(handlers, onaccept_udp) def dns_done(chan, data): From f9d7f40e876d211e7132b48aad21243fbd3d9871 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 24 May 2011 10:58:16 +1000 Subject: [PATCH 40/90] Make UDP support optional. --- client.py | 44 +++++++++++++++++++++++++++++++++----------- firewall.py | 43 ++++++++++++++++++++++++------------------- main.py | 6 +++--- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/client.py b/client.py index 3c7db23..14bf3e6 100644 --- a/client.py +++ b/client.py @@ -3,7 +3,10 @@ import helpers, ssnet, ssh, ssyslog from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from helpers import * -import socket_ext as socket +try: + import socket_ext as socket +except ImportError: + import socket _extra_fd = os.open('/dev/null', os.O_RDONLY) @@ -155,7 +158,7 @@ def print_listening(self, what): debug1('%s listening on %r.\n' % (what, listenip, )) class FirewallClient: - def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy): + def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy, udp): self.auto_nets = [] self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude @@ -164,7 +167,7 @@ def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v ['-v'] * (helpers.verbose or 0) + ['--firewall', str(port_v6), str(port_v4), str(dnsport_v6), str(dnsport_v4), - str(tproxy or 0)]) + str(tproxy or 0), str(udp or 0)]) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ @@ -433,7 +436,8 @@ def onaccept_udp(listener_sock): mux.send(chan, ssnet.CMD_UDP_DATA, hdr+data[0]) expire_connections(now) - udp_listener.add_handler(handlers, onaccept_udp) + if udp_listener: + udp_listener.add_handler(handlers, onaccept_udp) def dns_done(chan, data): peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) @@ -475,6 +479,7 @@ def main(listenip_v6, listenip_v4, ssh_cmd, remotename, python, latency_control, dns, tproxy, seed_hosts, auto_nets, subnets_include, subnets_exclude, syslog, daemon, pidfile): + if syslog: ssyslog.start_syslog() if daemon: @@ -484,7 +489,19 @@ def main(listenip_v6, listenip_v4, log("%s\n" % e) return 5 debug1('Starting sshuttle proxy.\n') - + + if tproxy: + try: + getattr(socket.socket,"recvmsg") + debug1("tproxy UDP support enabled.\n") + udp = True + except AttributeError, e: + debug1("tproxy UDP support requires recvmsg function.\n") + udp = False + else: + debug1("UDP support requires tproxy; disabling UDP.\n") + udp = False + if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]: # if both ports given, no need to search for a spare port ports = [ 0, ] @@ -503,14 +520,17 @@ def main(listenip_v6, listenip_v4, tcp_listener = independent_listener() tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - udp_listener = independent_listener(socket.SOCK_DGRAM) - udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if tproxy: tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + + if udp: + udp_listener = independent_listener(socket.SOCK_DGRAM) + udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) + else: + udp_listener = None if listenip_v6 and listenip_v6[1]: lv6 = listenip_v6 @@ -534,7 +554,8 @@ def main(listenip_v6, listenip_v4, try: tcp_listener.bind(lv6, lv4) - udp_listener.bind(lv6, lv4) + if udp_listener: + udp_listener.bind(lv6, lv4) bound = True break except socket.error, e: @@ -548,7 +569,8 @@ def main(listenip_v6, listenip_v4, raise last_e tcp_listener.listen(10) tcp_listener.print_listening("TCP redirector") - udp_listener.print_listening("UDP redirector") + if udp_listener: + udp_listener.print_listening("UDP redirector") bound = False if dns: @@ -592,7 +614,7 @@ def main(listenip_v6, listenip_v4, dnsport_v4 = 0 dnslistener = None - fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy) + fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy, udp) try: return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, diff --git a/firewall.py b/firewall.py index fd0dd33..e3da821 100644 --- a/firewall.py +++ b/firewall.py @@ -80,7 +80,7 @@ def ipt_ttl(family, *args): # 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_nat(port, dnsport, family, subnets): +def do_iptables_nat(port, dnsport, family, subnets, udp): # only ipv4 supported with NAT if family != socket.AF_INET: return @@ -134,7 +134,7 @@ def do_iptables_nat(port, dnsport, family, subnets): '--dport', '53', '--to-ports', str(dnsport)) -def do_iptables_tproxy(port, dnsport, family, subnets): +def do_iptables_tproxy(port, dnsport, family, subnets, udp): if family not in [socket.AF_INET, socket.AF_INET6]: return if not port: @@ -176,6 +176,7 @@ def do_iptables_tproxy(port, dnsport, family, subnets): ipt(family, table, '-A', divert_chain, '-j', 'ACCEPT') ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'tcp', '-p', 'tcp') + if subnets and udp: ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'udp', '-p', 'udp') @@ -184,31 +185,35 @@ def do_iptables_tproxy(port, dnsport, family, subnets): if subnets: for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True): if f != family: - pass - elif sexclude: + continue + + if sexclude: ipt(family, table, '-A', mark_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') - ipt(family, table, '-A', mark_chain, '-j', 'RETURN', - '--dest', '%s/%s' % (snet,swidth), - '-m', 'udp', '-p', 'udp') ipt(family, table, '-A', tproxy_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') - ipt(family, table, '-A', tproxy_chain, '-j', 'RETURN', - '--dest', '%s/%s' % (snet,swidth), - '-m', 'udp', '-p', 'udp') else: ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') - ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', - '--dest', '%s/%s' % (snet,swidth), - '-m', 'udp', '-p', 'udp') ipt(family, table, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp', '--on-port', str(port)) + + if sexclude and udp: + ipt(family, table, '-A', mark_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'udp', '-p', 'udp') + ipt(family, table, '-A', tproxy_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'udp', '-p', 'udp') + elif udp: + ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + '--dest', '%s/%s' % (snet,swidth), + '-m', 'udp', '-p', 'udp') ipt(family, table, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', '%s/%s' % (snet,swidth), '-m', 'udp', '-p', 'udp', @@ -320,7 +325,7 @@ def ipfw(*args): raise Fatal('%r returned %d' % (argv, rv)) -def do_ipfw(port, dnsport, family, subnets): +def do_ipfw(port, dnsport, family, subnets, udp): # IPv6 support TODO if family != socket.AF_INET: return @@ -486,7 +491,7 @@ def restore_etc_hosts(port): # 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 rules # are hopefully harmless. -def main(port_v6, port_v4, dnsport_v6, dnsport_v4, tproxy, syslog): +def main(port_v6, port_v4, dnsport_v6, dnsport_v4, tproxy, udp, syslog): assert(port_v6 >= 0) assert(port_v6 <= 65535) assert(port_v4 >= 0) @@ -551,8 +556,8 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, tproxy, syslog): try: if line: debug1('firewall manager: starting transproxy.\n') - do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, subnets) - do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets) + do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, subnets, udp) + do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets, udp) sys.stdout.write('STARTED\n') try: @@ -581,6 +586,6 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, tproxy, syslog): debug1('firewall manager: undoing changes.\n') except: pass - do_it(port_v6, 0, socket.AF_INET6, []) - do_it(port_v4, 0, socket.AF_INET, []) + do_it(port_v6, 0, socket.AF_INET6, [], udp) + do_it(port_v4, 0, socket.AF_INET, [], udp) restore_etc_hosts(port_v6 or port_v4) diff --git a/main.py b/main.py index c24e4f6..d26d814 100644 --- a/main.py +++ b/main.py @@ -118,11 +118,11 @@ def parse_ipport6(s): server.tproxy = opt.tproxy sys.exit(server.main()) elif opt.firewall: - if len(extra) != 5: - o.fatal('exactly three arguments expected') + if len(extra) != 6: + o.fatal('exactly six arguments expected') sys.exit(firewall.main(int(extra[0]), int(extra[1]), int(extra[2]), int(extra[3]), - int(extra[4]), opt.syslog)) + int(extra[4]), int(extra[5]), opt.syslog)) elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: From 316301c6714f0215eafc42f4a6bbbde90edb03e9 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 24 May 2011 12:10:16 +1000 Subject: [PATCH 41/90] Various random and obscure issues resolved. --- client.py | 22 ++++++++-------- firewall.py | 76 +++++++++++++++++++++++++++++++---------------------- helpers.py | 9 +++++++ main.py | 18 ++++++++----- server.py | 1 - 5 files changed, 76 insertions(+), 50 deletions(-) diff --git a/client.py b/client.py index 14bf3e6..18bee3a 100644 --- a/client.py +++ b/client.py @@ -158,16 +158,16 @@ def print_listening(self, what): debug1('%s listening on %r.\n' % (what, listenip, )) class FirewallClient: - def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy, udp): + def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method, udp): self.auto_nets = [] self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude - self.tproxy = tproxy + self.method = method argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] + ['-v'] * (helpers.verbose or 0) + ['--firewall', str(port_v6), str(port_v4), str(dnsport_v6), str(dnsport_v4), - str(tproxy or 0), str(udp or 0)]) + method, str(udp or 0)]) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ @@ -239,7 +239,7 @@ def done(self): def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, - dnslistener, tproxy, seed_hosts, auto_nets, + dnslistener, method, seed_hosts, auto_nets, syslog, daemon): handlers = [] if helpers.verbose >= 1: @@ -251,7 +251,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_c try: (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python, stderr=ssyslog._p and ssyslog._p.stdin, - options=dict(latency_control=latency_control, tproxy=tproxy)) + options=dict(latency_control=latency_control, method=method)) except socket.error, e: if e.args[0] == errno.EPIPE: raise Fatal("failed to establish ssh session (1)") @@ -352,7 +352,7 @@ def onaccept_tcp(listener_sock): return else: raise - if tproxy: + if method == "tproxy": dstip = sock.getsockname(); else: dstip = original_dst(sock) @@ -477,7 +477,7 @@ def ondns(listener_sock): def main(listenip_v6, listenip_v4, ssh_cmd, remotename, python, latency_control, dns, - tproxy, seed_hosts, auto_nets, + method, seed_hosts, auto_nets, subnets_include, subnets_exclude, syslog, daemon, pidfile): if syslog: @@ -490,7 +490,7 @@ def main(listenip_v6, listenip_v4, return 5 debug1('Starting sshuttle proxy.\n') - if tproxy: + if method == "tproxy": try: getattr(socket.socket,"recvmsg") debug1("tproxy UDP support enabled.\n") @@ -520,7 +520,7 @@ def main(listenip_v6, listenip_v4, tcp_listener = independent_listener() tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if tproxy: + if method == "tproxy": tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) if udp: @@ -614,12 +614,12 @@ def main(listenip_v6, listenip_v4, dnsport_v4 = 0 dnslistener = None - fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, tproxy, udp) + fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method, udp) try: return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, - tproxy, seed_hosts, auto_nets, syslog, + method, seed_hosts, auto_nets, syslog, daemon) finally: try: diff --git a/firewall.py b/firewall.py index e3da821..0ade55c 100644 --- a/firewall.py +++ b/firewall.py @@ -20,7 +20,7 @@ def ipt_chain_exists(family, table, name): elif family == socket.AF_INET: cmd = 'iptables' else: - raise Fatal("Unsupported socket type '%s'"%family) + raise Fatal("Unsupported family '%s'"%family_to_string(family)) argv = [cmd, '-t', table, '-nL'] p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) for line in p.stdout: @@ -51,7 +51,7 @@ def ipt(family, *args): elif family == socket.AF_INET: ipt4(*args) else: - raise Fatal("Unsupported socket type '%s'"%family) + raise Fatal("Unsupported family '%s'"%family_to_string(family)) _no_ttl_module = False @@ -74,7 +74,6 @@ def ipt_ttl(family, *args): ipt(family, *args) - # 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- @@ -82,13 +81,10 @@ def ipt_ttl(family, *args): # "-A OUTPUT"). def do_iptables_nat(port, dnsport, family, subnets, udp): # only ipv4 supported with NAT - if family != socket.AF_INET: - return - if not port: - subnets = None - port = dnsport - if not port: - return + if family not in [socket.AF_INET, ]: + raise Fatal("Address family '%s' unsupported by nat method"%family_to_string(family)) + if udp: + raise Fatal("UDP not supported by nat method") table = "nat" chain = 'sshuttle-%s' % port @@ -135,13 +131,8 @@ def do_iptables_nat(port, dnsport, family, subnets, udp): '--to-ports', str(dnsport)) def do_iptables_tproxy(port, dnsport, family, subnets, udp): - if family not in [socket.AF_INET, socket.AF_INET6]: - return - if not port: - subnets = None - port = dnsport - if not port: - return + if family not in [socket.AF_INET, sock.socket.AF_INET6]: + raise Fatal("Address family '%s' unsupported by tproxy method"%family_to_string(family)) table = "mangle" mark_chain = 'sshuttle-m-%s' % port @@ -326,9 +317,11 @@ def ipfw(*args): def do_ipfw(port, dnsport, family, subnets, udp): - # IPv6 support TODO - if family != socket.AF_INET: - return + # IPv6 not supported + if family not in [socket.AF_INET, ]: + raise Fatal("Address family '%s' unsupported by ipfw method"%family_to_string(family)) + if udp: + raise Fatal("UDP not supported by ipfw method") sport = str(port) xsport = str(port+1) @@ -491,7 +484,7 @@ def restore_etc_hosts(port): # 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 rules # are hopefully harmless. -def main(port_v6, port_v4, dnsport_v6, dnsport_v4, tproxy, udp, syslog): +def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog): assert(port_v6 >= 0) assert(port_v6 <= 65535) assert(port_v4 >= 0) @@ -504,15 +497,21 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, tproxy, udp, syslog): if os.getuid() != 0: 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'): - if tproxy: - do_it = do_iptables_tproxy - else: + if method == "auto": + if program_exists('ipfw'): + do_it = do_ipfw + elif program_exists('iptables'): do_it = do_iptables_nat + else: + raise Fatal("can't find either ipfw or iptables; check your PATH") + elif method == "nat": + do_it = do_iptables_nat + elif method == "tproxy": + do_it = do_iptables_tproxy + elif method == "ipfw": + do_it = do_ipfw else: - raise Fatal("can't find either ipfw or iptables; check your PATH") + raise Fatal("Unknown method '%s'"%method) # because of limitations of the 'su' command, the *real* stdin/stdout # are both attached to stdout initially. Clone stdout into stdin so we @@ -556,8 +555,19 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, tproxy, udp, syslog): try: if line: debug1('firewall manager: starting transproxy.\n') - do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, subnets, udp) - do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets, udp) + + subnets_v6 = filter(lambda i: i[0]==socket.AF_INET6, subnets) + if port_v6: + do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, fsubnets_v6, udp) + elif len(subnets_v6) > 0: + debug1("IPv6 subnets defined but IPv6 disabled\n") + + subnets_v4 = filter(lambda i: i[0]==socket.AF_INET, subnets) + if port_v4: + do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets_v4, udp) + elif len(subnets_v4) > 0: + debug1("IPv4 subnets defined but IPv4 disabled\n") + sys.stdout.write('STARTED\n') try: @@ -586,6 +596,8 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, tproxy, udp, syslog): debug1('firewall manager: undoing changes.\n') except: pass - do_it(port_v6, 0, socket.AF_INET6, [], udp) - do_it(port_v4, 0, socket.AF_INET, [], udp) + if port_v6: + do_it(port_v6, 0, socket.AF_INET6, [], udp) + if port_v4: + do_it(port_v4, 0, socket.AF_INET, [], udp) restore_etc_hosts(port_v6 or port_v4) diff --git a/helpers.py b/helpers.py index 4b684fa..b2bd78f 100644 --- a/helpers.py +++ b/helpers.py @@ -78,3 +78,12 @@ def guess_address_family(ip): return socket.AF_INET6 else: return socket.AF_INET + +def family_to_string(family): + if family == socket.AF_INET6: + return "AF_INET6" + elif family == socket.AF_INET: + return "AF_INET" + else: + return str(family) + diff --git a/main.py b/main.py index d26d814..173be95 100644 --- a/main.py +++ b/main.py @@ -84,7 +84,7 @@ def parse_ipport6(s): H,auto-hosts scan for remote hostnames and update local /etc/hosts N,auto-nets automatically determine subnets to route dns capture local DNS requests and forward to the remote DNS server -tproxy tproxy support +method= auto, nat, tproxy, or ipfw python= path to python interpreter on the remote server r,remote= ssh hostname (and optional username) of remote sshuttle server x,exclude= exclude this subnet (can be used more than once) @@ -115,14 +115,14 @@ def parse_ipport6(s): if len(extra) != 0: o.fatal('no arguments expected') server.latency_control = opt.latency_control - server.tproxy = opt.tproxy + server.method = opt.method sys.exit(server.main()) elif opt.firewall: if len(extra) != 6: o.fatal('exactly six arguments expected') sys.exit(firewall.main(int(extra[0]), int(extra[1]), int(extra[2]), int(extra[3]), - int(extra[4]), int(extra[5]), opt.syslog)) + extra[4], int(extra[5]), opt.syslog)) elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: @@ -144,8 +144,14 @@ def parse_ipport6(s): sh = [] else: sh = None + if not opt.method: + method = "auto" + elif opt.method in [ "auto", "nat", "tproxy", "ipfw" ]: + method = opt.method + else: + o.fatal("method %s not supported"%opt.method) if not opt.listen: - if opt.tproxy: + if opt.method == "tproxy": ipport_v6 = parse_ipport6('[::1]:0') else: ipport_v6 = None @@ -155,7 +161,7 @@ def parse_ipport6(s): ipport_v4 = None list = opt.listen.split(",") for ip in list: - if '[' in ip and ']' in ip and opt.tproxy: + if '[' in ip and ']' in ip and opt.method == "tproxy": ipport_v6 = parse_ipport6(ip) else: ipport_v4 = parse_ipport4(ip) @@ -165,7 +171,7 @@ def parse_ipport6(s): opt.python, opt.latency_control, opt.dns, - opt.tproxy, + method, sh, opt.auto_nets, parse_subnets(includes), diff --git a/server.py b/server.py index bc43003..6454f86 100644 --- a/server.py +++ b/server.py @@ -276,7 +276,6 @@ def udp_req(channel, cmd, data): def udp_open(channel, data): debug2('Incoming UDP open.\n') family = int(data) - debug2('Incoming UDP open. %d.\n'%family) mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd, data) if channel in udphandlers: raise Fatal('UDP connection channel %d already open'%channel) From 42bfa96c4f08686327d0befe0c84e39f20bf61e2 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 24 May 2011 12:15:31 +1000 Subject: [PATCH 42/90] Fix debugging. --- client.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/client.py b/client.py index 18bee3a..8b837ff 100644 --- a/client.py +++ b/client.py @@ -356,7 +356,7 @@ def onaccept_tcp(listener_sock): dstip = sock.getsockname(); else: dstip = original_dst(sock) - debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], + debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], dstip[0],dstip[1])) if dstip[1] == sock.getsockname()[1] and islocal(dstip[0],sock.family): debug1("-- ignored: that's my address!\n") @@ -390,16 +390,11 @@ def onaccept_udp(listener_sock): now = time.time() dstip = None family = None - print "a", srcip, data, adata, flags for a in adata: - print "b",a.cmsg_level, a.cmsg_type if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR: family,port = struct.unpack('=HH', a.cmsg_data[0:4]) port = socket.htons(port) - print "c4", family, port, socket.AF_INET, socket.AF_INET6 if family == socket.AF_INET: - print "IPV4" - print struct.unpack("=BBBBBBBBBBBBBBBB",a.cmsg_data) start = 4 length = 4 else: @@ -410,20 +405,19 @@ def onaccept_udp(listener_sock): elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR: family,port = struct.unpack('=HH', a.cmsg_data[0:4]) port = socket.htons(port) - print "c6", family, port, socket.AF_INET, socket.AF_INET6 if family == socket.AF_INET6: - print "IPV6" start = 8 length = 16 else: raise Fatal("Unsupported socket type '%s'"%family) ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) dstip = (ip, port) - print dstip break if not dstip: - debug1("-- ignored: couldn't determine destination IP address\n") + debug1("-- ignored UDP from %s:%r: couldn't determine destination IP address\n"%srcip) return + debug1('Accept UDP: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], + dstip[0],dstip[1])) if srcip in udp_by_src: chan,timeout = udp_by_src[srcip] else: From a88784eb2df6d6725082cc2a90a6b1af6e005189 Mon Sep 17 00:00:00 2001 From: Brian May Date: Wed, 25 May 2011 14:33:51 +1000 Subject: [PATCH 43/90] Remove redundant family check. --- firewall.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/firewall.py b/firewall.py index 0ade55c..e7d972d 100644 --- a/firewall.py +++ b/firewall.py @@ -109,9 +109,7 @@ def do_iptables_nat(port, dnsport, family, subnets, udp): # excludes to come first. That's why the columns are in such a non- # intuitive order. for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True): - if f != family: - pass - elif sexclude: + if sexclude: ipt(family, table, '-A', chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp') @@ -175,9 +173,6 @@ def do_iptables_tproxy(port, dnsport, family, subnets, udp): if subnets: for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True): - if f != family: - continue - if sexclude: ipt(family, table, '-A', mark_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), From 0a6ebf8b48b4b6c64a7b0aee4799ade5fa1c45c3 Mon Sep 17 00:00:00 2001 From: Brian May Date: Wed, 25 May 2011 16:03:59 +1000 Subject: [PATCH 44/90] Fix broken parameter. --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index 8b837ff..4d45d8c 100644 --- a/client.py +++ b/client.py @@ -330,7 +330,7 @@ def expire_connections(now): for src,(chan,timeout) in udp_by_src.items(): if timeout < now: debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer)) - mux.send(chan, ssnet.CMD_UDP_CLOSE, None) + mux.send(chan, ssnet.CMD_UDP_CLOSE, '') del mux.channels[chan] del udp_by_src[src] debug3('Remaining UDP channels: %d\n' % len(udp_by_src)) From 655e6fe65e19f9863e6ec2ff537defe63fe32cbe Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 26 May 2011 09:54:35 +1000 Subject: [PATCH 45/90] Fix typos. --- firewall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firewall.py b/firewall.py index e7d972d..b68ba48 100644 --- a/firewall.py +++ b/firewall.py @@ -129,7 +129,7 @@ def do_iptables_nat(port, dnsport, family, subnets, udp): '--to-ports', str(dnsport)) def do_iptables_tproxy(port, dnsport, family, subnets, udp): - if family not in [socket.AF_INET, sock.socket.AF_INET6]: + if family not in [socket.AF_INET, socket.AF_INET6]: raise Fatal("Address family '%s' unsupported by tproxy method"%family_to_string(family)) table = "mangle" @@ -553,7 +553,7 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog): subnets_v6 = filter(lambda i: i[0]==socket.AF_INET6, subnets) if port_v6: - do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, fsubnets_v6, udp) + do_wait = do_it(port_v6, dnsport_v6, socket.AF_INET6, subnets_v6, udp) elif len(subnets_v6) > 0: debug1("IPv6 subnets defined but IPv6 disabled\n") From 8754b81d7a0f4d3a5578d2770d9fb95851c2e86c Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 31 May 2011 15:38:13 +1000 Subject: [PATCH 46/90] Merge in upstream changes. --- client.py | 278 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 144 insertions(+), 134 deletions(-) diff --git a/client.py b/client.py index 4d45d8c..3c262c7 100644 --- a/client.py +++ b/client.py @@ -119,11 +119,11 @@ def setsockopt(self, level, optname, value): if self.v4: self.v4.setsockopt(level, optname, value) - def add_handler(self, handlers, handler): + def add_handler(self, handlers, handler, method, mux): if self.v6: - handlers.append(Handler([self.v6], lambda: handler(self.v6))) + handlers.append(Handler([self.v6], lambda: handler(self.v6, method, mux, handlers))) if self.v4: - handlers.append(Handler([self.v4], lambda: handler(self.v4))) + handlers.append(Handler([self.v4], lambda: handler(self.v4, method, mux, handlers))) def listen(self, backlog): if self.v6: @@ -238,6 +238,144 @@ def done(self): raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) +dnsreqs = {} +udp_by_src = {} +def expire_connections(now): + for chan,(peer,sock,timeout) in dnsreqs.items(): + if timeout < now: + debug3('expiring dnsreqs channel=%d peer=%r\n' % (chan, peer)) + del mux.channels[chan] + del dnsreqs[chan] + debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) + for src,(chan,timeout) in udp_by_src.items(): + if timeout < now: + debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer)) + mux.send(chan, ssnet.CMD_UDP_CLOSE, '') + del mux.channels[chan] + del udp_by_src[src] + debug3('Remaining UDP channels: %d\n' % len(udp_by_src)) + + +def onaccept_tcp(listener, method, mux, handlers): + global _extra_fd + try: + sock,srcip = listener.accept() + except socket.error, e: + if e.args[0] in [errno.EMFILE, errno.ENFILE]: + debug1('Rejected incoming connection: too many open files!\n') + # free up an fd so we can eat the connection + os.close(_extra_fd) + try: + sock,srcip = listener.accept() + sock.close() + finally: + _extra_fd = os.open('/dev/null', os.O_RDONLY) + return + else: + raise + if method == "tproxy": + dstip = sock.getsockname(); + else: + dstip = original_dst(sock) + debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], + dstip[0],dstip[1])) + if dstip[1] == sock.getsockname()[1] and islocal(dstip[0],sock.family): + debug1("-- ignored: that's my address!\n") + sock.close() + return + chan = mux.next_channel() + if not chan: + log('warning: too many open channels. Discarded connection.\n') + sock.close() + return + mux.send(chan, ssnet.CMD_TCP_CONNECT, '%d,%s,%r' % (sock.family, dstip[0], dstip[1])) + outwrap = MuxWrapper(mux, chan) + handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) + expire_connections(time.time()) + + +def udp_done(chan, data, family, dstip): + (src,srcport,data) = data.split(",",2) + srcip = (src,int(srcport)) + debug3('doing send from %r port %d to %r port %d\n' % (srcip[0],srcip[1],dstip[0],dstip[1],)) + + sock = socket.socket(family, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + sock.bind(srcip) + sock.sendto(data, dstip) + sock.close() + + +def onaccept_udp(listener, method, mux, handlers): + srcip, data, adata, flags = listener.recvmsg((4096,),socket.CMSG_SPACE(24)) + now = time.time() + dstip = None + family = None + for a in adata: + if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR: + family,port = struct.unpack('=HH', a.cmsg_data[0:4]) + port = socket.htons(port) + if family == socket.AF_INET: + start = 4 + length = 4 + else: + raise Fatal("Unsupported socket type '%s'"%family) + ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) + dstip = (ip, port) + break + elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR: + family,port = struct.unpack('=HH', a.cmsg_data[0:4]) + port = socket.htons(port) + if family == socket.AF_INET6: + start = 8 + length = 16 + else: + raise Fatal("Unsupported socket type '%s'"%family) + ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) + dstip = (ip, port) + break + if not dstip: + debug1("-- ignored UDP from %s:%r: couldn't determine destination IP address\n"%srcip) + return + debug1('Accept UDP: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], + dstip[0],dstip[1])) + if srcip in udp_by_src: + chan,timeout = udp_by_src[srcip] + else: + chan = mux.next_channel() + mux.channels[chan] = lambda cmd,data: udp_done(chan,data,family,dstip=srcip) + mux.send(chan, ssnet.CMD_UDP_OPEN, family) + udp_by_src[srcip] = chan,now+30 + + hdr = "%s,%r,"%(dstip[0], dstip[1]) + mux.send(chan, ssnet.CMD_UDP_DATA, hdr+data[0]) + + expire_connections(now) + + +def dns_done(chan, mux, data): + peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) + debug3('dns_done: channel=%d peer=%r\n' % (chan, peer)) + if peer: + del mux.channels[chan] + del dnsreqs[chan] + debug3('doing sendto %r\n' % (peer,)) + sock.sendto(data, peer) + + +def ondns(listener, method, mux, handlers): + pkt,peer = listener.recvfrom(4096) + now = time.time() + if pkt: + debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt))) + chan = mux.next_channel() + dnsreqs[chan] = peer,listener,now+30 + mux.send(chan, ssnet.CMD_DNS_REQ, pkt) + mux.channels[chan] = lambda cmd,data: dns_done(chan, mux, data) + expire_connections(now) + + def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, method, seed_hosts, auto_nets, syslog, daemon): @@ -318,141 +456,13 @@ def onhostlist(hostlist): fw.sethostip(name, ip) mux.got_host_list = onhostlist - dnsreqs = {} - udp_by_src = {} - def expire_connections(now): - for chan,(peer,sock,timeout) in dnsreqs.items(): - if timeout < now: - debug3('expiring dnsreqs channel=%d peer=%r\n' % (chan, peer)) - del mux.channels[chan] - del dnsreqs[chan] - debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) - for src,(chan,timeout) in udp_by_src.items(): - if timeout < now: - debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer)) - mux.send(chan, ssnet.CMD_UDP_CLOSE, '') - del mux.channels[chan] - del udp_by_src[src] - debug3('Remaining UDP channels: %d\n' % len(udp_by_src)) - - def onaccept_tcp(listener_sock): - global _extra_fd - try: - sock,srcip = listener_sock.accept() - except socket.error, e: - if e.args[0] in [errno.EMFILE, errno.ENFILE]: - debug1('Rejected incoming connection: too many open files!\n') - # free up an fd so we can eat the connection - os.close(_extra_fd) - try: - sock,srcip = listener_sock.accept() - sock.close() - finally: - _extra_fd = os.open('/dev/null', os.O_RDONLY) - return - else: - raise - if method == "tproxy": - dstip = sock.getsockname(); - else: - dstip = original_dst(sock) - debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], - dstip[0],dstip[1])) - if dstip[1] == sock.getsockname()[1] and islocal(dstip[0],sock.family): - debug1("-- ignored: that's my address!\n") - sock.close() - return - chan = mux.next_channel() - if not chan: - log('warning: too many open channels. Discarded connection.\n') - sock.close() - return - mux.send(chan, ssnet.CMD_TCP_CONNECT, '%d,%s,%r' % (sock.family, dstip[0], dstip[1])) - outwrap = MuxWrapper(mux, chan) - handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) - expire_connections(time.time()) - tcp_listener.add_handler(handlers, onaccept_tcp) - - def udp_done(chan, data, family, dstip): - (src,srcport,data) = data.split(",",2) - srcip = (src,int(srcport)) - debug3('doing send from %r port %d to %r port %d\n' % (srcip[0],srcip[1],dstip[0],dstip[1],)) - - sock = socket.socket(family, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - sock.bind(srcip) - sock.sendto(data, dstip) - sock.close() - - def onaccept_udp(listener_sock): - srcip, data, adata, flags = listener_sock.recvmsg((4096,),socket.CMSG_SPACE(24)) - now = time.time() - dstip = None - family = None - for a in adata: - if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR: - family,port = struct.unpack('=HH', a.cmsg_data[0:4]) - port = socket.htons(port) - if family == socket.AF_INET: - start = 4 - length = 4 - else: - raise Fatal("Unsupported socket type '%s'"%family) - ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) - dstip = (ip, port) - break - elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR: - family,port = struct.unpack('=HH', a.cmsg_data[0:4]) - port = socket.htons(port) - if family == socket.AF_INET6: - start = 8 - length = 16 - else: - raise Fatal("Unsupported socket type '%s'"%family) - ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) - dstip = (ip, port) - break - if not dstip: - debug1("-- ignored UDP from %s:%r: couldn't determine destination IP address\n"%srcip) - return - debug1('Accept UDP: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], - dstip[0],dstip[1])) - if srcip in udp_by_src: - chan,timeout = udp_by_src[srcip] - else: - chan = mux.next_channel() - mux.channels[chan] = lambda cmd,data: udp_done(chan,data,family,dstip=srcip) - mux.send(chan, ssnet.CMD_UDP_OPEN, family) - udp_by_src[srcip] = chan,now+30 - - hdr = "%s,%r,"%(dstip[0], dstip[1]) - mux.send(chan, ssnet.CMD_UDP_DATA, hdr+data[0]) + tcp_listener.add_handler(handlers, onaccept_tcp, method, mux) - expire_connections(now) if udp_listener: - udp_listener.add_handler(handlers, onaccept_udp) + udp_listener.add_handler(handlers, onaccept_udp, method, mux) - def dns_done(chan, data): - peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) - debug3('dns_done: channel=%d peer=%r\n' % (chan, peer)) - if peer: - del mux.channels[chan] - del dnsreqs[chan] - debug3('doing sendto %r\n' % (peer,)) - sock.sendto(data, peer) - def ondns(listener_sock): - pkt,peer = listener_sock.recvfrom(4096) - now = time.time() - if pkt: - debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt))) - chan = mux.next_channel() - dnsreqs[chan] = peer,listener_sock,now+30 - mux.send(chan, ssnet.CMD_DNS_REQ, pkt) - mux.channels[chan] = lambda cmd,data: dns_done(chan,data) - expire_connections(now) if dnslistener: - dnslistener.add_handler(handlers, ondns) + dnslistener.add_handler(handlers, ondns, method, mux) if seed_hosts != None: debug1('seed_hosts: %r\n' % seed_hosts) From 15306246f219487ea530ebe426475202cf69f710 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 31 May 2011 15:40:11 +1000 Subject: [PATCH 47/90] Merge in upstream ad57904fd64127c115021d94ef49ebb9bfc661c0. ssnet.py: deal with a possible connect/getsockopt(SO_ERROR) race. Seems to affect Linux servers. Ed Maste says the patch fixes it for him. --- ssnet.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ssnet.py b/ssnet.py index 3ed7209..4a769cc 100644 --- a/ssnet.py +++ b/ssnet.py @@ -158,6 +158,17 @@ def try_connect(self): debug3('%r: fixed connect result: %s\n' % (self, e)) if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]: pass # not connected yet + elif e.args[0] == 0: + # connected successfully (weird Linux bug?) + # Sometimes Linux seems to return EINVAL when it isn't + # invalid. This *may* be caused by a race condition + # between connect() and getsockopt(SO_ERROR) (ie. it + # finishes connecting in between the two, so there is no + # longer an error). However, I'm not sure of that. + # + # I did get at least one report that the problem went away + # when we added this, however. + self.connect_to = None elif e.args[0] == errno.EISCONN: # connected successfully (BSD) self.connect_to = None From a05e6d7a1231c332898a0b80a744be1e1e7369da Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 31 May 2011 15:41:25 +1000 Subject: [PATCH 48/90] Make variable name more consistant. --- client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client.py b/client.py index 3c262c7..731ad0e 100644 --- a/client.py +++ b/client.py @@ -377,7 +377,7 @@ def ondns(listener, method, mux, handlers): def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, - dnslistener, method, seed_hosts, auto_nets, + dns_listener, method, seed_hosts, auto_nets, syslog, daemon): handlers = [] if helpers.verbose >= 1: @@ -461,8 +461,8 @@ def onhostlist(hostlist): if udp_listener: udp_listener.add_handler(handlers, onaccept_udp, method, mux) - if dnslistener: - dnslistener.add_handler(handlers, ondns, method, mux) + if dns_listener: + dns_listener.add_handler(handlers, ondns, method, mux) if seed_hosts != None: debug1('seed_hosts: %r\n' % seed_hosts) @@ -583,8 +583,8 @@ def main(listenip_v6, listenip_v4, ports = xrange(12300,9000,-1) for port in ports: debug2(' %d' % port) - dnslistener = independent_listener(socket.SOCK_DGRAM) - dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + dns_listener = independent_listener(socket.SOCK_DGRAM) + dns_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if listenip_v6: lv6 = (listenip_v6[0],port) @@ -600,7 +600,7 @@ def main(listenip_v6, listenip_v4, dnsport_v4 = 0 try: - dnslistener.bind( lv6, lv4 ) + dns_listener.bind( lv6, lv4 ) bound = True break except socket.error, e: @@ -609,20 +609,20 @@ def main(listenip_v6, listenip_v4, else: raise e debug2('\n') - dnslistener.print_listening("DNS") + dns_listener.print_listening("DNS") if not bound: assert(last_e) raise last_e else: dnsport_v6 = 0 dnsport_v4 = 0 - dnslistener = None + dns_listener = None fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method, udp) try: return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, - python, latency_control, dnslistener, + python, latency_control, dns_listener, method, seed_hosts, auto_nets, syslog, daemon) finally: From 466ecd9aeaab60049d3c105698f45f31b81f3f1e Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 31 May 2011 15:59:51 +1000 Subject: [PATCH 49/90] Pass mux to expires(...) function. --- client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client.py b/client.py index 731ad0e..c09c0e8 100644 --- a/client.py +++ b/client.py @@ -240,7 +240,7 @@ def done(self): dnsreqs = {} udp_by_src = {} -def expire_connections(now): +def expire_connections(now, mux): for chan,(peer,sock,timeout) in dnsreqs.items(): if timeout < now: debug3('expiring dnsreqs channel=%d peer=%r\n' % (chan, peer)) @@ -291,7 +291,7 @@ def onaccept_tcp(listener, method, mux, handlers): mux.send(chan, ssnet.CMD_TCP_CONNECT, '%d,%s,%r' % (sock.family, dstip[0], dstip[1])) outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) - expire_connections(time.time()) + expire_connections(time.time(), mux) def udp_done(chan, data, family, dstip): @@ -351,7 +351,7 @@ def onaccept_udp(listener, method, mux, handlers): hdr = "%s,%r,"%(dstip[0], dstip[1]) mux.send(chan, ssnet.CMD_UDP_DATA, hdr+data[0]) - expire_connections(now) + expire_connections(now, mux) def dns_done(chan, mux, data): @@ -373,7 +373,7 @@ def ondns(listener, method, mux, handlers): dnsreqs[chan] = peer,listener,now+30 mux.send(chan, ssnet.CMD_DNS_REQ, pkt) mux.channels[chan] = lambda cmd,data: dns_done(chan, mux, data) - expire_connections(now) + expire_connections(now, mux) def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, From 8de452c2ec65f92050eaffa26662860c7a1462fe Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 6 Jun 2011 17:15:54 +1000 Subject: [PATCH 50/90] Test for wrong error value fixed. --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index c09c0e8..e72b018 100644 --- a/client.py +++ b/client.py @@ -563,7 +563,7 @@ def main(listenip_v6, listenip_v4, bound = True break except socket.error, e: - if e.errno == errno.EADDRNOTAVAIL: + if e.errno == errno.EADDRINUSE: last_e = e else: raise e From f073566075f2c31cd2267654c4c2182c55a0a652 Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 6 Jun 2011 17:18:11 +1000 Subject: [PATCH 51/90] Cosmetic improvements. --- client.py | 4 ++-- firewall.py | 2 +- helpers.py | 1 + server.py | 1 + ssnet.py | 3 ++- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/client.py b/client.py index e72b018..c1ab37e 100644 --- a/client.py +++ b/client.py @@ -279,7 +279,7 @@ def onaccept_tcp(listener, method, mux, handlers): dstip = original_dst(sock) debug1('Accept TCP: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], dstip[0],dstip[1])) - if dstip[1] == sock.getsockname()[1] and islocal(dstip[0],sock.family): + if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family): debug1("-- ignored: that's my address!\n") sock.close() return @@ -600,7 +600,7 @@ def main(listenip_v6, listenip_v4, dnsport_v4 = 0 try: - dns_listener.bind( lv6, lv4 ) + dns_listener.bind(lv6, lv4) bound = True break except socket.error, e: diff --git a/firewall.py b/firewall.py index b68ba48..8276539 100644 --- a/firewall.py +++ b/firewall.py @@ -52,7 +52,7 @@ def ipt(family, *args): ipt4(*args) else: raise Fatal("Unsupported family '%s'"%family_to_string(family)) - + _no_ttl_module = False def ipt_ttl(family, *args): diff --git a/helpers.py b/helpers.py index b2bd78f..e67124a 100644 --- a/helpers.py +++ b/helpers.py @@ -79,6 +79,7 @@ def guess_address_family(ip): else: return socket.AF_INET + def family_to_string(family): if family == socket.AF_INET6: return "AF_INET6" diff --git a/server.py b/server.py index 6454f86..c9ba90c 100644 --- a/server.py +++ b/server.py @@ -197,6 +197,7 @@ def main(): else: helpers.logprefix = 'server: ' debug1('latency control setting = %r\n' % latency_control) + routes = list(list_routes()) debug1('available routes:\n') for r in routes: diff --git a/ssnet.py b/ssnet.py index 4a769cc..67747a4 100644 --- a/ssnet.py +++ b/ssnet.py @@ -134,7 +134,8 @@ def try_connect(self): return # already connected self.rsock.setblocking(False) debug3('%r: trying connect to %r\n' % (self, self.connect_to)) - if self.rsock.family==socket.AF_INET and socket.inet_pton(self.rsock.family, self.connect_to[0])[0] == '\0': + family = self.rsock.family + if family==socket.AF_INET and socket.inet_pton(family, self.connect_to[0])[0] == '\0': self.seterr(Exception("Can't connect to %r: " "IP address starts with zero\n" % (self.connect_to,))) From cadf25356c9160091b03af3bb42943bfcf6dec37 Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 6 Jun 2011 17:21:19 +1000 Subject: [PATCH 52/90] Add address family to automatically detected routes. --- client.py | 4 ++-- server.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client.py b/client.py index c1ab37e..7e479fb 100644 --- a/client.py +++ b/client.py @@ -434,8 +434,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_c def onroutes(routestr): if auto_nets: for line in routestr.strip().split('\n'): - (ip,width) = line.split(',', 1) - fw.auto_nets.append((ip,int(width))) + (family,ip,width) = line.split(',', 2) + fw.auto_nets.append((family,ip,int(width))) # we definitely want to do this *after* starting ssh, or we might end # up intercepting the ssh connection! diff --git a/server.py b/server.py index c9ba90c..2602c3d 100644 --- a/server.py +++ b/server.py @@ -59,7 +59,7 @@ def _list_routes(): mask = _maskbits(maskw) # returns 32 if maskw is null width = min(ipw[1], mask) ip = ipw[0] & _shl(_shl(1, width) - 1, 32-width) - routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width)) + routes.append((socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width)) rv = p.wait() if rv != 0: log('WARNING: %r returned %d\n' % (argv, rv)) @@ -68,9 +68,9 @@ def _list_routes(): def list_routes(): - for (ip,width) in _list_routes(): + for (family, ip,width) in _list_routes(): if not ip.startswith('0.') and not ip.startswith('127.'): - yield (ip,width) + yield (family, ip,width) def _exc_dump(): @@ -201,7 +201,7 @@ def main(): routes = list(list_routes()) debug1('available routes:\n') for r in routes: - debug1(' %s/%d\n' % r) + debug1(' %d/%s/%d\n' % r) # synchronization header sys.stdout.write('\0\0SSHUTTLE0001') @@ -215,7 +215,7 @@ def main(): handlers.append(mux) routepkt = '' for r in routes: - routepkt += '%s,%d\n' % r + routepkt += '%d,%s,%d\n' % r mux.send(0, ssnet.CMD_ROUTES, routepkt) hw = Hostwatch() From a3dec9852b59c2734beee966bfe01557b14b3bc6 Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 6 Jun 2011 17:32:06 +1000 Subject: [PATCH 53/90] This was changed in 29d1fab5819529b8bce702bf6e9a38c4bcd1b64b, but I don't understand why. --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index 7e479fb..58045f8 100644 --- a/client.py +++ b/client.py @@ -288,7 +288,7 @@ def onaccept_tcp(listener, method, mux, handlers): log('warning: too many open channels. Discarded connection.\n') sock.close() return - mux.send(chan, ssnet.CMD_TCP_CONNECT, '%d,%s,%r' % (sock.family, dstip[0], dstip[1])) + mux.send(chan, ssnet.CMD_TCP_CONNECT, '%d,%s,%s' % (sock.family, dstip[0], dstip[1])) outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) expire_connections(time.time(), mux) From ce5cfeb0a859a9e9f4108680df73778cf413e537 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 7 Jun 2011 10:41:01 +1000 Subject: [PATCH 54/90] Make recvmsg work with different recvmsg libraries. --- client.py | 117 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 32 deletions(-) diff --git a/client.py b/client.py index 58045f8..b925303 100644 --- a/client.py +++ b/client.py @@ -3,10 +3,23 @@ import helpers, ssnet, ssh, ssyslog from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from helpers import * + +recvmsg = None try: - import socket_ext as socket -except ImportError: - import socket + # try getting recvmsg from python + import socket as pythonsocket + getattr(pythonsocket.socket,"recvmsg") + socket = pythonsocket + recvmsg = "python" +except AttributeError: + # try getting recvmsg from socket_ext library + try: + import socket_ext + getattr(socket_ext.socket,"recvmsg") + socket = socket_ext + recvmsg = "socket_ext" + except ImportError: + import socket _extra_fd = os.open('/dev/null', os.O_RDONLY) @@ -23,6 +36,72 @@ def got_signal(signum, frame): IPV6_ORIGDSTADDR = 74 IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR +if recvmsg == "python": + def recv_udp(listener, bufsize): + debug3('Accept UDP python using recvmsg.\n') + data, ancdata, msg_flags, srcip = listener.recvmsg(4096,socket.CMSG_SPACE(24)) + dstip = None + family = None + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR: + family,port = struct.unpack('=HH', cmsg_data[0:4]) + port = socket.htons(port) + if family == socket.AF_INET: + start = 4 + length = 4 + else: + raise Fatal("Unsupported socket type '%s'"%family) + ip = socket.inet_ntop(family, cmsg_data[start:start+length]) + dstip = (ip, port) + break + elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR: + family,port = struct.unpack('=HH', cmsg_data[0:4]) + port = socket.htons(port) + if family == socket.AF_INET6: + start = 8 + length = 16 + else: + raise Fatal("Unsupported socket type '%s'"%family) + ip = socket.inet_ntop(family, cmsg_data[start:start+length]) + dstip = (ip, port) + break + return (srcip, dstip, data) +elif recvmsg == "socket_ext": + def recv_udp(listener, bufsize): + debug3('Accept UDP using socket_ext recvmsg.\n') + srcip, data, adata, flags = listener.recvmsg((bufsize,),socket.CMSG_SPACE(24)) + dstip = None + family = None + for a in adata: + if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR: + family,port = struct.unpack('=HH', a.cmsg_data[0:4]) + port = socket.htons(port) + if family == socket.AF_INET: + start = 4 + length = 4 + else: + raise Fatal("Unsupported socket type '%s'"%family) + ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) + dstip = (ip, port) + break + elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR: + family,port = struct.unpack('=HH', a.cmsg_data[0:4]) + port = socket.htons(port) + if family == socket.AF_INET6: + start = 8 + length = 16 + else: + raise Fatal("Unsupported socket type '%s'"%family) + ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) + dstip = (ip, port) + break + return (srcip, dstip, data) +else: + def recv_udp(listener, bufsize): + debug3('Accept UDP using recvfrom.\n') + data, srcip = listener.recvfrom(bufsize) + return (srcip, None, data) + def check_daemon(pidfile): global _pidname _pidname = os.path.abspath(pidfile) @@ -308,33 +387,8 @@ def udp_done(chan, data, family, dstip): def onaccept_udp(listener, method, mux, handlers): - srcip, data, adata, flags = listener.recvmsg((4096,),socket.CMSG_SPACE(24)) now = time.time() - dstip = None - family = None - for a in adata: - if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR: - family,port = struct.unpack('=HH', a.cmsg_data[0:4]) - port = socket.htons(port) - if family == socket.AF_INET: - start = 4 - length = 4 - else: - raise Fatal("Unsupported socket type '%s'"%family) - ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) - dstip = (ip, port) - break - elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR: - family,port = struct.unpack('=HH', a.cmsg_data[0:4]) - port = socket.htons(port) - if family == socket.AF_INET6: - start = 8 - length = 16 - else: - raise Fatal("Unsupported socket type '%s'"%family) - ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) - dstip = (ip, port) - break + srcip, dstip, data = recv_udp(listener, 4096) if not dstip: debug1("-- ignored UDP from %s:%r: couldn't determine destination IP address\n"%srcip) return @@ -495,11 +549,10 @@ def main(listenip_v6, listenip_v4, debug1('Starting sshuttle proxy.\n') if method == "tproxy": - try: - getattr(socket.socket,"recvmsg") + if recvmsg is not None: debug1("tproxy UDP support enabled.\n") udp = True - except AttributeError, e: + else: debug1("tproxy UDP support requires recvmsg function.\n") udp = False else: From cb761cf5ba83215af50f46e389159876fa3d84b2 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 7 Jun 2011 10:42:00 +1000 Subject: [PATCH 55/90] Pass 1 as parameter, not True. How did this ever work? --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index b925303..9ea1de0 100644 --- a/client.py +++ b/client.py @@ -246,7 +246,7 @@ def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v ['-v'] * (helpers.verbose or 0) + ['--firewall', str(port_v6), str(port_v4), str(dnsport_v6), str(dnsport_v4), - method, str(udp or 0)]) + method, str(int(udp))]) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ From ab74d916879a0ddc0e528d1e95084dc8ef7ae273 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 7 Jun 2011 10:43:03 +1000 Subject: [PATCH 56/90] Variable name was wrong. --- client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client.py b/client.py index 9ea1de0..0363741 100644 --- a/client.py +++ b/client.py @@ -326,12 +326,12 @@ def expire_connections(now, mux): del mux.channels[chan] del dnsreqs[chan] debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) - for src,(chan,timeout) in udp_by_src.items(): + for peer,(chan,timeout) in udp_by_src.items(): if timeout < now: debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer)) mux.send(chan, ssnet.CMD_UDP_CLOSE, '') del mux.channels[chan] - del udp_by_src[src] + del udp_by_src[peer] debug3('Remaining UDP channels: %d\n' % len(udp_by_src)) From 4ef5af52bf8871db61dfe561ee67fc71b01f7e4e Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 7 Jun 2011 10:43:45 +1000 Subject: [PATCH 57/90] Previous change to recvmsg made family go out of scope. Fixed. --- client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client.py b/client.py index 0363741..3be6dda 100644 --- a/client.py +++ b/client.py @@ -398,8 +398,8 @@ def onaccept_udp(listener, method, mux, handlers): chan,timeout = udp_by_src[srcip] else: chan = mux.next_channel() - mux.channels[chan] = lambda cmd,data: udp_done(chan,data,family,dstip=srcip) - mux.send(chan, ssnet.CMD_UDP_OPEN, family) + mux.channels[chan] = lambda cmd,data: udp_done(chan,data, listener.family, dstip=srcip) + mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family) udp_by_src[srcip] = chan,now+30 hdr = "%s,%r,"%(dstip[0], dstip[1]) From 0c75c600a06cfcdce4f2c57007021a3a19e1f4e2 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 7 Jun 2011 10:44:44 +1000 Subject: [PATCH 58/90] We don't want to bind to the same port as the UDP listener. Really. --- client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client.py b/client.py index 3be6dda..fc2dcae 100644 --- a/client.py +++ b/client.py @@ -637,7 +637,6 @@ def main(listenip_v6, listenip_v4, for port in ports: debug2(' %d' % port) dns_listener = independent_listener(socket.SOCK_DGRAM) - dns_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if listenip_v6: lv6 = (listenip_v6[0],port) From e4dc4426176d15c9ba3a53d8a65ba0a6c45cbd83 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 7 Jun 2011 10:45:28 +1000 Subject: [PATCH 59/90] Fix errno. Was fixed above, but not here. --- client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.py b/client.py index fc2dcae..b26c890 100644 --- a/client.py +++ b/client.py @@ -656,7 +656,7 @@ def main(listenip_v6, listenip_v4, bound = True break except socket.error, e: - if e.errno == errno.EADDRNOTAVAIL: + if e.errno == errno.EADDRINUSE: last_e = e else: raise e From 0316c53143f05a612109aa67f5addc2424af5355 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 7 Jun 2011 10:46:41 +1000 Subject: [PATCH 60/90] Improve debugging. --- server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 2602c3d..6857245 100644 --- a/server.py +++ b/server.py @@ -261,14 +261,15 @@ def dns_req(channel, data): udphandlers = {} def udp_req(channel, cmd, data): - debug2('Incoming UDP request.\n') + debug2('Incoming UDP request channel=%d, cmd=%d\n' % (channel,cmd)) if cmd == ssnet.CMD_UDP_DATA: (dstip,dstport,data) = data.split(",",2) dstport = int(dstport) - debug2('Incoming UDP request. %r %d.\n'%(dstip,dstport)) + debug2('is incoming UDP data. %r %d.\n' % (dstip,dstport)) h = udphandlers[channel] h.send((dstip,dstport),data) elif cmd == ssnet.CMD_UDP_CLOSE: + debug2('is incoming UDP close\n') h = udphandlers[channel] h.sock.close() del mux.channels[channel] From 94cadd93fde74177ccdf633d40e61b0401d6250e Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 7 Jun 2011 10:47:12 +1000 Subject: [PATCH 61/90] Closing of connections has to happen in main function. --- server.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 6857245..d85a6dd 100644 --- a/server.py +++ b/server.py @@ -271,9 +271,8 @@ def udp_req(channel, cmd, data): elif cmd == ssnet.CMD_UDP_CLOSE: debug2('is incoming UDP close\n') h = udphandlers[channel] - h.sock.close() + h.ok = False del mux.channels[channel] - del udphandlers[channel] def udp_open(channel, data): debug2('Incoming UDP open.\n') @@ -308,3 +307,9 @@ def udp_open(channel, data): del dnshandlers[channel] h.sock.close() h.ok = False + for channel,h in udphandlers.items(): + if not h.ok: + debug3('expiring UDP channel=%d\n' % channel) + del udphandlers[channel] + h.sock.close() + h.ok = False From 00686436ef9bc470f474cd528d2bb7a63ff3d2ab Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 9 Jun 2011 16:30:10 +1000 Subject: [PATCH 62/90] Adjust whitespace. --- firewall.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firewall.py b/firewall.py index 8276539..513f97a 100644 --- a/firewall.py +++ b/firewall.py @@ -118,7 +118,7 @@ def do_iptables_nat(port, dnsport, family, subnets, udp): '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp', '--to-ports', str(port)) - + if dnsport: nslist = resolvconf_nameservers() for ip in filter(lambda i: guess_address_family(i)==family, nslist): @@ -128,6 +128,7 @@ def do_iptables_nat(port, dnsport, family, subnets, udp): '--dport', '53', '--to-ports', str(dnsport)) + def do_iptables_tproxy(port, dnsport, family, subnets, udp): if family not in [socket.AF_INET, socket.AF_INET6]: raise Fatal("Address family '%s' unsupported by tproxy method"%family_to_string(family)) @@ -204,7 +205,7 @@ def do_iptables_tproxy(port, dnsport, family, subnets, udp): '--dest', '%s/%s' % (snet,swidth), '-m', 'udp', '-p', 'udp', '--on-port', str(port)) - + def ipfw_rule_exists(n): argv = ['ipfw', 'list'] From a4c5498b8f69720c026742002164a231fc9a7e6b Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 9 Jun 2011 16:30:51 +1000 Subject: [PATCH 63/90] Make DNS work with tproxy. --- client.py | 77 +++++++++++++++++++++++++++++++++-------------------- firewall.py | 15 ++++++++--- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/client.py b/client.py index b26c890..6139da4 100644 --- a/client.py +++ b/client.py @@ -320,9 +320,9 @@ def done(self): dnsreqs = {} udp_by_src = {} def expire_connections(now, mux): - for chan,(peer,sock,timeout) in dnsreqs.items(): + for chan,timeout in dnsreqs.items(): if timeout < now: - debug3('expiring dnsreqs channel=%d peer=%r\n' % (chan, peer)) + debug3('expiring dnsreqs channel=%d\n' % chan) del mux.channels[chan] del dnsreqs[chan] debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) @@ -373,32 +373,31 @@ def onaccept_tcp(listener, method, mux, handlers): expire_connections(time.time(), mux) -def udp_done(chan, data, family, dstip): +def udp_done(chan, data, method, family, dstip): (src,srcport,data) = data.split(",",2) srcip = (src,int(srcport)) - debug3('doing send from %r port %d to %r port %d\n' % (srcip[0],srcip[1],dstip[0],dstip[1],)) + debug3('doing send from %r to %r\n' % (srcip,dstip,)) - sock = socket.socket(family, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - sock.bind(srcip) - sock.sendto(data, dstip) - sock.close() + sender = socket.socket(sock.family, socket.SOCK_DGRAM) + sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + sender.bind(srcip) + sender.sendto(data, dstip) + sender.close() def onaccept_udp(listener, method, mux, handlers): now = time.time() srcip, dstip, data = recv_udp(listener, 4096) if not dstip: - debug1("-- ignored UDP from %s:%r: couldn't determine destination IP address\n"%srcip) + debug1("-- ignored UDP from %r: couldn't determine destination IP address\n" % (srcip,)) return - debug1('Accept UDP: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], - dstip[0],dstip[1])) + debug1('Accept UDP: %r -> %r.\n' % (srcip,dstip,)) if srcip in udp_by_src: chan,timeout = udp_by_src[srcip] else: chan = mux.next_channel() - mux.channels[chan] = lambda cmd,data: udp_done(chan,data, listener.family, dstip=srcip) + mux.channels[chan] = lambda cmd,data: udp_done(chan, data, method, listener, dstip=srcip) mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family) udp_by_src[srcip] = chan,now+30 @@ -408,25 +407,34 @@ def onaccept_udp(listener, method, mux, handlers): expire_connections(now, mux) -def dns_done(chan, mux, data): - peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) - debug3('dns_done: channel=%d peer=%r\n' % (chan, peer)) - if peer: - del mux.channels[chan] - del dnsreqs[chan] - debug3('doing sendto %r\n' % (peer,)) - sock.sendto(data, peer) +def dns_done(chan, data, method, sock, srcip, dstip, mux): + debug3('dns_done: channel=%d src=%r dst=%r\n' % (chan,srcip,dstip)) + del mux.channels[chan] + del dnsreqs[chan] + if method == "tproxy": + debug3('doing send from %r to %r\n' % (srcip,dstip,)) + sender = socket.socket(sock.family, socket.SOCK_DGRAM) + sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + sender.bind(srcip) + sender.sendto(data, dstip) + sender.close() + else: + debug3('doing sendto %r\n' % (dstip,)) + sock.sendto(data, dstip) def ondns(listener, method, mux, handlers): - pkt,peer = listener.recvfrom(4096) now = time.time() - if pkt: - debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt))) - chan = mux.next_channel() - dnsreqs[chan] = peer,listener,now+30 - mux.send(chan, ssnet.CMD_DNS_REQ, pkt) - mux.channels[chan] = lambda cmd,data: dns_done(chan, mux, data) + srcip, dstip, data = recv_udp(listener, 4096) + if method == "tproxy" and not dstip: + debug1("-- ignored UDP from %r: couldn't determine destination IP address\n" % (srcip,)) + return + debug1('DNS request from %r to %r: %d bytes\n' % (srcip,dstip,len(data))) + chan = mux.next_channel() + dnsreqs[chan] = now+30 + mux.send(chan, ssnet.CMD_DNS_REQ, data) + mux.channels[chan] = lambda cmd,data: dns_done(chan, data, method, listener, srcip=dstip, dstip=srcip, mux=mux) expire_connections(now, mux) @@ -548,6 +556,9 @@ def main(listenip_v6, listenip_v4, return 5 debug1('Starting sshuttle proxy.\n') + if recvmsg is not None: + debug1("recvmsg %s support enabled.\n"%recvmsg) + if method == "tproxy": if recvmsg is not None: debug1("tproxy UDP support enabled.\n") @@ -555,6 +566,9 @@ def main(listenip_v6, listenip_v4, else: debug1("tproxy UDP support requires recvmsg function.\n") udp = False + if dns and recvmsg is None: + debug1("tproxy DNS support requires recvmsg function.\n") + dns = False else: debug1("UDP support requires tproxy; disabling UDP.\n") udp = False @@ -638,6 +652,11 @@ def main(listenip_v6, listenip_v4, debug2(' %d' % port) dns_listener = independent_listener(socket.SOCK_DGRAM) + if method == "tproxy": + dns_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + dns_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) + dns_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) + if listenip_v6: lv6 = (listenip_v6[0],port) dnsport_v6 = port diff --git a/firewall.py b/firewall.py index 513f97a..6897648 100644 --- a/firewall.py +++ b/firewall.py @@ -153,7 +153,7 @@ def do_iptables_tproxy(port, dnsport, family, subnets, udp): ipt(family, table, '-F', divert_chain) ipt(family, table, '-X', divert_chain) - if subnets: + if subnets or dnsport: ipt(family, table, '-N', mark_chain) ipt(family, table, '-F', mark_chain) ipt(family, table, '-N', divert_chain) @@ -170,8 +170,17 @@ def do_iptables_tproxy(port, dnsport, family, subnets, udp): ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'udp', '-p', 'udp') - #ipt(family, table, '-A', mark_chain, '-o', 'lo', '-j', 'RETURN') - + if dnsport: + nslist = resolvconf_nameservers() + for ip in filter(lambda i: guess_address_family(i)==family, nslist): + ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + '--dest', '%s/32' % ip, + '-m', 'udp', '-p', 'udp', '--dport', '53') + ipt(family, table, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', + '--dest', '%s/32' % ip, + '-m', 'udp', '-p', 'udp', '--dport', '53', + '--on-port', str(dnsport)) + if subnets: for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True): if sexclude: From 4693039f769a9560b9604b7040901cfe8815aae9 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 16 Jun 2011 14:11:56 +1000 Subject: [PATCH 64/90] Rename independent_listener to MultiListener. --- client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client.py b/client.py index 6139da4..818dea1 100644 --- a/client.py +++ b/client.py @@ -186,7 +186,7 @@ def original_dst(sock): raise -class independent_listener: +class MultiListener: def __init__(self, type=socket.SOCK_STREAM, proto=0): self.v6 = socket.socket(socket.AF_INET6, type, proto) @@ -588,14 +588,14 @@ def main(listenip_v6, listenip_v4, debug2('Binding redirector:') for port in ports: debug2(' %d' % port) - tcp_listener = independent_listener() + tcp_listener = MultiListener() tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if method == "tproxy": tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) if udp: - udp_listener = independent_listener(socket.SOCK_DGRAM) + udp_listener = MultiListener(socket.SOCK_DGRAM) udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) @@ -650,7 +650,7 @@ def main(listenip_v6, listenip_v4, ports = xrange(12300,9000,-1) for port in ports: debug2(' %d' % port) - dns_listener = independent_listener(socket.SOCK_DGRAM) + dns_listener = MultiListener(socket.SOCK_DGRAM) if method == "tproxy": dns_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) From f8de19d44dd641219142b54a6952c1bf5bc60ea5 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 16 Jun 2011 14:13:38 +1000 Subject: [PATCH 65/90] Fix style issues. --- client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client.py b/client.py index 818dea1..539394a 100644 --- a/client.py +++ b/client.py @@ -231,10 +231,11 @@ def bind(self, address_v6, address_v4): def print_listening(self, what): if self.v6: listenip = self.v6.getsockname() - debug1('%s listening on %r.\n' % (what, listenip, )) + debug1('%s listening on %r.\n' % (what, listenip)) if self.v4: listenip = self.v4.getsockname() - debug1('%s listening on %r.\n' % (what, listenip, )) + debug1('%s listening on %r.\n' % (what, listenip)) + class FirewallClient: def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method, udp): From 2822b329604443b8436f1fd9e8c6499725ca3566 Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 16 Jun 2011 14:14:23 +1000 Subject: [PATCH 66/90] Rename parameter. --- client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client.py b/client.py index 539394a..7cd7abf 100644 --- a/client.py +++ b/client.py @@ -198,11 +198,11 @@ def setsockopt(self, level, optname, value): if self.v4: self.v4.setsockopt(level, optname, value) - def add_handler(self, handlers, handler, method, mux): + def add_handler(self, handlers, callback, method, mux): if self.v6: - handlers.append(Handler([self.v6], lambda: handler(self.v6, method, mux, handlers))) + handlers.append(Handler([self.v6], lambda: callback(self.v6, method, mux, handlers))) if self.v4: - handlers.append(Handler([self.v4], lambda: handler(self.v4, method, mux, handlers))) + handlers.append(Handler([self.v4], lambda: callback(self.v4, method, mux, handlers))) def listen(self, backlog): if self.v6: From 2e5e65a6a0b062813a03e534d3f888808fbfc48a Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 16 Jun 2011 15:45:50 +1000 Subject: [PATCH 67/90] Merge in upstream suggestions. --- firewall.py | 122 ++++++++++++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/firewall.py b/firewall.py index 6897648..e52a279 100644 --- a/firewall.py +++ b/firewall.py @@ -31,31 +31,21 @@ def ipt_chain_exists(family, table, name): raise Fatal('%r returned %d' % (argv, rv)) -def ipt4(table, *args): - argv = ['iptables', '-t', table] + list(args) - debug1('>> %s\n' % ' '.join(argv)) - rv = ssubprocess.call(argv) - if rv: - raise Fatal('%r returned %d' % (argv, rv)) - -def ipt6(table, *args): - argv = ['ip6tables', '-t', table] + list(args) - debug1('>> %s\n' % ' '.join(argv)) - rv = ssubprocess.call(argv) - if rv: - raise Fatal('%r returned %d' % (argv, rv)) - -def ipt(family, *args): +def _ipt(family, table, *args): if family == socket.AF_INET6: - ipt6(*args) + argv = ['iptables', '-t', table] + list(args) elif family == socket.AF_INET: - ipt4(*args) + argv = ['ip6tables', '-t', table] + list(args) else: raise Fatal("Unsupported family '%s'"%family_to_string(family)) + debug1('>> %s\n' % ' '.join(argv)) + rv = ssubprocess.call(argv) + if rv: + raise Fatal('%r returned %d' % (argv, rv)) _no_ttl_module = False -def ipt_ttl(family, *args): +def _ipt_ttl(family, *args): global _no_ttl_module if not _no_ttl_module: # we avoid infinite loops by generating server-side connections @@ -63,15 +53,15 @@ def ipt_ttl(family, *args): # connections, in case client == server. try: argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42'] - ipt(family, *argsplus) + _ipt(family, *argsplus) except Fatal: - ipt(family, *args) + _ipt(family, *args) # we only get here if the non-ttl attempt succeeds log('sshuttle: warning: your iptables is missing ' 'the ttl module.\n') _no_ttl_module = True else: - ipt(family, *args) + _ipt(family, *args) # We name the chain based on the transproxy port number so that it's possible @@ -87,20 +77,25 @@ def do_iptables_nat(port, dnsport, family, subnets, udp): raise Fatal("UDP not supported by nat method") table = "nat" + def ipt(*args): + return _ipt(family, table, *args) + def ipt_ttl(*args): + return _ipt_ttl(family, table, *args) + chain = 'sshuttle-%s' % port # basic cleanup/setup of chains if ipt_chain_exists(family, table, chain): - nonfatal(ipt, family, table, '-D', 'OUTPUT', '-j', chain) - nonfatal(ipt, family, table, '-D', 'PREROUTING', '-j', chain) - nonfatal(ipt, family, table, '-F', chain) - ipt(family, table, '-X', chain) + nonfatal(ipt, '-D', 'OUTPUT', '-j', chain) + nonfatal(ipt, '-D', 'PREROUTING', '-j', chain) + nonfatal(ipt, '-F', chain) + ipt('-X', chain) if subnets or dnsport: - ipt(family, table, '-N', chain) - ipt(family, table, '-F', chain) - ipt(family, table, '-I', 'OUTPUT', '1', '-j', chain) - ipt(family, table, '-I', 'PREROUTING', '1', '-j', chain) + ipt('-N', chain) + ipt('-F', chain) + ipt('-I', 'OUTPUT', '1', '-j', chain) + ipt('-I', 'PREROUTING', '1', '-j', chain) if subnets: # create new subnet entries. Note that we're sorting in a very @@ -110,11 +105,11 @@ def do_iptables_nat(port, dnsport, family, subnets, udp): # intuitive order. for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True): if sexclude: - ipt(family, table, '-A', chain, '-j', 'RETURN', + ipt('-A', chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp') else: - ipt_ttl(family, table, '-A', chain, '-j', 'REDIRECT', + ipt_ttl('-A', chain, '-j', 'REDIRECT', '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp', '--to-ports', str(port)) @@ -122,7 +117,7 @@ def do_iptables_nat(port, dnsport, family, subnets, udp): if dnsport: nslist = resolvconf_nameservers() for ip in filter(lambda i: guess_address_family(i)==family, nslist): - ipt_ttl(family, table, '-A', chain, '-j', 'REDIRECT', + ipt_ttl('-A', chain, '-j', 'REDIRECT', '--dest', '%s/32' % ip, '-p', 'udp', '--dport', '53', @@ -134,37 +129,42 @@ def do_iptables_tproxy(port, dnsport, family, subnets, udp): raise Fatal("Address family '%s' unsupported by tproxy method"%family_to_string(family)) table = "mangle" + def ipt(*args): + return _ipt(family, table, *args) + def ipt_ttl(*args): + return _ipt_ttl(family, table, *args) + mark_chain = 'sshuttle-m-%s' % port tproxy_chain = 'sshuttle-t-%s' % port divert_chain = 'sshuttle-d-%s' % port # basic cleanup/setup of chains if ipt_chain_exists(family, table, mark_chain): - ipt(family, table, '-D', 'OUTPUT', '-j', mark_chain) - ipt(family, table, '-F', mark_chain) - ipt(family, table, '-X', mark_chain) + ipt('-D', 'OUTPUT', '-j', mark_chain) + ipt('-F', mark_chain) + ipt('-X', mark_chain) if ipt_chain_exists(family, table, tproxy_chain): - ipt(family, table, '-D', 'PREROUTING', '-j', tproxy_chain) - ipt(family, table, '-F', tproxy_chain) - ipt(family, table, '-X', tproxy_chain) + ipt('-D', 'PREROUTING', '-j', tproxy_chain) + ipt('-F', tproxy_chain) + ipt('-X', tproxy_chain) if ipt_chain_exists(family, table, divert_chain): - ipt(family, table, '-F', divert_chain) - ipt(family, table, '-X', divert_chain) + ipt('-F', divert_chain) + ipt('-X', divert_chain) if subnets or dnsport: - ipt(family, table, '-N', mark_chain) - ipt(family, table, '-F', mark_chain) - ipt(family, table, '-N', divert_chain) - ipt(family, table, '-F', divert_chain) - ipt(family, table, '-N', tproxy_chain) - ipt(family, table, '-F', tproxy_chain) - ipt(family, table, '-I', 'OUTPUT', '1', '-j', mark_chain) - ipt(family, table, '-I', 'PREROUTING', '1', '-j', tproxy_chain) - ipt(family, table, '-A', divert_chain, '-j', 'MARK', '--set-mark', '1') - ipt(family, table, '-A', divert_chain, '-j', 'ACCEPT') - ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, + ipt('-N', mark_chain) + ipt('-F', mark_chain) + ipt('-N', divert_chain) + ipt('-F', divert_chain) + ipt('-N', tproxy_chain) + ipt('-F', tproxy_chain) + ipt('-I', 'OUTPUT', '1', '-j', mark_chain) + ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain) + ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1') + ipt('-A', divert_chain, '-j', 'ACCEPT') + ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'tcp', '-p', 'tcp') if subnets and udp: ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, @@ -173,10 +173,10 @@ def do_iptables_tproxy(port, dnsport, family, subnets, udp): if dnsport: nslist = resolvconf_nameservers() for ip in filter(lambda i: guess_address_family(i)==family, nslist): - ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', '--dest', '%s/32' % ip, '-m', 'udp', '-p', 'udp', '--dport', '53') - ipt(family, table, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', + ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', '%s/32' % ip, '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', str(dnsport)) @@ -184,33 +184,33 @@ def do_iptables_tproxy(port, dnsport, family, subnets, udp): if subnets: for f,swidth,sexclude,snet in sorted(subnets, key=lambda s: s[1], reverse=True): if sexclude: - ipt(family, table, '-A', mark_chain, '-j', 'RETURN', + ipt('-A', mark_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') - ipt(family, table, '-A', tproxy_chain, '-j', 'RETURN', + ipt('-A', tproxy_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') else: - ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp') - ipt(family, table, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', + ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', '%s/%s' % (snet,swidth), '-m', 'tcp', '-p', 'tcp', '--on-port', str(port)) if sexclude and udp: - ipt(family, table, '-A', mark_chain, '-j', 'RETURN', + ipt('-A', mark_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'udp', '-p', 'udp') - ipt(family, table, '-A', tproxy_chain, '-j', 'RETURN', + ipt('-A', tproxy_chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-m', 'udp', '-p', 'udp') elif udp: - ipt(family, table, '-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', '--dest', '%s/%s' % (snet,swidth), '-m', 'udp', '-p', 'udp') - ipt(family, table, '-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', + ipt('-A', tproxy_chain, '-j', 'TPROXY', '--tproxy-mark', '0x1/0x1', '--dest', '%s/%s' % (snet,swidth), '-m', 'udp', '-p', 'udp', '--on-port', str(port)) From 1c365b95336b8e190717c0e34c478554d35b2b38 Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 20 Jun 2011 13:21:17 +1000 Subject: [PATCH 68/90] Fix typo. --- firewall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firewall.py b/firewall.py index e52a279..ad06e45 100644 --- a/firewall.py +++ b/firewall.py @@ -33,9 +33,9 @@ def ipt_chain_exists(family, table, name): def _ipt(family, table, *args): if family == socket.AF_INET6: - argv = ['iptables', '-t', table] + list(args) - elif family == socket.AF_INET: argv = ['ip6tables', '-t', table] + list(args) + elif family == socket.AF_INET: + argv = ['iptables', '-t', table] + list(args) else: raise Fatal("Unsupported family '%s'"%family_to_string(family)) debug1('>> %s\n' % ' '.join(argv)) From 014ca7ee67dbf36db741ee6454f39d81c83562fa Mon Sep 17 00:00:00 2001 From: Brian May Date: Fri, 24 Jun 2011 10:47:34 +1000 Subject: [PATCH 69/90] Fix tproxy error. --- firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firewall.py b/firewall.py index ad06e45..2a7f6f0 100644 --- a/firewall.py +++ b/firewall.py @@ -167,7 +167,7 @@ def ipt_ttl(*args): ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'tcp', '-p', 'tcp') if subnets and udp: - ipt(family, table, '-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, + ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, '-m', 'udp', '-p', 'udp') if dnsport: From 9061aa3d99b8b11b4885b22cbe73b324d6f69653 Mon Sep 17 00:00:00 2001 From: Brian May Date: Fri, 24 Jun 2011 10:48:09 +1000 Subject: [PATCH 70/90] Have firewall work out method used. --- client.py | 30 +++++++++++++++--------------- firewall.py | 11 ++++++----- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/client.py b/client.py index 7cd7abf..c8ff039 100644 --- a/client.py +++ b/client.py @@ -242,7 +242,6 @@ def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, dnsport_v self.auto_nets = [] self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude - self.method = method argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] + ['-v'] * (helpers.verbose or 0) + ['--firewall', str(port_v6), str(port_v4), @@ -284,8 +283,9 @@ def setup(): raise Fatal(e) line = self.pfile.readline() self.check() - if line != 'READY\n': + if line[0:5] != 'READY': raise Fatal('%r expected READY, got %r' % (self.argv, line)) + self.method = line[6:-1] def check(self): rv = self.p.poll() @@ -592,15 +592,9 @@ def main(listenip_v6, listenip_v4, tcp_listener = MultiListener() tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if method == "tproxy": - tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - if udp: udp_listener = MultiListener(socket.SOCK_DGRAM) udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) - udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) else: udp_listener = None @@ -653,11 +647,6 @@ def main(listenip_v6, listenip_v4, debug2(' %d' % port) dns_listener = MultiListener(socket.SOCK_DGRAM) - if method == "tproxy": - dns_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - dns_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) - dns_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) - if listenip_v6: lv6 = (listenip_v6[0],port) dnsport_v6 = port @@ -691,11 +680,22 @@ def main(listenip_v6, listenip_v4, dns_listener = None fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, subnets_exclude, dnsport_v6, dnsport_v4, method, udp) - + + if fw.method == "tproxy": + tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + if udp_listener: + udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) + udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) + if dns_listener: + dns_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + dns_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) + dns_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) + try: return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, dns_listener, - method, seed_hosts, auto_nets, syslog, + fw.method, seed_hosts, auto_nets, syslog, daemon) finally: try: diff --git a/firewall.py b/firewall.py index 2a7f6f0..0509e03 100644 --- a/firewall.py +++ b/firewall.py @@ -504,12 +504,13 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog): if method == "auto": if program_exists('ipfw'): - do_it = do_ipfw + method = "ipfw" elif program_exists('iptables'): - do_it = do_iptables_nat + method = "nat" else: raise Fatal("can't find either ipfw or iptables; check your PATH") - elif method == "nat": + + if method == "nat": do_it = do_iptables_nat elif method == "tproxy": do_it = do_iptables_tproxy @@ -527,8 +528,8 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog): ssyslog.start_syslog() ssyslog.stderr_to_syslog() - debug1('firewall manager ready.\n') - sys.stdout.write('READY\n') + debug1('firewall manager ready method %s.\n'%method) + sys.stdout.write('READY %s\n'%method) sys.stdout.flush() # ctrl-c shouldn't be passed along to me. When the main sshuttle dies, From b689e98ee7a502998bdc5c7b0cf4bec4e18519b9 Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 5 Jul 2011 13:13:10 +1000 Subject: [PATCH 71/90] Cosmetic changes. Use Exception instead of Fatal for errors that should never happen. --- firewall.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/firewall.py b/firewall.py index 0509e03..66368d0 100644 --- a/firewall.py +++ b/firewall.py @@ -20,7 +20,7 @@ def ipt_chain_exists(family, table, name): elif family == socket.AF_INET: cmd = 'iptables' else: - raise Fatal("Unsupported family '%s'"%family_to_string(family)) + raise Exception('Unsupported family "%s"'%family_to_string(family)) argv = [cmd, '-t', table, '-nL'] p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) for line in p.stdout: @@ -37,7 +37,7 @@ def _ipt(family, table, *args): elif family == socket.AF_INET: argv = ['iptables', '-t', table] + list(args) else: - raise Fatal("Unsupported family '%s'"%family_to_string(family)) + raise Exception('Unsupported family "%s"'%family_to_string(family)) debug1('>> %s\n' % ' '.join(argv)) rv = ssubprocess.call(argv) if rv: @@ -71,10 +71,10 @@ def _ipt_ttl(family, *args): # "-A OUTPUT"). def do_iptables_nat(port, dnsport, family, subnets, udp): # only ipv4 supported with NAT - if family not in [socket.AF_INET, ]: - raise Fatal("Address family '%s' unsupported by nat method"%family_to_string(family)) + if family != socket.AF_INET: + raise Exception('Address family "%s" unsupported by nat method'%family_to_string(family)) if udp: - raise Fatal("UDP not supported by nat method") + raise Exception("UDP not supported by nat method") table = "nat" def ipt(*args): @@ -126,7 +126,7 @@ def ipt_ttl(*args): def do_iptables_tproxy(port, dnsport, family, subnets, udp): if family not in [socket.AF_INET, socket.AF_INET6]: - raise Fatal("Address family '%s' unsupported by tproxy method"%family_to_string(family)) + raise Exception('Address family "%s" unsupported by tproxy method'%family_to_string(family)) table = "mangle" def ipt(*args): @@ -324,9 +324,9 @@ def ipfw(*args): def do_ipfw(port, dnsport, family, subnets, udp): # IPv6 not supported if family not in [socket.AF_INET, ]: - raise Fatal("Address family '%s' unsupported by ipfw method"%family_to_string(family)) + raise Exception('Address family "%s" unsupported by ipfw method'%family_to_string(family)) if udp: - raise Fatal("UDP not supported by ipfw method") + raise Exception("UDP not supported by ipfw method") sport = str(port) xsport = str(port+1) @@ -517,7 +517,7 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog): elif method == "ipfw": do_it = do_ipfw else: - raise Fatal("Unknown method '%s'"%method) + raise Fatal('Unknown method "%s"'%method) # because of limitations of the 'su' command, the *real* stdin/stdout # are both attached to stdout initially. Clone stdout into stdin so we From 89dcedfdcb1527ab715af7321ab4d25b1fa65d9f Mon Sep 17 00:00:00 2001 From: Brian May Date: Mon, 11 Jul 2011 11:46:03 +1000 Subject: [PATCH 72/90] Mostly cosmetic changes. --- client.py | 3 +++ firewall.py | 4 ++-- main.py | 9 +++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/client.py b/client.py index c8ff039..657f8b3 100644 --- a/client.py +++ b/client.py @@ -36,6 +36,7 @@ def got_signal(signum, frame): IPV6_ORIGDSTADDR = 74 IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR + if recvmsg == "python": def recv_udp(listener, bufsize): debug3('Accept UDP python using recvmsg.\n') @@ -102,6 +103,7 @@ def recv_udp(listener, bufsize): data, srcip = listener.recvfrom(bufsize) return (srcip, None, data) + def check_daemon(pidfile): global _pidname _pidname = os.path.abspath(pidfile) @@ -653,6 +655,7 @@ def main(listenip_v6, listenip_v4, else: lv6 = None dnsport_v6 = 0 + if listenip_v4: lv4 = (listenip_v4[0],port) dnsport_v4 = port diff --git a/firewall.py b/firewall.py index 66368d0..e082159 100644 --- a/firewall.py +++ b/firewall.py @@ -517,7 +517,7 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog): elif method == "ipfw": do_it = do_ipfw else: - raise Fatal('Unknown method "%s"'%method) + raise Exception('Unknown method "%s"'%method) # because of limitations of the 'su' command, the *real* stdin/stdout # are both attached to stdout initially. Clone stdout into stdin so we @@ -572,7 +572,7 @@ def main(port_v6, port_v4, dnsport_v6, dnsport_v4, method, udp, syslog): if port_v4: do_wait = do_it(port_v4, dnsport_v4, socket.AF_INET, subnets_v4, udp) elif len(subnets_v4) > 0: - debug1("IPv4 subnets defined but IPv4 disabled\n") + debug1('IPv4 subnets defined but IPv4 disabled\n') sys.stdout.write('STARTED\n') diff --git a/main.py b/main.py index 173be95..161479c 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ from helpers import * +# 1.2.3.4/5 or just 1.2.3.4 def parse_subnet4(s): m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s) if not m: @@ -20,6 +21,8 @@ def parse_subnet4(s): raise Fatal('*/%d is greater than the maximum of 32' % width) return(socket.AF_INET, '%d.%d.%d.%d' % (a,b,c,d), width) + +# 1:2::3/64 or just 1:2::3 def parse_subnet6(s): m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s) if not m: @@ -33,6 +36,7 @@ def parse_subnet6(s): raise Fatal('*/%d is greater than the maximum of 128' % width) return(socket.AF_INET6, net, width) + # list of: # 1.2.3.4/5 or just 1.2.3.4 # 1:2::3/64 or just 1:2::3 @@ -46,6 +50,7 @@ def parse_subnets(subnets_str): subnets.append(subnet) return subnets + # 1.2.3.4:567 or just 1.2.3.4 or just 567 def parse_ipport4(s): s = str(s) @@ -63,8 +68,8 @@ def parse_ipport4(s): a = b = c = d = 0 return ('%d.%d.%d.%d' % (a,b,c,d), port) -# [1:2::3]:456 or [1:2::3] or 456 +# [1:2::3]:456 or [1:2::3] or 456 def parse_ipport6(s): s = str(s) m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s) @@ -74,6 +79,7 @@ def parse_ipport6(s): (ip,port) = (ip or '::', int(port or 0)) return (ip, port) + optspec = """ sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] sshuttle --server @@ -115,7 +121,6 @@ def parse_ipport6(s): if len(extra) != 0: o.fatal('no arguments expected') server.latency_control = opt.latency_control - server.method = opt.method sys.exit(server.main()) elif opt.firewall: if len(extra) != 6: From ca6e4d934cc77d5b9e01c05355a9f1e7a8c3d1fb Mon Sep 17 00:00:00 2001 From: Brian May Date: Tue, 19 Jul 2011 10:03:04 +1000 Subject: [PATCH 73/90] Fix UDP bugs. --- client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client.py b/client.py index 657f8b3..e861d09 100644 --- a/client.py +++ b/client.py @@ -96,7 +96,7 @@ def recv_udp(listener, bufsize): ip = socket.inet_ntop(family, a.cmsg_data[start:start+length]) dstip = (ip, port) break - return (srcip, dstip, data) + return (srcip, dstip, data[0]) else: def recv_udp(listener, bufsize): debug3('Accept UDP using recvfrom.\n') @@ -381,7 +381,7 @@ def udp_done(chan, data, method, family, dstip): srcip = (src,int(srcport)) debug3('doing send from %r to %r\n' % (srcip,dstip,)) - sender = socket.socket(sock.family, socket.SOCK_DGRAM) + sender = socket.socket(family, socket.SOCK_DGRAM) sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) sender.bind(srcip) @@ -400,12 +400,12 @@ def onaccept_udp(listener, method, mux, handlers): chan,timeout = udp_by_src[srcip] else: chan = mux.next_channel() - mux.channels[chan] = lambda cmd,data: udp_done(chan, data, method, listener, dstip=srcip) + mux.channels[chan] = lambda cmd,data: udp_done(chan, data, method, listener.family, dstip=srcip) mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family) udp_by_src[srcip] = chan,now+30 hdr = "%s,%r,"%(dstip[0], dstip[1]) - mux.send(chan, ssnet.CMD_UDP_DATA, hdr+data[0]) + mux.send(chan, ssnet.CMD_UDP_DATA, hdr+data) expire_connections(now, mux) From f8fe6798532e5f934c88252e8c7608ba504610bd Mon Sep 17 00:00:00 2001 From: Brian May Date: Thu, 28 Jul 2011 15:53:00 +1000 Subject: [PATCH 74/90] Warn instead of crashing if any socket errors sending data. --- client.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client.py b/client.py index e861d09..3ecd8bc 100644 --- a/client.py +++ b/client.py @@ -381,12 +381,15 @@ def udp_done(chan, data, method, family, dstip): srcip = (src,int(srcport)) debug3('doing send from %r to %r\n' % (srcip,dstip,)) - sender = socket.socket(family, socket.SOCK_DGRAM) - sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - sender.bind(srcip) - sender.sendto(data, dstip) - sender.close() + try: + sender = socket.socket(family, socket.SOCK_DGRAM) + sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + sender.bind(srcip) + sender.sendto(data, dstip) + sender.close() + except socket.error, e: + debug1('-- ignored socket error sending UDP data: %r\n'%e) def onaccept_udp(listener, method, mux, handlers): From 8b159a5df714963c0c3fb25952b35f6a8a9b86e3 Mon Sep 17 00:00:00 2001 From: Brian May Date: Fri, 26 Aug 2011 10:03:20 +1000 Subject: [PATCH 75/90] Move towards IPv6 DNS support. --- firewall.py | 6 +++--- helpers.py | 7 +++++-- server.py | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/firewall.py b/firewall.py index e082159..1e233c3 100644 --- a/firewall.py +++ b/firewall.py @@ -116,7 +116,7 @@ def ipt_ttl(*args): if dnsport: nslist = resolvconf_nameservers() - for ip in filter(lambda i: guess_address_family(i)==family, nslist): + for f,ip in filter(lambda i: i[0]==family, nslist): ipt_ttl('-A', chain, '-j', 'REDIRECT', '--dest', '%s/32' % ip, '-p', 'udp', @@ -172,7 +172,7 @@ def ipt_ttl(*args): if dnsport: nslist = resolvconf_nameservers() - for ip in filter(lambda i: guess_address_family(i)==family, nslist): + for f,ip in filter(lambda i: i[0]==family, nslist): ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', '--dest', '%s/32' % ip, '-m', 'udp', '-p', 'udp', '--dport', '53') @@ -406,7 +406,7 @@ def do_ipfw(port, dnsport, family, subnets, udp): divertsock.bind(('0.0.0.0', port)) # IP field is ignored nslist = resolvconf_nameservers() - for ip in nslist: + for f,ip in filter(lambda i: i[0]==family, nslist): # relabel and then catch outgoing DNS requests ipfw('add', sport, 'divert', sport, 'log', 'udp', diff --git a/helpers.py b/helpers.py index e67124a..f08b731 100644 --- a/helpers.py +++ b/helpers.py @@ -42,7 +42,10 @@ def resolvconf_nameservers(): for line in open('/etc/resolv.conf'): words = line.lower().split() if len(words) >= 2 and words[0] == 'nameserver': - l.append(words[1]) + if ':' in words[1]: + l.append((socket.AF_INET6,words[1])) + else: + l.append((socket.AF_INET,words[1])) return l @@ -55,7 +58,7 @@ def resolvconf_random_nameserver(): random.shuffle(l) return l[0] else: - return '127.0.0.1' + return (socket.AF_INET,'127.0.0.1') def islocal(ip,family): diff --git a/server.py b/server.py index d85a6dd..bc0bea8 100644 --- a/server.py +++ b/server.py @@ -126,7 +126,8 @@ def try_send(self): if self.tries >= 3: return self.tries += 1 - self.peer = resolvconf_random_nameserver() + # FIXME! Support IPv6 nameservers + self.peer = resolvconf_random_nameserver()[1] self.sock.connect((self.peer, 53)) debug2('DNS: sending to %r\n' % self.peer) try: From 73337235d3d60670f2407c01c589e8f24523c672 Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Mon, 22 Oct 2012 21:24:00 -0400 Subject: [PATCH 76/90] Fixed a bug where lack of IPv6 destination = fatal There was a problem where trying to bind .v4 and .v6 listeners would set them to None if there was nothing to bind (if, say, you weren't binding an IPv6 listener). Later, the code then would try to call a member function of the listener. The member function would not do anything if there was no listener, but trying to dereference None yielded the broken behavior. --- client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client.py b/client.py index 3ecd8bc..cb034cd 100644 --- a/client.py +++ b/client.py @@ -691,12 +691,16 @@ def main(listenip_v6, listenip_v4, tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) if udp_listener: udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) - udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) + if udp_listener.v4 is not None: + udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) + if udp_listener.v6 is not None: + udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) if dns_listener: dns_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - dns_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) - dns_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) + if dns_listener.v4 is not None: + dns_listener.v4.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1) + if dns_listener.v6 is not None: + dns_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1) try: return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, From ca727a3a1d9ffa6052480d55942033cdfe70f8d2 Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Mon, 22 Oct 2012 21:37:55 -0400 Subject: [PATCH 77/90] Mass relocation of files to their own subdirectory --- LICENSE => src/LICENSE | 0 Makefile => src/Makefile | 0 README.md => src/README.md | 0 all.do => src/all.do | 0 assembler.py => src/assembler.py | 0 clean.do => src/clean.do | 0 client.py => src/client.py | 0 {compat => src/compat}/__init__.py | 0 {compat => src/compat}/ssubprocess.py | 0 default.8.do => src/default.8.do | 0 do => src/do | 0 firewall.py => src/firewall.py | 0 helpers.py => src/helpers.py | 0 hostwatch.py => src/hostwatch.py | 0 main.py => src/main.py | 0 options.py => src/options.py | 0 server.py => src/server.py | 0 ssh.py => src/ssh.py | 0 sshuttle => src/sshuttle | 0 sshuttle.md => src/sshuttle.md | 0 ssnet.py => src/ssnet.py | 0 ssyslog.py => src/ssyslog.py | 0 stresstest.py => src/stresstest.py | 0 {ui-macos => src/ui-macos}/.gitignore | 0 {ui-macos => src/ui-macos}/Info.plist | 0 {ui-macos => src/ui-macos}/MainMenu.xib | 0 {ui-macos => src/ui-macos}/UserDefaults.plist | 0 {ui-macos => src/ui-macos}/all.do | 0 {ui-macos => src/ui-macos}/app.icns | Bin {ui-macos => src/ui-macos}/askpass.py | 0 {ui-macos => src/ui-macos}/bits/.gitignore | 0 {ui-macos => src/ui-macos}/bits/PkgInfo | 0 {ui-macos => src/ui-macos}/bits/runpython.c | 0 {ui-macos => src/ui-macos}/bits/runpython.do | 0 {ui-macos => src/ui-macos}/chicken-tiny-bw.png | Bin {ui-macos => src/ui-macos}/chicken-tiny-err.png | Bin {ui-macos => src/ui-macos}/chicken-tiny.png | Bin {ui-macos => src/ui-macos}/clean.do | 0 {ui-macos => src/ui-macos}/debug.app.do | 0 {ui-macos => src/ui-macos}/default.app.do | 0 {ui-macos => src/ui-macos}/default.app.tar.gz.do | 0 {ui-macos => src/ui-macos}/default.app.zip.do | 0 {ui-macos => src/ui-macos}/default.nib.do | 0 {ui-macos => src/ui-macos}/dist.do | 0 {ui-macos => src/ui-macos}/git-export.do | 0 {ui-macos => src/ui-macos}/main.py | 0 {ui-macos => src/ui-macos}/models.py | 0 {ui-macos => src/ui-macos}/my.py | 0 {ui-macos => src/ui-macos}/run.do | 0 {ui-macos => src/ui-macos}/sources.list.do | 0 {ui-macos => src/ui-macos}/sshuttle | 0 51 files changed, 0 insertions(+), 0 deletions(-) rename LICENSE => src/LICENSE (100%) rename Makefile => src/Makefile (100%) rename README.md => src/README.md (100%) rename all.do => src/all.do (100%) rename assembler.py => src/assembler.py (100%) rename clean.do => src/clean.do (100%) rename client.py => src/client.py (100%) rename {compat => src/compat}/__init__.py (100%) rename {compat => src/compat}/ssubprocess.py (100%) rename default.8.do => src/default.8.do (100%) rename do => src/do (100%) rename firewall.py => src/firewall.py (100%) rename helpers.py => src/helpers.py (100%) rename hostwatch.py => src/hostwatch.py (100%) rename main.py => src/main.py (100%) rename options.py => src/options.py (100%) rename server.py => src/server.py (100%) rename ssh.py => src/ssh.py (100%) rename sshuttle => src/sshuttle (100%) rename sshuttle.md => src/sshuttle.md (100%) rename ssnet.py => src/ssnet.py (100%) rename ssyslog.py => src/ssyslog.py (100%) rename stresstest.py => src/stresstest.py (100%) rename {ui-macos => src/ui-macos}/.gitignore (100%) rename {ui-macos => src/ui-macos}/Info.plist (100%) rename {ui-macos => src/ui-macos}/MainMenu.xib (100%) rename {ui-macos => src/ui-macos}/UserDefaults.plist (100%) rename {ui-macos => src/ui-macos}/all.do (100%) rename {ui-macos => src/ui-macos}/app.icns (100%) rename {ui-macos => src/ui-macos}/askpass.py (100%) rename {ui-macos => src/ui-macos}/bits/.gitignore (100%) rename {ui-macos => src/ui-macos}/bits/PkgInfo (100%) rename {ui-macos => src/ui-macos}/bits/runpython.c (100%) rename {ui-macos => src/ui-macos}/bits/runpython.do (100%) rename {ui-macos => src/ui-macos}/chicken-tiny-bw.png (100%) rename {ui-macos => src/ui-macos}/chicken-tiny-err.png (100%) rename {ui-macos => src/ui-macos}/chicken-tiny.png (100%) rename {ui-macos => src/ui-macos}/clean.do (100%) rename {ui-macos => src/ui-macos}/debug.app.do (100%) rename {ui-macos => src/ui-macos}/default.app.do (100%) rename {ui-macos => src/ui-macos}/default.app.tar.gz.do (100%) rename {ui-macos => src/ui-macos}/default.app.zip.do (100%) rename {ui-macos => src/ui-macos}/default.nib.do (100%) rename {ui-macos => src/ui-macos}/dist.do (100%) rename {ui-macos => src/ui-macos}/git-export.do (100%) rename {ui-macos => src/ui-macos}/main.py (100%) rename {ui-macos => src/ui-macos}/models.py (100%) rename {ui-macos => src/ui-macos}/my.py (100%) rename {ui-macos => src/ui-macos}/run.do (100%) rename {ui-macos => src/ui-macos}/sources.list.do (100%) rename {ui-macos => src/ui-macos}/sshuttle (100%) diff --git a/LICENSE b/src/LICENSE similarity index 100% rename from LICENSE rename to src/LICENSE diff --git a/Makefile b/src/Makefile similarity index 100% rename from Makefile rename to src/Makefile diff --git a/README.md b/src/README.md similarity index 100% rename from README.md rename to src/README.md diff --git a/all.do b/src/all.do similarity index 100% rename from all.do rename to src/all.do diff --git a/assembler.py b/src/assembler.py similarity index 100% rename from assembler.py rename to src/assembler.py diff --git a/clean.do b/src/clean.do similarity index 100% rename from clean.do rename to src/clean.do diff --git a/client.py b/src/client.py similarity index 100% rename from client.py rename to src/client.py diff --git a/compat/__init__.py b/src/compat/__init__.py similarity index 100% rename from compat/__init__.py rename to src/compat/__init__.py diff --git a/compat/ssubprocess.py b/src/compat/ssubprocess.py similarity index 100% rename from compat/ssubprocess.py rename to src/compat/ssubprocess.py diff --git a/default.8.do b/src/default.8.do similarity index 100% rename from default.8.do rename to src/default.8.do diff --git a/do b/src/do similarity index 100% rename from do rename to src/do diff --git a/firewall.py b/src/firewall.py similarity index 100% rename from firewall.py rename to src/firewall.py diff --git a/helpers.py b/src/helpers.py similarity index 100% rename from helpers.py rename to src/helpers.py diff --git a/hostwatch.py b/src/hostwatch.py similarity index 100% rename from hostwatch.py rename to src/hostwatch.py diff --git a/main.py b/src/main.py similarity index 100% rename from main.py rename to src/main.py diff --git a/options.py b/src/options.py similarity index 100% rename from options.py rename to src/options.py diff --git a/server.py b/src/server.py similarity index 100% rename from server.py rename to src/server.py diff --git a/ssh.py b/src/ssh.py similarity index 100% rename from ssh.py rename to src/ssh.py diff --git a/sshuttle b/src/sshuttle similarity index 100% rename from sshuttle rename to src/sshuttle diff --git a/sshuttle.md b/src/sshuttle.md similarity index 100% rename from sshuttle.md rename to src/sshuttle.md diff --git a/ssnet.py b/src/ssnet.py similarity index 100% rename from ssnet.py rename to src/ssnet.py diff --git a/ssyslog.py b/src/ssyslog.py similarity index 100% rename from ssyslog.py rename to src/ssyslog.py diff --git a/stresstest.py b/src/stresstest.py similarity index 100% rename from stresstest.py rename to src/stresstest.py diff --git a/ui-macos/.gitignore b/src/ui-macos/.gitignore similarity index 100% rename from ui-macos/.gitignore rename to src/ui-macos/.gitignore diff --git a/ui-macos/Info.plist b/src/ui-macos/Info.plist similarity index 100% rename from ui-macos/Info.plist rename to src/ui-macos/Info.plist diff --git a/ui-macos/MainMenu.xib b/src/ui-macos/MainMenu.xib similarity index 100% rename from ui-macos/MainMenu.xib rename to src/ui-macos/MainMenu.xib diff --git a/ui-macos/UserDefaults.plist b/src/ui-macos/UserDefaults.plist similarity index 100% rename from ui-macos/UserDefaults.plist rename to src/ui-macos/UserDefaults.plist diff --git a/ui-macos/all.do b/src/ui-macos/all.do similarity index 100% rename from ui-macos/all.do rename to src/ui-macos/all.do diff --git a/ui-macos/app.icns b/src/ui-macos/app.icns similarity index 100% rename from ui-macos/app.icns rename to src/ui-macos/app.icns diff --git a/ui-macos/askpass.py b/src/ui-macos/askpass.py similarity index 100% rename from ui-macos/askpass.py rename to src/ui-macos/askpass.py diff --git a/ui-macos/bits/.gitignore b/src/ui-macos/bits/.gitignore similarity index 100% rename from ui-macos/bits/.gitignore rename to src/ui-macos/bits/.gitignore diff --git a/ui-macos/bits/PkgInfo b/src/ui-macos/bits/PkgInfo similarity index 100% rename from ui-macos/bits/PkgInfo rename to src/ui-macos/bits/PkgInfo diff --git a/ui-macos/bits/runpython.c b/src/ui-macos/bits/runpython.c similarity index 100% rename from ui-macos/bits/runpython.c rename to src/ui-macos/bits/runpython.c diff --git a/ui-macos/bits/runpython.do b/src/ui-macos/bits/runpython.do similarity index 100% rename from ui-macos/bits/runpython.do rename to src/ui-macos/bits/runpython.do diff --git a/ui-macos/chicken-tiny-bw.png b/src/ui-macos/chicken-tiny-bw.png similarity index 100% rename from ui-macos/chicken-tiny-bw.png rename to src/ui-macos/chicken-tiny-bw.png diff --git a/ui-macos/chicken-tiny-err.png b/src/ui-macos/chicken-tiny-err.png similarity index 100% rename from ui-macos/chicken-tiny-err.png rename to src/ui-macos/chicken-tiny-err.png diff --git a/ui-macos/chicken-tiny.png b/src/ui-macos/chicken-tiny.png similarity index 100% rename from ui-macos/chicken-tiny.png rename to src/ui-macos/chicken-tiny.png diff --git a/ui-macos/clean.do b/src/ui-macos/clean.do similarity index 100% rename from ui-macos/clean.do rename to src/ui-macos/clean.do diff --git a/ui-macos/debug.app.do b/src/ui-macos/debug.app.do similarity index 100% rename from ui-macos/debug.app.do rename to src/ui-macos/debug.app.do diff --git a/ui-macos/default.app.do b/src/ui-macos/default.app.do similarity index 100% rename from ui-macos/default.app.do rename to src/ui-macos/default.app.do diff --git a/ui-macos/default.app.tar.gz.do b/src/ui-macos/default.app.tar.gz.do similarity index 100% rename from ui-macos/default.app.tar.gz.do rename to src/ui-macos/default.app.tar.gz.do diff --git a/ui-macos/default.app.zip.do b/src/ui-macos/default.app.zip.do similarity index 100% rename from ui-macos/default.app.zip.do rename to src/ui-macos/default.app.zip.do diff --git a/ui-macos/default.nib.do b/src/ui-macos/default.nib.do similarity index 100% rename from ui-macos/default.nib.do rename to src/ui-macos/default.nib.do diff --git a/ui-macos/dist.do b/src/ui-macos/dist.do similarity index 100% rename from ui-macos/dist.do rename to src/ui-macos/dist.do diff --git a/ui-macos/git-export.do b/src/ui-macos/git-export.do similarity index 100% rename from ui-macos/git-export.do rename to src/ui-macos/git-export.do diff --git a/ui-macos/main.py b/src/ui-macos/main.py similarity index 100% rename from ui-macos/main.py rename to src/ui-macos/main.py diff --git a/ui-macos/models.py b/src/ui-macos/models.py similarity index 100% rename from ui-macos/models.py rename to src/ui-macos/models.py diff --git a/ui-macos/my.py b/src/ui-macos/my.py similarity index 100% rename from ui-macos/my.py rename to src/ui-macos/my.py diff --git a/ui-macos/run.do b/src/ui-macos/run.do similarity index 100% rename from ui-macos/run.do rename to src/ui-macos/run.do diff --git a/ui-macos/sources.list.do b/src/ui-macos/sources.list.do similarity index 100% rename from ui-macos/sources.list.do rename to src/ui-macos/sources.list.do diff --git a/ui-macos/sshuttle b/src/ui-macos/sshuttle similarity index 100% rename from ui-macos/sshuttle rename to src/ui-macos/sshuttle From 32ea23e5cca1b3946f17401418e720bab37c3012 Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sat, 19 Jan 2013 20:10:31 -0500 Subject: [PATCH 78/90] Moved docs out of the src directory --- src/LICENSE => LICENSE | 0 src/README.md => README.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/LICENSE => LICENSE (100%) rename src/README.md => README.md (100%) diff --git a/src/LICENSE b/LICENSE similarity index 100% rename from src/LICENSE rename to LICENSE diff --git a/src/README.md b/README.md similarity index 100% rename from src/README.md rename to README.md From e4aa4efa4f03c27f98ff1295271424b722998d61 Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sat, 19 Jan 2013 20:11:11 -0500 Subject: [PATCH 79/90] First version; still has debugging --- packaging/sshuttle.conf | 75 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 packaging/sshuttle.conf diff --git a/packaging/sshuttle.conf b/packaging/sshuttle.conf new file mode 100644 index 0000000..6fa5a71 --- /dev/null +++ b/packaging/sshuttle.conf @@ -0,0 +1,75 @@ +description "Create a tunnel over SSH proxy" +author "Jim Wyllie " + +manual +nice -5 + +# Edit this file with network prefixes that should be loaded through the SSH +# tunnel. +env PREFIX_LOCATION=/etc/sshuttle/prefixes.conf + +# Try all the keys in a given key directory +env KEY_LOCATION=/etc/sshuttle/keys + +# Routing table; defaults to 100 +env ROUTE_TABLE=100 + +# fwmark; defaults to 1 +env FWMARK=1 + +start on (local-filesystems and net-device-up IFACE!=lo) +stop on stopping network-services + +#respawn + +pre-start script + # Make sure we have created the routes + sudo ip rule add fwmark ${FWMARK} lookup ${ROUTE_TABLE} + logger "Starting sshuttle..." + + if [ -f "${PREFIX_LOCATION}" ]; then + cat "${PREFIX_LOCATION}" | while read ROUTE; do + + logger "Working on route: ${ROUTE}" + + # Skip comments + if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then + continue + fi + + # Skip empty lines + if [ -z "${ROUTE}" ]; then + continue + fi + + logger "Adding route command: ip route add local ${ROUTE} dev lo table ${ROUTE_TABLE}" + ip route add local ${ROUTE} dev lo table ${ROUTE_TABLE} + done + fi +end script + +post-stop script + if [ -f "${PREFIX_LOCATION}" ]; then + cat "${PREFIX_LOCATION}" | while read ROUTE; do + + logger "Working on route: ${ROUTE}" + + # Skip comments + if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then + continue + fi + + # Skip empty lines + if [ -z "${ROUTE}" ]; then + continue + fi + + logger "Deleting route command: ip route del local ${ROUTE} dev lo table ${ROUTE_TABLE}" + ip route del local ${ROUTE} dev lo table ${ROUTE_TABLE} + done + fi + + ip rule del fwmark ${FWMARK} +end script + +exec sleep 60 From 7b0448b5f4bd3dec42142c089024e554b629ce5d Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sat, 19 Jan 2013 20:37:27 -0500 Subject: [PATCH 80/90] Added -s to accept subnets from a config file --- src/main.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main.py b/src/main.py index 161479c..8b62677 100644 --- a/src/main.py +++ b/src/main.py @@ -37,6 +37,26 @@ def parse_subnet6(s): return(socket.AF_INET6, net, width) +# Subnet file, supporting empty lines and hash-started comment lines +def parse_subnet_file(s): + try: + handle = open(s, 'r') + except OSError, e: + raise Fatal('Unable to open subnet file: %s' % s) + + raw_config_lines = handle.readlines() + config_lines = [] + for line_no, line in enumerate(raw_config_lines): + line = line.strip() + if len(line) == 0: + continue + if line[0] == '#': + continue + config_lines.append(line) + + return config_lines + + # list of: # 1.2.3.4/5 or just 1.2.3.4 # 1:2::3/64 or just 1:2::3 @@ -100,6 +120,7 @@ def parse_ipport6(s): no-latency-control sacrifice latency to improve bandwidth benchmarks wrap= restart counting channel numbers after this number (for testing) D,daemon run in the background as a daemon +s,subnets= file where the subnets are stored, instead of on the command line syslog send log messages to syslog (default if you use --daemon) pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] server (internal use only) @@ -131,8 +152,8 @@ def parse_ipport6(s): elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: - if len(extra) < 1 and not opt.auto_nets: - o.fatal('at least one subnet (or -N) expected') + if len(extra) < 1 and not opt.auto_nets and not opt.subnets: + o.fatal('at least one subnet, subnet file, or -N expected') includes = extra excludes = ['127.0.0.0/8'] for k,v in flags: @@ -149,6 +170,8 @@ def parse_ipport6(s): sh = [] else: sh = None + if opt.subnets: + includes = parse_subnet_file(opt.subnets) if not opt.method: method = "auto" elif opt.method in [ "auto", "nat", "tproxy", "ipfw" ]: From 0b1d403684f23bf4facf48c6416910dc37ddc480 Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 16:45:15 -0500 Subject: [PATCH 81/90] Adding more robust exit codes --- packaging/sshuttle.conf | 37 ++++++++++++++++++++++++++----------- src/main.py | 11 +++++++++-- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packaging/sshuttle.conf b/packaging/sshuttle.conf index 6fa5a71..aa2be91 100644 --- a/packaging/sshuttle.conf +++ b/packaging/sshuttle.conf @@ -1,4 +1,4 @@ -description "Create a tunnel over SSH proxy" +description "Create a transparent proxy over SSH" author "Jim Wyllie " manual @@ -8,15 +8,24 @@ nice -5 # tunnel. env PREFIX_LOCATION=/etc/sshuttle/prefixes.conf -# Try all the keys in a given key directory -env KEY_LOCATION=/etc/sshuttle/keys - # Routing table; defaults to 100 env ROUTE_TABLE=100 # fwmark; defaults to 1 env FWMARK=1 +# SSH tunnel configuration file +env SSHUTTLE_TUNNEL_FILE=/etc/sshuttle/tunnel.conf + +# File containing the tunnel proxy name / host / whatever +env TUNNEL_PROXY="/etc/sshuttle/tunnel.conf" + +# Any other commands needed to run before or after loading the SSH tunnel. +# This is where you can put any of your hacks to set up tunnels-in-tunnels, +# etc. Scripts in this directory are executed in order. +env MISC_START_DIR=/etc/sshuttle/pre-start.d +env MISC_STOP_DIR=/etc/sshuttle/post-stop.d + start on (local-filesystems and net-device-up IFACE!=lo) stop on stopping network-services @@ -30,8 +39,6 @@ pre-start script if [ -f "${PREFIX_LOCATION}" ]; then cat "${PREFIX_LOCATION}" | while read ROUTE; do - logger "Working on route: ${ROUTE}" - # Skip comments if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then continue @@ -42,18 +49,21 @@ pre-start script continue fi - logger "Adding route command: ip route add local ${ROUTE} dev lo table ${ROUTE_TABLE}" + logger "Adding route: ${ROUTE}" ip route add local ${ROUTE} dev lo table ${ROUTE_TABLE} done fi + + for RUNFILE in ${MISC_START_DIR}/*; do + logger "Executing ${RUNFILE}" + /bin/sh -c "${RUNFILE}" + done end script post-stop script if [ -f "${PREFIX_LOCATION}" ]; then cat "${PREFIX_LOCATION}" | while read ROUTE; do - logger "Working on route: ${ROUTE}" - # Skip comments if [ -n "$(echo ${ROUTE} | egrep "^[ ]*#")" ]; then continue @@ -64,12 +74,17 @@ post-stop script continue fi - logger "Deleting route command: ip route del local ${ROUTE} dev lo table ${ROUTE_TABLE}" + logger "Deleting route: ${ROUTE}" ip route del local ${ROUTE} dev lo table ${ROUTE_TABLE} done fi ip rule del fwmark ${FWMARK} + + for RUNFILE in "${MISC_STOP_DIR}/*"; do + logger "Executing ${RUNFILE}" + /bin/sh -c "${RUNFILE}" + done end script -exec sleep 60 +exec /home/jim/Projects/sshuttle.udp/src/sshuttle --method=tproxy --listen 0.0.0.0 --remote sshuttle_tunnel -s /etc/sshuttle/prefixes.conf -e "ssh -F ${TUNNEL_PROXY}" diff --git a/src/main.py b/src/main.py index 8b62677..8d277c9 100644 --- a/src/main.py +++ b/src/main.py @@ -193,7 +193,7 @@ def parse_ipport6(s): ipport_v6 = parse_ipport6(ip) else: ipport_v4 = parse_ipport4(ip) - sys.exit(client.main(ipport_v6, ipport_v4, + return_code = client.main(ipport_v6, ipport_v4, opt.ssh_cmd, remotename, opt.python, @@ -204,7 +204,14 @@ def parse_ipport6(s): opt.auto_nets, parse_subnets(includes), parse_subnets(excludes), - opt.syslog, opt.daemon, opt.pidfile)) + opt.syslog, opt.daemon, opt.pidfile) + + if return_code == 0: + log('Normal exit code, exiting...') + else: + log('Abnormal exit code detected, failing...' % return_code) + sys.exit(return_code) + except Fatal, e: log('fatal: %s\n' % e) sys.exit(99) From ac180098d6560fab70d8963d0be5eb16919c84c2 Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 16:51:23 -0500 Subject: [PATCH 82/90] Added the PyXAPI requirement to the readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7b1eece..6318b4f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ just switch your wireless off and on. Sshuttle makes the kernel setting it changes permanent, so this won't happen again, even after a reboot. +Required Software +================= + + - You need PyXAPI, available here: + http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/ + sshuttle: where transparent proxy meets VPN meets ssh ===================================================== From ce5c3a3f7da77f642f150c14264c195aa49d59c8 Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 17:37:10 -0500 Subject: [PATCH 83/90] Changed the file to be more "canonical" --- packaging/sshuttle.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/sshuttle.conf b/packaging/sshuttle.conf index aa2be91..0adff3f 100644 --- a/packaging/sshuttle.conf +++ b/packaging/sshuttle.conf @@ -87,4 +87,4 @@ post-stop script done end script -exec /home/jim/Projects/sshuttle.udp/src/sshuttle --method=tproxy --listen 0.0.0.0 --remote sshuttle_tunnel -s /etc/sshuttle/prefixes.conf -e "ssh -F ${TUNNEL_PROXY}" +exec /usr/bin/sshuttle --dns --method=tproxy --listen 0.0.0.0 --remote sshuttle_tunnel -s /etc/sshuttle/prefixes.conf -e "ssh -F ${TUNNEL_PROXY}" From 5fa881fc6c144632e2662108f2fab2a6aaa8e41d Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 17:37:31 -0500 Subject: [PATCH 84/90] Changed the sshuttle binary to point to install --- src/sshuttle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sshuttle b/src/sshuttle index 2d234d5..6ee47f1 100755 --- a/src/sshuttle +++ b/src/sshuttle @@ -4,7 +4,8 @@ for i in 1 2 3 4 5 6 7 8 9 10; do [ -L "$EXE" ] || break EXE=$(readlink "$EXE") done -DIR=$(dirname "$EXE") +#DIR=$(dirname "$EXE") +DIR=/usr/share/sshuttle if python2 -V 2>/dev/null; then exec python2 "$DIR/main.py" python2 "$@" else From 901db36cdd911b59855f8140956e1c817066623c Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 17:38:40 -0500 Subject: [PATCH 85/90] Sample tunnel configuration --- packaging/tunnel.conf | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packaging/tunnel.conf diff --git a/packaging/tunnel.conf b/packaging/tunnel.conf new file mode 100644 index 0000000..64fb54e --- /dev/null +++ b/packaging/tunnel.conf @@ -0,0 +1,19 @@ +# Here is where you can specify any SSH tunnel options See ssh_config(5) for +# details. You need to leave the Host line intact, but everything else can +# specify whatever you want +Host sshuttle_tunnel + +# REQUIRED: Set this to be the host to which you would like to connect your +# tunnel +#Hostname localhost + +# REQUIRED: Set this to be the target SSH user on the remote system +#User foo + +# --------------------------------------------------------------------------- +# The rest are all optional; see ssh_config(5) for the full list of what can +# be specified. Some very commonly needed ones are below. +# --------------------------------------------------------------------------- + +# SSH key used for connecting +#IdentityFile /path/to/key From d390c993fbdb998252ffa1b8812af1e7ea17b9e0 Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 17:39:20 -0500 Subject: [PATCH 86/90] Added a sample prefixes file --- packaging/prefixes.conf | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packaging/prefixes.conf diff --git a/packaging/prefixes.conf b/packaging/prefixes.conf new file mode 100644 index 0000000..ca472b9 --- /dev/null +++ b/packaging/prefixes.conf @@ -0,0 +1,5 @@ +# Output prefixes here, one per line. Prefix is in: +# prefix/netmask format +# Like this: +# 192.168.0.0/16 +# 192.0.43.10/32 From e518238cffb68b33af995576bbc8bece78d1081b Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 17:39:34 -0500 Subject: [PATCH 87/90] Added a control file for the Debian package --- packaging/control | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packaging/control diff --git a/packaging/control b/packaging/control new file mode 100644 index 0000000..ff162eb --- /dev/null +++ b/packaging/control @@ -0,0 +1,26 @@ +Package: sshuttle +Version: 0.2 +Architecture: i386 +Maintainer: Jim Wyllie +Depends: autossh, upstart, python (>=2.6) +Section: utils +Priority: optional +Homepage: http://github.com/jwyllie83/sshuttle.udp +Description: "Full-featured" VPN over an SSH tunnel, allowing full remote + access somewhere where all you have is an SSH connection. It works well if + you generally find yourself in the following situation: + . + - Your client machine (or router) is Linux, FreeBSD, or MacOS. + - You have access to a remote network via ssh. + - You don't necessarily have admin access on the remote network. + - You do not wish to, or can't, use other VPN software + - You don't want to create an ssh port forward for every + single host/port on the remote network. + - You hate openssh's port forwarding because it's randomly + slow and/or stupid. + - You can't use openssh's PermitTunnel feature because + it's disabled by default on openssh servers; plus it does + TCP-over-TCP, which has suboptimal performance + . + It also has hooks for more complicated setups (VPN-in-a-SSH-VPN, etc) to allow + you to set it up as you like. From 4519307fedf317d2d3dfbd97870f28aa0cfc4e57 Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 17:39:45 -0500 Subject: [PATCH 88/90] Added a shell script to make a .deb package --- packaging/make_deb | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 packaging/make_deb diff --git a/packaging/make_deb b/packaging/make_deb new file mode 100755 index 0000000..96fa589 --- /dev/null +++ b/packaging/make_deb @@ -0,0 +1,41 @@ +#!/bin/bash +# +# This script puts together a .deb package suitable for installing on an Ubuntu +# system + +B="/tmp/sshuttle/build" + +if [ ! -x /usr/bin/dpkg ]; then + echo 'Unable to build: dpkg not found on system' + exit 1 +fi + +# Create the new directory structure +mkdir -p ${B}/etc/sshuttle/pre-start.d +mkdir -p ${B}/etc/sshuttle/post-stop.d +mkdir -p ${B}/usr/share/sshuttle +mkdir -p ${B}/usr/bin +mkdir -p ${B}/etc/init +mkdir -p ${B}/DEBIAN + +# Copy over all of the files +cp -r ../src/* ${B}/usr/share/sshuttle +cp ../src/sshuttle ${B}/usr/bin +cp -r sshuttle.conf ${B}/etc/init +cp prefixes.conf ${B}/etc/sshuttle +cp tunnel.conf ${B}/etc/sshuttle + +# Copy the control file over, as well +cp control ${B}/DEBIAN + +# Create the md5sum manifest +if [ -x /usr/bin/md5sum ]; then + cd ${B} + find . -type f | egrep -v DEBIAN | sed -re 's/^..//' | xargs md5sum > ${B}/DEBIAN/md5sums + cd ${OLDPWD} +fi + +# Build the debian package +VERSION=$(egrep -e '^Version' control | sed -re 's/^[^:]*: //') +dpkg --build ${B} ./sshuttle-${VERSION}.deb +rm -rf ${B} From e32a4efbca1810902f768bf2235bceb3736f093a Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 17:45:02 -0500 Subject: [PATCH 89/90] Added some requirements --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6318b4f..3837d94 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ Required Software - You need PyXAPI, available here: http://www.pps.univ-paris-diderot.fr/~ylg/PyXAPI/ + - You also need autossh, available in various package management systems + - Python 2.x, both locally and the remote system sshuttle: where transparent proxy meets VPN meets ssh From b3009b8f434f35d9e50550892bef1970264d7a5f Mon Sep 17 00:00:00 2001 From: Jim Wyllie Date: Sun, 20 Jan 2013 18:00:54 -0500 Subject: [PATCH 90/90] Added some Ubuntu notes --- README.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3837d94..d24ebea 100644 --- a/README.md +++ b/README.md @@ -59,19 +59,34 @@ Prerequisites Linux.) - If you use MacOS or BSD on your client machine: - Your kernel needs to be compiled with IPFIREWALL_FORWARD + 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: +Obtaining sshuttle +------------------ + + - First, go get PyXAPI from the link above + + - Clone github.com/jwyllie83/sshuttle/tree/local + + +Usage on (Ubuntu) Linux ----------------------- - - git clone git://github.com/apenwarr/sshuttle - on your client machine. You'll need root or sudo - access, and python needs to be installed. + - `cd packaging; ./make_deb` + + - `sudo dpkg -i ./sshuttle-VERSION.deb` + + - Check out the files in `/etc/sshuttle`; configure them so your tunnel works + + - `sudo service sshuttle start` + + +Usage on other Linuxes and OSes +------------------------------- - - The most basic use of sshuttle looks like: ./sshuttle -r username@sshserver 0.0.0.0/0 -vv - There is a shortcut for 0.0.0.0/0 for those that value @@ -91,6 +106,9 @@ then the remote ssh password. Or you might have sudo and ssh set up to not require passwords, in which case you won't be prompted at all.) +Usage Notes +----------- + 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 your local network can make connections to your remote network.