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
This commit is contained in:
João Vieira 2017-05-07 04:18:13 +01:00 committed by Brian May
parent ef83a5c573
commit c4a41ada09
15 changed files with 353 additions and 206 deletions

View File

@ -31,11 +31,18 @@ Options
.. option:: subnets .. option:: subnets
A list of subnets to route over the VPN, in the form 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), 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 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 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] .. option:: --method [auto|nat|tproxy|pf]
@ -54,9 +61,11 @@ Options
connections from other machines on your network (ie. to connections from other machines on your network (ie. to
run :program:`sshuttle` on a router) try enabling IP Forwarding in run :program:`sshuttle` on a router) try enabling IP Forwarding in
your kernel, then using ``--listen 0.0.0.0:0``. 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 For the tproxy and pf methods this can be an IPv6 address. Use this option
required, to provide both IPv4 and IPv6 addresses. twice if required, to provide both IPv4 and IPv6 addresses.
.. option:: -H, --auto-hosts .. option:: -H, --auto-hosts
@ -176,7 +185,7 @@ Options
.. option:: --disable-ipv6 .. 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 .. option:: --firewall

View File

@ -4,7 +4,7 @@ Overview
As far as I know, sshuttle is the only program that solves the following As far as I know, sshuttle is the only program that solves the following
common case: 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. - You have access to a remote network via ssh.

View File

@ -41,7 +41,7 @@ order to get the ``recvmsg()`` function. See :doc:`tproxy` for more
information. information.
MacOS / FreeBSD / OpenBSD MacOS / FreeBSD / OpenBSD / pfSense
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
Method: pf Method: pf
@ -65,8 +65,9 @@ cmd.exe with Administrator access. See :doc:`windows` for more information.
Server side Requirements Server side Requirements
------------------------ ------------------------
Server requirements are more relaxed, however it is recommended that you use The server can run in any version of Python between 2.4 and 3.6.
Python 2.7 or Python 3.5. 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 Additional Suggested Software

View File

@ -255,12 +255,13 @@ class FirewallClient:
def start(self): def start(self):
self.pfile.write(b'ROUTES\n') self.pfile.write(b'ROUTES\n')
for (family, ip, width) in self.subnets_include + self.auto_nets: for (family, ip, width, fport, lport) \
self.pfile.write(b'%d,%d,0,%s\n' in self.subnets_include + self.auto_nets:
% (family, width, ip.encode("ASCII"))) self.pfile.write(b'%d,%d,0,%s,%d,%d\n'
for (family, ip, width) in self.subnets_exclude: % (family, width, ip.encode("ASCII"), fport, lport))
self.pfile.write(b'%d,%d,1,%s\n' for (family, ip, width, fport, lport) in self.subnets_exclude:
% (family, width, ip.encode("ASCII"))) self.pfile.write(b'%d,%d,1,%s,%d,%d\n'
% (family, width, ip.encode("ASCII"), fport, lport))
self.pfile.write(b'NSLIST\n') self.pfile.write(b'NSLIST\n')
for (family, ip) in self.nslist: 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)) debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
else: else:
debug2("Adding auto net %d/%s/%d\n" % (family, ip, width)) 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 # we definitely want to do this *after* starting ssh, or we might end
# up intercepting the ssh connection! # up intercepting the ssh connection!
@ -591,11 +592,11 @@ def main(listenip_v6, listenip_v4,
if required.ipv4 and \ if required.ipv4 and \
not any(listenip_v4[0] == sex[1] for sex in subnets_v4): 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 \ if required.ipv6 and \
not any(listenip_v6[0] == sex[1] for sex in subnets_v6): 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 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 # if both ports given, no need to search for a spare port

View File

@ -1,10 +1,11 @@
import re import re
import socket
import sshuttle.helpers as helpers import sshuttle.helpers as helpers
import sshuttle.client as client import sshuttle.client as client
import sshuttle.firewall as firewall import sshuttle.firewall as firewall
import sshuttle.hostwatch as hostwatch import sshuttle.hostwatch as hostwatch
import sshuttle.ssyslog as ssyslog 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 from sshuttle.helpers import family_ip_tuple, log, Fatal
@ -46,10 +47,11 @@ def main():
ipport_v4 = None ipport_v4 = None
list = opt.listen.split(",") list = opt.listen.split(",")
for ip in list: for ip in list:
if '[' in ip and ']' in ip: family, ip, port = parse_ipport(ip)
ipport_v6 = parse_ipport6(ip) if family == socket.AF_INET6:
ipport_v6 = (ip, port)
else: else:
ipport_v4 = parse_ipport4(ip) ipport_v4 = (ip, port)
else: else:
# parse_ipport4('127.0.0.1:0') # parse_ipport4('127.0.0.1:0')
ipport_v4 = "auto" ipport_v4 = "auto"

View File

@ -74,6 +74,15 @@ def setup_daemon():
return sys.stdin, sys.stdout 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 # This is some voodoo for setting up the kernel's transparent
# proxying stuff. If subnets is empty, we just delete our sshuttle rules; # proxying stuff. If subnets is empty, we just delete our sshuttle rules;
# otherwise we delete it, then make them from scratch. # otherwise we delete it, then make them from scratch.
@ -119,10 +128,17 @@ def main(method_name, syslog):
elif line.startswith("NSLIST\n"): elif line.startswith("NSLIST\n"):
break break
try: try:
(family, width, exclude, ip) = line.strip().split(',', 3) (family, width, exclude, ip, fport, lport) = \
line.strip().split(',', 5)
except: except:
raise Fatal('firewall: expected route or NSLIST but got %r' % line) 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) debug2('firewall manager: Got subnets: %r\n' % subnets)
nslist = [] nslist = []

