summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJulian Wollrath <jwollrath@web.de>2018-03-04 17:32:08 +0100
committerBrian May <brian@linuxpenguins.xyz>2018-03-13 07:36:00 +1100
commit1940b524f16940d21c703b858e7ac78daa450446 (patch)
tree92e53db1d027845b709f92541f4d2d4e56633885
parentd11f5b9d16e1d5b30c63a1bf2801c459492cf6e4 (diff)
Add nat-like method using nftables instead of iptables
-rw-r--r--sshuttle/linux.py34
-rw-r--r--sshuttle/methods/__init__.py4
-rw-r--r--sshuttle/methods/nft.py80
-rw-r--r--sshuttle/options.py4
4 files changed, 119 insertions, 3 deletions
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)
"""
)