diff --git a/sshuttle/client.py b/sshuttle/client.py index ffe598e..0711912 100644 --- a/sshuttle/client.py +++ b/sshuttle/client.py @@ -549,6 +549,7 @@ def main(listenip_v6, listenip_v4, listenip_v6 = None required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None + required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None required.udp = avail.udp required.dns = len(nslist) > 0 @@ -571,6 +572,14 @@ def main(listenip_v6, listenip_v4, if listenip_v4 == "auto": listenip_v4 = ('127.0.0.1', 0) + if required.ipv4 and \ + not any(listenip_v4[0] == sex[1] for sex in subnets_v4): + subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32)) + + if required.ipv6 and \ + not any(listenip_v6[0] == sex[1] for sex in subnets_v6): + subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128)) + 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, ] diff --git a/sshuttle/methods/pf.py b/sshuttle/methods/pf.py index bb82a2d..75d85e7 100644 --- a/sshuttle/methods/pf.py +++ b/sshuttle/methods/pf.py @@ -11,7 +11,7 @@ from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string from sshuttle.methods import BaseMethod -_pf_context = {'started_by_sshuttle': False, 'Xtoken': None} +_pf_context = {'started_by_sshuttle': False, 'Xtoken': []} _pf_fd = None @@ -121,18 +121,19 @@ class Generic(object): 'I', self.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL ioctl(pf_get_dev(), pf.DIOCCHANGERULE, pr) + def _inet_version(self, family): + return b'inet' if family == socket.AF_INET else b'inet6' + + def _lo_addr(self, family): + return b'127.0.0.1' if family == socket.AF_INET else b'::1' + def add_rules(self, anchor, rules): assert isinstance(rules, bytes) debug3("rules:\n" + rules.decode("ASCII")) pfctl('-a %s -f /dev/stdin' % anchor, rules) - def has_running_instances(self): - # This should cover most scenarios. - p = ssubprocess.Popen(['pgrep', '-f', 'python.*sshuttle'], - stdout=ssubprocess.PIPE, - stderr=ssubprocess.PIPE) - o, e = p.communicate() - return len(o.splitlines()) > 0 + def has_skip_loopback(self): + return b'skip' in pfctl('-s Interfaces -i lo -v')[0] @@ -178,17 +179,20 @@ class FreeBsd(Generic): memmove(addressof(pr) + self.POOL_TICKET_OFFSET, ppa[4:8], 4) super(FreeBsd, self)._add_anchor_rule(type, name, pr=pr) - def add_rules(self, anchor, includes, port, dnsport, nslist): + def add_rules(self, anchor, includes, port, dnsport, nslist, family): + inet_version = self._inet_version(family) + lo_addr = self._lo_addr(family) + tables = [ b'table {%s}' % b','.join(includes) ] translating_rules = [ - b'rdr pass on lo0 proto tcp ' - b'to -> 127.0.0.1 port %r' % port + b'rdr pass on lo0 %s proto tcp to ' + b'-> %s port %r' % (inet_version, lo_addr, port) ] filtering_rules = [ - b'pass out route-to lo0 inet proto tcp ' - b'to keep state' + b'pass out route-to lo0 %s proto tcp ' + b'to keep state' % inet_version ] if len(nslist) > 0: @@ -196,11 +200,11 @@ class FreeBsd(Generic): 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) + b'rdr pass on lo0 %s proto udp to ' + b'port 53 -> %s port %r' % (inet_version, lo_addr, dnsport)) filtering_rules.append( - b'pass out route-to lo0 inet proto udp to ' - b' port 53 keep state') + b'pass out route-to lo0 %s proto udp to ' + b' port 53 keep state' % inet_version) rules = b'\n'.join(tables + translating_rules + filtering_rules) \ + b'\n' @@ -239,21 +243,24 @@ class OpenBsd(Generic): # before adding anchors and rules we must override the skip lo # that comes by default in openbsd pf.conf so the rules we will add, # which rely on translating/filtering packets on lo, can work - if not self.has_running_instances(): + if self.has_skip_loopback(): pfctl('-f /dev/stdin', b'match on lo\n') super(OpenBsd, self).add_anchors(anchor) - def add_rules(self, anchor, includes, port, dnsport, nslist): + def add_rules(self, anchor, includes, port, dnsport, nslist, family): + inet_version = self._inet_version(family) + lo_addr = self._lo_addr(family) + tables = [ b'table {%s}' % b','.join(includes) ] translating_rules = [ - b'pass in on lo0 inet proto tcp ' - b'to divert-to 127.0.0.1 port %r' % port + b'pass in on lo0 %s proto tcp to ' + b'divert-to %s port %r' % (inet_version, lo_addr, port) ] filtering_rules = [ - b'pass out inet proto tcp ' - b'to route-to lo0 keep state' + b'pass out %s proto tcp to ' + b'route-to lo0 keep state' % inet_version ] if len(nslist) > 0: @@ -261,11 +268,11 @@ class OpenBsd(Generic): b'table {%s}' % b','.join([ns[1].encode("ASCII") for ns in nslist])) translating_rules.append( - b'pass in on lo0 inet proto udp to ' - b'port 53 rdr-to 127.0.0.1 port %r' % dnsport) + b'pass in on lo0 %s proto udp to port 53 ' + b'rdr-to %s port %r' % (inet_version, lo_addr, dnsport)) filtering_rules.append( - b'pass out inet proto udp to ' - b' port 53 route-to lo0 keep state') + b'pass out %s proto udp to port 53 ' + b'route-to lo0 keep state' % inet_version) rules = b'\n'.join(tables + translating_rules + filtering_rules) \ + b'\n' @@ -303,19 +310,18 @@ class Darwin(FreeBsd): def enable(self): o = pfctl('-E') - _pf_context['Xtoken'] = \ - re.search(b'Token : (.+)', o[1]).group(1) + _pf_context['Xtoken'].append(re.search(b'Token : (.+)', o[1]).group(1)) def disable(self, anchor): pfctl('-a %s -F all' % anchor) - if _pf_context['Xtoken'] is not None: - pfctl('-X %s' % _pf_context['Xtoken'].decode("ASCII")) + if _pf_context['Xtoken']: + pfctl('-X %s' % _pf_context['Xtoken'].pop().decode("ASCII")) def add_anchors(self, anchor): # before adding anchors and rules we must override the skip lo # that in some cases ends up in the chain so the rules we will add, # which rely on translating/filtering packets on lo, can work - if not self.has_running_instances(): + if self.has_skip_loopback(): pfctl('-f /dev/stdin', b'pass on lo\n') super(Darwin, self).add_anchors(anchor) @@ -362,9 +368,17 @@ def pf_get_dev(): return _pf_fd +def pf_get_anchor(family, port): + return 'sshuttle%s-%d' % ('' if family == socket.AF_INET else '6', port) + class Method(BaseMethod): + def get_supported_features(self): + result = super(Method, self).get_supported_features() + result.ipv6 = True + return result + def get_tcp_dstip(self, sock): pfile = self.firewall.pfile @@ -390,7 +404,7 @@ class Method(BaseMethod): translating_rules = [] filtering_rules = [] - if family != socket.AF_INET: + if family not in [socket.AF_INET, socket.AF_INET6]: raise Exception( 'Address family "%s" unsupported by pf method_name' % family_to_string(family)) @@ -409,20 +423,20 @@ class Method(BaseMethod): snet.encode("ASCII"), swidth)) - anchor = 'sshuttle-%d' % port + anchor = pf_get_anchor(family, port) pf.add_anchors(anchor) - pf.add_rules(anchor, includes, port, dnsport, nslist) + pf.add_rules(anchor, includes, port, dnsport, nslist, family) pf.enable() def restore_firewall(self, port, family, udp): - if family != socket.AF_INET: + if family not in [socket.AF_INET, socket.AF_INET6]: 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") - pf.disable('sshuttle-%d' % port) + pf.disable(pf_get_anchor(family, port)) def firewall_command(self, line): if line.startswith('QUERY_PF_NAT '): diff --git a/sshuttle/options.py b/sshuttle/options.py index 301e8fe..b0e62c3 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -187,7 +187,7 @@ parser.add_argument( "-x", "--exclude", metavar="IP/MASK", action="append", - default=[parse_subnet('127.0.0.1/8')], + default=[], type=parse_subnet, help=""" exclude this subnet (can be used more than once) diff --git a/sshuttle/server.py b/sshuttle/server.py index 85ee6f7..896af93 100644 --- a/sshuttle/server.py +++ b/sshuttle/server.py @@ -286,6 +286,12 @@ def main(latency_control, auto_hosts): def new_channel(channel, data): (family, dstip, dstport) = data.decode("ASCII").split(',', 2) family = int(family) + # AF_INET is the same constant on Linux and BSD but AF_INET6 + # is different. As the client and server can be running on + # different platforms we can not just set the socket family + # to what comes in the wire. + if family != socket.AF_INET: + family = socket.AF_INET6 dstport = int(dstport) outwrap = ssnet.connect_dst(family, dstip, dstport) handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) diff --git a/sshuttle/tests/client/test_methods_pf.py b/sshuttle/tests/client/test_methods_pf.py index 39bd156..4ec6fc5 100644 --- a/sshuttle/tests/client/test_methods_pf.py +++ b/sshuttle/tests/client/test_methods_pf.py @@ -10,7 +10,7 @@ from sshuttle.methods.pf import FreeBsd, Darwin, OpenBsd def test_get_supported_features(): method = get_method('pf') features = method.get_supported_features() - assert not features.ipv6 + assert features.ipv6 assert not features.udp assert features.dns @@ -155,6 +155,8 @@ def test_firewall_command_openbsd(mock_pf_get_dev, mock_ioctl, mock_stdout): def pfctl(args, stdin=None): + if args == '-s Interfaces -i lo -v': + return (b'lo0 (skip)',) if args == '-s all': return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n', b'little lamb\n') @@ -174,19 +176,45 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_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 == [] + # IPV6 + + 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')], + False) + assert mock_ioctl.mock_calls == [ + 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 Interfaces -i lo -v'), + call('-f /dev/stdin', b'pass on lo\n'), + call('-s all'), + call('-a sshuttle6-1024 -f /dev/stdin', + b'table {' + b'!2404:6800:4004:80c::101f/128,2404:6800:4004:80c::/64' + b'}\n' + b'table {2404:6800:4004:80c::33}\n' + b'rdr pass on lo0 inet6 proto tcp ' + b'to -> ::1 port 1024\n' + b'rdr pass on lo0 inet6 proto udp ' + b'to port 53 -> ::1 port 1026\n' + b'pass out route-to lo0 inet6 proto tcp ' + b'to keep state\n' + b'pass out route-to lo0 inet6 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() with pytest.raises(Exception) as excinfo: method.setup_firewall( @@ -215,14 +243,15 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl): call(mock_pf_get_dev(), 0xCC20441A, ANY), ] assert mock_pfctl.mock_calls == [ + call('-s Interfaces -i lo -v'), call('-f /dev/stdin', b'pass on lo\n'), call('-s all'), call('-a sshuttle-1025 -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'rdr pass on lo0 inet proto tcp ' b'to -> 127.0.0.1 port 1025\n' - b'rdr pass on lo0 proto udp ' + b'rdr pass on lo0 inet 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' @@ -256,19 +285,34 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_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 == [] + 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')], + False) + + assert mock_pfctl.mock_calls == [ + call('-s all'), + call('-a sshuttle6-1024 -f /dev/stdin', + b'table {' + b'!2404:6800:4004:80c::101f/128,2404:6800:4004:80c::/64' + b'}\n' + b'table {2404:6800:4004:80c::33}\n' + b'rdr pass on lo0 inet6 proto tcp ' + b'to -> ::1 port 1024\n' + b'rdr pass on lo0 inet6 proto udp ' + b'to port 53 -> ::1 port 1026\n' + b'pass out route-to lo0 inet6 proto tcp ' + b'to keep state\n' + b'pass out route-to lo0 inet6 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() with pytest.raises(Exception) as excinfo: method.setup_firewall( @@ -301,9 +345,9 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): call('-a sshuttle-1025 -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'rdr pass on lo0 inet proto tcp ' b'to -> 127.0.0.1 port 1025\n' - b'rdr pass on lo0 proto udp ' + b'rdr pass on lo0 inet 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' @@ -337,20 +381,41 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_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 == [] + 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')], + False) + assert mock_ioctl.mock_calls == [ + call(mock_pf_get_dev(), 0xcd48441a, ANY), + call(mock_pf_get_dev(), 0xcd48441a, ANY), + ] + assert mock_pfctl.mock_calls == [ + call('-s Interfaces -i lo -v'), + call('-f /dev/stdin', b'match on lo\n'), + call('-s all'), + call('-a sshuttle6-1024 -f /dev/stdin', + b'table {' + b'!2404:6800:4004:80c::101f/128,2404:6800:4004:80c::/64' + b'}\n' + b'table {2404:6800:4004:80c::33}\n' + b'pass in on lo0 inet6 proto tcp to ' + b' divert-to ::1 port 1024\n' + b'pass in on lo0 inet6 proto udp ' + b'to port 53 rdr-to ::1 port 1026\n' + b'pass out inet6 proto tcp to ' + b' route-to lo0 keep state\n' + b'pass out inet6 proto udp to ' + b' port 53 route-to lo0 keep state\n'), + call('-e'), + ] + mock_pf_get_dev.reset_mock() + mock_ioctl.reset_mock() + mock_pfctl.reset_mock() + with pytest.raises(Exception) as excinfo: method.setup_firewall( 1025, 1027, @@ -374,6 +439,7 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): call(mock_pf_get_dev(), 0xcd48441a, ANY), ] assert mock_pfctl.mock_calls == [ + call('-s Interfaces -i lo -v'), call('-f /dev/stdin', b'match on lo\n'), call('-s all'), call('-a sshuttle-1025 -f /dev/stdin', @@ -381,7 +447,7 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl): b'table {1.2.3.33}\n' b'pass in on lo0 inet proto tcp to divert-to 127.0.0.1 port 1025\n' b'pass in on lo0 inet proto udp to ' - b'port 53 rdr-to 127.0.0.1 port 1027\n' + b' port 53 rdr-to 127.0.0.1 port 1027\n' b'pass out inet proto tcp to ' b' route-to lo0 keep state\n' b'pass out inet proto udp to '