View File

@ -1,4 +1,5 @@
import socket import socket
from sshuttle.firewall import subnet_weight
from sshuttle.helpers import family_to_string from sshuttle.helpers import family_to_string
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal
from sshuttle.methods import BaseMethod from sshuttle.methods import BaseMethod
@ -38,22 +39,21 @@ class Method(BaseMethod):
_ipt('-I', 'OUTPUT', '1', '-j', chain) _ipt('-I', 'OUTPUT', '1', '-j', chain)
_ipt('-I', 'PREROUTING', '1', '-j', chain) _ipt('-I', 'PREROUTING', '1', '-j', chain)
# create new subnet entries. Note that we're sorting in a very # create new subnet entries.
# particular order: we need to go from most-specific (largest for f, swidth, sexclude, snet, fport, lport \
# swidth) to least-specific, and at any given level of specificity, in sorted(subnets, key=subnet_weight, reverse=True):
# we want excludes to come first. That's why the columns are in tcp_ports = ('-p', 'tcp')
# such a non- intuitive order. if fport:
for f, swidth, sexclude, snet \ tcp_ports = tcp_ports + ('--dport', '%d:%d' % (fport, lport))
in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude: if sexclude:
_ipt('-A', chain, '-j', 'RETURN', _ipt('-A', chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp') *tcp_ports)
else: else:
_ipt_ttl('-A', chain, '-j', 'REDIRECT', _ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp', *(tcp_ports + ('--to-ports', str(port))))
'--to-ports', str(port))
for f, ip in [i for i in nslist if i[0] == family]: for f, ip in [i for i in nslist if i[0] == family]:
_ipt_ttl('-A', chain, '-j', 'REDIRECT', _ipt_ttl('-A', chain, '-j', 'REDIRECT',

View File

@ -9,6 +9,7 @@ import shlex
from fcntl import ioctl from fcntl import ioctl
from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \ from ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
sizeof, addressof, memmove sizeof, addressof, memmove
from sshuttle.firewall import subnet_weight
from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string from sshuttle.helpers import debug1, debug2, debug3, Fatal, family_to_string
from sshuttle.methods import BaseMethod from sshuttle.methods import BaseMethod
@ -186,16 +187,18 @@ class FreeBsd(Generic):
inet_version = self._inet_version(family) inet_version = self._inet_version(family)
lo_addr = self._lo_addr(family) lo_addr = self._lo_addr(family)
tables = [ tables = []
b'table <forward_subnets> {%s}' % b','.join(includes)
]
translating_rules = [ translating_rules = [
b'rdr pass on lo0 %s proto tcp to <forward_subnets> ' b'rdr pass on lo0 %s proto tcp to %s '
b'-> %s port %r' % (inet_version, lo_addr, port) b'-> %s port %r' % (inet_version, subnet, lo_addr, port)
for exclude, subnet in includes if not exclude
] ]
filtering_rules = [ filtering_rules = [
b'pass out route-to lo0 %s proto tcp ' 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: if len(nslist) > 0:
@ -254,16 +257,18 @@ class OpenBsd(Generic):
inet_version = self._inet_version(family) inet_version = self._inet_version(family)
lo_addr = self._lo_addr(family) lo_addr = self._lo_addr(family)
tables = [ tables = []
b'table <forward_subnets> {%s}' % b','.join(includes)
]
translating_rules = [ translating_rules = [
b'pass in on lo0 %s proto tcp to <forward_subnets> ' b'pass in on lo0 %s proto tcp to %s '
b'divert-to %s port %r' % (inet_version, lo_addr, port) b'divert-to %s port %r' % (inet_version, subnet, lo_addr, port)
for exclude, subnet in includes if not exclude
] ]
filtering_rules = [ filtering_rules = [
b'pass out %s proto tcp to <forward_subnets> ' b'pass out %s proto tcp to %s '
b'route-to lo0 keep state' % inet_version 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: if len(nslist) > 0:
@ -429,12 +434,12 @@ class Method(BaseMethod):
# If a given subnet is both included and excluded, list the # If a given subnet is both included and excluded, list the
# exclusion first; the table will ignore the second, opposite # exclusion first; the table will ignore the second, opposite
# definition # definition
for f, swidth, sexclude, snet in sorted( for f, swidth, sexclude, snet, fport, lport \
subnets, key=lambda s: (s[1], s[2]), reverse=True): in sorted(subnets, key=subnet_weight, reverse=True):
includes.append(b"%s%s/%d" % includes.append((sexclude, b"%s/%d%s" % (
(b"!" if sexclude else b"", snet.encode("ASCII"),
snet.encode("ASCII"), swidth,
swidth)) b" port %d:%d" % (fport, lport) if fport else b"")))
anchor = pf_get_anchor(family, port) anchor = pf_get_anchor(family, port)
pf.add_anchors(anchor) pf.add_anchors(anchor)

View File

@ -1,4 +1,5 @@
import struct import struct
from sshuttle.firewall import subnet_weight
from sshuttle.helpers import family_to_string from sshuttle.helpers import family_to_string
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
from sshuttle.methods import BaseMethod from sshuttle.methods import BaseMethod
@ -163,6 +164,11 @@ class Method(BaseMethod):
def _ipt_ttl(*args): def _ipt_ttl(*args):
return ipt_ttl(family, table, *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 mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port divert_chain = 'sshuttle-d-%s' % port
@ -197,33 +203,44 @@ class Method(BaseMethod):
'-m', 'udp', '-p', 'udp', '--dport', '53', '-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport)) '--on-port', str(dnsport))
for f, swidth, sexclude, snet \ for f, swidth, sexclude, snet, fport, lport \
in sorted(subnets, key=lambda s: s[1], reverse=True): in sorted(subnets, key=subnet_weight, reverse=True):
tcp_ports = ('-p', 'tcp')
tcp_ports = _ipt_proto_ports(tcp_ports, fport, lport)
if sexclude: if sexclude:
_ipt('-A', mark_chain, '-j', 'RETURN', _ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp') '-m', 'tcp',
*tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'RETURN', _ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp') '-m', 'tcp',
*tcp_ports)
else: else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp') '-m', 'tcp',
*tcp_ports)
_ipt('-A', tproxy_chain, '-j', 'TPROXY', _ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp', '-m', 'tcp',
'--on-port', str(port)) *(tcp_ports + ('--on-port', str(port))))
if udp: if udp:
udp_ports = ('-p', 'udp')
udp_ports = _ipt_proto_ports(udp_ports, fport, lport)
if sexclude: if sexclude:
_ipt('-A', mark_chain, '-j', 'RETURN', _ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp') '-m', 'udp',
*udp_ports)
_ipt('-A', tproxy_chain, '-j', 'RETURN', _ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp') '-m', 'udp',
*udp_ports)
else: else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1', _ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
@ -231,8 +248,8 @@ class Method(BaseMethod):
_ipt('-A', tproxy_chain, '-j', 'TPROXY', _ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth), '--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp', '-m', 'udp',
'--on-port', str(port)) *(udp_ports + ('--on-port', str(port))))
def restore_firewall(self, port, family, udp): def restore_firewall(self, port, family, udp):
if family not in [socket.AF_INET, socket.AF_INET6]: if family not in [socket.AF_INET, socket.AF_INET6]:

View File

@ -4,41 +4,8 @@ from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
from sshuttle import __version__ 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 # Subnet file, supporting empty lines and hash-started comment lines
def parse_subnet_file(s): def parse_subnetport_file(s):
try: try:
handle = open(s, 'r') handle = open(s, 'r')
except OSError: except OSError:
@ -52,47 +19,66 @@ def parse_subnet_file(s):
continue continue
if line[0] == '#': if line[0] == '#':
continue continue
subnets.append(parse_subnet(line)) subnets.append(parse_subnetport(line))
return subnets return subnets
# 1.2.3.4/5 or just 1.2.3.4 # 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 or just 1:2::3 # [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
def parse_subnet(subnet_str): # example.com:123 or just example.com
if ':' in subnet_str: def parse_subnetport(s):
return parse_subnet6(subnet_str) if s.count(':') > 1:
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
else: 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 # 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) 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: if not m:
raise Fatal('%r is not a valid IP:port format' % s) 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 try:
def parse_ipport6(s): addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
s = str(s) except socket.gaierror:
m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s) raise Fatal('%r is not a valid IP:port format' % s)
if not m:
raise Fatal('%s is not a valid IP:port format' % s) family, _, _, _, addr = min(addrinfo)
(ip, port) = m.groups() return (family,) + addr[:2]
(ip, port) = (ip or '::', int(port or 0))
return (ip, port)
def parse_list(list): def parse_list(list):
@ -116,9 +102,9 @@ parser = ArgumentParser(
) )
parser.add_argument( parser.add_argument(
"subnets", "subnets",
metavar="IP/MASK [IP/MASK...]", metavar="IP/MASK[:PORT[-PORT]]...",
nargs="*", nargs="*",
type=parse_subnet, type=parse_subnetport,
help=""" help="""
capture and forward traffic to these subnets (whitespace separated) capture and forward traffic to these subnets (whitespace separated)
""" """
@ -185,10 +171,10 @@ parser.add_argument(
) )
parser.add_argument( parser.add_argument(
"-x", "--exclude", "-x", "--exclude",
metavar="IP/MASK", metavar="IP/MASK[:PORT[-PORT]]",
action="append", action="append",
default=[], default=[],
type=parse_subnet, type=parse_subnetport,
help=""" help="""
exclude this subnet (can be used more than once) exclude this subnet (can be used more than once)
""" """
@ -198,7 +184,7 @@ parser.add_argument(
metavar="PATH", metavar="PATH",
action=Concat, action=Concat,
dest="exclude", dest="exclude",
type=parse_subnet_file, type=parse_subnetport_file,
help=""" help="""
exclude the subnets in a file (whitespace separated) exclude the subnets in a file (whitespace separated)
""" """
@ -271,7 +257,7 @@ parser.add_argument(
action=Concat, action=Concat,
dest="subnets_file", dest="subnets_file",
default=[], default=[],
type=parse_subnet_file, type=parse_subnetport_file,
help=""" help="""
file where the subnets are stored, instead of on the command line file where the subnets are stored, instead of on the command line
""" """

View File

@ -6,10 +6,10 @@ import sshuttle.firewall
def setup_daemon(): def setup_daemon():
stdin = io.StringIO(u"""ROUTES stdin = io.StringIO(u"""ROUTES
2,24,0,1.2.3.0 2,24,0,1.2.3.0,8000,9000
2,32,1,1.2.3.66 2,32,1,1.2.3.66,8080,8080
10,64,0,2404:6800:4004:80c:: 10,64,0,2404:6800:4004:80c::,0,0
10,128,1,2404:6800:4004:80c::101f 10,128,1,2404:6800:4004:80c::101f,80,80
NSLIST NSLIST
2,1.2.3.33 2,1.2.3.33
10,2404:6800:4004:80c::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, 1024, 1026,
[(10, u'2404:6800:4004:80c::33')], [(10, u'2404:6800:4004:80c::33')],
10, 10,
[(10, 64, False, u'2404:6800:4004:80c::'), [(10, 64, False, u'2404:6800:4004:80c::', 0, 0),
(10, 128, True, u'2404:6800:4004:80c::101f')], (10, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
True), True),
call().setup_firewall( call().setup_firewall(
1025, 1027, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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), True),
call().restore_firewall(1024, 10, True), call().restore_firewall(1024, 10, True),
call().restore_firewall(1025, 2, True), call().restore_firewall(1025, 2, True),

View File

@ -86,8 +86,8 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
1024, 1026, 1024, 1026,
[(10, u'2404:6800:4004:80c::33')], [(10, u'2404:6800:4004:80c::33')],
10, 10,
[(10, 64, False, u'2404:6800:4004:80c::'), [(10, 64, False, u'2404:6800:4004:80c::', 0, 0),
(10, 128, True, u'2404:6800:4004:80c::101f')], (10, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
True) True)
assert str(excinfo.value) \ assert str(excinfo.value) \
== 'Address family "AF_INET6" unsupported by nat method_name' == '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, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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) True)
assert str(excinfo.value) == 'UDP not supported by nat method_name' assert str(excinfo.value) == 'UDP not supported by nat method_name'
assert mock_ipt_chain_exists.mock_calls == [] 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, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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) False)
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(2, 'nat', 'sshuttle-1025') call(2, 'nat', 'sshuttle-1025')
] ]
assert mock_ipt_ttl.mock_calls == [ assert mock_ipt_ttl.mock_calls == [
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT', 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', call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
'--dest', u'1.2.3.33/32', '-p', 'udp', '--dest', u'1.2.3.33/32', '-p', 'udp',
'--dport', '53', '--to-ports', '1027') '--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', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
call(2, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'), call(2, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
call(2, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN', 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_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock() mock_ipt_ttl.reset_mock()

View File

@ -182,8 +182,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1024, 1026, 1024, 1026,
[(10, u'2404:6800:4004:80c::33')], [(10, u'2404:6800:4004:80c::33')],
10, 10,
[(10, 64, False, u'2404:6800:4004:80c::'), [(10, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(10, 128, True, u'2404:6800:4004:80c::101f')], (10, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False) False)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY), 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('-f /dev/stdin', b'pass on lo\n'),
call('-s all'), call('-s all'),
call('-a sshuttle6-1024 -f /dev/stdin', 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'table <dns_servers> {2404:6800:4004:80c::33}\n'
b'rdr pass on lo0 inet6 proto tcp ' b'rdr pass on lo0 inet6 proto tcp to '
b'to <forward_subnets> -> ::1 port 1024\n' b'2404:6800:4004:80c::/64 port 8000:9000 -> ::1 port 1024\n'
b'rdr pass on lo0 inet6 proto udp ' b'rdr pass on lo0 inet6 proto udp '
b'to <dns_servers> port 53 -> ::1 port 1026\n' b'to <dns_servers> port 53 -> ::1 port 1026\n'
b'pass out route-to lo0 inet6 proto tcp ' b'pass out quick inet6 proto tcp to '
b'to <forward_subnets> keep state\n' 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'pass out route-to lo0 inet6 proto udp '
b'to <dns_servers> port 53 keep state\n'), b'to <dns_servers> port 53 keep state\n'),
call('-E'), call('-E'),
@ -221,7 +220,8 @@ def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1025, 1027, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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) True)
assert str(excinfo.value) == 'UDP not supported by pf method_name' assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] 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, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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) False)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY), 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('-f /dev/stdin', b'pass on lo\n'),
call('-s all'), call('-s all'),
call('-a sshuttle-1025 -f /dev/stdin', 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'table <dns_servers> {1.2.3.33}\n'
b'rdr pass on lo0 inet proto tcp ' b'rdr pass on lo0 inet proto tcp to 1.2.3.0/24 '
b'to <forward_subnets> -> 127.0.0.1 port 1025\n' b'-> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 inet proto udp ' b'rdr pass on lo0 inet proto udp '
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n' b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
b'pass out route-to lo0 inet proto tcp ' b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
b'to <forward_subnets> keep state\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'pass out route-to lo0 inet proto udp '
b'to <dns_servers> port 53 keep state\n'), b'to <dns_servers> port 53 keep state\n'),
call('-E'), call('-E'),
@ -289,23 +288,22 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1024, 1026, 1024, 1026,
[(10, u'2404:6800:4004:80c::33')], [(10, u'2404:6800:4004:80c::33')],
10, 10,
[(10, 64, False, u'2404:6800:4004:80c::'), [(10, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(10, 128, True, u'2404:6800:4004:80c::101f')], (10, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False) False)
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-s all'), call('-s all'),
call('-a sshuttle6-1024 -f /dev/stdin', 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'table <dns_servers> {2404:6800:4004:80c::33}\n'
b'rdr pass on lo0 inet6 proto tcp ' b'rdr pass on lo0 inet6 proto tcp to 2404:6800:4004:80c::/64 '
b'to <forward_subnets> -> ::1 port 1024\n' b'port 8000:9000 -> ::1 port 1024\n'
b'rdr pass on lo0 inet6 proto udp ' b'rdr pass on lo0 inet6 proto udp '
b'to <dns_servers> port 53 -> ::1 port 1026\n' b'to <dns_servers> port 53 -> ::1 port 1026\n'
b'pass out route-to lo0 inet6 proto tcp ' b'pass out quick inet6 proto tcp to '
b'to <forward_subnets> keep state\n' 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'pass out route-to lo0 inet6 proto udp '
b'to <dns_servers> port 53 keep state\n'), b'to <dns_servers> port 53 keep state\n'),
call('-e'), call('-e'),
@ -319,7 +317,8 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1025, 1027, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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) True)
assert str(excinfo.value) == 'UDP not supported by pf method_name' assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] 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, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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) False)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY),
@ -343,14 +342,13 @@ def test_setup_firewall_freebsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
assert mock_pfctl.mock_calls == [ assert mock_pfctl.mock_calls == [
call('-s all'), call('-s all'),
call('-a sshuttle-1025 -f /dev/stdin', 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'table <dns_servers> {1.2.3.33}\n'
b'rdr pass on lo0 inet proto tcp ' b'rdr pass on lo0 inet proto tcp to 1.2.3.0/24 -> '
b'to <forward_subnets> -> 127.0.0.1 port 1025\n' b'127.0.0.1 port 1025\n'
b'rdr pass on lo0 inet proto udp ' b'rdr pass on lo0 inet proto udp '
b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n' b'to <dns_servers> port 53 -> 127.0.0.1 port 1027\n'
b'pass out route-to lo0 inet proto tcp ' b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
b'to <forward_subnets> keep state\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'pass out route-to lo0 inet proto udp '
b'to <dns_servers> port 53 keep state\n'), b'to <dns_servers> port 53 keep state\n'),
call('-e'), call('-e'),
@ -385,8 +383,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1024, 1026, 1024, 1026,
[(10, u'2404:6800:4004:80c::33')], [(10, u'2404:6800:4004:80c::33')],
10, 10,
[(10, 64, False, u'2404:6800:4004:80c::'), [(10, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(10, 128, True, u'2404:6800:4004:80c::101f')], (10, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
False) False)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
@ -398,16 +396,15 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
call('-f /dev/stdin', b'match on lo\n'), call('-f /dev/stdin', b'match on lo\n'),
call('-s all'), call('-s all'),
call('-a sshuttle6-1024 -f /dev/stdin', 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'table <dns_servers> {2404:6800:4004:80c::33}\n'
b'pass in on lo0 inet6 proto tcp to ' b'pass in on lo0 inet6 proto tcp to 2404:6800:4004:80c::/64 '
b'<forward_subnets> divert-to ::1 port 1024\n' b'port 8000:9000 divert-to ::1 port 1024\n'
b'pass in on lo0 inet6 proto udp ' b'pass in on lo0 inet6 proto udp '
b'to <dns_servers> port 53 rdr-to ::1 port 1026\n' b'to <dns_servers> port 53 rdr-to ::1 port 1026\n'
b'pass out inet6 proto tcp to ' b'pass out quick inet6 proto tcp to '
b'<forward_subnets> route-to lo0 keep state\n' b'2404:6800:4004:80c::101f/128 port 8080:8080\n'
b'pass out inet6 proto tcp to 2404:6800:4004:80c::/64 '
b'port 8000:9000 route-to lo0 keep state\n'
b'pass out inet6 proto udp to ' b'pass out inet6 proto udp to '
b'<dns_servers> port 53 route-to lo0 keep state\n'), b'<dns_servers> port 53 route-to lo0 keep state\n'),
call('-e'), call('-e'),
@ -421,7 +418,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1025, 1027, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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) True)
assert str(excinfo.value) == 'UDP not supported by pf method_name' assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == [] assert mock_pf_get_dev.mock_calls == []
@ -432,7 +430,8 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
1025, 1027, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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) False)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xcd48441a, ANY), call(mock_pf_get_dev(), 0xcd48441a, ANY),
@ -443,13 +442,13 @@ def test_setup_firewall_openbsd(mock_pf_get_dev, mock_ioctl, mock_pfctl):
call('-f /dev/stdin', b'match on lo\n'), call('-f /dev/stdin', b'match on lo\n'),
call('-s all'), call('-s all'),
call('-a sshuttle-1025 -f /dev/stdin', 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'table <dns_servers> {1.2.3.33}\n'
b'pass in on lo0 inet proto tcp to <forward_subnets> divert-to 127.0.0.1 port 1025\n' b'pass in on lo0 inet proto tcp to 1.2.3.0/24 divert-to '
b'127.0.0.1 port 1025\n'
b'pass in on lo0 inet proto udp to ' b'pass in on lo0 inet proto udp to '
b'<dns_servers> port 53 rdr-to 127.0.0.1 port 1027\n' b'<dns_servers> port 53 rdr-to 127.0.0.1 port 1027\n'
b'pass out inet proto tcp to ' b'pass out quick inet proto tcp to 1.2.3.66/32 port 80:80\n'
b'<forward_subnets> route-to lo0 keep state\n' b'pass out inet proto tcp to 1.2.3.0/24 route-to lo0 keep state\n'
b'pass out inet proto udp to ' b'pass out inet proto udp to '
b'<dns_servers> port 53 route-to lo0 keep state\n'), b'<dns_servers> port 53 route-to lo0 keep state\n'),
call('-e'), call('-e'),

