Merge pull request #550 from skuhl/nft-ipv6

IPv6 support in nft method.
This commit is contained in:
Brian May 2020-10-25 15:55:33 +11:00 committed by GitHub
commit 5c8c707208
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 97 additions and 29 deletions

View File

@ -40,11 +40,15 @@ Options
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 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. 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 A hostname can be provided instead of an IP address. If the
to during startup will be routed over the VPN. Valid examples are hostname resolves to multiple IPs, all of the IPs are included.
example.com, example.com:8000 and example.com:8000-9000. 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 <auto|nat|nft|tproxy|pf> .. option:: --method <auto|nat|nft|tproxy|pf|ipfw>
Which firewall method should sshuttle use? For auto, sshuttle attempts to Which firewall method should sshuttle use? For auto, sshuttle attempts to
guess the appropriate method depending on what it can find in PATH. The 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 You can use any name resolving to an IP address of the machine running
:program:`sshuttle`, e.g. ``--listen localhost``. :program:`sshuttle`, e.g. ``--listen localhost``.
For the tproxy and pf methods this can be an IPv6 address. Use this option For the nft, tproxy and pf methods this can be an IPv6 address. Use
with comma separated values if required, to provide both IPv4 and IPv6 this option with comma separated values if required, to provide both
addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``. IPv4 and IPv6 addresses, e.g. ``--listen 127.0.0.1:0,[::1]:0``.
.. option:: -H, --auto-hosts .. option:: -H, --auto-hosts
@ -92,6 +96,10 @@ Options
are taken automatically from the server's routing are taken automatically from the server's routing
table. 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 .. option:: --dns
Capture local DNS requests and forward to the remote DNS Capture local DNS requests and forward to the remote DNS
@ -122,9 +130,9 @@ Options
.. option:: --python .. option:: --python
Specify the name/path of the remote python interpreter. Specify the name/path of the remote python interpreter. The
The default is just ``python``, which means to use the default is to use ``python3`` (or ``python``, if ``python3``
default python interpreter on the remote system's PATH. fails) in the remote system's PATH.
.. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]> .. option:: -r <[username@]sshserver[:port]>, --remote=<[username@]sshserver[:port]>
@ -221,7 +229,8 @@ Options
.. option:: --disable-ipv6 .. 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 .. option:: --firewall

View File

@ -20,6 +20,18 @@ Requires:
* iptables DNAT, REDIRECT, and ttl modules. * 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 Linux with TPROXY method
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -3,24 +3,34 @@ import importlib
import socket import socket
import struct import struct
import errno import errno
import ipaddress
from sshuttle.helpers import Fatal, debug3 from sshuttle.helpers import Fatal, debug3
def original_dst(sock): def original_dst(sock):
ip = "0.0.0.0"
port = -1
try: try:
family = sock.family
SO_ORIGINAL_DST = 80 SO_ORIGINAL_DST = 80
SOCKADDR_MIN = 16
sockaddr_in = sock.getsockopt(socket.SOL_IP, if family == socket.AF_INET:
SO_ORIGINAL_DST, SOCKADDR_MIN) SOCKADDR_MIN = 16
(proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) sockaddr_in = sock.getsockopt(socket.SOL_IP,
# FIXME: decoding is IPv4 only. SO_ORIGINAL_DST, SOCKADDR_MIN)
assert(socket.htons(proto) == socket.AF_INET) port, raw_ip = struct.unpack_from('!2xH4s', sockaddr_in[:8])
ip = '%d.%d.%d.%d' % (a, b, c, d) ip = str(ipaddress.IPv4Address(raw_ip))
return (ip, port) 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: except socket.error as e:
if e.args[0] == errno.ENOPROTOOPT: if e.args[0] == errno.ENOPROTOOPT:
return sock.getsockname() return sock.getsockname()
raise raise
return (ip, port)
class Features(object): class Features(object):

View File

@ -16,7 +16,10 @@ class Method(BaseMethod):
if udp: if udp:
raise Exception("UDP not supported by nft") 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): def _nft(action, *args):
return nft(family, table, 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 # This TTL hack allows the client and server to run on the
# same host. The connections the sshuttle server makes will # same host. The connections the sshuttle server makes will
# have TTL set to 63. # 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 # Redirect DNS traffic as requested. This includes routing traffic
# to localhost DNS servers through sshuttle. # to localhost DNS servers through sshuttle.
@ -57,7 +63,11 @@ class Method(BaseMethod):
# create new subnet entries. # create new subnet entries.
for _, swidth, sexclude, snet, fport, lport \ for _, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=subnet_weight, reverse=True): 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: if fport and fport != lport:
tcp_ports = \ tcp_ports = \
tcp_ports + \ tcp_ports + \
@ -66,21 +76,38 @@ class Method(BaseMethod):
tcp_ports = tcp_ports + ('tcp', 'dport', '%d' % (fport)) tcp_ports = tcp_ports + ('tcp', 'dport', '%d' % (fport))
if sexclude: if sexclude:
_nft('add rule', chain, *(tcp_ports + ( if family == socket.AF_INET:
'ip daddr %s/%s' % (snet, swidth), 'return'))) _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: else:
_nft('add rule', chain, *(tcp_ports + ( if family == socket.AF_INET:
'ip daddr %s/%s' % (snet, swidth), _nft('add rule', chain, *(tcp_ports + (
('redirect to :' + str(port))))) '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): def restore_firewall(self, port, family, udp, user):
if udp: if udp:
raise Exception("UDP not supported by nft method_name") 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): def _nft(action, *args):
return nft(family, table, action, *args) return nft(family, table, action, *args)
# basic cleanup/setup of chains # basic cleanup/setup of chains
nonfatal(_nft, 'delete table', '') nonfatal(_nft, 'delete table', '')
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.ipv6 = True
return result

View File

@ -18,12 +18,22 @@ def test_get_supported_features():
def test_get_tcp_dstip(): def test_get_tcp_dstip():
sock = Mock() sock = Mock()
sock.family = AF_INET
sock.getsockopt.return_value = struct.pack( sock.getsockopt.return_value = struct.pack(
'!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1) '!HHBBBB', socket.ntohs(AF_INET), 1024, 127, 0, 0, 1)
method = get_method('nat') method = get_method('nat')
assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024) assert method.get_tcp_dstip(sock) == ('127.0.0.1', 1024)
assert sock.mock_calls == [call.getsockopt(0, 80, 16)] 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(): def test_recv_udp():
sock = Mock() sock = Mock()