From 1940b524f16940d21c703b858e7ac78daa450446 Mon Sep 17 00:00:00 2001 From: Julian Wollrath Date: Sun, 4 Mar 2018 17:32:08 +0100 Subject: [PATCH] Add nat-like method using nftables instead of iptables --- sshuttle/linux.py | 34 +++++++++++++++ sshuttle/methods/__init__.py | 4 +- sshuttle/methods/nft.py | 80 ++++++++++++++++++++++++++++++++++++ sshuttle/options.py | 4 +- 4 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 sshuttle/methods/nft.py diff --git a/sshuttle/linux.py b/sshuttle/linux.py index bd21180..c0bf28b 100644 --- a/sshuttle/linux.py +++ b/sshuttle/linux.py @@ -1,3 +1,4 @@ +import re import os import socket import subprocess as ssubprocess @@ -49,6 +50,39 @@ def ipt(family, table, *args): raise Fatal('%r returned %d' % (argv, rv)) +def nft(family, table, action, *args): + if family == socket.AF_INET: + argv = ['nft', action, 'ip', table] + list(args) + elif family == socket.AF_INET6: + argv = ['nft', action, 'ip6', table] + list(args) + else: + raise Exception('Unsupported family "%s"' % family_to_string(family)) + debug1('>> %s\n' % ' '.join(argv)) + env = { + 'PATH': os.environ['PATH'], + 'LC_ALL': "C", + } + rv = ssubprocess.call(argv, env=env) + if rv: + raise Fatal('%r returned %d' % (argv, rv)) + + +def nft_get_handle(expression, chain): + cmd = 'nft' + argv = [cmd, 'list', expression, '-a'] + env = { + 'PATH': os.environ['PATH'], + 'LC_ALL': "C", + } + p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=env) + for line in p.stdout: + if (b'jump %s' % chain.encode('utf-8')) in line: + return re.sub('.*# ', '', line.decode('utf-8')) + rv = p.wait() + if rv: + raise Fatal('%r returned %d' % (argv, rv)) + + _no_ttl_module = False diff --git a/sshuttle/methods/__init__.py b/sshuttle/methods/__init__.py index 36a3f79..6d776e6 100644 --- a/sshuttle/methods/__init__.py +++ b/sshuttle/methods/__init__.py @@ -102,12 +102,14 @@ def get_method(method_name): def get_auto_method(): if _program_exists('iptables'): method_name = "nat" + elif _program_exists('nft'): + method_name = "nft" elif _program_exists('pfctl'): method_name = "pf" elif _program_exists('ipfw'): method_name = "ipfw" else: raise Fatal( - "can't find either iptables or pfctl; check your PATH") + "can't find either iptables, nft or pfctl; check your PATH") return get_method(method_name) diff --git a/sshuttle/methods/nft.py b/sshuttle/methods/nft.py new file mode 100644 index 0000000..cd28a5b --- /dev/null +++ b/sshuttle/methods/nft.py @@ -0,0 +1,80 @@ +import socket +from sshuttle.firewall import subnet_weight +from sshuttle.linux import nft, nft_get_handle, 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, + user): + if udp: + raise Exception("UDP not supported by nft") + + table = "nat" + + def _nft(action, *args): + return nft(family, table, action, *args) + + chain = 'sshuttle-%s' % port + + # basic cleanup/setup of chains + _nft('add table', '') + _nft('add chain', 'prerouting', + '{ type nat hook prerouting priority -100; policy accept; }') + _nft('add chain', 'postrouting', + '{ type nat hook postrouting priority 100; policy accept; }') + _nft('add chain', 'output', + '{ type nat hook output priority -100; policy accept; }') + _nft('add chain', chain) + _nft('flush chain', chain) + _nft('add rule', 'output jump %s' % chain) + _nft('add rule', 'prerouting jump %s' % chain) + + # create new subnet entries. + for _, swidth, sexclude, snet, fport, lport \ + in sorted(subnets, key=subnet_weight, reverse=True): + tcp_ports = ('ip', 'protocol', 'tcp') + if fport: + tcp_ports = tcp_ports + ('dport { %d-%d }' % (fport, lport)) + + if sexclude: + _nft('add rule', chain, *(tcp_ports + ( + 'ip daddr %s/%s' % (snet, swidth), 'return'))) + else: + _nft('add rule', chain, *(tcp_ports + ( + 'ip daddr %s/%s' % (snet, swidth), 'ip ttl != 42', + ('redirect to :' + str(port))))) + + for _, ip in [i for i in nslist if i[0] == family]: + if family == socket.AF_INET: + _nft('add rule', chain, 'ip protocol udp ip daddr %s' % ip, + 'udp dport { 53 }', 'ip ttl != 42', + ('redirect to :' + str(dnsport))) + elif family == socket.AF_INET6: + _nft('add rule', chain, 'ip6 protocol udp ip6 daddr %s' % ip, + 'udp dport { 53 }', 'ip ttl != 42', + ('redirect to :' + str(dnsport))) + + def restore_firewall(self, port, family, udp, user): + if udp: + raise Exception("UDP not supported by nft method_name") + + table = "nat" + + def _nft(action, *args): + return nft(family, table, action, *args) + + chain = 'sshuttle-%s' % port + + # basic cleanup/setup of chains + handle = nft_get_handle('chain ip nat output', chain) + nonfatal(_nft, 'delete rule', 'output', handle) + handle = nft_get_handle('chain ip nat prerouting', chain) + nonfatal(_nft, 'delete rule', 'prerouting', handle) + nonfatal(_nft, 'delete chain', chain) diff --git a/sshuttle/options.py b/sshuttle/options.py index b88b0e8..6810f3c 100644 --- a/sshuttle/options.py +++ b/sshuttle/options.py @@ -160,7 +160,7 @@ parser.add_argument( parser.add_argument( "--method", - choices=["auto", "nat", "tproxy", "pf", "ipfw"], + choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"], metavar="TYPE", default="auto", help=""" @@ -230,7 +230,7 @@ parser.add_argument( metavar="HOSTNAME[,HOSTNAME]", default=[], help=""" - comma-separated list of hostnames for initial scan (may be used with + comma-separated list of hostnames for initial scan (may be used with or without --auto-hosts) """ )