View File

@ -102,8 +102,8 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
1024, 1026, 1024, 1026,
[(10, u'2404:6800:4004:80c::33')], [(10, u'2404:6800:4004:80c::33')],
10, 10,
[(10, 64, False, u'2404:6800:4004:80c::'), [(10, 64, False, u'2404:6800:4004:80c::', 8000, 9000),
(10, 128, True, u'2404:6800:4004:80c::101f')], (10, 128, True, u'2404:6800:4004:80c::101f', 8080, 8080)],
True) True)
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(10, 'mangle', 'sshuttle-m-1024'), call(10, 'mangle', 'sshuttle-m-1024'),
@ -144,28 +144,30 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'), '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1026'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128', '--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'tcp', '-p', 'tcp'), '-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN', call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128', '--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'tcp', '-p', 'tcp'), '-m', 'tcp', '-p', 'tcp', '--dport', '8080:8080'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN', call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128', '--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp'), '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN', call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'RETURN',
'--dest', u'2404:6800:4004:80c::101f/128', '--dest', u'2404:6800:4004:80c::101f/128',
'-m', 'udp', '-p', 'udp'), '-m', 'udp', '-p', 'udp', '--dport', '8080:8080'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp'), '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'tcp', '-p', 'tcp', '--on-port', '1024'), '-m', 'tcp', '-p', 'tcp', '--dport', '8000:9000',
'--on-port', '1024'),
call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK', call(10, 'mangle', '-A', 'sshuttle-m-1024', '-j', 'MARK',
'--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64', '--set-mark', '1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp'), '-m', 'udp', '-p', 'udp'),
call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY', call(10, 'mangle', '-A', 'sshuttle-t-1024', '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64', '--tproxy-mark', '0x1/0x1', '--dest', u'2404:6800:4004:80c::/64',
'-m', 'udp', '-p', 'udp', '--on-port', '1024') '-m', 'udp', '-p', 'udp', '--dport', '8000:9000',
'--on-port', '1024')
] ]
mock_ipt_chain_exists.reset_mock() mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock() mock_ipt_ttl.reset_mock()
@ -198,7 +200,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
1025, 1027, 1025, 1027,
[(2, u'1.2.3.33')], [(2, u'1.2.3.33')],
2, 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) True)
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(2, 'mangle', 'sshuttle-m-1025'), call(2, 'mangle', 'sshuttle-m-1025'),
@ -237,13 +239,17 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
'--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32', '--tproxy-mark', '0x1/0x1', '--dest', u'1.2.3.33/32',
'-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'), '-m', 'udp', '-p', 'udp', '--dport', '53', '--on-port', '1027'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'), '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
'--dport', '80:80'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN', call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp'), '--dest', u'1.2.3.66/32', '-m', 'tcp', '-p', 'tcp',
'--dport', '80:80'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN', call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'), '--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
'--dport', '80:80'),
call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN', call(2, 'mangle', '-A', 'sshuttle-t-1025', '-j', 'RETURN',
'--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp'), '--dest', u'1.2.3.66/32', '-m', 'udp', '-p', 'udp',
'--dport', '80:80'),
call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK', call(2, 'mangle', '-A', 'sshuttle-m-1025', '-j', 'MARK',
'--set-mark', '1', '--dest', u'1.2.3.0/24', '--set-mark', '1', '--dest', u'1.2.3.0/24',
'-m', 'tcp', '-p', 'tcp'), '-m', 'tcp', '-p', 'tcp'),

