diff --git a/sshuttle/__main__.py b/sshuttle/__main__.py index 6664abb..db7965f 100644 --- a/sshuttle/__main__.py +++ b/sshuttle/__main__.py @@ -149,13 +149,10 @@ helpers.verbose = opt.verbose try: if opt.firewall: - if len(extra) != 6: - o.fatal('exactly six arguments expected') - port, dnsport = int(extra[0]), int(extra[1]) - nslist = [family_ip_tuple(ns) for ns in parse_list(opt.ns_hosts)] - sys.exit(firewall.main(int(extra[0]), int(extra[1]), - int(extra[2]), int(extra[3]), nslist, - extra[4], int(extra[5]), opt.syslog)) + if len(extra) != 0: + o.fatal('exactly zero arguments expected') + result = firewall.main(opt.method, opt.syslog) + sys.exit(result) elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: @@ -183,23 +180,20 @@ try: if opt.subnets: includes = parse_subnet_file(opt.subnets) if not opt.method: - method = "auto" + method_name = "auto" elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]: - method = opt.method + method_name = opt.method else: - o.fatal("method %s not supported" % opt.method) + o.fatal("method_name %s not supported" % opt.method) if not opt.listen: - if opt.method == "tproxy": - ipport_v6 = parse_ipport6('[::1]:0') - else: - ipport_v6 = None - ipport_v4 = parse_ipport4('127.0.0.1:0') + ipport_v6 = "auto" # parse_ipport6('[::1]:0') + ipport_v4 = "auto" # 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 and opt.method == "tproxy": + if '[' in ip and ']' in ip: ipport_v6 = parse_ipport6(ip) else: ipport_v4 = parse_ipport4(ip) @@ -210,7 +204,7 @@ try: opt.latency_control, opt.dns, nslist, - method, + method_name, sh, opt.auto_nets, parse_subnets(includes), diff --git a/sshuttle/client.py b/sshuttle/client.py index c623844..492a529 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -1,4 +1,4 @@ -import struct +import socket import errno import re import signal @@ -13,23 +13,7 @@ import sys from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \ resolvconf_nameservers - -recvmsg = None -try: - # 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 +from sshuttle.methods import get_method _extra_fd = os.open('/dev/null', os.O_RDONLY) @@ -40,83 +24,6 @@ def got_signal(signum, frame): _pidname = None -IP_TRANSPARENT = 19 -IP_ORIGDSTADDR = 20 -IP_RECVORIGDSTADDR = IP_ORIGDSTADDR -SOL_IPV6 = 41 -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[0]) -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): @@ -185,40 +92,7 @@ def daemon_cleanup(): else: raise -pf_command_file = None - - -def pf_dst(sock): - peer = sock.getpeername() - proxy = sock.getsockname() - - argv = (sock.family, socket.IPPROTO_TCP, - peer[0], peer[1], proxy[0], proxy[1]) - pf_command_file.write("QUERY_PF_NAT %r,%r,%s,%r,%s,%r\n" % argv) - pf_command_file.flush() - line = pf_command_file.readline() - debug2("QUERY_PF_NAT %r,%r,%s,%r,%s,%r" % argv + ' > ' + line) - if line.startswith('QUERY_PF_NAT_SUCCESS '): - (ip, port) = line[21:].split(',') - return (ip, int(port)) - - return sock.getsockname() - - -def original_dst(sock): - try: - SO_ORIGINAL_DST = 80 - SOCKADDR_MIN = 16 - sockaddr_in = sock.getsockopt(socket.SOL_IP, - SO_ORIGINAL_DST, SOCKADDR_MIN) - (proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) - assert(socket.htons(proto) == socket.AF_INET) - ip = '%d.%d.%d.%d' % (a, b, c, d) - return (ip, port) - except socket.error as e: - if e.args[0] == errno.ENOPROTOOPT: - return sock.getsockname() - raise +firewall = None class MultiListener: @@ -280,24 +154,18 @@ class MultiListener: class FirewallClient: - def __init__(self, port_v6, port_v4, subnets_include, subnets_exclude, - dnsport_v6, dnsport_v4, nslist, method, udp): + def __init__(self, method_name): self.auto_nets = [] - self.subnets_include = subnets_include - self.subnets_exclude = subnets_exclude python_path = os.path.dirname(os.path.dirname(__file__)) - argvbase = (["PYTHONPATH=%s" % python_path] + - [sys.executable, sys.argv[0]] + + argvbase = ([sys.executable, sys.argv[0]] + ['-v'] * (helpers.verbose or 0) + - ['--firewall', str(port_v6), str(port_v4), - str(dnsport_v6), str(dnsport_v4), - method, str(int(udp))]) - if dnsport_v4 or dnsport_v6: - argvbase += ['--ns-hosts', ' '.join([ip for _, ip in nslist])] + ['--method', method_name] + + ['--firewall']) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ - ['sudo', '-p', '[local sudo] Password: '] + argvbase, + ['sudo', '-p', '[local sudo] Password: ', + ('PYTHONPATH=%s' % python_path), '--'] + argvbase, argvbase ] @@ -337,7 +205,19 @@ class FirewallClient: self.check() if line[0:5] != b'READY': raise Fatal('%r expected READY, got %r' % (self.argv, line)) - self.method = line[6:-1] + method_name = line[6:-1] + self.method = get_method(method_name.decode("ASCII")) + + def setup(self, subnets_include, subnets_exclude, nslist, + redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp): + self.subnets_include = subnets_include + self.subnets_exclude = subnets_exclude + self.nslist = nslist + self.redirectport_v6 = redirectport_v6 + self.redirectport_v4 = redirectport_v4 + self.dnsport_v6 = dnsport_v6 + self.dnsport_v4 = dnsport_v4 + self.udp = udp def check(self): rv = self.p.poll() @@ -346,18 +226,30 @@ class FirewallClient: def start(self): self.pfile.write(b'ROUTES\n') - try: - for (family, ip, width) in self.subnets_include + self.auto_nets: - self.pfile.write(b'%d,%d,0,%s\n' - % (family, width, ip.encode("ASCII"))) - for (family, ip, width) in self.subnets_exclude: - self.pfile.write(b'%d,%d,1,%s\n' - % (family, width, ip.encode("ASCII"))) - except Exception as e: - debug1("exception occured %r" % e) - raise - self.pfile.write(b'GO\n') + for (family, ip, width) in self.subnets_include + self.auto_nets: + self.pfile.write(b'%d,%d,0,%s\n' + % (family, width, ip.encode("ASCII"))) + for (family, ip, width) in self.subnets_exclude: + self.pfile.write(b'%d,%d,1,%s\n' + % (family, width, ip.encode("ASCII"))) + + self.pfile.write(b'NSLIST\n') + for (family, ip) in self.nslist: + self.pfile.write(b'%d,%s\n' + % (family, ip.encode("ASCII"))) + + self.pfile.write( + b'PORTS %d,%d,%d,%d\n' + % (self.redirectport_v6, self.redirectport_v4, + self.dnsport_v6, self.dnsport_v4)) + + udp = 0 + if self.udp: + udp = 1 + + self.pfile.write(b'GO %d\n' % udp) self.pfile.flush() + line = self.pfile.readline() self.check() if line != b'STARTED\n': @@ -413,12 +305,8 @@ def onaccept_tcp(listener, method, mux, handlers): return else: raise - if method == b"tproxy": - dstip = sock.getsockname() - elif method == b"pf": - dstip = pf_dst(sock) - else: - dstip = original_dst(sock) + + dstip = method.get_tcp_dstip(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): @@ -437,37 +325,26 @@ def onaccept_tcp(listener, method, mux, handlers): expire_connections(time.time(), mux) -def udp_done(chan, data, method, family, dstip): +def udp_done(chan, data, method, sock, dstip): (src, srcport, data) = data.split(",", 2) srcip = (src, int(srcport)) debug3('doing send from %r to %r\n' % (srcip, dstip,)) - - 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 as e: - debug1('-- ignored socket error sending UDP data: %r\n' % e) + method.send_udp(sock, srcip, dstip, data) def onaccept_udp(listener, method, mux, handlers): now = time.time() - srcip, dstip, data = recv_udp(listener, 4096) - if not dstip: - debug1( - "-- ignored UDP from %r: " - "couldn't determine destination IP address\n" % (srcip,)) + t = method.recv_udp(listener, 4096) + if t is None: return + srcip, dstip, data = t 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, method, listener.family, dstip=srcip) + chan, data, method, listener, dstip=srcip) mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family) udp_by_src[srcip] = chan, now + 30 @@ -481,27 +358,15 @@ 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 == b"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) + method.send_udp(sock, srcip, dstip, data) def ondns(listener, method, mux, handlers): now = time.time() - srcip, dstip, data = recv_udp(listener, 4096) - if method == b"tproxy" and not dstip: - debug1( - "-- ignored UDP from %r: " - "couldn't determine destination IP address\n" % (srcip,)) + t = method.recv_udp(listener, 4096) + if t is None: return + srcip, dstip, data = t debug1('DNS request from %r to %r: %d bytes\n' % (srcip, dstip, len(data))) chan = mux.next_channel() dnsreqs[chan] = now + 30 @@ -513,8 +378,11 @@ def ondns(listener, method, mux, handlers): def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, - dns_listener, method, seed_hosts, auto_nets, + dns_listener, seed_hosts, auto_nets, syslog, daemon): + + method = fw.method + handlers = [] if helpers.verbose >= 1: helpers.logprefix = 'c : ' @@ -526,7 +394,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, (serverproc, serversock) = ssh.connect( ssh_cmd, remotename, python, stderr=ssyslog._p and ssyslog._p.stdin, - options=dict(latency_control=latency_control, method=method)) + options=dict(latency_control=latency_control)) except socket.error as e: if e.args[0] == errno.EPIPE: raise Fatal("failed to establish ssh session (1)") @@ -618,7 +486,7 @@ 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, seed_hosts, auto_nets, + method_name, seed_hosts, auto_nets, subnets_include, subnets_exclude, syslog, daemon, pidfile): if syslog: @@ -631,22 +499,20 @@ 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) + fw = FirewallClient(method_name) - if method == b"tproxy": - if recvmsg is not None: - debug1("tproxy UDP support enabled.\n") - udp = True + features = fw.method.get_supported_features() + if listenip_v6 == "auto": + if features.ipv6: + listenip_v6 = ('::1', 0) 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 + listenip_v6 = None + + 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 @@ -703,6 +569,7 @@ def main(listenip_v6, listenip_v4, last_e = e else: raise e + debug2('\n') if not bound: assert(last_e) @@ -716,6 +583,7 @@ def main(listenip_v6, listenip_v4, if dns or nslist: if dns: nslist += resolvconf_nameservers() + dns = True # search for spare port for DNS debug2('Binding DNS:') ports = range(12300, 9000, -1) @@ -756,35 +624,25 @@ def main(listenip_v6, listenip_v4, dnsport_v4 = 0 dns_listener = None - fw = FirewallClient(redirectport_v6, redirectport_v4, subnets_include, - subnets_exclude, dnsport_v6, dnsport_v4, nslist, - method, udp) + fw.method.check_settings(udp, dns) + fw.method.setup_tcp_listener(tcp_listener) + if udp_listener: + fw.method.setup_udp_listener(tcp_listener) + if dns_listener: + fw.method.setup_udp_listener(dns_listener) - if fw.method == b"tproxy": - tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) - if udp_listener: - udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 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) - 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) + fw.setup(subnets_include, subnets_exclude, nslist, + redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, + udp) - if fw.method == b"pf": - global pf_command_file - pf_command_file = fw.pfile + # kludge for PF method. + global firewall + firewall = fw try: return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, python, latency_control, dns_listener, - fw.method, seed_hosts, auto_nets, syslog, + seed_hosts, auto_nets, syslog, daemon) finally: try: diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py index 57571e5..3ccf1ab 100644 --- a/sshuttle/firewall.py +++ b/sshuttle/firewall.py @@ -1,549 +1,11 @@ import errno import socket -import select import signal -import struct -import subprocess as ssubprocess import sshuttle.ssyslog as ssyslog import sys import os -import re -from sshuttle.helpers import log, debug1, debug3, islocal, \ - Fatal, family_to_string -from fcntl import ioctl -from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \ - sizeof, addressof, memmove - - -# python doesn't have a definition for this -IPPROTO_DIVERT = 254 - - -def nonfatal(func, *args): - try: - func(*args) - except Fatal as e: - log('error: %s\n' % e) - - -def ipt_chain_exists(family, table, name): - if family == socket.AF_INET6: - cmd = 'ip6tables' - elif family == socket.AF_INET: - cmd = 'iptables' - else: - 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: - if line.startswith(b'Chain %s ' % name.encode("ASCII")): - return True - rv = p.wait() - if rv: - raise Fatal('%r returned %d' % (argv, rv)) - - -def _ipt(family, table, *args): - if family == socket.AF_INET6: - argv = ['ip6tables', '-t', table] + list(args) - elif family == socket.AF_INET: - argv = ['iptables', '-t', table] + list(args) - else: - raise Exception('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): - global _no_ttl_module - if not _no_ttl_module: - # we avoid infinite loops by generating server-side connections - # with ttl 42. This makes the client side not recapture those - # connections, in case client == server. - try: - argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42'] - _ipt(family, *argsplus) - except Fatal: - _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) - - -# We name the chain based on the transproxy port number so that it's possible -# to run multiple copies of sshuttle at the same time. Of course, the -# multiple copies shouldn't have overlapping subnets, or only the most- -# recently-started one will win (because we use "-I OUTPUT 1" instead of -# "-A OUTPUT"). -def do_iptables_nat(port, dnsport, nslist, family, subnets, udp): - # only ipv4 supported with NAT - if family != socket.AF_INET: - raise Exception( - 'Address family "%s" unsupported by nat method' - % family_to_string(family)) - if udp: - raise Exception("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, '-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)) - - -def do_iptables_tproxy(port, dnsport, nslist, family, subnets, 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) - ipt('-F', mark_chain) - ipt('-X', mark_chain) - - if ipt_chain_exists(family, table, 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('-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 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)) - - -def do_ipfw(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' - % family_to_string(family)) - if udp: - raise Exception("UDP not supported by ipfw method") - - 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 - - -def pfctl(args, stdin=None): - argv = ['pfctl'] + list(args.split(" ")) - debug1('>> %s\n' % ' '.join(argv)) - - p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE, - stdout=ssubprocess.PIPE, - stderr=ssubprocess.PIPE) - o = p.communicate(stdin) - if p.returncode: - raise Fatal('%r returned %d' % (argv, p.returncode)) - - return o - -_pf_context = {'started_by_sshuttle': False, 'Xtoken': ''} - - -def do_pf(port, dnsport, nslist, family, subnets, udp): - global _pf_started_by_sshuttle - tables = [] - translating_rules = [] - filtering_rules = [] - - if subnets: - 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)) - - tables.append('table {%s}' % ','.join(includes)) - translating_rules.append( - 'rdr pass on lo0 proto tcp ' - 'to -> 127.0.0.1 port %r' % port) - filtering_rules.append( - 'pass out route-to lo0 inet proto tcp ' - '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') - - rules = '\n'.join(tables + translating_rules + filtering_rules) + '\n' - - 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") - - 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') - - -def program_exists(name): - paths = (os.getenv('PATH') or os.defpath).split(os.pathsep) - for p in paths: - fn = '%s/%s' % (p, name) - if os.path.exists(fn): - return not os.path.isdir(fn) and os.access(fn, os.X_OK) - +from sshuttle.helpers import debug1, debug2, Fatal +from sshuttle.methods import get_auto_method, get_method hostmap = {} @@ -589,122 +51,6 @@ def restore_etc_hosts(port): rewrite_etc_hosts(port) -# 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_fd = None - - -def pf_get_dev(): - global _pf_fd - if _pf_fd is None: - _pf_fd = os.open('/dev/pf', os.O_RDWR) - - return _pf_fd - - -def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port): - [proto, family, src_port, dst_port] = [ - int(v) for v in [proto, family, src_port, dst_port]] - - length = 4 if family == socket.AF_INET else 16 - - pnl = pfioc_natlook() - pnl.proto = proto - pnl.direction = PF_OUT - pnl.af = family - memmove(addressof(pnl.saddr), socket.inet_pton(pnl.af, src_ip), length) - pnl.sxport.port = socket.htons(src_port) - memmove(addressof(pnl.daddr), socket.inet_pton(pnl.af, dst_ip), length) - pnl.dxport.port = socket.htons(dst_port) - - ioctl(pf_get_dev(), DIOCNATLOOK, ( - c_char * sizeof(pnl)).from_address(addressof(pnl))) - - ip = socket.inet_ntop( - pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr))) - port = socket.ntohs(pnl.rdxport.port) - return (ip, port) - - -def pf_add_anchor_rule(type, name): - ACTION_OFFSET = 0 - POOL_TICKET_OFFSET = 8 - ANCHOR_CALL_OFFSET = 1040 - RULE_ACTION_OFFSET = 3068 - - pr = pfioc_rule() - ppa = pfioc_pooladdr() - - ioctl(pf_get_dev(), 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 - 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) - - memmove(addressof(pr) + ACTION_OFFSET, struct.pack( - 'I', PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL - ioctl(pf_get_dev(), DIOCCHANGERULE, pr) - - # This is some voodoo for setting up the kernel's transparent # proxying stuff. If subnets is empty, we just delete our sshuttle rules; # otherwise we delete it, then make them from scratch. @@ -713,42 +59,14 @@ def pf_add_anchor_rule(type, name): # 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, - nslist, method, udp, 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) - +def main(method_name, syslog): if os.getuid() != 0: raise Fatal('you must be root (or enable su/sudo) to set the firewall') - if method == "auto": - if program_exists('ipfw'): - method = "ipfw" - elif program_exists('iptables'): - method = "nat" - elif program_exists('pfctl'): - method = "pf" - else: - raise Fatal( - "can't find either ipfw, iptables or pfctl; check your PATH") - - if method == "nat": - do_it = do_iptables_nat - elif method == "tproxy": - do_it = do_iptables_tproxy - elif method == "ipfw": - do_it = do_ipfw - elif method == "pf": - do_it = do_pf + if method_name == "auto": + method = get_auto_method() else: - raise Exception('Unknown method "%s"' % method) + method = get_method(method_name) # because of limitations of the 'su' command, the *real* stdin/stdout # are both attached to stdout initially. Clone stdout into stdin so we @@ -759,8 +77,8 @@ def main(port_v6, port_v4, ssyslog.start_syslog() ssyslog.stderr_to_syslog() - debug1('firewall manager ready method %s.\n' % method) - sys.stdout.write('READY %s\n' % method) + debug1('firewall manager ready method name %s.\n' % method.name) + sys.stdout.write('READY %s\n' % method.name) sys.stdout.flush() # don't disappear if our controlling terminal or stdout/stderr @@ -788,35 +106,86 @@ def main(port_v6, port_v4, line = sys.stdin.readline(128) if not line: raise Fatal('firewall: expected route but got %r' % line) - elif line == 'GO\n': + elif line.startswith("NSLIST\n"): break try: (family, width, exclude, ip) = line.strip().split(',', 3) except: - raise Fatal('firewall: expected route or GO but got %r' % line) + 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) + + nslist = [] + if line != 'NSLIST\n': + raise Fatal('firewall: expected NSLIST but got %r' % line) + while 1: + line = sys.stdin.readline(128) + if not line: + raise Fatal('firewall: expected nslist but got %r' % line) + elif line.startswith("PORTS "): + break + try: + (family, ip) = line.strip().split(',', 1) + 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) + + if not line.startswith('PORTS '): + raise Fatal('firewall: expected PORTS but got %r' % line) + _, _, ports = line.partition(" ") + ports = ports.split(",") + if len(ports) != 4: + raise Fatal('firewall: expected 4 ports but got %n' % len(ports)) + port_v6 = int(ports[0]) + port_v4 = int(ports[1]) + dnsport_v6 = int(ports[2]) + dnsport_v4 = int(ports[3]) + + 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) + + debug2('Got ports: %d,%d,%d,%d\n' + % (port_v6, port_v4, dnsport_v6, dnsport_v4)) + + line = sys.stdin.readline(128) + if not line: + raise Fatal('firewall: expected GO but got %r' % line) + elif not line.startswith("GO "): + raise Fatal('firewall: expected GO but got %r' % line) + + _, _, udp = line.partition(" ") + udp = bool(int(udp)) + debug2('Got udp: %r\n' % udp) try: - if line: - debug1('firewall manager: starting transproxy.\n') + do_wait = None + debug1('firewall manager: starting transproxy.\n') - subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] - if port_v6: - do_wait = do_it( - port_v6, dnsport_v6, nslist, - socket.AF_INET6, subnets_v6, udp) - elif len(subnets_v6) > 0: - debug1("IPv6 subnets defined but IPv6 disabled\n") + subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] + if port_v6 > 0: + do_wait = method.setup_firewall( + port_v6, dnsport_v6, nslist, + socket.AF_INET6, subnets_v6, udp) + elif len(subnets_v6) > 0: + debug1("IPv6 subnets defined but IPv6 disabled\n") - subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET] - if port_v4: - do_wait = do_it( - port_v4, dnsport_v4, nslist, - socket.AF_INET, subnets_v4, udp) - elif len(subnets_v4) > 0: - debug1('IPv4 subnets defined but IPv4 disabled\n') + subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET] + if port_v4 > 0: + do_wait = method.setup_firewall( + port_v4, dnsport_v4, nslist, + socket.AF_INET, subnets_v4, udp) + elif len(subnets_v4) > 0: + debug1('IPv4 subnets defined but IPv4 disabled\n') - sys.stdout.write('STARTED\n') + sys.stdout.write('STARTED\n') try: sys.stdout.flush() @@ -836,16 +205,9 @@ def main(port_v6, port_v4, (name, ip) = line[5:].strip().split(',', 1) hostmap[name] = ip rewrite_etc_hosts(port_v6 or port_v4) - elif line.startswith('QUERY_PF_NAT '): - try: - dst = pf_query_nat(*(line[13:].split(','))) - sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst) - except IOError as e: - sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e) - - sys.stdout.flush() elif line: - raise Fatal('expected EOF, got %r' % line) + if not method.firewall_command(line): + raise Fatal('expected EOF, got %r' % line) else: break finally: @@ -854,7 +216,7 @@ def main(port_v6, port_v4, except: pass if port_v6: - do_it(port_v6, 0, [], socket.AF_INET6, [], udp) + method.setup_firewall(port_v6, 0, [], socket.AF_INET6, [], udp) if port_v4: - do_it(port_v4, 0, [], socket.AF_INET, [], udp) + method.setup_firewall(port_v4, 0, [], socket.AF_INET, [], udp) restore_etc_hosts(port_v6 or port_v4) diff --git a/sshuttle/linux.py b/sshuttle/linux.py new file mode 100644 index 0000000..f11172b --- /dev/null +++ b/sshuttle/linux.py @@ -0,0 +1,62 @@ +import socket +import subprocess as ssubprocess +from sshuttle.helpers import log, debug1, Fatal, family_to_string + + +def nonfatal(func, *args): + try: + func(*args) + except Fatal as e: + log('error: %s\n' % e) + + +def ipt_chain_exists(family, table, name): + if family == socket.AF_INET6: + cmd = 'ip6tables' + elif family == socket.AF_INET: + cmd = 'iptables' + else: + 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: + if line.startswith(b'Chain %s ' % name.encode("ASCII")): + return True + rv = p.wait() + if rv: + raise Fatal('%r returned %d' % (argv, rv)) + + +def ipt(family, table, *args): + if family == socket.AF_INET6: + argv = ['ip6tables', '-t', table] + list(args) + elif family == socket.AF_INET: + argv = ['iptables', '-t', table] + list(args) + else: + raise Exception('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): + global _no_ttl_module + if not _no_ttl_module: + # we avoid infinite loops by generating server-side connections + # with ttl 42. This makes the client side not recapture those + # connections, in case client == server. + try: + argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42'] + ipt(family, *argsplus) + except Fatal: + 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) diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py new file mode 100644 index 0000000..603b1cf --- /dev/null +++ b/sshuttle/methods/__init__.py @@ -0,0 +1,94 @@ +import os +import importlib +import socket +import struct +import errno +from sshuttle.helpers import Fatal, debug3 + + +def original_dst(sock): + try: + SO_ORIGINAL_DST = 80 + SOCKADDR_MIN = 16 + sockaddr_in = sock.getsockopt(socket.SOL_IP, + SO_ORIGINAL_DST, SOCKADDR_MIN) + (proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) + assert(socket.htons(proto) == socket.AF_INET) + ip = '%d.%d.%d.%d' % (a, b, c, d) + return (ip, port) + except socket.error as e: + if e.args[0] == errno.ENOPROTOOPT: + return sock.getsockname() + raise + + +class Features(object): + pass + + +class BaseMethod(object): + def __init__(self, name): + self.name = name + + def get_supported_features(self): + result = Features() + result.ipv6 = False + result.udp = False + return result + + def get_tcp_dstip(self, sock): + return original_dst(sock) + + def recv_udp(self, udp_listener, bufsize): + debug3('Accept UDP using recvfrom.\n') + data, srcip = udp_listener.recvfrom(bufsize) + return (srcip, None, data) + + def send_udp(self, sock, srcip, dstip, data): + if srcip is not None: + Fatal("Method %s send_udp does not support setting srcip to %r" + % (self.name, srcip)) + sock.sendto(data, dstip) + + def setup_tcp_listener(self, tcp_listener): + pass + + 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 setup_firewall(self, port, dnsport, nslist, family, subnets, udp): + raise NotImplementedError() + + def firewall_command(self, line): + return False + + +def _program_exists(name): + paths = (os.getenv('PATH') or os.defpath).split(os.pathsep) + for p in paths: + fn = '%s/%s' % (p, name) + if os.path.exists(fn): + return not os.path.isdir(fn) and os.access(fn, os.X_OK) + + +def get_method(method_name): + module = importlib.import_module("sshuttle.methods.%s" % method_name) + return module.Method(method_name) + + +def get_auto_method(): + if _program_exists('ipfw'): + method_name = "ipfw" + elif _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") + + return get_method(method_name) diff --git a/sshuttle/methods/ipfw.py b/sshuttle/methods/ipfw.py new file mode 100644 index 0000000..678d00f --- /dev/null +++ b/sshuttle/methods/ipfw.py @@ -0,0 +1,237 @@ +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 new file mode 100644 index 0000000..bf5ef5c --- /dev/null +++ b/sshuttle/methods/nat.py @@ -0,0 +1,70 @@ +import socket +from sshuttle.helpers import family_to_string +from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal +from sshuttle.methods import BaseMethod + + +class Method(BaseMethod): + + # We name the chain based on the transproxy port number so that it's + # possible to run multiple copies of sshuttle at the same time. Of course, + # the multiple copies shouldn't have overlapping subnets, or only the most- + # recently-started one will win (because we use "-I OUTPUT 1" instead of + # "-A OUTPUT"). + def setup_firewall(self, port, dnsport, nslist, family, subnets, 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 new file mode 100644 index 0000000..81a8e91 --- /dev/null +++ b/sshuttle/methods/pf.py @@ -0,0 +1,236 @@ +import os +import sys +import re +import socket +import struct +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 +from sshuttle.methods import BaseMethod + + +def pfctl(args, stdin=None): + argv = ['pfctl'] + list(args.split(" ")) + debug1('>> %s\n' % ' '.join(argv)) + + p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE, + stdout=ssubprocess.PIPE, + stderr=ssubprocess.PIPE) + o = p.communicate(stdin) + if p.returncode: + raise Fatal('%r returned %d' % (argv, p.returncode)) + + 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_fd = None + + +def pf_get_dev(): + global _pf_fd + if _pf_fd is None: + _pf_fd = os.open('/dev/pf', os.O_RDWR) + + return _pf_fd + + +def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port): + [proto, family, src_port, dst_port] = [ + int(v) for v in [proto, family, src_port, dst_port]] + + length = 4 if family == socket.AF_INET else 16 + + pnl = pfioc_natlook() + pnl.proto = proto + pnl.direction = PF_OUT + pnl.af = family + memmove(addressof(pnl.saddr), socket.inet_pton(pnl.af, src_ip), length) + pnl.sxport.port = socket.htons(src_port) + memmove(addressof(pnl.daddr), socket.inet_pton(pnl.af, dst_ip), length) + pnl.dxport.port = socket.htons(dst_port) + + ioctl(pf_get_dev(), DIOCNATLOOK, ( + c_char * sizeof(pnl)).from_address(addressof(pnl))) + + ip = socket.inet_ntop( + pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr))) + port = socket.ntohs(pnl.rdxport.port) + return (ip, port) + + +def pf_add_anchor_rule(type, name): + ACTION_OFFSET = 0 + POOL_TICKET_OFFSET = 8 + ANCHOR_CALL_OFFSET = 1040 + RULE_ACTION_OFFSET = 3068 + + pr = pfioc_rule() + ppa = pfioc_pooladdr() + + ioctl(pf_get_dev(), 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 + 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) + + memmove(addressof(pr) + ACTION_OFFSET, struct.pack( + 'I', PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL + ioctl(pf_get_dev(), DIOCCHANGERULE, pr) + + +class Method(BaseMethod): + + def get_tcp_dstip(self, sock): + # yuck + from sshuttle.client import firewall + + peer = sock.getpeername() + proxy = sock.getsockname() + + argv = (sock.family, socket.IPPROTO_TCP, + peer[0], peer[1], proxy[0], proxy[1]) + firewall.pfile.write("QUERY_PF_NAT %r,%r,%s,%r,%s,%r\n" % argv) + firewall.pfile.flush() + line = firewall.pfile.readline() + debug2("QUERY_PF_NAT %r,%r,%s,%r,%s,%r" % argv + ' > ' + line) + if line.startswith('QUERY_PF_NAT_SUCCESS '): + (ip, port) = line[21:].split(',') + return (ip, 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 = [] + + if subnets: + 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)) + + tables.append('table {%s}' % ','.join(includes)) + translating_rules.append( + 'rdr pass on lo0 proto tcp ' + 'to -> 127.0.0.1 port %r' % port) + filtering_rules.append( + 'pass out route-to lo0 inet proto tcp ' + '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') + + rules = '\n'.join(tables + translating_rules + filtering_rules) \ + + '\n' + + 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") + + 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') + + def firewall_command(self, line): + if line.startswith('QUERY_PF_NAT '): + try: + dst = pf_query_nat(*(line[13:].split(','))) + sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst) + except IOError as e: + sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e) + + sys.stdout.flush() + return True + else: + return False diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py new file mode 100644 index 0000000..4c02286 --- /dev/null +++ b/sshuttle/methods/tproxy.py @@ -0,0 +1,253 @@ +import struct +from sshuttle.helpers import family_to_string +from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists +from sshuttle.methods import BaseMethod +from sshuttle.helpers import debug1, debug3, Fatal + +recvmsg = None +try: + # 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 + + +IP_TRANSPARENT = 19 +IP_ORIGDSTADDR = 20 +IP_RECVORIGDSTADDR = IP_ORIGDSTADDR +SOL_IPV6 = 41 +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[0]) +else: + def recv_udp(listener, bufsize): + debug3('Accept UDP using recvfrom.\n') + data, srcip = listener.recvfrom(bufsize) + return (srcip, None, data) + + +class Method(BaseMethod): + + def get_supported_features(self): + result = super(Method, self).get_supported_features() + result.ipv6 = True + result.udp = True + return result + + def get_tcp_dstip(self, sock): + return sock.getsockname() + + def recv_udp(self, udp_listener, bufsize): + srcip, dstip, data = recv_udp(udp_listener, bufsize) + if not dstip: + debug1( + "-- ignored UDP from %r: " + "couldn't determine destination IP address\n" % (srcip,)) + return None + return None + + def send_udp(self, sock, srcip, dstip, data): + 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 setup_tcp_listener(self, tcp_listener): + tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) + + def setup_udp_listener(self, udp_listener): + udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 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) + + def setup_firewall(self, port, dnsport, nslist, family, subnets, 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) + _ipt('-F', mark_chain) + _ipt('-X', mark_chain) + + if ipt_chain_exists(family, table, 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('-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 dns: + debug1("tproxy UDP support enabled.\n") + + if udp: + debug1("tproxy DNS support enabled.\n")