summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/manpage.rst19
-rw-r--r--docs/overview.rst2
-rw-r--r--docs/requirements.rst7
-rw-r--r--sshuttle/client.py19
-rw-r--r--sshuttle/cmdline.py10
-rw-r--r--sshuttle/firewall.py20
-rw-r--r--sshuttle/methods/nat.py20
-rw-r--r--sshuttle/methods/pf.py43
-rw-r--r--sshuttle/methods/tproxy.py39
-rw-r--r--sshuttle/options.py122
-rw-r--r--sshuttle/tests/client/test_firewall.py15
-rw-r--r--sshuttle/tests/client/test_methods_nat.py15
-rw-r--r--sshuttle/tests/client/test_methods_pf.py93
-rw-r--r--sshuttle/tests/client/test_methods_tproxy.py34
-rw-r--r--sshuttle/tests/client/test_options.py101
15 files changed, 353 insertions, 206 deletions
diff --git a/docs/manpage.rst b/docs/manpage.rst
index fe6633a..44a178e 100644
--- a/docs/manpage.rst
+++ b/docs/manpage.rst
@@ -31,11 +31,18 @@ Options
.. option:: subnets
A list of subnets to route over the VPN, in the form
- ``a.b.c.d[/width]``. Valid examples are 1.2.3.4 (a
+ ``a.b.c.d[/width][port[-port]]``. Valid examples are 1.2.3.4 (a
single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4),
1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0
netmask), and 0/0 ('just route everything through the
- VPN').
+ 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
+ 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.
.. option:: --method [auto|nat|tproxy|pf]
@@ -54,9 +61,11 @@ Options
connections from other machines on your network (ie. to
run :program:`sshuttle` on a router) try enabling IP Forwarding in
your kernel, then using ``--listen 0.0.0.0:0``.
+ You can use any name resolving to an IP address of the machine running
+ :program:`sshuttle`, e.g. ``--listen localhost``.
- For the tproxy method this can be an IPv6 address. Use this option twice if
- required, to provide both IPv4 and IPv6 addresses.
+ For the tproxy and pf methods this can be an IPv6 address. Use this option
+ twice if required, to provide both IPv4 and IPv6 addresses.
.. option:: -H, --auto-hosts
@@ -176,7 +185,7 @@ Options
.. option:: --disable-ipv6
- If using the tproxy method, this will disable IPv6 support.
+ If using tproxy or pf methods, this will disable IPv6 support.
.. option:: --firewall
diff --git a/docs/overview.rst b/docs/overview.rst
index dc32a80..a5f02c0 100644
--- a/docs/overview.rst
+++ b/docs/overview.rst
@@ -4,7 +4,7 @@ Overview
As far as I know, sshuttle is the only program that solves the following
common case:
-- Your client machine (or router) is Linux, FreeBSD, or MacOS.
+- Your client machine (or router) is Linux, MacOS, FreeBSD, OpenBSD or pfSense.
- You have access to a remote network via ssh.
diff --git a/docs/requirements.rst b/docs/requirements.rst
index d32b348..9e9b54f 100644
--- a/docs/requirements.rst
+++ b/docs/requirements.rst
@@ -41,7 +41,7 @@ order to get the ``recvmsg()`` function. See :doc:`tproxy` for more
information.
-MacOS / FreeBSD / OpenBSD
+MacOS / FreeBSD / OpenBSD / pfSense
~~~~~~~~~~~~~~~~~~~~~~~~~
Method: pf
@@ -65,8 +65,9 @@ cmd.exe with Administrator access. See :doc:`windows` for more information.
Server side Requirements
------------------------
-Server requirements are more relaxed, however it is recommended that you use
-Python 2.7 or Python 3.5.
+The server can run in any version of Python between 2.4 and 3.6.
+However it is recommended that you use Python 2.7, Python 3.5 or later whenever
+possible as support for older versions might be dropped in the future.
Additional Suggested Software
diff --git a/sshuttle/client.py b/sshuttle/client.py
index e4d2470..d366db8 100644
--- a/sshuttle/client.py
+++ b/sshuttle/client.py
@@ -255,12 +255,13 @@ class FirewallClient:
def start(self):
self.pfile.write(b'ROUTES\n')
- for (family, ip, width) in self.subnets_include + self.auto_nets:
- self.pfile.write(b'%d,%d,0,%s\n'
- % (family, width, ip.encode("ASCII")))
- for (family, ip, width) in self.subnets_exclude:
- self.pfile.write(b'%d,%d,1,%s\n'
- % (family, width, ip.encode("ASCII")))
+ for (family, ip, width, fport, lport) \
+ in self.subnets_include + self.auto_nets:
+ self.pfile.write(b'%d,%d,0,%s,%d,%d\n'
+ % (family, width, ip.encode("ASCII"), fport, lport))
+ for (family, ip, width, fport, lport) in self.subnets_exclude:
+ self.pfile.write(b'%d,%d,1,%s,%d,%d\n'
+ % (family, width, ip.encode("ASCII"), fport, lport))
self.pfile.write(b'NSLIST\n')
for (family, ip) in self.nslist:
@@ -484,7 +485,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
else:
debug2("Adding auto net %d/%s/%d\n" % (family, ip, width))
- fw.auto_nets.append((family, ip, width))
+ fw.auto_nets.append((family, ip, width, 0, 0))
# we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection!
@@ -591,11 +592,11 @@ def main(listenip_v6, listenip_v4,
if required.ipv4 and \
not any(listenip_v4[0] == sex[1] for sex in subnets_v4):
- subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32))
+ subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32, 0, 0))
if required.ipv6 and \
not any(listenip_v6[0] == sex[1] for sex in subnets_v6):
- subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128))
+ subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0))
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
# if both ports given, no need to search for a spare port
diff --git a/sshuttle/cmdline.py b/sshuttle/cmdline.py
index 4c25e5b..7b34267 100644
--- a/sshuttle/cmdline.py
+++ b/sshuttle/cmdline.py
@@ -1,10 +1,11 @@
import re
+import socket
import sshuttle.helpers as helpers
import sshuttle.client as client
import sshuttle.firewall as firewall
import sshuttle.hostwatch as hostwatch
import sshuttle.ssyslog as ssyslog
-from sshuttle.options import parser, parse_ipport6, parse_ipport4
+from sshuttle.options import parser, parse_ipport
from sshuttle.helpers import family_ip_tuple, log, Fatal
@@ -46,10 +47,11 @@ def main():
ipport_v4 = None
list = opt.listen.split(",")
for ip in list:
- if '[' in ip and ']' in ip:
- ipport_v6 = parse_ipport6(ip)
+ family, ip, port = parse_ipport(ip)
+ if family == socket.AF_INET6:
+ ipport_v6 = (ip, port)
else:
- ipport_v4 = parse_ipport4(ip)
+ ipport_v4 = (ip, port)
else:
# parse_ipport4('127.0.0.1:0')
ipport_v4 = "auto"
diff --git a/sshuttle/firewall.py b/sshuttle/firewall.py
index 0e30e9c..4f37735 100644
--- a/sshuttle/firewall.py
+++ b/sshuttle/firewall.py
@@ -74,6 +74,15 @@ def setup_daemon():
return sys.stdin, sys.stdout
+# Note that we're sorting in a very particular order:
+# we need to go from most-specific (largest swidth) to least-specific,
+# and at any given level of specificity, smaller port ranges come
+# before larger port ranges. On ties excludes come first.
+# s:(inet, subnet width, exclude flag, subnet, first port, last port)
+def subnet_weight(s):
+ return (s[1], s[-2] or -65535 - s[-1], s[2])
+
+
# This is some voodoo for setting up the kernel's transparent
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
# otherwise we delete it, then make them from scratch.
@@ -119,10 +128,17 @@ def main(method_name, syslog):
elif line.startswith("NSLIST\n"):
break
try:
- (family, width, exclude, ip) = line.strip().split(',', 3)
+ (family, width, exclude, ip, fport, lport) = \
+ line.strip().split(',', 5)
except:
raise Fatal('firewall: expected route or NSLIST but got %r' % line)
- subnets.append((int(family), int(width), bool(int(exclude)), ip))
+ subnets.append((
+ int(family),
+ int(width),
+ bool(int(exclude)),
+ ip,
+ int(fport),
+ int(lport)))
debug2('firewall manager: Got subnets: %r\n' % subnets)
nslist = []
diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py
index c5afc03..a5e7b96 100644
--- a/sshuttle/methods/nat.py
+++ b/sshuttle/methods/nat.py
@@ -1,4 +1,5 @@
import socket
+from sshuttle.firewall import subnet_weight
from sshuttle.helpers import family_to_string
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal
from sshuttle.methods import BaseMethod
@@ -38,22 +39,21 @@ class Method(BaseMethod):
_ipt('-I', 'OUTPUT', '1', '-j', chain)
_ipt('-I', 'PREROUTING', '1', '-j', chain)
- # create new subnet entries. Note that we're sorting in a very
- # particular order: we need to go from most-specific (largest
- # swidth) to least-specific, and at any given level of specificity,
- # we want excludes to come first. That's why the columns are in
- # such a non- intuitive order.
- for f, swidth, sexclude, snet \
- in sorted(subnets, key=lambda s: s[1], reverse=True):
+ # create new subnet entries.
+ for f, swidth, sexclude, snet, fport, lport \
+ in sorted(subnets, key=subnet_weight, reverse=True):
+ tcp_ports = ('-p', 'tcp')
+ if fport:
+ tcp_ports = tcp_ports + ('--dport', '%d:%d' % (fport, lport))
+
if sexclude:
_ipt('-A', chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
- '-p', 'tcp')
+ *tcp_ports)
else:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet, swidth),
- '-p', 'tcp',
- '--to-ports', str(port))
+ *(tcp_ports + ('--to-ports', str(port))))
for f, ip in [i for i in nslist if i[0] == family]:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
diff --git a/sshuttle/methods/pf.py b/sshuttle/methods/pf.py
index 5888b47..28d7ddd 100644
--- a/sshuttle/methods/pf.py
+++ b/sshuttle/methods/pf.py
@@ -9,6 +9,7 @@ import shlex
from fcntl import ioctl
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
sizeof, addressof, memmove
+from sshuttle.firewall import subnet_weight
from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string
from sshuttle.methods import BaseMethod
@@ -186,16 +187,18 @@ class FreeBsd(Generic):
inet_version = self._inet_version(family)
lo_addr = self._lo_addr(family)
- tables = [
- b'table <forward_subnets> {%s}' % b','.join(includes)
- ]
+ tables = []
translating_rules = [
- b'rdr pass on lo0 %s proto tcp to <forward_subnets> '
- b'-> %s port %r' % (inet_version, lo_addr, port)
+ b'rdr pass on lo0 %s proto tcp to %s '
+ b'-> %s port %r' % (inet_version, subnet, lo_addr, port)
+ for exclude, subnet in includes if not exclude
]
filtering_rules = [
b'pass out route-to lo0 %s proto tcp '
- b'to <forward_subnets> keep state' % inet_version
+ b'to %s keep state' % (inet_version, subnet)
+ if not exclude else
+ b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
+ for exclude, subnet in includes
]
if len(nslist) > 0:
@@ -254,16 +257,18 @@ class OpenBsd(Generic):
inet_version = self._inet_version(family)
lo_addr = self._lo_addr(family)
- tables = [
- b'table <forward_subnets> {%s}' % b','.join(includes)
- ]
+ tables = []
translating_rules = [
- b'pass in on lo0 %s proto tcp to <forward_subnets> '
- b'divert-to %s port %r' % (inet_version, lo_addr, port)
+ b'pass in on lo0 %s proto tcp to %s '
+ b'divert-to %s port %r' % (inet_version, subnet, lo_addr, port)
+ for exclude, subnet in includes if not exclude
]
filtering_rules = [
- b'pass out %s proto tcp to <forward_subnets> '
- b'route-to lo0 keep state' % inet_version
+ b'pass out %s proto tcp to %s '
+ b'route-to lo0 keep state' % (inet_version, subnet)
+ if not exclude else
+ b'pass out quick %s proto tcp to %s' % (inet_version, subnet)
+ for exclude, subnet in includes
]
if len(nslist) > 0:
@@ -429,12 +434,12 @@ class Method(BaseMethod):
# If a given subnet is both included and excluded, list the
# exclusion first; the table will ignore the second, opposite
# definition
- for f, swidth, sexclude, snet in sorted(
- subnets, key=lambda s: (s[1], s[2]), reverse=True):
- includes.append(b"%s%s/%d" %
- (b"!" if sexclude else b"",
- snet.encode("ASCII"),
- swidth))
+ for f, swidth, sexclude, snet, fport, lport \
+ in sorted(subnets, key=subnet_weight, reverse=True):
+ includes.append((sexclude, b"%s/%d%s" % (
+ snet.encode("ASCII"),
+ swidth,
+ b" port %d:%d" % (fport, lport) if fport else b"")))
anchor = pf_get_anchor(family, port)
pf.add_anchors(anchor)
diff --git a/sshuttle/methods/tproxy.py b/sshuttle/methods/tproxy.py
index 5094962..44b8fd7 100644
--- a/sshuttle/methods/tproxy.py
+++ b/sshuttle/methods/tproxy.py
@@ -1,4 +1,5 @@
import struct
+from sshuttle.firewall import subnet_weight
from sshuttle.helpers import family_to_string
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
from sshuttle.methods import BaseMethod
@@ -163,6 +164,11 @@ class Method(BaseMethod):
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
+ def _ipt_proto_ports(proto, fport, lport):
+ return proto + ('--dport', '%d:%d' % (fport, lport)) \
+ if fport else proto
+
+
mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port
@@ -197,33 +203,44 @@ class Method(BaseMethod):
'-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport))
- for f, swidth, sexclude, snet \
- in sorted(subnets, key=lambda s: s[1], reverse=True):
+ for f, swidth, sexclude, snet, fport, lport \
+ in sorted(subnets, key=subnet_weight, reverse=True):
+ tcp_ports = ('-p', 'tcp')
+ tcp_ports = _ipt_proto_ports(tcp_ports, fport, lport)
+
if sexclude:
_ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
- '-m', 'tcp', '-p', 'tcp')
+ '-m', 'tcp',
+ *tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
- '-m', 'tcp', '-p', 'tcp')
+ '-m', 'tcp',
+ *tcp_ports)
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
- '-m', 'tcp', '-p', 'tcp')
+ '-m', 'tcp',
+ *tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
- '-m', 'tcp', '-p', 'tcp',
- '--on-port', str(port))
+ '-m', 'tcp',
+ *(tcp_ports + ('--on-port', str(port))))
if udp:
+ udp_ports = ('-p', 'udp')
+ udp_ports = _ipt_proto_ports(udp_ports, fport, lport)
+
if sexclude:
_ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
- '-m', 'udp', '-p', 'udp')
+ '-m', 'udp',
+ *udp_ports)
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
- '-m', 'udp', '-p', 'udp')
+ '-m', 'udp',
+ *udp_ports)
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
@@ -231,8 +248,8 @@ class Method(BaseMethod):
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
- '-m', 'udp', '-p', 'udp',
- '--on-port', str(port))
+ '-m', 'udp',
+ *(udp_ports + ('--on-port', str(port))))
def restore_firewall(self, port, family, udp):
if family not in [socket.AF_INET, socket.AF_INET6]:
diff --git a/sshuttle/options.py b/sshuttle/options.py
index d97d7ae..659c014 100644
--- a/sshuttle/options.py
+++ b/sshuttle/options.py
@@ -4,41 +4,8 @@ from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
from sshuttle import __version__
-# 1.2.3.4/5 or just 1.2.3.4
-def parse_subnet4(s):
- m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s)
- if not m:
- raise Fatal('%r is not a valid IP subnet format' % s)
- (a, b, c, d, width) = m.groups()
- (a, b, c, d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0))
- if width is None:
- width = 32
- else:
- width = int(width)
- if a > 255 or b > 255 or c > 255 or d > 255:
- raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
- if width > 32:
- raise Fatal('*/%d is greater than the maximum of 32' % width)
- return(socket.AF_INET, '%d.%d.%d.%d' % (a, b, c, d), width)
-
-
-# 1:2::3/64 or just 1:2::3
-def parse_subnet6(s):
- m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s)
- if not m:
- raise Fatal('%r is not a valid IP subnet format' % s)
- (net, width) = m.groups()
- if width is None:
- width = 128
- else:
- width = int(width)
- if width > 128:
- raise Fatal('*/%d is greater than the maximum of 128' % width)
- return(socket.AF_INET6, net, width)
-
-
# Subnet file, supporting empty lines and hash-started comment lines
-def parse_subnet_file(s):
+def parse_subnetport_file(s):
try:
handle = open(s, 'r')
except OSError:
@@ -52,47 +19,66 @@ def parse_subnet_file(s):
continue
if line[0] == '#':
continue
- subnets.append(parse_subnet(line))
+ subnets.append(parse_subnetport(line))
return subnets
-# 1.2.3.4/5 or just 1.2.3.4
-# 1:2::3/64 or just 1:2::3
-def parse_subnet(subnet_str):
- if ':' in subnet_str:
- return parse_subnet6(subnet_str)
+# 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4
+# [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
+# example.com:123 or just example.com
+def parse_subnetport(s):
+ if s.count(':') > 1:
+ rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
else:
- return parse_subnet4(subnet_str)
+ rx = r'([\w\.]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
+
+ m = re.match(rx, s)
+ if not m:
+ raise Fatal('%r is not a valid address/mask:port format' % s)
+
+ addr, width, fport, lport = m.groups()
+ try:
+ addrinfo = socket.getaddrinfo(addr, 0, 0, socket.SOCK_STREAM)
+ except socket.gaierror:
+ raise Fatal('Unable to resolve address: %s' % addr)
+
+ family, _, _, _, addr = min(addrinfo)
+ max_width = 32 if family == socket.AF_INET else 128
+ width = int(width or max_width)
+ if not 0 <= width <= max_width:
+ raise Fatal('width %d is not between 0 and %d' % (width, max_width))
+
+ return (family, addr[0], width, int(fport or 0), int(lport or fport or 0))
# 1.2.3.4:567 or just 1.2.3.4 or just 567
-def parse_ipport4(s):
+# [1:2::3]:456 or [1:2::3] or just [::]:567
+# example.com:123 or just example.com
+def parse_ipport(s):
s = str(s)
- m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s)
+ if s.isdigit():
+ rx = r'()(\d+)$'
+ elif ']' in s:
+ rx = r'(?:\[([^]]+)])(?::(\d+))?$'
+ else:
+ rx = r'([\w\.]+)(?::(\d+))?$'
+
+ m = re.match(rx, s)
if not m:
raise Fatal('%r is not a valid IP:port format' % s)
- (a, b, c, d, port) = m.groups()
- (a, b, c, d, port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0),
- int(port or 0))
- if a > 255 or b > 255 or c > 255 or d > 255:
- raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d))
- if port > 65535:
- raise Fatal('*:%d is greater than the maximum of 65535' % port)
- if a is None:
- a = b = c = d = 0
- return ('%d.%d.%d.%d' % (a, b, c, d), port)
+ ip, port = m.groups()
+ ip = ip or '0.0.0.0'
+ port = int(port or 0)
-# [1:2::3]:456 or [1:2::3] or 456
-def parse_ipport6(s):
- s = str(s)
- m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s)
- if not m:
- raise Fatal('%s is not a valid IP:port format' % s)
- (ip, port) = m.groups()
- (ip, port) = (ip or '::', int(port or 0))
- return (ip, port)
+ try:
+ addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
+ except socket.gaierror:
+ raise Fatal('%r is not a valid IP:port format' % s)
+
+ family, _, _, _, addr = min(addrinfo)
+ return (family,) + addr[:2]
def parse_list(list):
@@ -116,9 +102,9 @@ parser = ArgumentParser(
)
parser.add_argument(
"subnets",
- metavar="IP/MASK [IP/MASK...]",
+ metavar="IP/MASK[:PORT[-PORT]]...",
nargs="*",
- type=parse_subnet,
+ type=parse_subnetport,
help="""
capture and forward traffic to these subnets (whitespace separated)
"""
@@ -185,10 +171,10 @@ parser.add_argument(
)
parser.add_argument(
"-x", "--exclude",
- metavar="IP/MASK",
+ metavar="IP/MASK[:PORT[-PORT]]",
action="append",
default=[],
- type=parse_subnet,
+ type=parse_subnetport,
help="""
exclude this subnet (can be used more than once)
"""
@@ -198,7 +184,7 @@ parser.add_argument(
metavar="PATH",
action=Concat,
dest="exclude",
- type=parse_subnet_file,
+ type=parse_subnetport_file,
help="""
exclude the subnets in a file (whitespace separated)
"""
@@ -271,7 +257,7 @@ parser.add_argument(
action=Concat,
dest="subnets_file",
default=[],
- type=parse_subnet_file,
+ type=parse_subnetport_file,
help="""
file where the subnets are stored, instead of on the command line
"""
diff --git a/sshuttle/tests/client/test_firewall.py b/sshuttle/tests/client/test_firewall.py
index fb4ba40..b61a23e 100644
--- a/sshuttle/tests/client/test_firewall.py
+++ b/sshuttle/tests/client/test_firewall.py
@@ -6,10 +6,10 @@ import sshuttle.firewall
def setup_daemon():
stdin = io.StringIO(u"""ROUTES
-2,24,0,1.2.3.0
-2,32,1,1.2.3.66
-10,64,0,2404:6800:4004:80c::
-10,128,1,2404:6800:4004:80c::101f
+2,24,0,1.2.3.0,8000,9000
+2,32,1,1.2.3.66,8080,8080
+10,64,0,2404:6800:4004:80c::,0,0
+10,128,1,2404:6800:4004:80c::101f,80,80
NSLIST
2,1.2.3.33
10,2404:6800:4004:80c::33
@@ -88,14 +88,15 @@ def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
1024, 1026,
[(10, u'2404:6800:4004:80c::33')],
10,
- [(10, 64, False, u'2404:6800:4004:80c::'),
- (10, 128, True, u'2404:6800:4004:80c::101f')],
+ [(10, 64, False, u'2404:6800:4004:80c::', 0, 0),
+ (10, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
True),
call().setup_firewall(
1025, 1027,
[(2, u'1.2.3.33')],
2,
- [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
+ [(2, 24, False, u'1.2.3.0', 8000, 9000),
+ (2, 32, True, u'1.2.3.66', 8080, 8080)],
True),
call().restore_firewall(1024, 10, True),
call().restore_firewall(1025, 2, True),
diff --git a/sshuttle/tests/client/test_methods_nat.py b/sshuttle/tests/client/test_methods_nat.py
index 2144e25..4ae571b 100644
--- a/sshuttle/tests/client/test_methods_nat.py
+++ b/sshuttle/tests/client/test_methods_nat.py
@@ -86,8 +86,8 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
1024, 1026,
[(10, u'2404:6800:4004:80c::33')],
10,
- [(10, 64, False, u'2404:6800:4004:80c::'),
- (10, 128, True, u'2404:6800:4004:80c::101f')],
+ [(10, 64, False, u'2404:6800:4004:80c::', 0, 0),
+ (10, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
True)
assert str(excinfo.value) \
== 'Address family "AF_INET6" unsupported by nat method_name'
@@ -100,7 +100,8 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
1025, 1027,
[(2, u'1.2.3.33')],
2,
- [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
+ [(2, 24, False, u'1.2.3.0', 8000, 9000),
+ (2, 32, True, u'1.2.3.66', 8080, 8080)],
True)
assert str(excinfo.value) == 'UDP not supported by nat method_name'
assert mock_ipt_chain_exists.mock_calls == []
@@ -111,14 +112,16 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
1025, 1027,
[(2, u'1.2.3.33')],
2,
- [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
+ [(2, 24, False, u'1.2.3.0', 8000, 9000),
+ (2, 32, True, u'1.2.3.66', 8080, 8080)],
False)
assert mock_ipt_chain_exists.mock_calls == [
call(2, 'nat', 'sshuttle-1025')
]
assert mock_ipt_ttl.mock_calls == [
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
- '--dest', u'1.2.3.0/24', '-p', 'tcp', '--to-ports', '1025'),
+ '--dest', u'1.2.3.0/24', '-p', 'tcp', '--dport', '8000:9000',
+ '--to-ports', '1025'),
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
'--dest', u'1.2.3.33/32', '-p', 'udp',
'--dport', '53', '--to-ports', '1027')
@@ -133,7 +136,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
call(2, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
call(2, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
- '--dest', u'1.2.3.66/32', '-p', 'tcp')
+ '--dest', u'1.2.3.66/32', '-p', 'tcp', '--dport', '8080:8080')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
diff --git a/sshuttle/tests/client/test_methods_pf.py b/sshuttle/tests/client/test_methods_pf.py
index 4ec6fc5..5df57af 100644
--- a/sshuttle/tests/client/test_methods_pf.py
+++ b/sshuttle/tests/client/test_methods_pf.py
@@ -182,8 +182,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1024, 1026,
[(10, u'2404:6800:4004:80c::33')],
10,
- [(10, 64, False, u'2404:6800:4004:80c::'),
- (10, 128, True, u'2404:6800:4004:80c::101f')],
+ [(10, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
+ (10, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
@@ -198,16 +198,15 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
call('-f /dev/stdin', b'pass on lo\n'),
call('-s all'),
call('-a sshuttle6-1024 -f /dev/stdin',
- b'table <forward_subnets> {'
- b'!2404:6800:4004:80c::101f/128,2404:6800:4004:80c::/64'
- b'}\n'
b'table <dns_servers> {2404:6800:4004:80c::33}\n'
- b'rdr pass on lo0 inet6 proto tcp '
- b'to <forward_subnets> -> ::1 port 1024\n'
+ b'rdr pass on lo0 inet6 proto tcp to '
+ b'2404:6800:4004:80c::/64 port 8000:9000 -> ::1 port 1024\n'
b'rdr pass on lo0 inet6 proto udp '
b'to <dns_servers> port 53 -> ::1 port 1026\n'
- b'pass out route-to lo0 inet6 proto tcp '
- b'to <forward_subnets> keep state\n'
+ b'pass out quick inet6 proto tcp to '
+ b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
+ b'pass out route-to lo0 inet6 proto tcp to '
+ b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
b'pass out route-to lo0 inet6 proto udp '
b'to <dns_servers> port 53 keep state\n'),
call('-E'),
@@ -221,7 +220,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1025, 1027,
[(2, u'1.2.3.33')],
2,
- [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
+ [(2, 24, False, u'1.2.3.0', 0, 0),
+ (2, 32, True, u'1.2.3.66', 80, 80)],
True)
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
@@ -232,7 +232,7 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1025, 1027,
[(2, u'1.2.3.33')],
2,
- [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
+ [(2, 24, False, u'1.2.3.0', 0, 0), (2, 32, True, u'1.2.3.66', 80, 80)],
False)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
@@ -247,14 +247,13 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
call('-f /dev/stdin', b'pass on lo\n'),
call('-s all'),
call('-a sshuttle-1025 -f /dev/stdin',
- b'table <forward_subnets> {!1.2.3.66/32,1.2.3.0/24}\n'
b'table <dns_servers> {1.2.3.33}\n'
- b'rdr pass on lo0 inet proto tcp '
- b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
+ b'rdr pass on lo0 inet proto tcp to 1.2.3.0/24 '
+ b'-> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 inet proto udp '
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
- b'pass out route-to lo0 inet proto tcp '
- b'to <forward_subnets> keep state\n'
+ b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
+ b'pass out route-to lo0 inet proto tcp to 1.2.3.0/24 keep state\n'
b'pass out route-to lo0 inet proto udp '
b'to <dns_servers> port 53 keep state\n'),
call('-E'),
@@ -289,23 +288,22 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1024, 1026,
[(10, u'2404:6800:4004:80c::33')],
10,
- [(10, 64, False, u'2404:6800:4004:80c::'),
- (10, 128, True, u'2404:6800:4004:80c::101f')],
+ [(10, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
+ (10, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False)
assert mock_pfctl.mock_calls == [
call('-s all'),
call('-a sshuttle6-1024 -f /dev/stdin',
- b'table <forward_subnets> {'
- b'!2404:6800:4004:80c::101f/128,2404:6800:4004:80c::/64'
- b'}\n'
b'table <dns_servers> {2404:6800:4004:80c::33}\n'
- b'rdr pass on lo0 inet6 proto tcp '
- b'to <forward_subnets> -> ::1 port 1024\n'
+ b'rdr pass on lo0 inet6 proto tcp to 2404:6800:4004:80c::/64 '
+ b'port 8000:9000 -> ::1 port 1024\n'
b'rdr pass on lo0 inet6 proto udp '
b'to <dns_servers> port 53 -> ::1 port 1026\n'
- b'pass out route-to lo0 inet6 proto tcp '
- b'to <forward_subnets> keep state\n'
+ b'pass out quick inet6 proto tcp to '
+ b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
+ b'pass out route-to lo0 inet6 proto tcp to '
+ b'2404:6800:4004:80c::/64 port 8000:9000 keep state\n'
b'pass out route-to lo0 inet6 proto udp '
b'to <dns_servers> port 53 keep state\n'),
call('-e'),
@@ -319,7 +317,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1025, 1027,
[(2, u'1.2.3.33')],
2,
- [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
+ [(2, 24, False, u'1.2.3.0', 0, 0),
+ (2, 32, True, u'1.2.3.66', 80, 80)],
True)
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
@@ -330,7 +329,7 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1025, 1027,
[(2, u'1.2.3.33')],
2,
-