summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Vieira <vieira+github@yubo.be>2017-05-07 04:18:13 +0100
committerBrian May <brian@linuxpenguins.xyz>2017-05-07 13:18:13 +1000
commitc4a41ada09ec6fbfeb1783eb0269cad013842982 (patch)
treea1c7483089e665164a788d06b5c7d1badc7d5cb4
parentef83a5c5736341573c21b39e347f39f5387b6faa (diff)
Adds support for tunneling specific port ranges (#144)
* Adds support for tunneling specific port ranges This set of changes implements the ability of specifying a port or port range for an IP or subnet to only tunnel those ports for that subnet. Also supports excluding a port or port range for a given IP or subnet. When, for a given subnet, there are intercepting ranges being added and excluded, the most specific, i.e., smaller range, takes precedence. In case of a tie the exclusion wins. For different subnets, the most specific, i.e., largest swidth, takes precedence independent of any eventual port ranges. Examples: Tunnels all traffic to the 188.0.0.0/8 subnet except those to port 443. ``` sshuttle -r <server> 188.0.0.0/8 -x 188.0.0.0/8:443 ``` Only tunnels traffic to port 80 of the 188.0.0.0/8 subnet. ``` sshuttle -r <server> 188.0.0.0/8:80 ``` Tunnels traffic to the 188.0.0.0/8 subnet and the port range that goes from 80 to 89. ``` sshuttle -r <server> 188.0.0.0/8:80-89 -x 188.0.0.0/8:80-90 ``` * Allow subnets to be specified with domain names Simplifies the implementation of address parsing by using socket.getaddrinfo(), which can handle domain resolution, IPv4 and IPv6 addresses. This was proposed and mostly implemented by @DavidBuchanan314 in #146. Signed-off-by: David Buchanan <DavidBuchanan314@users.noreply.github.com> Signed-off-by: João Vieira <vieira@yubo.be> * Also use getaddrinfo for parsing listen addr:port * Fixes tests for tunneling a port range * Updates documentation to include port/port range Adds some examples with subnet:port and subnet:port-port. Also clarifies the versions of Python supported on the server while maintaining the recommendation for Python 2.7, 3.5 or later. Mentions support for pfSense. * In Py2 only named arguments may follow *expression Fixes issue in Python 2.7 where *expression may only be followed by named arguments. * Use right regex to extract ip4/6, mask and ports * Tests for parse_subnetport
-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_