diff --git a/README.rst b/README.rst index 2b05d02..5ce929c 100644 --- a/README.rst +++ b/README.rst @@ -43,17 +43,9 @@ Client side Requirements | | | * IPv6 TCP + | | | | * IPv6 UDP + | +-------+--------+------------+-----------------------------------------------+ -| BSD | IPFW | * IPv4 TCP | Your kernel needs to be compiled with | -| | | | `IPFIREWALL_FORWARD` and you need to have ipfw| -| | | | available. | -+-------+--------+------------+-----------------------------------------------+ | MacOS | PF | * IPv4 TCP + You need to have the pfctl command. | +-------+--------+------------+-----------------------------------------------+ -The IPFW method is depreciated. It was originally required for MacOS support, -however is no longer maintained. It is likely to get removed from future -versions of sshuttle. - Server side Requirements ------------------------ @@ -80,25 +72,42 @@ later. There are some things you need to consider for TPROXY to work: -1. The following commands need to be run first as root. This only needs to be - done once after booting up:: +- The following commands need to be run first as root. This only needs to be + done once after booting up:: - ip route add local default dev lo table 100 - ip rule add fwmark 1 lookup 100 - ip -6 route add local default dev lo table 100 - ip -6 rule add fwmark 1 lookup 100 + ip route add local default dev lo table 100 + ip rule add fwmark 1 lookup 100 + ip -6 route add local default dev lo table 100 + ip -6 rule add fwmark 1 lookup 100 -2. The client needs to be run as root. e.g.:: +- The --auto-nets feature does not detect IPv6 routes automatically. Add IPv6 + routes manually. e.g. by adding '::/0' to the end of the command line. - sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ... +- The client needs to be run as root. e.g.:: -3. You do need the `--method=tproxy` parameter, as above. + sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ... -4. The routes for the outgoing packets must already exist. For example, if your - connection does not have IPv6 support, no IPv6 routes will exist, IPv6 - packets will not be generated and sshuttle cannot intercept them. Add some - dummy routes to external interfaces. Make sure they get removed however - after sshuttle exits. +- You may need to exclude the IP address of the server you are connecting to. + Otherwise sshuttle may attempt to intercept the ssh packets, which will not + work. Use the `--exclude` parameter for this. + +- Similarly, UDP return packets (including DNS) could get intercepted and + bounced back. This is the case if you have a broad subnet such as + ``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the + `--exclude` parameter for this. + +- You do need the `--method=tproxy` parameter, as above. + +- The routes for the outgoing packets must already exist. For example, if your + connection does not have IPv6 support, no IPv6 routes will exist, IPv6 + packets will not be generated and sshuttle cannot intercept them:: + + telnet -6 www.google.com 80 + Trying 2404:6800:4001:805::1010... + telnet: Unable to connect to remote host: Network is unreachable + + Add some dummy routes to external interfaces. Make sure they get removed + however after sshuttle exits. Obtaining sshuttle diff --git a/run b/run index 9232659..b4fa803 100755 --- a/run +++ b/run @@ -1,5 +1,5 @@ #!/bin/sh -if python3 -V 2>/dev/null; then +if python3.5 -V 2>/dev/null; then exec python3 -m "sshuttle" "$@" else exec python -m "sshuttle" "$@" diff --git a/sshuttle/__main__.py b/sshuttle/__main__.py index 5a47c4f..7267ca7 100644 --- a/sshuttle/__main__.py +++ b/sshuttle/__main__.py @@ -6,6 +6,7 @@ import sshuttle.options as options import sshuttle.client as client import sshuttle.firewall as firewall import sshuttle.hostwatch as hostwatch +import sshuttle.ssyslog as ssyslog from sshuttle.helpers import family_ip_tuple, log, Fatal @@ -119,7 +120,7 @@ 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 ns-hosts= capture and forward remote DNS requests to the following servers -method= auto, nat, tproxy, pf or ipfw +method= auto, nat, tproxy or pf 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) @@ -181,7 +182,7 @@ try: includes = parse_subnet_file(opt.subnets) if not opt.method: method_name = "auto" - elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]: + elif opt.method in ["auto", "nat", "tproxy", "pf"]: method_name = opt.method else: o.fatal("method_name %s not supported" % opt.method) @@ -197,6 +198,9 @@ try: ipport_v6 = parse_ipport6(ip) else: ipport_v4 = parse_ipport4(ip) + if opt.syslog: + ssyslog.start_syslog() + ssyslog.stderr_to_syslog() return_code = client.main(ipport_v6, ipport_v4, opt.ssh_cmd, remotename, @@ -209,7 +213,7 @@ try: opt.auto_nets, parse_subnets(includes), parse_subnets(excludes), - opt.syslog, opt.daemon, opt.pidfile) + opt.daemon, opt.pidfile) if return_code == 0: log('Normal exit code, exiting...') diff --git a/sshuttle/assembler.py b/sshuttle/assembler.py index 7d92230..7f21921 100644 --- a/sshuttle/assembler.py +++ b/sshuttle/assembler.py @@ -4,13 +4,15 @@ import imp z = zlib.decompressobj() while 1: - name = sys.stdin.readline().strip().decode("ASCII") + name = stdin.readline().strip() if name: - nbytes = int(sys.stdin.readline()) + name = name.decode("ASCII") + + nbytes = int(stdin.readline()) if verbosity >= 2: sys.stderr.write('server: assembling %r (%d bytes)\n' % (name, nbytes)) - content = z.decompress(sys.stdin.read(nbytes)) + content = z.decompress(stdin.read(nbytes)) module = imp.new_module(name) parents = name.rsplit(".", 1) diff --git a/sshuttle/client.py b/sshuttle/client.py index cf7c387..7a7b6d7 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -14,7 +14,7 @@ import platform from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \ resolvconf_nameservers -from sshuttle.methods import get_method +from sshuttle.methods import get_method, Features _extra_fd = os.open('/dev/null', os.O_RDONLY) @@ -67,7 +67,7 @@ def daemonize(): outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666) try: - os.write(outfd, '%d\n' % os.getpid()) + os.write(outfd, b'%d\n' % os.getpid()) finally: os.close(outfd) os.chdir("/") @@ -81,8 +81,6 @@ def daemonize(): os.dup2(si.fileno(), 1) si.close() - ssyslog.stderr_to_syslog() - def daemon_cleanup(): try: @@ -277,18 +275,25 @@ udp_by_src = {} def expire_connections(now, mux): + remove = [] for chan, timeout in dnsreqs.items(): if timeout < now: debug3('expiring dnsreqs channel=%d\n' % chan) + remove.append(chan) del mux.channels[chan] - del dnsreqs[chan] + for chan in remove: + del dnsreqs[chan] debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) + + remove = [] 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, '') + mux.send(chan, ssnet.CMD_UDP_CLOSE, b'') + remove.append(peer) del mux.channels[chan] - del udp_by_src[peer] + for peer in remove: + del udp_by_src[peer] debug3('Remaining UDP channels: %d\n' % len(udp_by_src)) @@ -330,7 +335,7 @@ def onaccept_tcp(listener, method, mux, handlers): def udp_done(chan, data, method, sock, dstip): - (src, srcport, data) = data.split(",", 2) + (src, srcport, data) = data.split(b",", 2) srcip = (src, int(srcport)) debug3('doing send from %r to %r\n' % (srcip, dstip,)) method.send_udp(sock, srcip, dstip, data) @@ -349,10 +354,10 @@ def onaccept_udp(listener, method, mux, handlers): chan = mux.next_channel() mux.channels[chan] = lambda cmd, data: udp_done( chan, data, method, listener, dstip=srcip) - mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family) + mux.send(chan, ssnet.CMD_UDP_OPEN, b"%d" % listener.family) udp_by_src[srcip] = chan, now + 30 - hdr = "%s,%r," % (dstip[0], dstip[1]) + hdr = b"%s,%d," % (dstip[0].encode("ASCII"), dstip[1]) mux.send(chan, ssnet.CMD_UDP_DATA, hdr + data) expire_connections(now, mux) @@ -382,8 +387,7 @@ def ondns(listener, method, mux, handlers): def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, - dns_listener, seed_hosts, auto_nets, - syslog, daemon): + dns_listener, seed_hosts, auto_nets, daemon): debug1('Starting client with Python version %s\n' % platform.python_version()) @@ -433,21 +437,26 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, if initstring != expected: raise Fatal('expected server init string %r; got %r' % (expected, initstring)) - debug1('connected.\n') - print('Connected.') + log('Connected.\n') sys.stdout.flush() if daemon: daemonize() log('daemonizing (%s).\n' % _pidname) - elif syslog: - debug1('switching to syslog.\n') - ssyslog.stderr_to_syslog() def onroutes(routestr): if auto_nets: - for line in routestr.strip().split('\n'): - (family, ip, width) = line.split(',', 2) - fw.auto_nets.append((int(family), ip, int(width))) + for line in routestr.strip().split(b'\n'): + (family, ip, width) = line.split(b',', 2) + family = int(family) + width = int(width) + ip = ip.decode("ASCII") + if family == socket.AF_INET6 and tcp_listener.v6 is None: + debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width)) + if family == socket.AF_INET and tcp_listener.v4 is None: + debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width)) + else: + debug2("Adding auto net %d/%s/%d\n" % (family, ip, width)) + fw.auto_nets.append((family, ip, width)) # we definitely want to do this *after* starting ssh, or we might end # up intercepting the ssh connection! @@ -493,10 +502,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, def main(listenip_v6, listenip_v4, ssh_cmd, remotename, python, latency_control, dns, nslist, method_name, seed_hosts, auto_nets, - subnets_include, subnets_exclude, syslog, daemon, pidfile): + subnets_include, subnets_exclude, daemon, pidfile): - if syslog: - ssyslog.start_syslog() if daemon: try: check_daemon(pidfile) @@ -507,19 +514,45 @@ def main(listenip_v6, listenip_v4, fw = FirewallClient(method_name) - features = fw.method.get_supported_features() + # Get family specific subnet lists + if dns: + nslist += resolvconf_nameservers() + + subnets = subnets_include + subnets_exclude # we don't care here + subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] + nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] + subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET] + nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET] + + # Check features available + avail = fw.method.get_supported_features() + required = Features() + if listenip_v6 == "auto": - if features.ipv6: + if avail.ipv6: listenip_v6 = ('::1', 0) else: listenip_v6 = None + required.ipv6 = len(subnets_v6) > 0 or len(nslist_v6) > 0 \ + or listenip_v6 is not None + required.udp = avail.udp + required.dns = len(nslist) > 0 + + fw.method.assert_features(required) + + if required.ipv6 and listenip_v6 is None: + raise Fatal("IPv6 required but not listening.") + + # display features enabled + debug1("IPv6 enabled: %r\n" % required.ipv6) + debug1("UDP enabled: %r\n" % required.udp) + debug1("DNS enabled: %r\n" % required.dns) + + # bind to required ports if listenip_v4 == "auto": listenip_v4 = ('127.0.0.1', 0) - udp = features.udp - debug1("UDP enabled: %r\n" % udp) - 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, ] @@ -538,7 +571,7 @@ def main(listenip_v6, listenip_v4, tcp_listener = MultiListener() tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if udp: + if required.udp: udp_listener = MultiListener(socket.SOCK_DGRAM) udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) else: @@ -586,10 +619,7 @@ def main(listenip_v6, listenip_v4, udp_listener.print_listening("UDP redirector") bound = False - if dns or nslist: - if dns: - nslist += resolvconf_nameservers() - dns = True + if required.dns: # search for spare port for DNS debug2('Binding DNS:') ports = range(12300, 9000, -1) @@ -630,22 +660,45 @@ def main(listenip_v6, listenip_v4, dnsport_v4 = 0 dns_listener = None - fw.method.check_settings(udp, dns) + # Last minute sanity checks. + # These should never fail. + # If these do fail, something is broken above. + if len(subnets_v6) > 0: + assert required.ipv6 + if redirectport_v6 == 0: + raise Fatal("IPv6 subnets defined but not listening") + + if len(nslist_v6) > 0: + assert required.dns + assert required.ipv6 + if dnsport_v6 == 0: + raise Fatal("IPv6 ns servers defined but not listening") + + if len(subnets_v4) > 0: + if redirectport_v4 == 0: + raise Fatal("IPv4 subnets defined but not listening") + + if len(nslist_v4) > 0: + if dnsport_v4 == 0: + raise Fatal("IPv4 ns servers defined but not listening") + + # setup method specific stuff on listeners fw.method.setup_tcp_listener(tcp_listener) if udp_listener: fw.method.setup_udp_listener(udp_listener) if dns_listener: fw.method.setup_udp_listener(dns_listener) + # start the firewall fw.setup(subnets_include, subnets_exclude, nslist, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, - udp) + required.udp) + # start the client process try: return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, dns_listener, - seed_hosts, auto_nets, syslog, - daemon) + seed_hosts, auto_nets, daemon) finally: try: if daemon: diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index f9803d9..f16aac6 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -5,14 +5,14 @@ import sshuttle.ssyslog as ssyslog import sys import os import platform +import traceback from sshuttle.helpers import debug1, debug2, Fatal from sshuttle.methods import get_auto_method, get_method -hostmap = {} HOSTSFILE = '/etc/hosts' -def rewrite_etc_hosts(port): +def rewrite_etc_hosts(hostmap, port): BAKFILE = '%s.sbak' % HOSTSFILE APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port old_content = '' @@ -37,7 +37,7 @@ def rewrite_etc_hosts(port): f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND)) f.close() - if st: + if st is not None: os.chown(tmpname, st.st_uid, st.st_gid) os.chmod(tmpname, st.st_mode) else: @@ -47,9 +47,7 @@ def rewrite_etc_hosts(port): def restore_etc_hosts(port): - global hostmap - hostmap = {} - rewrite_etc_hosts(port) + rewrite_etc_hosts({}, port) # Isolate function that needs to be replaced for tests @@ -86,8 +84,9 @@ def setup_daemon(): # are hopefully harmless. def main(method_name, syslog): stdin, stdout = setup_daemon() + hostmap = {} - debug1('Starting firewall with Python version %s\n' + debug1('firewall manager: Starting firewall with Python version %s\n' % platform.python_version()) if method_name == "auto": @@ -99,7 +98,7 @@ def main(method_name, syslog): ssyslog.start_syslog() ssyslog.stderr_to_syslog() - debug1('firewall manager ready method name %s.\n' % method.name) + debug1('firewall manager: ready method name %s.\n' % method.name) stdout.write('READY %s\n' % method.name) stdout.flush() @@ -124,7 +123,7 @@ def main(method_name, syslog): except: raise Fatal('firewall: expected route or NSLIST but got %r' % line) subnets.append((int(family), int(width), bool(int(exclude)), ip)) - debug2('Got subnets: %r\n' % subnets) + debug2('firewall manager: Got subnets: %r\n' % subnets) nslist = [] if line != 'NSLIST\n': @@ -140,8 +139,8 @@ def main(method_name, syslog): except: raise Fatal('firewall: expected nslist or PORTS but got %r' % line) nslist.append((int(family), ip)) - debug2('Got partial nslist: %r\n' % nslist) - debug2('Got nslist: %r\n' % nslist) + debug2('firewall manager: Got partial nslist: %r\n' % nslist) + debug2('firewall manager: Got nslist: %r\n' % nslist) if not line.startswith('PORTS '): raise Fatal('firewall: expected PORTS but got %r' % line) @@ -163,7 +162,7 @@ def main(method_name, syslog): assert(dnsport_v4 >= 0) assert(dnsport_v4 <= 65535) - debug2('Got ports: %d,%d,%d,%d\n' + debug2('firewall manager: Got ports: %d,%d,%d,%d\n' % (port_v6, port_v4, dnsport_v6, dnsport_v4)) line = stdin.readline(128) @@ -174,29 +173,27 @@ def main(method_name, syslog): _, _, udp = line.partition(" ") udp = bool(int(udp)) - debug2('Got udp: %r\n' % udp) + debug2('firewall manager: Got udp: %r\n' % udp) + + subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] + nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] + subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET] + nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET] try: - do_wait = None - debug1('firewall manager: starting transproxy.\n') + debug1('firewall manager: setting up.\n') - nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] - subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] - if port_v6 > 0: - do_wait = method.setup_firewall( + if len(subnets_v6) > 0 or len(nslist_v6) > 0: + debug2('firewall manager: setting up IPv6.\n') + method.setup_firewall( port_v6, dnsport_v6, nslist_v6, socket.AF_INET6, subnets_v6, udp) - elif len(subnets_v6) > 0: - debug1("IPv6 subnets defined but IPv6 disabled\n") - nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET] - subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET] - if port_v4 > 0: - do_wait = method.setup_firewall( + if len(subnets_v4) > 0 or len(nslist_v4) > 0: + debug2('firewall manager: setting up IPv4.\n') + method.setup_firewall( port_v4, dnsport_v4, nslist_v4, socket.AF_INET, subnets_v4, udp) - elif len(subnets_v4) > 0: - debug1('IPv4 subnets defined but IPv4 disabled\n') stdout.write('STARTED\n') @@ -211,16 +208,15 @@ def main(method_name, syslog): # to stay running so that we don't need a *second* password # authentication at shutdown time - that cleanup is important! while 1: - if do_wait is not None: - do_wait() line = stdin.readline(128) if line.startswith('HOST '): (name, ip) = line[5:].strip().split(',', 1) hostmap[name] = ip - rewrite_etc_hosts(port_v6 or port_v4) + debug2('firewall manager: setting up /etc/hosts.\n') + rewrite_etc_hosts(hostmap, port_v6 or port_v4) elif line: if not method.firewall_command(line): - raise Fatal('expected EOF, got %r' % line) + raise Fatal('firewall: expected command, got %r' % line) else: break finally: @@ -228,8 +224,41 @@ def main(method_name, syslog): debug1('firewall manager: undoing changes.\n') except: pass - if port_v6: - method.setup_firewall(port_v6, 0, [], socket.AF_INET6, [], udp) - if port_v4: - method.setup_firewall(port_v4, 0, [], socket.AF_INET, [], udp) - restore_etc_hosts(port_v6 or port_v4) + + try: + if len(subnets_v6) > 0 or len(nslist_v6) > 0: + debug2('firewall manager: undoing IPv6 changes.\n') + method.restore_firewall(port_v6, socket.AF_INET6, udp) + except: + try: + debug1("firewall manager: " + "Error trying to undo IPv6 firewall.\n") + for line in traceback.format_exc().splitlines(): + debug1("---> %s\n" % line) + except: + pass + + try: + if len(subnets_v4) > 0 or len(nslist_v4) > 0: + debug2('firewall manager: undoing IPv4 changes.\n') + method.restore_firewall(port_v4, socket.AF_INET, udp) + except: + try: + debug1("firewall manager: " + "Error trying to undo IPv4 firewall.\n") + for line in traceback.format_exc().splitlines(): + debug1("firewall manager: ---> %s\n" % line) + except: + pass + + try: + debug2('firewall manager: undoing /etc/hosts changes.\n') + restore_etc_hosts(port_v6 or port_v4) + except: + try: + debug1("firewall manager: " + "Error trying to undo /etc/hosts changes.\n") + for line in traceback.format_exc().splitlines(): + debug1("firewall manager: ---> %s\n" % line) + except: + pass diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index 2324b2b..1a61b64 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -17,9 +17,17 @@ else: return s def log(s): + global logprefix try: sys.stdout.flush() - sys.stderr.write(logprefix + s) + if s.find("\n") != -1: + prefix = logprefix + s = s.rstrip("\n") + for line in s.split("\n"): + sys.stderr.write(prefix + line + "\n") + prefix = "---> " + else: + sys.stderr.write(logprefix + s) sys.stderr.flush() except IOError: # this could happen if stderr gets forcibly disconnected, eg. because diff --git a/sshuttle/hostwatch.py b/sshuttle/hostwatch.py index 5311210..3ab9bca 100644 --- a/sshuttle/hostwatch.py +++ b/sshuttle/hostwatch.py @@ -9,7 +9,7 @@ import platform import subprocess as ssubprocess import sshuttle.helpers as helpers -from sshuttle.helpers import log, debug1, debug2, debug3 +from sshuttle.helpers import b, log, debug1, debug2, debug3 POLL_TIME = 60 * 15 NETSTAT_POLL_TIME = 30 @@ -37,7 +37,7 @@ def write_host_cache(): try: f = open(tmpname, 'wb') for name, ip in sorted(hostnames.items()): - f.write('%s,%s\n' % (name, ip)) + f.write(b('%s,%s\n' % (name, ip))) f.close() os.chmod(tmpname, 384) # 600 in octal, 'rw-------' os.rename(tmpname, CACHEFILE) @@ -124,7 +124,7 @@ def _check_netstat(): argv = ['netstat', '-n'] try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) - content = p.stdout.read() + content = p.stdout.read().decode("ASCII") p.wait() except OSError: _, e = sys.exc_info()[:2] diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index 35e828b..34699da 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -39,6 +39,7 @@ class BaseMethod(object): result = Features() result.ipv6 = False result.udp = False + result.dns = True return result def get_tcp_dstip(self, sock): @@ -61,13 +62,20 @@ class BaseMethod(object): def setup_udp_listener(self, udp_listener): pass - def check_settings(self, udp, dns): - if udp: - Fatal("UDP support not supported with method %s.\n" % self.name) + def assert_features(self, features): + avail = self.get_supported_features() + for key in ["udp", "dns", "ipv6"]: + if getattr(features, key) and not getattr(avail, key): + raise Fatal( + "Feature %s not supported with method %s.\n" % + (key, self.name)) def setup_firewall(self, port, dnsport, nslist, family, subnets, udp): raise NotImplementedError() + def restore_firewall(self, port, family, udp): + raise NotImplementedError() + def firewall_command(self, line): return False @@ -86,14 +94,12 @@ def get_method(method_name): def get_auto_method(): - if _program_exists('ipfw'): - method_name = "ipfw" - elif _program_exists('iptables'): + if _program_exists('iptables'): method_name = "nat" elif _program_exists('pfctl'): method_name = "pf" else: raise Fatal( - "can't find either ipfw, iptables or pfctl; check your PATH") + "can't find either iptables or pfctl; check your PATH") return get_method(method_name) diff --git a/sshuttle/methods/ipfw.py b/sshuttle/methods/ipfw.py deleted file mode 100644 index 678d00f..0000000 --- a/sshuttle/methods/ipfw.py +++ /dev/null @@ -1,237 +0,0 @@ -import sys -import select -import socket -import struct -import subprocess as ssubprocess -from sshuttle.helpers import log, debug1, debug3, islocal, \ - Fatal, family_to_string -from sshuttle.methods import BaseMethod - - -# python doesn't have a definition for this -IPPROTO_DIVERT = 254 - - -def ipfw_rule_exists(n): - argv = ['ipfw', 'list'] - p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) - found = False - for line in p.stdout: - if line.startswith('%05d ' % n): - if not ('ipttl 42' in line - or ('skipto %d' % (n + 1)) in line - or 'check-state' in line): - log('non-sshuttle ipfw rule: %r\n' % line.strip()) - raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n) - found = True - rv = p.wait() - if rv: - raise Fatal('%r returned %d' % (argv, rv)) - return found - - -_oldctls = {} - - -def _fill_oldctls(prefix): - argv = ['sysctl', prefix] - p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) - for line in p.stdout: - assert(line[-1] == '\n') - (k, v) = line[:-1].split(': ', 1) - _oldctls[k] = v - rv = p.wait() - if rv: - raise Fatal('%r returned %d' % (argv, rv)) - if not line: - raise Fatal('%r returned no data' % (argv,)) - - -def _sysctl_set(name, val): - argv = ['sysctl', '-w', '%s=%s' % (name, val)] - debug1('>> %s\n' % ' '.join(argv)) - return ssubprocess.call(argv, stdout=open('/dev/null', 'w')) - - -_changedctls = [] - - -def sysctl_set(name, val, permanent=False): - PREFIX = 'net.inet.ip' - assert(name.startswith(PREFIX + '.')) - val = str(val) - if not _oldctls: - _fill_oldctls(PREFIX) - if not (name in _oldctls): - debug1('>> No such sysctl: %r\n' % name) - return False - oldval = _oldctls[name] - if val != oldval: - rv = _sysctl_set(name, val) - if rv == 0 and permanent: - debug1('>> ...saving permanently in /etc/sysctl.conf\n') - f = open('/etc/sysctl.conf', 'a') - f.write('\n' - '# Added by sshuttle\n' - '%s=%s\n' % (name, val)) - f.close() - else: - _changedctls.append(name) - return True - - -def _udp_unpack(p): - src = (socket.inet_ntoa(p[12:16]), struct.unpack('!H', p[20:22])[0]) - dst = (socket.inet_ntoa(p[16:20]), struct.unpack('!H', p[22:24])[0]) - return src, dst - - -def _udp_repack(p, src, dst): - addrs = socket.inet_aton(src[0]) + socket.inet_aton(dst[0]) - ports = struct.pack('!HH', src[1], dst[1]) - return p[:12] + addrs + ports + p[24:] - - -_real_dns_server = [None] - - -def _handle_diversion(divertsock, dnsport): - p, tag = divertsock.recvfrom(4096) - src, dst = _udp_unpack(p) - debug3('got diverted packet from %r to %r\n' % (src, dst)) - if dst[1] == 53: - # outgoing DNS - debug3('...packet is a DNS request.\n') - _real_dns_server[0] = dst - dst = ('127.0.0.1', dnsport) - elif src[1] == dnsport: - if islocal(src[0], divertsock.family): - debug3('...packet is a DNS response.\n') - src = _real_dns_server[0] - else: - log('weird?! unexpected divert from %r to %r\n' % (src, dst)) - assert(0) - newp = _udp_repack(p, src, dst) - divertsock.sendto(newp, tag) - - -def ipfw(*args): - argv = ['ipfw', '-q'] + list(args) - debug1('>> %s\n' % ' '.join(argv)) - rv = ssubprocess.call(argv) - if rv: - raise Fatal('%r returned %d' % (argv, rv)) - - -class Method(BaseMethod): - - def setup_firewall(self, port, dnsport, nslist, family, subnets, udp): - # IPv6 not supported - if family not in [socket.AF_INET, ]: - raise Exception( - 'Address family "%s" unsupported by ipfw method_name' - % family_to_string(family)) - if udp: - raise Exception("UDP not supported by ipfw method_name") - - sport = str(port) - xsport = str(port + 1) - - # cleanup any existing rules - if ipfw_rule_exists(port): - ipfw('delete', sport) - - while _changedctls: - name = _changedctls.pop() - oldval = _oldctls[name] - _sysctl_set(name, oldval) - - if subnets or dnsport: - sysctl_set('net.inet.ip.fw.enable', 1) - changed = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True) - if changed: - log("\n" - " WARNING: ONE-TIME NETWORK DISRUPTION:\n" - " =====================================\n" - "sshuttle has changed a MacOS kernel setting to work around\n" - "a bug in MacOS 10.6. This will cause your network to drop\n" - "within 5-10 minutes unless you restart your network\n" - "interface (change wireless networks or unplug/plug the\n" - "ethernet port) NOW, then restart sshuttle. The fix is\n" - "permanent; you only have to do this once.\n\n") - sys.exit(1) - - ipfw('add', sport, 'check-state', 'ip', - 'from', 'any', 'to', 'any') - - if subnets: - # create new subnet entries - for f, swidth, sexclude, snet \ - in sorted(subnets, key=lambda s: s[1], reverse=True): - if sexclude: - ipfw('add', sport, 'skipto', xsport, - 'tcp', - 'from', 'any', 'to', '%s/%s' % (snet, swidth)) - else: - ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port, - 'tcp', - 'from', 'any', 'to', '%s/%s' % (snet, swidth), - 'not', 'ipttl', '42', 'keep-state', 'setup') - - # This part is much crazier than it is on Linux, because MacOS (at - # least 10.6, and probably other versions, and maybe FreeBSD too) - # doesn't correctly fixup the dstip/dstport for UDP packets when it - # puts them through a 'fwd' rule. It also doesn't fixup the - # srcip/srcport in the response packet. In Linux iptables, all that - # happens magically for us, so we just redirect the packets and relax. - # - # On MacOS, we have to fix the ports ourselves. For that, we use a - # 'divert' socket, which receives raw packets and lets us mangle them. - # - # Here's how it works. Let's say the local DNS server is 1.1.1.1:53, - # and the remote DNS server is 2.2.2.2:53, and the local transproxy - # port is 10.0.0.1:12300, and a client machine is making a request from - # 10.0.0.5:9999. We see a packet like this: - # 10.0.0.5:9999 -> 1.1.1.1:53 - # Since the destip:port matches one of our local nameservers, it will - # match a 'fwd' rule, thus grabbing it on the local machine. However, - # the local kernel will then see a packet addressed to *:53 and not - # know what to do with it; there's nobody listening on port 53. Thus, - # we divert it, rewriting it into this: - # 10.0.0.5:9999 -> 10.0.0.1:12300 - # This gets proxied out to the server, which sends it to 2.2.2.2:53, - # and the answer comes back, and the proxy sends it back out like this: - # 10.0.0.1:12300 -> 10.0.0.5:9999 - # But that's wrong! The original machine expected an answer from - # 1.1.1.1:53, so we have to divert the *answer* and rewrite it: - # 1.1.1.1:53 -> 10.0.0.5:9999 - # - # See? Easy stuff. - if dnsport: - divertsock = socket.socket(socket.AF_INET, socket.SOCK_RAW, - IPPROTO_DIVERT) - divertsock.bind(('0.0.0.0', port)) # IP field is ignored - - for f, ip in [i for i in nslist if i[0] == family]: - # relabel and then catch outgoing DNS requests - ipfw('add', sport, 'divert', sport, - 'udp', - 'from', 'any', 'to', '%s/32' % ip, '53', - 'not', 'ipttl', '42') - # relabel DNS responses - ipfw('add', sport, 'divert', sport, - 'udp', - 'from', 'any', str(dnsport), 'to', 'any', - 'not', 'ipttl', '42') - - def do_wait(): - while 1: - r, w, x = select.select([sys.stdin, divertsock], [], []) - if divertsock in r: - _handle_diversion(divertsock, dnsport) - if sys.stdin in r: - return - else: - do_wait = None - - return do_wait diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py index bf5ef5c..c5afc03 100644 --- a/sshuttle/methods/nat.py +++ b/sshuttle/methods/nat.py @@ -30,41 +30,60 @@ class Method(BaseMethod): chain = 'sshuttle-%s' % port + # basic cleanup/setup of chains + self.restore_firewall(port, family, udp) + + _ipt('-N', chain) + _ipt('-F', chain) + _ipt('-I', 'OUTPUT', '1', '-j', chain) + _ipt('-I', 'PREROUTING', '1', '-j', chain) + + # create new subnet entries. Note that we're sorting in a very + # particular order: we need to go from most-specific (largest + # swidth) 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[1], reverse=True): + if sexclude: + _ipt('-A', chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet, swidth), + '-p', 'tcp') + else: + _ipt_ttl('-A', chain, '-j', 'REDIRECT', + '--dest', '%s/%s' % (snet, swidth), + '-p', 'tcp', + '--to-ports', str(port)) + + for f, ip in [i for i in nslist if i[0] == family]: + _ipt_ttl('-A', chain, '-j', 'REDIRECT', + '--dest', '%s/32' % ip, + '-p', 'udp', + '--dport', '53', + '--to-ports', str(dnsport)) + + def restore_firewall(self, port, family, udp): + # only ipv4 supported with NAT + if family != socket.AF_INET: + raise Exception( + 'Address family "%s" unsupported by nat method_name' + % family_to_string(family)) + if udp: + raise Exception("UDP not supported by nat method_name") + + 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, '-D', 'OUTPUT', '-j', chain) nonfatal(_ipt, '-D', 'PREROUTING', '-j', chain) nonfatal(_ipt, '-F', chain) _ipt('-X', chain) - - if subnets or dnsport: - _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 - # particular order: we need to go from most-specific (largest - # swidth) 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[1], reverse=True): - if sexclude: - _ipt('-A', chain, '-j', 'RETURN', - '--dest', '%s/%s' % (snet, swidth), - '-p', 'tcp') - else: - _ipt_ttl('-A', chain, '-j', 'REDIRECT', - '--dest', '%s/%s' % (snet, swidth), - '-p', 'tcp', - '--to-ports', str(port)) - - if dnsport: - for f, ip in [i for i in nslist if i[0] == family]: - _ipt_ttl('-A', chain, '-j', 'REDIRECT', - '--dest', '%s/32' % ip, - '-p', 'udp', - '--dport', '53', - '--to-ports', str(dnsport)) diff --git a/sshuttle/methods/pf.py b/sshuttle/methods/pf.py index e85a021..45a678b 100644 --- a/sshuttle/methods/pf.py +++ b/sshuttle/methods/pf.py @@ -7,7 +7,7 @@ import subprocess as ssubprocess from fcntl import ioctl from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \ sizeof, addressof, memmove -from sshuttle.helpers import debug1, debug2, Fatal, family_to_string +from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string from sshuttle.methods import BaseMethod @@ -24,67 +24,88 @@ def pfctl(args, stdin=None): return o -_pf_context = {'started_by_sshuttle': False, 'Xtoken': ''} - - -# This are some classes and functions used to support pf in yosemite. -class pf_state_xport(Union): - _fields_ = [("port", c_uint16), - ("call_id", c_uint16), - ("spi", c_uint32)] - - -class pf_addr(Structure): - - class _pfa(Union): - _fields_ = [("v4", c_uint32), # struct in_addr - ("v6", c_uint32 * 4), # struct in6_addr - ("addr8", c_uint8 * 16), - ("addr16", c_uint16 * 8), - ("addr32", c_uint32 * 4)] - - _fields_ = [("pfa", _pfa)] - _anonymous_ = ("pfa",) - - -class pfioc_natlook(Structure): - _fields_ = [("saddr", pf_addr), - ("daddr", pf_addr), - ("rsaddr", pf_addr), - ("rdaddr", pf_addr), - ("sxport", pf_state_xport), - ("dxport", pf_state_xport), - ("rsxport", pf_state_xport), - ("rdxport", pf_state_xport), - ("af", c_uint8), # sa_family_t - ("proto", c_uint8), - ("proto_variant", c_uint8), - ("direction", c_uint8)] - -pfioc_rule = c_char * 3104 # sizeof(struct pfioc_rule) - -pfioc_pooladdr = c_char * 1136 # sizeof(struct pfioc_pooladdr) - -MAXPATHLEN = 1024 - -DIOCNATLOOK = ((0x40000000 | 0x80000000) | ( - (sizeof(pfioc_natlook) & 0x1fff) << 16) | ((ord('D')) << 8) | (23)) -DIOCCHANGERULE = ((0x40000000 | 0x80000000) | ( - (sizeof(pfioc_rule) & 0x1fff) << 16) | ((ord('D')) << 8) | (26)) -DIOCBEGINADDRS = ((0x40000000 | 0x80000000) | ( - (sizeof(pfioc_pooladdr) & 0x1fff) << 16) | ((ord('D')) << 8) | (51)) - -PF_CHANGE_ADD_TAIL = 2 -PF_CHANGE_GET_TICKET = 6 - -PF_PASS = 0 -PF_RDR = 8 - -PF_OUT = 2 - +_pf_context = {'started_by_sshuttle': False, 'Xtoken': None} _pf_fd = None +class OsDefs(object): + + def __init__(self, platform=None): + if platform is None: + platform = sys.platform + self.platform = platform + + # This are some classes and functions used to support pf in yosemite. + if platform == 'darwin': + class pf_state_xport(Union): + _fields_ = [("port", c_uint16), + ("call_id", c_uint16), + ("spi", c_uint32)] + else: + class pf_state_xport(Union): + _fields_ = [("port", c_uint16), + ("call_id", c_uint16)] + + class pf_addr(Structure): + + class _pfa(Union): + _fields_ = [("v4", c_uint32), # struct in_addr + ("v6", c_uint32 * 4), # struct in6_addr + ("addr8", c_uint8 * 16), + ("addr16", c_uint16 * 8), + ("addr32", c_uint32 * 4)] + + _fields_ = [("pfa", _pfa)] + _anonymous_ = ("pfa",) + + class pfioc_natlook(Structure): + _fields_ = [("saddr", pf_addr), + ("daddr", pf_addr), + ("rsaddr", pf_addr), + ("rdaddr", pf_addr), + ("sxport", pf_state_xport), + ("dxport", pf_state_xport), + ("rsxport", pf_state_xport), + ("rdxport", pf_state_xport), + ("af", c_uint8), # sa_family_t + ("proto", c_uint8), + ("proto_variant", c_uint8), + ("direction", c_uint8)] + self.pfioc_natlook = pfioc_natlook + + # sizeof(struct pfioc_rule) + self.pfioc_rule = c_char * \ + (3104 if platform == 'darwin' else 3040) + + # sizeof(struct pfioc_pooladdr) + self.pfioc_pooladdr = c_char * 1136 + + self.MAXPATHLEN = 1024 + + self.DIOCNATLOOK = ( + (0x40000000 | 0x80000000) | + ((sizeof(pfioc_natlook) & 0x1fff) << 16) | + ((ord('D')) << 8) | (23)) + self.DIOCCHANGERULE = ( + (0x40000000 | 0x80000000) | + ((sizeof(self.pfioc_rule) & 0x1fff) << 16) | + ((ord('D')) << 8) | (26)) + self.DIOCBEGINADDRS = ( + (0x40000000 | 0x80000000) | + ((sizeof(self.pfioc_pooladdr) & 0x1fff) << 16) | + ((ord('D')) << 8) | (51)) + + self.PF_CHANGE_ADD_TAIL = 2 + self.PF_CHANGE_GET_TICKET = 6 + + self.PF_PASS = 0 + self.PF_RDR = 8 + + self.PF_OUT = 2 + +osdefs = OsDefs() + + def pf_get_dev(): global _pf_fd if _pf_fd is None: @@ -103,16 +124,16 @@ def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port): assert len(packed_src_ip) == len(packed_dst_ip) length = len(packed_src_ip) - pnl = pfioc_natlook() + pnl = osdefs.pfioc_natlook() pnl.proto = proto - pnl.direction = PF_OUT + pnl.direction = osdefs.PF_OUT pnl.af = family memmove(addressof(pnl.saddr), packed_src_ip, length) - pnl.sxport.port = socket.htons(src_port) memmove(addressof(pnl.daddr), packed_dst_ip, length) + pnl.sxport.port = socket.htons(src_port) pnl.dxport.port = socket.htons(dst_port) - ioctl(pf_get_dev(), DIOCNATLOOK, + ioctl(pf_get_dev(), osdefs.DIOCNATLOOK, (c_char * sizeof(pnl)).from_address(addressof(pnl))) ip = socket.inet_ntop( @@ -125,26 +146,26 @@ def pf_add_anchor_rule(type, name): ACTION_OFFSET = 0 POOL_TICKET_OFFSET = 8 ANCHOR_CALL_OFFSET = 1040 - RULE_ACTION_OFFSET = 3068 + RULE_ACTION_OFFSET = 3068 if osdefs.platform == 'darwin' else 2968 - pr = pfioc_rule() - ppa = pfioc_pooladdr() + pr = osdefs.pfioc_rule() + ppa = osdefs.pfioc_pooladdr() - ioctl(pf_get_dev(), DIOCBEGINADDRS, ppa) + ioctl(pf_get_dev(), osdefs.DIOCBEGINADDRS, ppa) memmove(addressof(pr) + POOL_TICKET_OFFSET, ppa[4:8], 4) # pool_ticket memmove(addressof(pr) + ANCHOR_CALL_OFFSET, name, - min(MAXPATHLEN, len(name))) # anchor_call = name + min(osdefs.MAXPATHLEN, len(name))) # anchor_call = name memmove(addressof(pr) + RULE_ACTION_OFFSET, struct.pack('I', type), 4) # rule.action = type memmove(addressof(pr) + ACTION_OFFSET, struct.pack( - 'I', PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET - ioctl(pf_get_dev(), DIOCCHANGERULE, pr) + 'I', osdefs.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET + ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr) memmove(addressof(pr) + ACTION_OFFSET, struct.pack( - 'I', PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL - ioctl(pf_get_dev(), DIOCCHANGERULE, pr) + 'I', osdefs.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL + ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr) class Method(BaseMethod): @@ -156,19 +177,20 @@ class Method(BaseMethod): proxy = sock.getsockname() argv = (sock.family, socket.IPPROTO_TCP, - peer[0], peer[1], proxy[0], proxy[1]) - pfile.write("QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % argv) + peer[0].encode("ASCII"), peer[1], + proxy[0].encode("ASCII"), proxy[1]) + out_line = b"QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % argv + pfile.write(out_line) pfile.flush() - line = pfile.readline() - debug2("QUERY_PF_NAT %d,%d,%s,%d,%s,%d" % argv + ' > ' + line) - if line.startswith('QUERY_PF_NAT_SUCCESS '): - (ip, port) = line[21:].split(',') - return (ip, int(port)) + in_line = pfile.readline() + debug2(out_line.decode("ASCII") + ' > ' + in_line.decode("ASCII")) + if in_line.startswith(b'QUERY_PF_NAT_SUCCESS '): + (ip, port) = in_line[21:].split(b',') + return (ip.decode("ASCII"), int(port)) return sock.getsockname() def setup_firewall(self, port, dnsport, nslist, family, subnets, udp): - global _pf_started_by_sshuttle tables = [] translating_rules = [] filtering_rules = [] @@ -180,57 +202,72 @@ class Method(BaseMethod): if udp: raise Exception("UDP not supported by pf method_name") - if subnets: + if len(subnets) > 0: includes = [] # If a given subnet is both included and excluded, list the # exclusion first; the table will ignore the second, opposite # definition for f, swidth, sexclude, snet in sorted( subnets, key=lambda s: (s[1], s[2]), reverse=True): - includes.append("%s%s/%s" % - ("!" if sexclude else "", snet, swidth)) + includes.append(b"%s%s/%d" % + (b"!" if sexclude else b"", + snet.encode("ASCII"), + swidth)) - tables.append('table {%s}' % ','.join(includes)) + tables.append( + b'table {%s}' % b','.join(includes)) translating_rules.append( - 'rdr pass on lo0 proto tcp ' - 'to -> 127.0.0.1 port %r' % port) + b'rdr pass on lo0 proto tcp ' + b'to -> 127.0.0.1 port %r' % port) filtering_rules.append( - 'pass out route-to lo0 inet proto tcp ' - 'to keep state') + b'pass out route-to lo0 inet proto tcp ' + b'to keep state') - if dnsport: - tables.append('table {%s}' % ','.join( - [ns[1] for ns in nslist])) - translating_rules.append( - 'rdr pass on lo0 proto udp to ' - ' port 53 -> 127.0.0.1 port %r' % dnsport) - filtering_rules.append( - 'pass out route-to lo0 inet proto udp to ' - ' port 53 keep state') + if len(nslist) > 0: + tables.append( + b'table {%s}' % + b','.join([ns[1].encode("ASCII") for ns in nslist])) + translating_rules.append( + b'rdr pass on lo0 proto udp to ' + b' port 53 -> 127.0.0.1 port %r' % dnsport) + filtering_rules.append( + b'pass out route-to lo0 inet proto udp to ' + b' port 53 keep state') - rules = '\n'.join(tables + translating_rules + filtering_rules) \ - + '\n' + rules = b'\n'.join(tables + translating_rules + filtering_rules) \ + + b'\n' + assert isinstance(rules, bytes) + debug3("rules:\n" + rules.decode("ASCII")) - pf_status = pfctl('-s all')[0] - if '\nrdr-anchor "sshuttle" all\n' not in pf_status: - pf_add_anchor_rule(PF_RDR, "sshuttle") - if '\nanchor "sshuttle" all\n' not in pf_status: - pf_add_anchor_rule(PF_PASS, "sshuttle") + pf_status = pfctl('-s all')[0] + if b'\nrdr-anchor "sshuttle" all\n' not in pf_status: + pf_add_anchor_rule(osdefs.PF_RDR, b"sshuttle") + if b'\nanchor "sshuttle" all\n' not in pf_status: + pf_add_anchor_rule(osdefs.PF_PASS, b"sshuttle") - pfctl('-a sshuttle -f /dev/stdin', rules) - if sys.platform == "darwin": - o = pfctl('-E') - _pf_context['Xtoken'] = \ - re.search(r'Token : (.+)', o[1]).group(1) - elif 'INFO:\nStatus: Disabled' in pf_status: - pfctl('-e') - _pf_context['started_by_sshuttle'] = True - else: - pfctl('-a sshuttle -F all') - if sys.platform == "darwin": - pfctl('-X %s' % _pf_context['Xtoken']) - elif _pf_context['started_by_sshuttle']: - pfctl('-d') + pfctl('-a sshuttle -f /dev/stdin', rules) + if osdefs.platform == "darwin": + o = pfctl('-E') + _pf_context['Xtoken'] = \ + re.search(b'Token : (.+)', o[1]).group(1) + elif b'INFO:\nStatus: Disabled' in pf_status: + pfctl('-e') + _pf_context['started_by_sshuttle'] = True + + def restore_firewall(self, port, family, udp): + if family != socket.AF_INET: + raise Exception( + 'Address family "%s" unsupported by pf method_name' + % family_to_string(family)) + if udp: + raise Exception("UDP not supported by pf method_name") + + pfctl('-a sshuttle -F all') + if osdefs.platform == "darwin": + if _pf_context['Xtoken'] is not None: + pfctl('-X %s' % _pf_context['Xtoken'].decode("ASCII")) + elif _pf_context['started_by_sshuttle']: + pfctl('-d') def firewall_command(self, line): if line.startswith('QUERY_PF_NAT '): diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py index 049c843..03353b8 100644 --- a/sshuttle/methods/tproxy.py +++ b/sshuttle/methods/tproxy.py @@ -59,6 +59,7 @@ if recvmsg == "python": ip = socket.inet_ntop(family, cmsg_data[start:start + length]) dstip = (ip, port) break + print("xxxxx", srcip, dstip) return (srcip, dstip, data) elif recvmsg == "socket_ext": def recv_udp(listener, bufsize): @@ -105,7 +106,12 @@ class Method(BaseMethod): def get_supported_features(self): result = super(Method, self).get_supported_features() result.ipv6 = True - result.udp = True + if recvmsg is None: + result.udp = False + result.dns = False + else: + result.udp = True + result.dns = True return result def get_tcp_dstip(self, sock): @@ -162,6 +168,91 @@ class Method(BaseMethod): tproxy_chain = 'sshuttle-t-%s' % port divert_chain = 'sshuttle-d-%s' % port + # basic cleanup/setup of chains + self.restore_firewall(port, family, udp) + + _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 udp: + _ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, + '-m', 'udp', '-p', 'udp') + + for f, ip in [i for i in nslist if i[0] == family]: + _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + '--dest', '%s/32' % ip, + '-m', 'udp', '-p', 'udp', '--dport', '53') + _ipt('-A', tproxy_chain, '-j', 'TPROXY', + '--tproxy-mark', '0x1/0x1', + '--dest', '%s/32' % ip, + '-m', 'udp', '-p', 'udp', '--dport', '53', + '--on-port', str(dnsport)) + + for f, swidth, sexclude, snet \ + in sorted(subnets, key=lambda s: s[1], reverse=True): + if sexclude: + _ipt('-A', mark_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet, swidth), + '-m', 'tcp', '-p', 'tcp') + _ipt('-A', tproxy_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet, swidth), + '-m', 'tcp', '-p', 'tcp') + else: + _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + '--dest', '%s/%s' % (snet, swidth), + '-m', 'tcp', '-p', 'tcp') + _ipt('-A', tproxy_chain, '-j', 'TPROXY', + '--tproxy-mark', '0x1/0x1', + '--dest', '%s/%s' % (snet, swidth), + '-m', 'tcp', '-p', 'tcp', + '--on-port', str(port)) + + if udp: + if sexclude: + _ipt('-A', mark_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet, swidth), + '-m', 'udp', '-p', 'udp') + _ipt('-A', tproxy_chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet, swidth), + '-m', 'udp', '-p', 'udp') + else: + _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', + '--dest', '%s/%s' % (snet, swidth), + '-m', 'udp', '-p', 'udp') + _ipt('-A', tproxy_chain, '-j', 'TPROXY', + '--tproxy-mark', '0x1/0x1', + '--dest', '%s/%s' % (snet, swidth), + '-m', 'udp', '-p', 'udp', + '--on-port', str(port)) + + def restore_firewall(self, port, family, udp): + if family not in [socket.AF_INET, socket.AF_INET6]: + raise Exception( + '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('-D', 'OUTPUT', '-j', mark_chain) @@ -176,81 +267,3 @@ class Method(BaseMethod): if ipt_chain_exists(family, table, divert_chain): _ipt('-F', divert_chain) _ipt('-X', divert_chain) - - if subnets or dnsport: - _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('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain, - '-m', 'udp', '-p', 'udp') - - if dnsport: - for f, ip in [i for i in nslist if i[0] == family]: - _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', - '--dest', '%s/32' % ip, - '-m', 'udp', '-p', 'udp', '--dport', '53') - _ipt('-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: - _ipt('-A', mark_chain, '-j', 'RETURN', - '--dest', '%s/%s' % (snet, swidth), - '-m', 'tcp', '-p', 'tcp') - _ipt('-A', tproxy_chain, '-j', 'RETURN', - '--dest', '%s/%s' % (snet, swidth), - '-m', 'tcp', '-p', 'tcp') - else: - _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', - '--dest', '%s/%s' % (snet, swidth), - '-m', 'tcp', '-p', 'tcp') - _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('-A', mark_chain, '-j', 'RETURN', - '--dest', '%s/%s' % (snet, swidth), - '-m', 'udp', '-p', 'udp') - _ipt('-A', tproxy_chain, '-j', 'RETURN', - '--dest', '%s/%s' % (snet, swidth), - '-m', 'udp', '-p', 'udp') - elif udp: - _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', - '--dest', '%s/%s' % (snet, swidth), - '-m', 'udp', '-p', 'udp') - _ipt('-A', tproxy_chain, '-j', 'TPROXY', - '--tproxy-mark', '0x1/0x1', - '--dest', '%s/%s' % (snet, swidth), - '-m', 'udp', '-p', 'udp', - '--on-port', str(port)) - - def check_settings(self, udp, dns): - if udp and recvmsg is None: - Fatal("tproxy UDP support requires recvmsg function.\n") - - if dns and recvmsg is None: - Fatal("tproxy DNS support requires recvmsg function.\n") - - if udp: - debug1("tproxy UDP support enabled.\n") - - if dns: - debug1("tproxy DNS support enabled.\n") diff --git a/sshuttle/server.py b/sshuttle/server.py index d207360..c021627 100644 --- a/sshuttle/server.py +++ b/sshuttle/server.py @@ -17,22 +17,23 @@ from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \ def _ipmatch(ipstr): - if ipstr == 'default': - ipstr = '0.0.0.0/0' - m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) + if ipstr == b('default'): + ipstr = b('0.0.0.0/0') + m = re.match(b('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$'), ipstr) if m: g = m.groups() ips = g[0] width = int(g[4] or 32) if g[1] is None: - ips += '.0.0.0' + ips += b('.0.0.0') width = min(width, 8) elif g[2] is None: - ips += '.0.0' + ips += b('.0.0') width = min(width, 16) elif g[3] is None: - ips += '.0' + ips += b('.0') width = min(width, 24) + ips = ips.decode("ASCII") return (struct.unpack('!I', socket.inet_aton(ips))[0], width) @@ -57,11 +58,12 @@ def _shl(n, bits): def _list_routes(): + # FIXME: IPv4 only argv = ['netstat', '-rn'] p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) routes = [] for line in p.stdout: - cols = re.split(r'\s+', line.decode("ASCII")) + cols = re.split(b('\s+'), line) ipw = _ipmatch(cols[0]) if not ipw: continue # some lines won't be parseable; never mind @@ -249,20 +251,20 @@ def main(latency_control): mux.send(0, ssnet.CMD_ROUTES, b(routepkt)) hw = Hostwatch() - hw.leftover = '' + hw.leftover = b('') def hostwatch_ready(sock): assert(hw.pid) content = hw.sock.recv(4096) if content: - lines = (hw.leftover + content).split('\n') + lines = (hw.leftover + content).split(b('\n')) if lines[-1]: # no terminating newline: entry isn't complete yet! hw.leftover = lines.pop() lines.append('') else: - hw.leftover = '' - mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines)) + hw.leftover = b('') + mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines)) else: raise Fatal('hostwatch process died') @@ -274,7 +276,7 @@ def main(latency_control): mux.got_host_req = got_host_req def new_channel(channel, data): - (family, dstip, dstport) = data.decode("ASCII").split(',', 2) + (family, dstip, dstport) = data.split(b(','), 2) family = int(family) dstport = int(dstport) outwrap = ssnet.connect_dst(family, dstip, dstport) @@ -332,14 +334,20 @@ def main(latency_control): if dnshandlers: now = time.time() - for channel, h in list(dnshandlers.items()): + remove = [] + for channel, h in dnshandlers.items(): if h.timeout < now or not h.ok: debug3('expiring dnsreqs channel=%d\n' % channel) - del dnshandlers[channel] + remove.append(channel) h.ok = False + for channel in remove: + del dnshandlers[channel] if udphandlers: - for channel, h in list(udphandlers.items()): + remove = [] + for channel, h in udphandlers.items(): if not h.ok: debug3('expiring UDP channel=%d\n' % channel) - del udphandlers[channel] + remove.append(channel) h.ok = False + for channel in remove: + del udphandlers[channel] diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index bd2228c..7691c80 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -89,10 +89,10 @@ def connect(ssh_cmd, rhostport, python, stderr, options): b"\n") pyscript = r""" - import sys, os; + import sys; verbosity=%d; - sys.stdin = os.fdopen(0, "rb"); - exec(compile(sys.stdin.read(%d), "assembler.py", "exec")) + stdin=getattr(sys.stdin,"buffer",sys.stdin); + exec(compile(stdin.read(%d), "assembler.py", "exec")) """ % (helpers.verbose or 0, len(content)) pyscript = re.sub(r'\s+', ' ', pyscript.strip()) @@ -108,7 +108,7 @@ def connect(ssh_cmd, rhostport, python, stderr, options): if python: pycmd = "'%s' -c '%s'" % (python, pyscript) else: - pycmd = ("P=python2; $P -V 2>/dev/null || P=python; " + pycmd = ("P=python3.5; $P -V 2>/dev/null || P=python; " "exec \"$P\" -c '%s'") % pyscript argv = (sshl + portl + diff --git a/sshuttle/sshuttle.md b/sshuttle/sshuttle.md index 8f772a1..82eb580 100644 --- a/sshuttle/sshuttle.md +++ b/sshuttle/sshuttle.md @@ -227,7 +227,7 @@ conflicts between client and server. Unlike most VPNs, sshuttle forwards sessions, not packets. That is, it uses kernel transparent proxying (`iptables -REDIRECT` rules on Linux, or `ipfw fwd` rules on BSD) to +REDIRECT` rules on Linux) to capture outgoing TCP sessions, then creates entirely separate TCP sessions out to the original destination at the other end of the tunnel. @@ -256,24 +256,6 @@ between the two separate streams, so a tcp-based tunnel is fine. -# BUGS - -On MacOS 10.6 (at least up to 10.6.6), your network will -stop responding about 10 minutes after the first time you -start sshuttle, because of a MacOS kernel bug relating to -arp and the net.inet.ip.scopedroute sysctl. To fix it, -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. - -On MacOS, sshuttle will set the kernel boot flag -net.inet.ip.scopedroute to 0, which interferes with OS X -Internet Sharing and some VPN clients. To reset this flag, -you can remove any reference to net.inet.ip.scopedroute from -/Library/Preferences/SystemConfiguration/com.apple.Boot.plist -and reboot. - - # SEE ALSO `ssh`(1), `python`(1) diff --git a/sshuttle/tests/test_firewall.py b/sshuttle/tests/test_firewall.py index 540a5bf..fb4ba40 100644 --- a/sshuttle/tests/test_firewall.py +++ b/sshuttle/tests/test_firewall.py @@ -1,9 +1,5 @@ from mock import Mock, patch, call import io -import os -import os.path -import shutil -import filecmp import sshuttle.firewall @@ -19,27 +15,27 @@ NSLIST 10,2404:6800:4004:80c::33 PORTS 1024,1025,1026,1027 GO 1 +HOST 1.2.3.3,existing """) stdout = Mock() return stdin, stdout -@patch('sshuttle.firewall.HOSTSFILE', new='tmp/hosts') -@patch('sshuttle.firewall.hostmap', new={ - 'myhost': '1.2.3.4', - 'myotherhost': '1.2.3.5', -}) -def test_rewrite_etc_hosts(): - if not os.path.isdir("tmp"): - os.mkdir("tmp") +def test_rewrite_etc_hosts(tmpdir): + orig_hosts = tmpdir.join("hosts.orig") + orig_hosts.write("1.2.3.3 existing\n") - with open("tmp/hosts.orig", "w") as f: - f.write("1.2.3.3 existing\n") + new_hosts = tmpdir.join("hosts") + orig_hosts.copy(new_hosts) - shutil.copyfile("tmp/hosts.orig", "tmp/hosts") + hostmap = { + 'myhost': '1.2.3.4', + 'myotherhost': '1.2.3.5', + } + with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)): + sshuttle.firewall.rewrite_etc_hosts(hostmap, 10) - sshuttle.firewall.rewrite_etc_hosts(10) - with open("tmp/hosts") as f: + with new_hosts.open() as f: line = f.readline() s = line.split() assert s == ['1.2.3.3', 'existing'] @@ -57,39 +53,37 @@ def test_rewrite_etc_hosts(): line = f.readline() assert line == "" - sshuttle.firewall.restore_etc_hosts(10) - assert filecmp.cmp("tmp/hosts.orig", "tmp/hosts", shallow=False) is True + with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)): + sshuttle.firewall.restore_etc_hosts(10) + assert orig_hosts.computehash() == new_hosts.computehash() -@patch('sshuttle.firewall.HOSTSFILE', new='tmp/hosts') +@patch('sshuttle.firewall.rewrite_etc_hosts') @patch('sshuttle.firewall.setup_daemon') @patch('sshuttle.firewall.get_method') -def test_main(mock_get_method, mock_setup_daemon): +def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts): stdin, stdout = setup_daemon() mock_setup_daemon.return_value = stdin, stdout - if not os.path.isdir("tmp"): - os.mkdir("tmp") + mock_get_method("not_auto").name = "test" + mock_get_method.reset_mock() - sshuttle.firewall.main("test", False) + sshuttle.firewall.main("not_auto", False) - with open("tmp/hosts") as f: - line = f.readline() - s = line.split() - assert s == ['1.2.3.3', 'existing'] + assert mock_rewrite_etc_hosts.mock_calls == [ + call({'1.2.3.3': 'existing'}, 1024), + call({}, 1024), + ] - line = f.readline() - assert line == "" - - stdout.mock_calls == [ + assert stdout.mock_calls == [ call.write('READY test\n'), call.flush(), call.write('STARTED\n'), call.flush() ] - mock_setup_daemon.mock_calls == [call()] - mock_get_method.mock_calls == [ - call('test'), + assert mock_setup_daemon.mock_calls == [call()] + assert mock_get_method.mock_calls == [ + call('not_auto'), call().setup_firewall( 1024, 1026, [(10, u'2404:6800:4004:80c::33')], @@ -103,7 +97,6 @@ def test_main(mock_get_method, mock_setup_daemon): 2, [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')], True), - call().setup_firewall()(), - call().setup_firewall(1024, 0, [], 10, [], True), - call().setup_firewall(1025, 0, [], 2, [], True), + call().restore_firewall(1024, 10, True), + call().restore_firewall(1025, 2, True), ] diff --git a/sshuttle/tests/test_helpers.py b/sshuttle/tests/test_helpers.py index 3017b19..67c6682 100644 --- a/sshuttle/tests/test_helpers.py +++ b/sshuttle/tests/test_helpers.py @@ -11,12 +11,32 @@ import sshuttle.helpers @patch('sshuttle.helpers.sys.stderr') def test_log(mock_stderr, mock_stdout): sshuttle.helpers.log("message") + sshuttle.helpers.log("abc") + sshuttle.helpers.log("message 1\n") + sshuttle.helpers.log("message 2\nline2\nline3\n") + sshuttle.helpers.log("message 3\nline2\nline3") assert mock_stdout.mock_calls == [ call.flush(), + call.flush(), + call.flush(), + call.flush(), + call.flush(), ] assert mock_stderr.mock_calls == [ call.write('prefix: message'), call.flush(), + call.write('prefix: abc'), + call.flush(), + call.write('prefix: message 1\n'), + call.flush(), + call.write('prefix: message 2\n'), + call.write('---> line2\n'), + call.write('---> line3\n'), + call.flush(), + call.write('prefix: message 3\n'), + call.write('---> line2\n'), + call.write('---> line3\n'), + call.flush(), ] diff --git a/sshuttle/tests/test_methods_nat.py b/sshuttle/tests/test_methods_nat.py index 433c709..2144e25 100644 --- a/sshuttle/tests/test_methods_nat.py +++ b/sshuttle/tests/test_methods_nat.py @@ -3,6 +3,7 @@ from mock import Mock, patch, call import socket import struct +from sshuttle.helpers import Fatal from sshuttle.methods import get_method @@ -11,6 +12,7 @@ def test_get_supported_features(): features = method.get_supported_features() assert not features.ipv6 assert not features.udp + assert features.dns def test_get_tcp_dstip(): @@ -52,10 +54,18 @@ def test_setup_udp_listener(): assert listener.mock_calls == [] -def test_check_settings(): +def test_assert_features(): method = get_method('nat') - method.check_settings(True, True) - method.check_settings(False, True) + features = method.get_supported_features() + method.assert_features(features) + + features.udp = True + with pytest.raises(Fatal): + method.assert_features(features) + + features.ipv6 = True + with pytest.raises(Fatal): + method.assert_features(features) def test_firewall_command(): @@ -129,7 +139,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() - method.setup_firewall(1025, 0, [], 2, [], False) + method.restore_firewall(1025, 2, False) assert mock_ipt_chain_exists.mock_calls == [ call(2, 'nat', 'sshuttle-1025') ] diff --git a/sshuttle/tests/test_methods_pf.py b/sshuttle/tests/test_methods_pf.py index 5d9e759..30d10fa 100644 --- a/sshuttle/tests/test_methods_pf.py +++ b/sshuttle/tests/test_methods_pf.py @@ -3,6 +3,8 @@ from mock import Mock, patch, call, ANY import socket from sshuttle.methods import get_method +from sshuttle.helpers import Fatal +from sshuttle.methods.pf import OsDefs def test_get_supported_features(): @@ -10,8 +12,10 @@ def test_get_supported_features(): features = method.get_supported_features() assert not features.ipv6 assert not features.udp + assert features.dns +@patch('sshuttle.helpers.verbose', new=3) def test_get_tcp_dstip(): sock = Mock() sock.getpeername.return_value = ("127.0.0.1", 1024) @@ -20,7 +24,7 @@ def test_get_tcp_dstip(): firewall = Mock() firewall.pfile.readline.return_value = \ - "QUERY_PF_NAT_SUCCESS 127.0.0.3,1026\n" + b"QUERY_PF_NAT_SUCCESS 127.0.0.3,1026\n" method = get_method('pf') method.set_firewall(firewall) @@ -31,7 +35,7 @@ def test_get_tcp_dstip(): call.getsockname(), ] assert firewall.mock_calls == [ - call.pfile.write('QUERY_PF_NAT 2,6,127.0.0.1,1024,127.0.0.2,1025\n'), + call.pfile.write(b'QUERY_PF_NAT 2,6,127.0.0.1,1024,127.0.0.2,1025\n'), call.pfile.flush(), call.pfile.readline() ] @@ -67,16 +71,25 @@ def test_setup_udp_listener(): assert listener.mock_calls == [] -def test_check_settings(): +def test_assert_features(): method = get_method('pf') - method.check_settings(True, True) - method.check_settings(False, True) + features = method.get_supported_features() + method.assert_features(features) + + features.udp = True + with pytest.raises(Fatal): + method.assert_features(features) + + features.ipv6 = True + with pytest.raises(Fatal): + method.assert_features(features) +@patch('sshuttle.methods.pf.osdefs', OsDefs('darwin')) @patch('sshuttle.methods.pf.sys.stdout') @patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.pf_get_dev') -def test_firewall_command(mock_pf_get_dev, mock_ioctl, mock_stdout): +def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout): method = get_method('pf') assert not method.firewall_command("somthing") @@ -87,7 +100,7 @@ def test_firewall_command(mock_pf_get_dev, mock_ioctl, mock_stdout): assert mock_pf_get_dev.mock_calls == [call()] assert mock_ioctl.mock_calls == [ - call(mock_pf_get_dev(), 3226747927, ANY), + call(mock_pf_get_dev(), 0xc0544417, ANY), ] assert mock_stdout.mock_calls == [ call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'), @@ -95,13 +108,46 @@ def test_firewall_command(mock_pf_get_dev, mock_ioctl, mock_stdout): ] -# FIXME - test fails with platform=='darwin' due re.search not liking Mock -# objects. -@patch('sshuttle.methods.pf.sys.platform', 'not_darwin') +@patch('sshuttle.methods.pf.osdefs', OsDefs('notdarwin')) +@patch('sshuttle.methods.pf.sys.stdout') +@patch('sshuttle.methods.pf.ioctl') +@patch('sshuttle.methods.pf.pf_get_dev') +def test_firewall_command_notdarwin(mock_pf_get_dev, mock_ioctl, mock_stdout): + method = get_method('pf') + assert not method.firewall_command("somthing") + + command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % ( + socket.AF_INET, socket.IPPROTO_TCP, + "127.0.0.1", 1025, "127.0.0.2", 1024) + assert method.firewall_command(command) + + assert mock_pf_get_dev.mock_calls == [call()] + assert mock_ioctl.mock_calls == [ + call(mock_pf_get_dev(), 0xc04c4417, ANY), + ] + assert mock_stdout.mock_calls == [ + call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'), + call.flush(), + ] + + +def pfctl(args, stdin=None): + if args == '-s all': + return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n', + b'little lamb\n') + if args == '-E': + return (b'\n', b'Token : abcdefg\n') + return None + + +@patch('sshuttle.helpers.verbose', new=3) +@patch('sshuttle.methods.pf.osdefs', OsDefs('darwin')) @patch('sshuttle.methods.pf.pfctl') @patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.pf_get_dev') -def test_setup_firewall(mock_pf_get_dev, mock_ioctl, mock_pfctl): +def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): + mock_pfctl.side_effect = pfctl + method = get_method('pf') assert method.name == 'pf' @@ -138,23 +184,119 @@ def test_setup_firewall(mock_pf_get_dev, mock_ioctl, mock_pfctl): [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')], False) assert mock_ioctl.mock_calls == [ - call(mock_pf_get_dev(), 3295691827, ANY), - call(mock_pf_get_dev(), 3424666650, ANY), - call(mock_pf_get_dev(), 3424666650, ANY), - call(mock_pf_get_dev(), 3295691827, ANY), - call(mock_pf_get_dev(), 3424666650, ANY), - call(mock_pf_get_dev(), 3424666650, ANY), + call(mock_pf_get_dev(), 0xC4704433, ANY), + call(mock_pf_get_dev(), 0xCC20441A, ANY), + call(mock_pf_get_dev(), 0xCC20441A, ANY), + call(mock_pf_get_dev(), 0xC4704433, ANY), + call(mock_pf_get_dev(), 0xCC20441A, ANY), + call(mock_pf_get_dev(), 0xCC20441A, ANY), + ] + assert mock_pfctl.mock_calls == [ + call('-s all'), + call('-a sshuttle -f /dev/stdin', + b'table {!1.2.3.66/32,1.2.3.0/24}\n' + b'table {1.2.3.33}\n' + b'rdr pass on lo0 proto tcp ' + b'to -> 127.0.0.1 port 1025\n' + b'rdr pass on lo0 proto udp ' + b'to port 53 -> 127.0.0.1 port 1027\n' + b'pass out route-to lo0 inet proto tcp ' + b'to keep state\n' + b'pass out route-to lo0 inet proto udp ' + b'to port 53 keep state\n'), + call('-E'), ] - # FIXME - needs more work - # print(mock_pfctl.mock_calls) - # assert mock_pfctl.mock_calls == [] mock_pf_get_dev.reset_mock() mock_ioctl.reset_mock() mock_pfctl.reset_mock() - method.setup_firewall(1025, 0, [], 2, [], False) + method.restore_firewall(1025, 2, False) assert mock_ioctl.mock_calls == [] - assert mock_pfctl.mock_calls == [call('-a sshuttle -F all')] + assert mock_pfctl.mock_calls == [ + call('-a sshuttle -F all'), + call("-X abcdefg"), + ] + mock_pf_get_dev.reset_mock() + mock_pfctl.reset_mock() + mock_ioctl.reset_mock() + + +@patch('sshuttle.helpers.verbose', new=3) +@patch('sshuttle.methods.pf.osdefs', OsDefs('notdarwin')) +@patch('sshuttle.methods.pf.pfctl') +@patch('sshuttle.methods.pf.ioctl') +@patch('sshuttle.methods.pf.pf_get_dev') +def test_setup_firewall_notdarwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): + mock_pfctl.side_effect = pfctl + + method = get_method('pf') + assert method.name == 'pf' + + with pytest.raises(Exception) as excinfo: + method.setup_firewall( + 1024, 1026, + [(10, u'2404:6800:4004:80c::33')], + 10, + [(10, 64, False, u'2404:6800:4004:80c::'), + (10, 128, True, u'2404:6800:4004:80c::101f')], + True) + assert str(excinfo.value) \ + == 'Address family "AF_INET6" unsupported by pf method_name' + assert mock_pf_get_dev.mock_calls == [] + assert mock_ioctl.mock_calls == [] + assert mock_pfctl.mock_calls == [] + + with pytest.raises(Exception) as excinfo: + method.setup_firewall( + 1025, 1027, + [(2, u'1.2.3.33')], + 2, + [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')], + True) + assert str(excinfo.value) == 'UDP not supported by pf method_name' + assert mock_pf_get_dev.mock_calls == [] + assert mock_ioctl.mock_calls == [] + assert mock_pfctl.mock_calls == [] + + method.setup_firewall( + 1025, 1027, + [(2, u'1.2.3.33')], + 2, + [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')], + False) + assert mock_ioctl.mock_calls == [ + call(mock_pf_get_dev(), 0xC4704433, ANY), + call(mock_pf_get_dev(), 0xCBE0441A, ANY), + call(mock_pf_get_dev(), 0xCBE0441A, ANY), + call(mock_pf_get_dev(), 0xC4704433, ANY), + call(mock_pf_get_dev(), 0xCBE0441A, ANY), + call(mock_pf_get_dev(), 0xCBE0441A, ANY), + ] + assert mock_pfctl.mock_calls == [ + call('-s all'), + call('-a sshuttle -f /dev/stdin', + b'table {!1.2.3.66/32,1.2.3.0/24}\n' + b'table {1.2.3.33}\n' + b'rdr pass on lo0 proto tcp ' + b'to -> 127.0.0.1 port 1025\n' + b'rdr pass on lo0 proto udp ' + b'to port 53 -> 127.0.0.1 port 1027\n' + b'pass out route-to lo0 inet proto tcp ' + b'to keep state\n' + b'pass out route-to lo0 inet proto udp ' + b'to port 53 keep state\n'), + call('-e'), + ] + mock_pf_get_dev.reset_mock() + mock_ioctl.reset_mock() + mock_pfctl.reset_mock() + + method.restore_firewall(1025, 2, False) + assert mock_ioctl.mock_calls == [] + assert mock_pfctl.mock_calls == [ + call('-a sshuttle -F all'), + call("-d"), + ] mock_pf_get_dev.reset_mock() mock_pfctl.reset_mock() mock_ioctl.reset_mock() diff --git a/sshuttle/tests/test_methods_tproxy.py b/sshuttle/tests/test_methods_tproxy.py index 82efed5..3401958 100644 --- a/sshuttle/tests/test_methods_tproxy.py +++ b/sshuttle/tests/test_methods_tproxy.py @@ -3,11 +3,22 @@ from mock import Mock, patch, call from sshuttle.methods import get_method -def test_get_supported_features(): +@patch("sshuttle.methods.tproxy.recvmsg") +def test_get_supported_features_recvmsg(mock_recvmsg): method = get_method('tproxy') features = method.get_supported_features() assert features.ipv6 assert features.udp + assert features.dns + + +@patch("sshuttle.methods.tproxy.recvmsg", None) +def test_get_supported_features_norecvmsg(): + method = get_method('tproxy') + features = method.get_supported_features() + assert features.ipv6 + assert not features.udp + assert not features.dns def test_get_tcp_dstip(): @@ -66,10 +77,10 @@ def test_setup_udp_listener(): ] -def test_check_settings(): +def test_assert_features(): method = get_method('tproxy') - method.check_settings(True, True) - method.check_settings(False, True) + features = method.get_supported_features() + method.assert_features(features) def test_firewall_command(): @@ -160,7 +171,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() - method.setup_firewall(1025, 0, [], 10, [], True) + method.restore_firewall(1025, 10, True) assert mock_ipt_chain_exists.mock_calls == [ call(10, 'mangle', 'sshuttle-m-1025'), call(10, 'mangle', 'sshuttle-t-1025'), @@ -250,7 +261,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() - method.setup_firewall(1025, 0, [], 2, [], True) + method.restore_firewall(1025, 2, True) assert mock_ipt_chain_exists.mock_calls == [ call(2, 'mangle', 'sshuttle-m-1025'), call(2, 'mangle', 'sshuttle-t-1025'), diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e2e93f8 --- /dev/null +++ b/tox.ini @@ -0,0 +1,16 @@ +[tox] +downloadcache = {toxworkdir}/cache/ +envlist = + py27, + py35, + +[testenv] +basepython = + py27: python2.7 + py35: python3.5 +commands = + py.test +deps = + pytest + mock + setuptools>=17.1