From c026a92cad92dea30fe1bbc4802e41a1fc0499ff Mon Sep 17 00:00:00 2001 From: Scott Kuhl Date: Fri, 28 May 2021 17:55:17 -0400 Subject: [PATCH] Add IPv6 support to nat (iptables) method. Adding IPv6 support to the nat method is straightforward after the previous work to add IPv6 support for nft. --- docs/requirements.rst | 4 +- sshuttle/methods/nat.py | 16 +++--- tests/client/test_methods_nat.py | 83 ++++++++++++++++++++++++++------ 3 files changed, 80 insertions(+), 23 deletions(-) diff --git a/docs/requirements.rst b/docs/requirements.rst index f4499ea..fb04178 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -15,10 +15,12 @@ Supports: * IPv4 TCP * IPv4 DNS +* IPv6 TCP +* IPv6 DNS Requires: -* iptables DNAT, REDIRECT, and ttl modules. +* iptables DNAT, REDIRECT, and ttl modules. ip6tables for IPv6. Linux with nft method ~~~~~~~~~~~~~~~~~~~~~ diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py index 569593d..baa9998 100644 --- a/sshuttle/methods/nat.py +++ b/sshuttle/methods/nat.py @@ -14,14 +14,12 @@ class Method(BaseMethod): # "-A OUTPUT"). def setup_firewall(self, port, dnsport, nslist, family, subnets, udp, user, ttl): - # only ipv4 supported with NAT - if family != socket.AF_INET: + if family != socket.AF_INET and family != socket.AF_INET6: 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): @@ -53,13 +51,18 @@ 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. - _ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'ttl', '--ttl', '%s' % ttl) + if family == socket.AF_INET: + _ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'ttl', '--ttl', + '%s' % ttl) + else: # ipv6, ttl is renamed to 'hop limit' + _ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'hl', '--hl-eq', + '%s' % ttl) # Redirect DNS traffic as requested. This includes routing traffic # to localhost DNS servers through sshuttle. for _, ip in [i for i in nslist if i[0] == family]: _ipt('-A', chain, '-j', 'REDIRECT', - '--dest', '%s/32' % ip, + '--dest', '%s' % ip, '-p', 'udp', '--dport', '53', '--to-ports', str(dnsport)) @@ -87,7 +90,7 @@ class Method(BaseMethod): def restore_firewall(self, port, family, udp, user): # only ipv4 supported with NAT - if family != socket.AF_INET: + if family != socket.AF_INET and family != socket.AF_INET6: raise Exception( 'Address family "%s" unsupported by nat method_name' % family_to_string(family)) @@ -123,6 +126,7 @@ class Method(BaseMethod): def get_supported_features(self): result = super(Method, self).get_supported_features() result.user = True + result.ipv6 = True return result def is_supported(self): diff --git a/tests/client/test_methods_nat.py b/tests/client/test_methods_nat.py index dbe19ff..10b3ddb 100644 --- a/tests/client/test_methods_nat.py +++ b/tests/client/test_methods_nat.py @@ -11,7 +11,7 @@ from sshuttle.methods import get_method def test_get_supported_features(): method = get_method('nat') features = method.get_supported_features() - assert not features.ipv6 + assert features.ipv6 assert not features.udp assert features.dns @@ -92,18 +92,51 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): method = get_method('nat') assert method.name == 'nat' - with pytest.raises(Exception) as excinfo: - method.setup_firewall( - 1024, 1026, - [(AF_INET6, u'2404:6800:4004:80c::33')], - AF_INET6, - [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0), - (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], - True, - None, - 63) - assert str(excinfo.value) \ - == 'Address family "AF_INET6" unsupported by nat method_name' + assert mock_ipt_chain_exists.mock_calls == [] + assert mock_ipt_ttl.mock_calls == [] + assert mock_ipt.mock_calls == [] + method.setup_firewall( + 1024, 1026, + [(AF_INET6, u'2404:6800:4004:80c::33')], + AF_INET6, + [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0), + (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)], + False, + None, + 63) + + assert mock_ipt_chain_exists.mock_calls == [ + call(AF_INET6, 'nat', 'sshuttle-1024') + ] + assert mock_ipt_ttl.mock_calls == [ + call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN', + '-m', 'hl', '--hl-eq', '63') + ] + assert mock_ipt.mock_calls == [ + call(AF_INET6, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1024'), + call(AF_INET6, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1024'), + call(AF_INET6, 'nat', '-F', 'sshuttle-1024'), + call(AF_INET6, 'nat', '-X', 'sshuttle-1024'), + call(AF_INET6, 'nat', '-N', 'sshuttle-1024'), + call(AF_INET6, 'nat', '-F', 'sshuttle-1024'), + call(AF_INET6, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1024'), + call(AF_INET6, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1024'), + call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT', + '--dest', u'2404:6800:4004:80c::33', '-p', 'udp', + '--dport', '53', '--to-ports', '1026'), + call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN', + '-m', 'addrtype', '--dst-type', 'LOCAL'), + call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN', + '--dest', u'2404:6800:4004:80c::101f/128', '-p', 'tcp', + '--dport', '80:80'), + call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT', + '--dest', u'2404:6800:4004:80c::/64', '-p', 'tcp', + '--to-ports', '1024') + ] + mock_ipt_chain_exists.reset_mock() + mock_ipt_ttl.reset_mock() + mock_ipt.reset_mock() + assert mock_ipt_chain_exists.mock_calls == [] assert mock_ipt_ttl.mock_calls == [] assert mock_ipt.mock_calls == [] @@ -149,7 +182,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'), call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT', - '--dest', u'1.2.3.33/32', '-p', 'udp', + '--dest', u'1.2.3.33', '-p', 'udp', '--dport', '53', '--to-ports', '1027'), call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', '-m', 'addrtype', '--dst-type', 'LOCAL'), @@ -169,11 +202,29 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt): ] assert mock_ipt_ttl.mock_calls == [] assert mock_ipt.mock_calls == [ - call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'), - call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'), + call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', + 'sshuttle-1025'), + call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', + 'sshuttle-1025'), call(AF_INET, 'nat', '-F', 'sshuttle-1025'), call(AF_INET, 'nat', '-X', 'sshuttle-1025') ] mock_ipt_chain_exists.reset_mock() mock_ipt_ttl.reset_mock() mock_ipt.reset_mock() + + method.restore_firewall(1025, AF_INET6, False, None) + assert mock_ipt_chain_exists.mock_calls == [ + call(AF_INET6, 'nat', 'sshuttle-1025') + ] + assert mock_ipt_ttl.mock_calls == [] + assert mock_ipt.mock_calls == [ + call(AF_INET6, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'), + call(AF_INET6, 'nat', '-D', 'PREROUTING', '-j', + 'sshuttle-1025'), + call(AF_INET6, 'nat', '-F', 'sshuttle-1025'), + call(AF_INET6, 'nat', '-X', 'sshuttle-1025') + ] + mock_ipt_chain_exists.reset_mock() + mock_ipt_ttl.reset_mock() + mock_ipt.reset_mock()