View File

@ -0,0 +1,101 @@
import socket
import pytest
import sshuttle.options
from argparse import ArgumentTypeError as Fatal
_ip4_reprs = {
'0.0.0.0': '0.0.0.0',
'255.255.255.255': '255.255.255.255',
'10.0': '10.0.0.0',
'184.172.10.74': '184.172.10.74',
'3098282570': '184.172.10.74',
'0xb8.0xac.0x0a.0x4a': '184.172.10.74',
'0270.0254.0012.0112': '184.172.10.74',
'localhost': '127.0.0.1'
}
_ip4_swidths = (1, 8, 22, 27, 32)
_ip6_reprs = {
'::': '::',
'::1': '::1',
'fc00::': 'fc00::',
'2a01:7e00:e000:188::1': '2a01:7e00:e000:188::1'
}
_ip6_swidths = (48, 64, 96, 115, 128)
def test_parse_subnetport_ip4():
for ip_repr, ip in _ip4_reprs.items():
assert sshuttle.options.parse_subnetport(ip_repr) \
== (socket.AF_INET, ip, 32, 0, 0)
with pytest.raises(Fatal) as excinfo:
sshuttle.options.parse_subnetport('10.256.0.0')
assert str(excinfo.value) == 'Unable to resolve address: 10.256.0.0'
def test_parse_subnetport_ip4_with_mask():
for ip_repr, ip in _ip4_reprs.items():
for swidth in _ip4_swidths:
assert sshuttle.options.parse_subnetport(
'/'.join((ip_repr, str(swidth)))
) == (socket.AF_INET, ip, swidth, 0, 0)
assert sshuttle.options.parse_subnetport('0/0') \
== (socket.AF_INET, '0.0.0.0', 0, 0, 0)
with pytest.raises(Fatal) as excinfo:
sshuttle.options.parse_subnetport('10.0.0.0/33')
assert str(excinfo.value) == 'width 33 is not between 0 and 32'
def test_parse_subnetport_ip4_with_port():
for ip_repr, ip in _ip4_reprs.items():
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80'))) \
== (socket.AF_INET, ip, 32, 80, 80)
assert sshuttle.options.parse_subnetport(':'.join((ip_repr, '80-90'))) \
== (socket.AF_INET, ip, 32, 80, 90)
def test_parse_subnetport_ip4_with_mask_and_port():
for ip_repr, ip in _ip4_reprs.items():
assert sshuttle.options.parse_subnetport(ip_repr + '/32:80') \
== (socket.AF_INET, ip, 32, 80, 80)
assert sshuttle.options.parse_subnetport(ip_repr + '/16:80-90') \
== (socket.AF_INET, ip, 16, 80, 90)
def test_parse_subnetport_ip6():
for ip_repr, ip in _ip6_reprs.items():
assert sshuttle.options.parse_subnetport(ip_repr) \
== (socket.AF_INET6, ip, 128, 0, 0)
with pytest.raises(Fatal) as excinfo:
sshuttle.options.parse_subnetport('2001::1::3f')
assert str(excinfo.value) == 'Unable to resolve address: 2001::1::3f'
def test_parse_subnetport_ip6_with_mask():
for ip_repr, ip in _ip6_reprs.items():
for swidth in _ip4_swidths + _ip6_swidths:
assert sshuttle.options.parse_subnetport(
'/'.join((ip_repr, str(swidth)))
) == (socket.AF_INET6, ip, swidth, 0, 0)
assert sshuttle.options.parse_subnetport('::/0') \
== (socket.AF_INET6, '::', 0, 0, 0)
with pytest.raises(Fatal) as excinfo:
sshuttle.options.parse_subnetport('fc00::/129')
assert str(excinfo.value) == 'width 129 is not between 0 and 128'
def test_parse_subnetport_ip6_with_port():
for ip_repr, ip in _ip6_reprs.items():
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80') \
== (socket.AF_INET6, ip, 128, 80, 80)
assert sshuttle.options.parse_subnetport('[' + ip_repr + ']:80-90') \
== (socket.AF_INET6, ip, 128, 80, 90)
def test_parse_subnetport_ip6_with_mask_and_port():
for ip_repr, ip in _ip6_reprs.items():
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/128]:80') \
== (socket.AF_INET6, ip, 128, 80, 80)
assert sshuttle.options.parse_subnetport('[' + ip_repr + '/16]:80-90') \
== (socket.AF_INET6, ip, 16, 80, 90)