From 6d86e44fb4b67f4d4c2b4453e44c97c11a754c33 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Tue, 20 Oct 2020 23:38:27 -0400 Subject: [PATCH 1/2] IPv6 support in nft method. This works for me but needs testing by others. Remember to specify a ::0/0 subnet or similar to route IPv6 through sshuttle. I'm adding this to nft before nat since it is not sshuttle's default method on Linux. Documentation updates may be required too. This patch uses the ipaddress module, but that appears to be included since Python 3.3. --- sshuttle/methods/__init__.py | 26 ++++++++++++------ sshuttle/methods/nft.py | 45 +++++++++++++++++++++++++------- tests/client/test_methods_nat.py | 10 +++++++ 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index 7a1d493..3968c9d 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -3,24 +3,34 @@ import importlib import socket import struct import errno +import ipaddress from sshuttle.helpers import Fatal, debug3 def original_dst(sock): + ip = "0.0.0.0" + port = -1 try: + family = sock.family 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]) - # FIXME: decoding is IPv4 only. - assert(socket.htons(proto) == socket.AF_INET) - ip = '%d.%d.%d.%d' % (a, b, c, d) - return (ip, port) + + if family == socket.AF_INET: + SOCKADDR_MIN = 16 + sockaddr_in = sock.getsockopt(socket.SOL_IP, + SO_ORIGINAL_DST, SOCKADDR_MIN) + port, raw_ip = struct.unpack_from('!2xH4s', sockaddr_in[:8]) + ip = str(ipaddress.IPv4Address(raw_ip)) + elif family == socket.AF_INET6: + sockaddr_in = sock.getsockopt(41, SO_ORIGINAL_DST, 64) + port, raw_ip = struct.unpack_from("!2xH4x16s", sockaddr_in) + ip = str(ipaddress.IPv6Address(raw_ip)) + else: + raise Fatal("fw: Unknown family type.") except socket.error as e: if e.args[0] == errno.ENOPROTOOPT: return sock.getsockname() raise + return (ip, port) class Features(object): diff --git a/sshuttle/methods/nft.py b/sshuttle/methods/nft.py index 3ec47e0..058baa9 100644 --- a/sshuttle/methods/nft.py +++ b/sshuttle/methods/nft.py @@ -16,7 +16,10 @@ class Method(BaseMethod): if udp: raise Exception("UDP not supported by nft") - table = 'sshuttle-%s' % port + if family == socket.AF_INET: + table = 'sshuttle-ipv4-%s' % port + if family == socket.AF_INET6: + table = 'sshuttle-ipv6-%s' % port def _nft(action, *args): return nft(family, table, action, *args) @@ -37,7 +40,10 @@ class Method(BaseMethod): # This TTL hack allows the client and server to run on the # same host. The connections the sshuttle server makes will # have TTL set to 63. - _nft('add rule', chain, 'ip ttl == 63 return') + if family == socket.AF_INET: + _nft('add rule', chain, 'ip ttl == 63 return') + elif family == socket.AF_INET6: + _nft('add rule', chain, 'ip6 hoplimit == 63 return') # Redirect DNS traffic as requested. This includes routing traffic # to localhost DNS servers through sshuttle. @@ -57,7 +63,11 @@ class Method(BaseMethod): # create new subnet entries. for _, swidth, sexclude, snet, fport, lport \ in sorted(subnets, key=subnet_weight, reverse=True): - tcp_ports = ('ip', 'protocol', 'tcp') + if family == socket.AF_INET: + tcp_ports = ('ip', 'protocol', 'tcp') + elif family == socket.AF_INET6: + tcp_ports = ('ip6', 'nexthdr', 'tcp') + if fport and fport != lport: tcp_ports = \ tcp_ports + \ @@ -66,21 +76,38 @@ class Method(BaseMethod): tcp_ports = tcp_ports + ('tcp', 'dport', '%d' % (fport)) if sexclude: - _nft('add rule', chain, *(tcp_ports + ( - 'ip daddr %s/%s' % (snet, swidth), 'return'))) + if family == socket.AF_INET: + _nft('add rule', chain, *(tcp_ports + ( + 'ip daddr %s/%s' % (snet, swidth), 'return'))) + elif family == socket.AF_INET6: + _nft('add rule', chain, *(tcp_ports + ( + 'ip6 daddr %s/%s' % (snet, swidth), 'return'))) else: - _nft('add rule', chain, *(tcp_ports + ( - 'ip daddr %s/%s' % (snet, swidth), - ('redirect to :' + str(port))))) + if family == socket.AF_INET: + _nft('add rule', chain, *(tcp_ports + ( + 'ip daddr %s/%s' % (snet, swidth), + ('redirect to :' + str(port))))) + elif family == socket.AF_INET6: + _nft('add rule', chain, *(tcp_ports + ( + 'ip6 daddr %s/%s' % (snet, swidth), + ('redirect to :' + str(port))))) def restore_firewall(self, port, family, udp, user): if udp: raise Exception("UDP not supported by nft method_name") - table = 'sshuttle-%s' % port + if family == socket.AF_INET: + table = 'sshuttle-ipv4-%s' % port + if family == socket.AF_INET6: + table = 'sshuttle-ipv6-%s' % port def _nft(action, *args): return nft(family, table, action, *args) # basic cleanup/setup of chains nonfatal(_nft, 'delete table', '') + + def get_supported_features(self): + result = super(Method, self).get_supported_features() + result.ipv6 = True + return result diff --git a/tests/client/test_methods_nat.py b/tests/client/test_methods_nat.py index 11a901b..a9d2a25 100644 --- a/tests/client/test_methods_nat.py +++ b/tests/client/test_methods_nat.py @@ -18,12 +18,22 @@ def test_get_supported_features(): def test_get_tcp_dstip(): sock = Mock() + sock.family = AF_INET sock.getsockopt.return_value = struct.pack( '!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1) method = get_method('nat') assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024) assert sock.mock_calls == [call.getsockopt(0, 80, 16)] + sock = Mock() + sock.family = AF_INET6 + sock.getsockopt.return_value = struct.pack( + '!HH4xBBBBBBBBBBBBBBBB', socket.ntohs(AF_INET6), + 1024, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1) + method = get_method('nft') + assert method.get_tcp_dstip(sock) == ('::1', 1024) + assert sock.mock_calls == [call.getsockopt(41, 80, 64)] + def test_recv_udp(): sock = Mock() From c02b93e719a5c33df85d35e6ac6559c377fa0eb4 Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Thu, 22 Oct 2020 20:17:03 -0400 Subject: [PATCH 2/2] nft IPv6 documentation (and other minor doc updates) Update docs to indicate that IPv6 is supported with the nft method. - Adds nft into the requirements.rst file. - Update description of what happens when a hostname is used in a subnet. - Add ipfw to list of methods. - Indicate that --auto-nets does not work with IPv6. Previously this was only mentioned in tproxy.rst - Clarify that we try to use "python3" on the server before trying "python". --- docs/manpage.rst | 33 +++++++++++++++++++++------------ docs/requirements.rst | 12 ++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/docs/manpage.rst b/docs/manpage.rst index ecc32cd..9c59c17 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -37,14 +37,18 @@ Options netmask), and 0/0 ('just route everything through the VPN'). Any of the previous examples are also valid if you append a port or a port range, so 1.2.3.4:8000 will only tunnel traffic - that has as the destination port 8000 of 1.2.3.4 and + that has as the destination port 8000 of 1.2.3.4 and 1.2.3.0/24:8000-9000 will tunnel traffic going to any port between 8000 and 9000 (inclusive) for all IPs in the 1.2.3.0/24 subnet. - It is also possible to use a name in which case the first IP it resolves - to during startup will be routed over the VPN. Valid examples are - example.com, example.com:8000 and example.com:8000-9000. + A hostname can be provided instead of an IP address. If the + hostname resolves to multiple IPs, all of the IPs are included. + If a width is provided with a hostname that the width is applied + to all of the hostnames IPs (if they are all either IPv4 or IPv6). + Widths cannot be supplied to hostnames that resolve to both IPv4 + and IPv6. Valid examples are example.com, example.com:8000, + example.com/24, example.com/24:8000 and example.com:8000-9000. -.. option:: --method +.. option:: --method Which firewall method should sshuttle use? For auto, sshuttle attempts to guess the appropriate method depending on what it can find in PATH. The @@ -64,9 +68,9 @@ Options You can use any name resolving to an IP address of the machine running :program:`sshuttle`, e.g. ``--listen localhost``. - For the tproxy and pf methods this can be an IPv6 address. Use this option - with comma separated values if required, to provide both IPv4 and IPv6 - addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``. + For the nft, tproxy and pf methods this can be an IPv6 address. Use + this option with comma separated values if required, to provide both + IPv4 and IPv6 addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``. .. option:: -H, --auto-hosts @@ -92,6 +96,10 @@ Options are taken automatically from the server's routing table. + This feature does not detect IPv6 routes. Specify IPv6 subnets + manually. For example, specify the ``::/0`` subnet on the command + line to route all IPv6 traffic. + .. option:: --dns Capture local DNS requests and forward to the remote DNS @@ -122,9 +130,9 @@ Options .. option:: --python - Specify the name/path of the remote python interpreter. - The default is just ``python``, which means to use the - default python interpreter on the remote system's PATH. + Specify the name/path of the remote python interpreter. The + default is to use ``python3`` (or ``python``, if ``python3`` + fails) in the remote system's PATH. .. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]> @@ -221,7 +229,8 @@ Options .. option:: --disable-ipv6 - If using tproxy or pf methods, this will disable IPv6 support. + Disable IPv6 support for methods that support it (nft, tproxy, and + pf). .. option:: --firewall diff --git a/docs/requirements.rst b/docs/requirements.rst index 27072b4..335b3c4 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -20,6 +20,18 @@ Requires: * iptables DNAT, REDIRECT, and ttl modules. +Linux with nft method +~~~~~~~~~~~~~~~~~~~~~ +Supports + +* IPv4 TCP +* IPv4 DNS +* IPv6 TCP +* IPv6 DNS + +Requires: + +* nftables Linux with TPROXY method ~~~~~~~~~~~~~~~~~~~~~~~~