Compare commits

...

57 Commits
v0.73 ... v0.74

Author SHA1 Message Date
2e237b8fbe Remember to increment version. 2016-01-10 10:05:50 +11:00
098916a8de Version 0.74 2016-01-10 10:02:14 +11:00
d3624332dc Fix documentation.
Should work even with different python versions on client and server.
2016-01-10 10:01:47 +11:00
b4b283b214 fixes the sshuttle entry-point in setup.py
This fixes the following error:

    "import_name": entry.suffix.split(".")[0],
    AttributeError: 'NoneType' object has no attribute 'split'

See
https://pythonhosted.org/setuptools/setuptools.html#automatic-script-creation
2016-01-09 20:04:58 +00:00
1c46f25e13 Fixed str being used as bytes in hostwatch
This should solve the TypeError reported in #53 and some others I found
while testing the fix.

Closes: #53
2016-01-07 14:16:03 +11:00
11838d65c2 Adds support for FreeBSD PF
The PF firewall that is included in the FreeBSD base system does not
have exactly the same data structures as the OSX version. This commit
fixes the offsets and some field types that are also different. Tested
with FreeBSD 10.2 and OSX 10.11.2.
2016-01-05 18:00:57 +11:00
e433c599e4 IPv6 routes must be added manually 2015-12-15 14:26:39 +11:00
ba60d22478 Add another test. 2015-12-15 14:23:42 +11:00
3db38c992a Replace numbered points with dot points. 2015-12-15 14:23:19 +11:00
1e81bf3dfc Mirror setup/restore logic 2015-12-15 13:39:00 +11:00
7362ba9f52 If listenip_v6 we should declare ipv6 required 2015-12-15 13:31:03 +11:00
b207d1d0d6 Fixes for --auto-nets 2015-12-15 13:30:34 +11:00
56e3b22820 Add FIXME comment. 2015-12-15 13:29:04 +11:00
02fa49627f Fix server side Python3 issues.
Closes: #49.
2015-12-15 12:51:29 +11:00
ce5187100c Add to TPROXY documentation 2015-12-15 11:48:34 +11:00
bdc7d3a97c Fix UDP Python 3.5 issues.
Closes: #48
2015-12-15 11:41:48 +11:00
90654b4fb9 Simplify selection of features 2015-12-15 11:40:55 +11:00
6b4e36c528 Declare DNS support as feature 2015-12-14 21:00:31 +11:00
eed917f062 Don't declare udp feature without recvmsg 2015-12-14 20:59:26 +11:00
74f2d9ca7e Ensure Fatal errors are really Fatal 2015-12-14 20:51:49 +11:00
1e04eb1616 Updates to TPROXY docs. 2015-12-14 20:27:47 +11:00
117afc7a68 Fixed dictionary changed size during iteration
The removal loop should probably be outside the iteration loop.
2015-12-14 16:46:11 +11:00
c61984088b Test PF on non-darwin. 2015-12-14 09:28:43 +11:00
e63e121354 Print PF rules used.
Also support multiline debug output better.
2015-12-14 09:21:15 +11:00
2b235331d0 Split setup_firewall method.
* setup_firewall sets the firewall up.
* restore_firewall restores the firewall to initial state.
2015-12-13 11:56:18 +11:00
2eeea9536a Fixed str being used as bytes in daemonize 2015-12-09 16:32:39 +11:00
9a77d03edf Respect --syslog as soon as possible
When executing with the option --syslog start redirecting to
syslog immediately after the command line options are validated.
This way when using with some init daemon, e.g., upstart all the
relevant information (connection failures, etc) can be retrieved from
the log instead of being lost to stdout or stderr.
2015-12-09 14:46:11 +11:00
4fdd715bc1 Don't change object while iterating
Closes: #40
2015-12-09 10:29:40 +11:00
bea723c598 Add tox.ini file. 2015-12-07 13:17:09 +11:00
1ae4fce6b3 Fix logging with pf method and Python 3.5 2015-12-07 13:16:47 +11:00
118171af7f Fix get_tcp_dstip with MacOSX/Python3.5 2015-12-07 07:14:26 +11:00
3367124e6b Fix more brokenness. 2015-12-06 11:45:49 +11:00
aaa6062329 Remove IPFW support.
This is no longer used by modern MacOSX and not getting tested.

It also required a do_wait() function which was a complication for
sshuttle as a whole.

Can get resurrected if required.
2015-12-06 11:33:52 +11:00
da4ce19121 Fix MacOSX tests. 2015-12-06 11:24:38 +11:00
12d4b304c3 Fix another MacOSX/Python3.5 issue. 2015-12-06 11:24:11 +11:00
bd97506f7d Fixup firewall tests. 2015-12-06 11:02:31 +11:00
53c07f7d90 hostmap shouldn't be global. 2015-12-06 11:00:12 +11:00
7e0c1534df Be more explicit 2015-12-06 10:58:51 +11:00
a3fbf860ff Fix more MacOSX/Python3.5 issues. 2015-12-05 20:21:36 +11:00
7a9e36d211 Fix MacOSX/Python3.5 issues.
Closes: #36.
2015-12-05 16:41:33 +11:00
65e81d51c6 Try Python3.5 by default.
Python 3.0, 3.1, 3.2, and 3.4 not supported however.
2015-12-05 14:41:22 +11:00
43084eb49a Fix typo. 2015-12-05 14:40:33 +11:00
bbb4d31c3f Add accidentally removed line. 2015-12-05 14:39:07 +11:00
f7682d4c33 Make firewall messages consistent 2015-12-05 14:26:20 +11:00
d07a775d50 Don't fail if can't revert errors
We will log the errors, however no point in failing; not only can this
hide errors that occured setting up the firewall, but is pointless as we
can't actually handle these errors in a good way anyway.
2015-12-05 14:14:01 +11:00
50a6e87237 Don't use Xtoken if not set 2015-12-05 14:12:57 +11:00
ed0a92e714 Remove reference to obsolete global 2015-12-05 14:12:24 +11:00
36a1d7ead9 Python 3.5 fix. 2015-12-01 10:29:24 +11:00
43d6ad6a51 Print Python version used for the various stages. 2015-12-01 10:03:24 +11:00
5ab76a6ba9 Merge pull request #33 from felixonmars/master
Fix bug reported by @matiwinnetou in #31
2015-12-01 09:47:41 +11:00
61f9ae6fb4 Fix bug reported by @matiwinnetou in #31 2015-11-30 23:45:24 +08:00
191df92824 Ensure tempfiles are chmod 600 2015-11-28 16:13:56 +11:00
6dfbc467c0 Ensure verbose is never None.
None >= 1 not valid under Python3.

Fixes #31.
2015-11-28 16:03:01 +11:00
c06c972039 Prefer Python3 by default. 2015-11-28 16:02:47 +11:00
da62fe5b80 Merge pull request #30 from felixonmars/master
Add tests_require into setup.py
2015-11-27 20:01:30 +11:00
698351cf44 Add tests_require into setup.py
pytest and mock are needed for running tests.
2015-11-27 12:52:03 +08:00
13457c773b Improve summary line. 2015-11-27 14:28:52 +11:00
25 changed files with 848 additions and 697 deletions

7
CHANGES.rst Normal file
View File

@ -0,0 +1,7 @@
Release 0.74 (Jan 10, 2016)
===========================
* Add CHANGES.rst file.
* Numerous bug fixes.
* Python 3.5 fixes.
* PF fixes, especially for BSD.

View File

@ -43,21 +43,13 @@ Client side Requirements
| | | * IPv6 TCP + | | | | * IPv6 TCP + |
| | | * IPv6 UDP + | | | | * IPv6 UDP + |
+-------+--------+------------+-----------------------------------------------+ +-------+--------+------------+-----------------------------------------------+
| BSD | IPFW | * IPv4 TCP | Your kernel needs to be compiled with |
| | | | `IPFIREWALL_FORWARD` and you need to have ipfw|
| | | | available. |
+-------+--------+------------+-----------------------------------------------+
| MacOS | PF | * IPv4 TCP + You need to have the pfctl command. | | MacOS | PF | * IPv4 TCP + You need to have the pfctl command. |
+-------+--------+------------+-----------------------------------------------+ +-------+--------+------------+-----------------------------------------------+
The IPFW method is depreciated. It was originally required for MacOS support,
however is no longer maintained. It is likely to get removed from future
versions of sshuttle.
Server side Requirements Server side Requirements
------------------------ ------------------------
Python 2.7 or Python 3.5. This should match what is used on the client side. Python 2.7 or Python 3.5.
Additional Suggested Software Additional Suggested Software
@ -80,25 +72,42 @@ later.
There are some things you need to consider for TPROXY to work: There are some things you need to consider for TPROXY to work:
1. The following commands need to be run first as root. This only needs to be - The following commands need to be run first as root. This only needs to be
done once after booting up:: done once after booting up::
ip route add local default dev lo table 100 ip route add local default dev lo table 100
ip rule add fwmark 1 lookup 100 ip rule add fwmark 1 lookup 100
ip -6 route add local default dev lo table 100 ip -6 route add local default dev lo table 100
ip -6 rule add fwmark 1 lookup 100 ip -6 rule add fwmark 1 lookup 100
2. The client needs to be run as root. e.g.:: - The --auto-nets feature does not detect IPv6 routes automatically. Add IPv6
routes manually. e.g. by adding '::/0' to the end of the command line.
sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ... - The client needs to be run as root. e.g.::
3. You do need the `--method=tproxy` parameter, as above. sudo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" $HOME/tree/sshuttle.tproxy/sshuttle --method=tproxy ...
4. The routes for the outgoing packets must already exist. For example, if your - You may need to exclude the IP address of the server you are connecting to.
connection does not have IPv6 support, no IPv6 routes will exist, IPv6 Otherwise sshuttle may attempt to intercept the ssh packets, which will not
packets will not be generated and sshuttle cannot intercept them. Add some work. Use the `--exclude` parameter for this.
dummy routes to external interfaces. Make sure they get removed however
after sshuttle exits. - Similarly, UDP return packets (including DNS) could get intercepted and
bounced back. This is the case if you have a broad subnet such as
``0.0.0.0/0`` or ``::/0`` that includes the IP address of the client. Use the
`--exclude` parameter for this.
- You do need the `--method=tproxy` parameter, as above.
- The routes for the outgoing packets must already exist. For example, if your
connection does not have IPv6 support, no IPv6 routes will exist, IPv6
packets will not be generated and sshuttle cannot intercept them::
telnet -6 www.google.com 80
Trying 2404:6800:4001:805::1010...
telnet: Unable to connect to remote host: Network is unreachable
Add some dummy routes to external interfaces. Make sure they get removed
however after sshuttle exits.
Obtaining sshuttle Obtaining sshuttle

View File

@ -1 +1 @@
0.73 0.74

4
run
View File

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
if python2 -V 2>/dev/null; then if python3.5 -V 2>/dev/null; then
exec python2 -m "sshuttle" "$@" exec python3 -m "sshuttle" "$@"
else else
exec python -m "sshuttle" "$@" exec python -m "sshuttle" "$@"
fi fi

View File

@ -28,7 +28,7 @@ setup(
url='https://github.com/sshuttle/sshuttle', url='https://github.com/sshuttle/sshuttle',
author='Brian May', author='Brian May',
author_email='brian@linuxpenguins.xyz', author_email='brian@linuxpenguins.xyz',
description='Transparent proxy server that works as a poor man\'s VPN.', description='Full-featured" VPN over an SSH tunnel',
packages=find_packages(), packages=find_packages(),
license="GPL2+", license="GPL2+",
long_description=open('README.rst').read(), long_description=open('README.rst').read(),
@ -45,8 +45,9 @@ setup(
], ],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'sshuttle = sshuttle.__main__', 'sshuttle = sshuttle:__main__',
], ],
}, },
tests_require=['pytest', 'mock'],
keywords="ssh vpn", keywords="ssh vpn",
) )

View File

@ -6,6 +6,7 @@ import sshuttle.options as options
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
from sshuttle.helpers import family_ip_tuple, log, Fatal from sshuttle.helpers import family_ip_tuple, log, Fatal
@ -119,7 +120,7 @@ H,auto-hosts scan for remote hostnames and update local /etc/hosts
N,auto-nets automatically determine subnets to route N,auto-nets automatically determine subnets to route
dns capture local DNS requests and forward to the remote DNS server dns capture local DNS requests and forward to the remote DNS server
ns-hosts= capture and forward remote DNS requests to the following servers ns-hosts= capture and forward remote DNS requests to the following servers
method= auto, nat, tproxy, pf or ipfw method= auto, nat, tproxy or pf
python= path to python interpreter on the remote server python= path to python interpreter on the remote server
r,remote= ssh hostname (and optional username) of remote sshuttle server r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once) x,exclude= exclude this subnet (can be used more than once)
@ -145,7 +146,7 @@ if opt.daemon:
if opt.wrap: if opt.wrap:
import sshuttle.ssnet as ssnet import sshuttle.ssnet as ssnet
ssnet.MAX_CHANNEL = int(opt.wrap) ssnet.MAX_CHANNEL = int(opt.wrap)
helpers.verbose = opt.verbose helpers.verbose = opt.verbose or 0
try: try:
if opt.firewall: if opt.firewall:
@ -181,7 +182,7 @@ try:
includes = parse_subnet_file(opt.subnets) includes = parse_subnet_file(opt.subnets)
if not opt.method: if not opt.method:
method_name = "auto" method_name = "auto"
elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]: elif opt.method in ["auto", "nat", "tproxy", "pf"]:
method_name = opt.method method_name = opt.method
else: else:
o.fatal("method_name %s not supported" % opt.method) o.fatal("method_name %s not supported" % opt.method)
@ -197,6 +198,9 @@ try:
ipport_v6 = parse_ipport6(ip) ipport_v6 = parse_ipport6(ip)
else: else:
ipport_v4 = parse_ipport4(ip) ipport_v4 = parse_ipport4(ip)
if opt.syslog:
ssyslog.start_syslog()
ssyslog.stderr_to_syslog()
return_code = client.main(ipport_v6, ipport_v4, return_code = client.main(ipport_v6, ipport_v4,
opt.ssh_cmd, opt.ssh_cmd,
remotename, remotename,
@ -209,7 +213,7 @@ try:
opt.auto_nets, opt.auto_nets,
parse_subnets(includes), parse_subnets(includes),
parse_subnets(excludes), parse_subnets(excludes),
opt.syslog, opt.daemon, opt.pidfile) opt.daemon, opt.pidfile)
if return_code == 0: if return_code == 0:
log('Normal exit code, exiting...') log('Normal exit code, exiting...')

View File

@ -4,13 +4,15 @@ import imp
z = zlib.decompressobj() z = zlib.decompressobj()
while 1: while 1:
name = sys.stdin.readline().strip() name = stdin.readline().strip()
if name: if name:
nbytes = int(sys.stdin.readline()) name = name.decode("ASCII")
nbytes = int(stdin.readline())
if verbosity >= 2: if verbosity >= 2:
sys.stderr.write('server: assembling %r (%d bytes)\n' sys.stderr.write('server: assembling %r (%d bytes)\n'
% (name, nbytes)) % (name, nbytes))
content = z.decompress(sys.stdin.read(nbytes)) content = z.decompress(stdin.read(nbytes))
module = imp.new_module(name) module = imp.new_module(name)
parent, _, parent_name = name.rpartition(".") parent, _, parent_name = name.rpartition(".")

View File

@ -10,10 +10,11 @@ import sshuttle.ssnet as ssnet
import sshuttle.ssh as ssh import sshuttle.ssh as ssh
import sshuttle.ssyslog as ssyslog import sshuttle.ssyslog as ssyslog
import sys import sys
import platform
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \ from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
resolvconf_nameservers resolvconf_nameservers
from sshuttle.methods import get_method from sshuttle.methods import get_method, Features
_extra_fd = os.open('/dev/null', os.O_RDONLY) _extra_fd = os.open('/dev/null', os.O_RDONLY)
@ -66,7 +67,7 @@ def daemonize():
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666) outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
try: try:
os.write(outfd, '%d\n' % os.getpid()) os.write(outfd, b'%d\n' % os.getpid())
finally: finally:
os.close(outfd) os.close(outfd)
os.chdir("/") os.chdir("/")
@ -80,8 +81,6 @@ def daemonize():
os.dup2(si.fileno(), 1) os.dup2(si.fileno(), 1)
si.close() si.close()
ssyslog.stderr_to_syslog()
def daemon_cleanup(): def daemon_cleanup():
try: try:
@ -259,8 +258,8 @@ class FirewallClient:
raise Fatal('%r expected STARTED, got %r' % (self.argv, line)) raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
def sethostip(self, hostname, ip): def sethostip(self, hostname, ip):
assert(not re.search(r'[^-\w]', hostname)) assert(not re.search(b'[^-\w]', hostname))
assert(not re.search(r'[^0-9.]', ip)) assert(not re.search(b'[^0-9.]', ip))
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip)) self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
self.pfile.flush() self.pfile.flush()
@ -276,18 +275,25 @@ udp_by_src = {}
def expire_connections(now, mux): def expire_connections(now, mux):
remove = []
for chan, timeout in dnsreqs.items(): for chan, timeout in dnsreqs.items():
if timeout < now: if timeout < now:
debug3('expiring dnsreqs channel=%d\n' % chan) debug3('expiring dnsreqs channel=%d\n' % chan)
remove.append(chan)
del mux.channels[chan] del mux.channels[chan]
del dnsreqs[chan] for chan in remove:
del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
remove = []
for peer, (chan, timeout) in udp_by_src.items(): for peer, (chan, timeout) in udp_by_src.items():
if timeout < now: if timeout < now:
debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer)) debug3('expiring UDP channel channel=%d peer=%r\n' % (chan, peer))
mux.send(chan, ssnet.CMD_UDP_CLOSE, '') mux.send(chan, ssnet.CMD_UDP_CLOSE, b'')
remove.append(peer)
del mux.channels[chan] del mux.channels[chan]
del udp_by_src[peer] for peer in remove:
del udp_by_src[peer]
debug3('Remaining UDP channels: %d\n' % len(udp_by_src)) debug3('Remaining UDP channels: %d\n' % len(udp_by_src))
@ -329,7 +335,7 @@ def onaccept_tcp(listener, method, mux, handlers):
def udp_done(chan, data, method, sock, dstip): def udp_done(chan, data, method, sock, dstip):
(src, srcport, data) = data.split(",", 2) (src, srcport, data) = data.split(b",", 2)
srcip = (src, int(srcport)) srcip = (src, int(srcport))
debug3('doing send from %r to %r\n' % (srcip, dstip,)) debug3('doing send from %r to %r\n' % (srcip, dstip,))
method.send_udp(sock, srcip, dstip, data) method.send_udp(sock, srcip, dstip, data)
@ -348,10 +354,10 @@ def onaccept_udp(listener, method, mux, handlers):
chan = mux.next_channel() chan = mux.next_channel()
mux.channels[chan] = lambda cmd, data: udp_done( mux.channels[chan] = lambda cmd, data: udp_done(
chan, data, method, listener, dstip=srcip) chan, data, method, listener, dstip=srcip)
mux.send(chan, ssnet.CMD_UDP_OPEN, listener.family) mux.send(chan, ssnet.CMD_UDP_OPEN, b"%d" % listener.family)
udp_by_src[srcip] = chan, now + 30 udp_by_src[srcip] = chan, now + 30
hdr = "%s,%r," % (dstip[0], dstip[1]) hdr = b"%s,%d," % (dstip[0].encode("ASCII"), dstip[1])
mux.send(chan, ssnet.CMD_UDP_DATA, hdr + data) mux.send(chan, ssnet.CMD_UDP_DATA, hdr + data)
expire_connections(now, mux) expire_connections(now, mux)
@ -381,8 +387,10 @@ def ondns(listener, method, mux, handlers):
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control, python, latency_control,
dns_listener, seed_hosts, auto_nets, dns_listener, seed_hosts, auto_nets, daemon):
syslog, daemon):
debug1('Starting client with Python version %s\n'
% platform.python_version())
method = fw.method method = fw.method
@ -429,21 +437,26 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
if initstring != expected: if initstring != expected:
raise Fatal('expected server init string %r; got %r' raise Fatal('expected server init string %r; got %r'
% (expected, initstring)) % (expected, initstring))
debug1('connected.\n') log('Connected.\n')
print('Connected.')
sys.stdout.flush() sys.stdout.flush()
if daemon: if daemon:
daemonize() daemonize()
log('daemonizing (%s).\n' % _pidname) log('daemonizing (%s).\n' % _pidname)
elif syslog:
debug1('switching to syslog.\n')
ssyslog.stderr_to_syslog()
def onroutes(routestr): def onroutes(routestr):
if auto_nets: if auto_nets:
for line in routestr.strip().split('\n'): for line in routestr.strip().split(b'\n'):
(family, ip, width) = line.split(',', 2) (family, ip, width) = line.split(b',', 2)
fw.auto_nets.append((int(family), ip, int(width))) family = int(family)
width = int(width)
ip = ip.decode("ASCII")
if family == socket.AF_INET6 and tcp_listener.v6 is None:
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
if family == socket.AF_INET and tcp_listener.v4 is None:
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))
# 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!
@ -460,7 +473,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
debug2('got host list: %r\n' % hostlist) debug2('got host list: %r\n' % hostlist)
for line in hostlist.strip().split(): for line in hostlist.strip().split():
if line: if line:
name, ip = line.split(',', 1) name, ip = line.split(b',', 1)
fw.sethostip(name, ip) fw.sethostip(name, ip)
mux.got_host_list = onhostlist mux.got_host_list = onhostlist
@ -474,7 +487,7 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
if seed_hosts is not None: if seed_hosts is not None:
debug1('seed_hosts: %r\n' % seed_hosts) debug1('seed_hosts: %r\n' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts)) mux.send(0, ssnet.CMD_HOST_REQ, b'\n'.join(seed_hosts))
while 1: while 1:
rv = serverproc.poll() rv = serverproc.poll()
@ -489,10 +502,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
def main(listenip_v6, listenip_v4, def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns, nslist, ssh_cmd, remotename, python, latency_control, dns, nslist,
method_name, seed_hosts, auto_nets, method_name, seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile): subnets_include, subnets_exclude, daemon, pidfile):
if syslog:
ssyslog.start_syslog()
if daemon: if daemon:
try: try:
check_daemon(pidfile) check_daemon(pidfile)
@ -503,19 +514,45 @@ def main(listenip_v6, listenip_v4,
fw = FirewallClient(method_name) fw = FirewallClient(method_name)
features = fw.method.get_supported_features() # Get family specific subnet lists
if dns:
nslist += resolvconf_nameservers()
subnets = subnets_include + subnets_exclude # we don't care here
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET]
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
# Check features available
avail = fw.method.get_supported_features()
required = Features()
if listenip_v6 == "auto": if listenip_v6 == "auto":
if features.ipv6: if avail.ipv6:
listenip_v6 = ('::1', 0) listenip_v6 = ('::1', 0)
else: else:
listenip_v6 = None listenip_v6 = None
required.ipv6 = len(subnets_v6) > 0 or len(nslist_v6) > 0 \
or listenip_v6 is not None
required.udp = avail.udp
required.dns = len(nslist) > 0
fw.method.assert_features(required)
if required.ipv6 and listenip_v6 is None:
raise Fatal("IPv6 required but not listening.")
# display features enabled
debug1("IPv6 enabled: %r\n" % required.ipv6)
debug1("UDP enabled: %r\n" % required.udp)
debug1("DNS enabled: %r\n" % required.dns)
# bind to required ports
if listenip_v4 == "auto": if listenip_v4 == "auto":
listenip_v4 = ('127.0.0.1', 0) listenip_v4 = ('127.0.0.1', 0)
udp = features.udp
debug1("UDP enabled: %r\n" % udp)
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
ports = [0, ] ports = [0, ]
@ -534,7 +571,7 @@ def main(listenip_v6, listenip_v4,
tcp_listener = MultiListener() tcp_listener = MultiListener()
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if udp: if required.udp:
udp_listener = MultiListener(socket.SOCK_DGRAM) udp_listener = MultiListener(socket.SOCK_DGRAM)
udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
else: else:
@ -582,10 +619,7 @@ def main(listenip_v6, listenip_v4,
udp_listener.print_listening("UDP redirector") udp_listener.print_listening("UDP redirector")
bound = False bound = False
if dns or nslist: if required.dns:
if dns:
nslist += resolvconf_nameservers()
dns = True
# search for spare port for DNS # search for spare port for DNS
debug2('Binding DNS:') debug2('Binding DNS:')
ports = range(12300, 9000, -1) ports = range(12300, 9000, -1)
@ -626,22 +660,45 @@ def main(listenip_v6, listenip_v4,
dnsport_v4 = 0 dnsport_v4 = 0
dns_listener = None dns_listener = None
fw.method.check_settings(udp, dns) # Last minute sanity checks.
# These should never fail.
# If these do fail, something is broken above.
if len(subnets_v6) > 0:
assert required.ipv6
if redirectport_v6 == 0:
raise Fatal("IPv6 subnets defined but not listening")
if len(nslist_v6) > 0:
assert required.dns
assert required.ipv6
if dnsport_v6 == 0:
raise Fatal("IPv6 ns servers defined but not listening")
if len(subnets_v4) > 0:
if redirectport_v4 == 0:
raise Fatal("IPv4 subnets defined but not listening")
if len(nslist_v4) > 0:
if dnsport_v4 == 0:
raise Fatal("IPv4 ns servers defined but not listening")
# setup method specific stuff on listeners
fw.method.setup_tcp_listener(tcp_listener) fw.method.setup_tcp_listener(tcp_listener)
if udp_listener: if udp_listener:
fw.method.setup_udp_listener(udp_listener) fw.method.setup_udp_listener(udp_listener)
if dns_listener: if dns_listener:
fw.method.setup_udp_listener(dns_listener) fw.method.setup_udp_listener(dns_listener)
# start the firewall
fw.setup(subnets_include, subnets_exclude, nslist, fw.setup(subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
udp) required.udp)
# start the client process
try: try:
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename, return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control, dns_listener, python, latency_control, dns_listener,
seed_hosts, auto_nets, syslog, seed_hosts, auto_nets, daemon)
daemon)
finally: finally:
try: try:
if daemon: if daemon:

View File

@ -4,14 +4,15 @@ import signal
import sshuttle.ssyslog as ssyslog import sshuttle.ssyslog as ssyslog
import sys import sys
import os import os
import platform
import traceback
from sshuttle.helpers import debug1, debug2, Fatal from sshuttle.helpers import debug1, debug2, Fatal
from sshuttle.methods import get_auto_method, get_method from sshuttle.methods import get_auto_method, get_method
hostmap = {}
HOSTSFILE = '/etc/hosts' HOSTSFILE = '/etc/hosts'
def rewrite_etc_hosts(port): def rewrite_etc_hosts(hostmap, port):
BAKFILE = '%s.sbak' % HOSTSFILE BAKFILE = '%s.sbak' % HOSTSFILE
APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port APPEND = '# sshuttle-firewall-%d AUTOCREATED' % port
old_content = '' old_content = ''
@ -36,19 +37,17 @@ def rewrite_etc_hosts(port):
f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND)) f.write('%-30s %s\n' % ('%s %s' % (ip, name), APPEND))
f.close() f.close()
if st: if st is not None:
os.chown(tmpname, st.st_uid, st.st_gid) os.chown(tmpname, st.st_uid, st.st_gid)
os.chmod(tmpname, st.st_mode) os.chmod(tmpname, st.st_mode)
else: else:
os.chown(tmpname, 0, 0) os.chown(tmpname, 0, 0)
os.chmod(tmpname, 0o644) os.chmod(tmpname, 0o600)
os.rename(tmpname, HOSTSFILE) os.rename(tmpname, HOSTSFILE)
def restore_etc_hosts(port): def restore_etc_hosts(port):
global hostmap rewrite_etc_hosts({}, port)
hostmap = {}
rewrite_etc_hosts(port)
# Isolate function that needs to be replaced for tests # Isolate function that needs to be replaced for tests
@ -85,6 +84,10 @@ def setup_daemon():
# are hopefully harmless. # are hopefully harmless.
def main(method_name, syslog): def main(method_name, syslog):
stdin, stdout = setup_daemon() stdin, stdout = setup_daemon()
hostmap = {}
debug1('firewall manager: Starting firewall with Python version %s\n'
% platform.python_version())
if method_name == "auto": if method_name == "auto":
method = get_auto_method() method = get_auto_method()
@ -95,7 +98,7 @@ def main(method_name, syslog):
ssyslog.start_syslog() ssyslog.start_syslog()
ssyslog.stderr_to_syslog() ssyslog.stderr_to_syslog()
debug1('firewall manager ready method name %s.\n' % method.name) debug1('firewall manager: ready method name %s.\n' % method.name)
stdout.write('READY %s\n' % method.name) stdout.write('READY %s\n' % method.name)
stdout.flush() stdout.flush()
@ -120,7 +123,7 @@ def main(method_name, syslog):
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))
debug2('Got subnets: %r\n' % subnets) debug2('firewall manager: Got subnets: %r\n' % subnets)
nslist = [] nslist = []
if line != 'NSLIST\n': if line != 'NSLIST\n':
@ -136,8 +139,8 @@ def main(method_name, syslog):
except: except:
raise Fatal('firewall: expected nslist or PORTS but got %r' % line) raise Fatal('firewall: expected nslist or PORTS but got %r' % line)
nslist.append((int(family), ip)) nslist.append((int(family), ip))
debug2('Got partial nslist: %r\n' % nslist) debug2('firewall manager: Got partial nslist: %r\n' % nslist)
debug2('Got nslist: %r\n' % nslist) debug2('firewall manager: Got nslist: %r\n' % nslist)
if not line.startswith('PORTS '): if not line.startswith('PORTS '):
raise Fatal('firewall: expected PORTS but got %r' % line) raise Fatal('firewall: expected PORTS but got %r' % line)
@ -159,7 +162,7 @@ def main(method_name, syslog):
assert(dnsport_v4 >= 0) assert(dnsport_v4 >= 0)
assert(dnsport_v4 <= 65535) assert(dnsport_v4 <= 65535)
debug2('Got ports: %d,%d,%d,%d\n' debug2('firewall manager: Got ports: %d,%d,%d,%d\n'
% (port_v6, port_v4, dnsport_v6, dnsport_v4)) % (port_v6, port_v4, dnsport_v6, dnsport_v4))
line = stdin.readline(128) line = stdin.readline(128)
@ -170,29 +173,27 @@ def main(method_name, syslog):
_, _, udp = line.partition(" ") _, _, udp = line.partition(" ")
udp = bool(int(udp)) udp = bool(int(udp))
debug2('Got udp: %r\n' % udp) debug2('firewall manager: Got udp: %r\n' % udp)
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET]
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
try: try:
do_wait = None debug1('firewall manager: setting up.\n')
debug1('firewall manager: starting transproxy.\n')
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] if len(subnets_v6) > 0 or len(nslist_v6) > 0:
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] debug2('firewall manager: setting up IPv6.\n')
if port_v6 > 0: method.setup_firewall(
do_wait = method.setup_firewall(
port_v6, dnsport_v6, nslist_v6, port_v6, dnsport_v6, nslist_v6,
socket.AF_INET6, subnets_v6, udp) socket.AF_INET6, subnets_v6, udp)
elif len(subnets_v6) > 0:
debug1("IPv6 subnets defined but IPv6 disabled\n")
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET] if len(subnets_v4) > 0 or len(nslist_v4) > 0:
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET] debug2('firewall manager: setting up IPv4.\n')
if port_v4 > 0: method.setup_firewall(
do_wait = method.setup_firewall(
port_v4, dnsport_v4, nslist_v4, port_v4, dnsport_v4, nslist_v4,
socket.AF_INET, subnets_v4, udp) socket.AF_INET, subnets_v4, udp)
elif len(subnets_v4) > 0:
debug1('IPv4 subnets defined but IPv4 disabled\n')
stdout.write('STARTED\n') stdout.write('STARTED\n')
@ -207,16 +208,15 @@ def main(method_name, syslog):
# to stay running so that we don't need a *second* password # to stay running so that we don't need a *second* password
# authentication at shutdown time - that cleanup is important! # authentication at shutdown time - that cleanup is important!
while 1: while 1:
if do_wait is not None:
do_wait()
line = stdin.readline(128) line = stdin.readline(128)
if line.startswith('HOST '): if line.startswith('HOST '):
(name, ip) = line[5:].strip().split(',', 1) (name, ip) = line[5:].strip().split(',', 1)
hostmap[name] = ip hostmap[name] = ip
rewrite_etc_hosts(port_v6 or port_v4) debug2('firewall manager: setting up /etc/hosts.\n')
rewrite_etc_hosts(hostmap, port_v6 or port_v4)
elif line: elif line:
if not method.firewall_command(line): if not method.firewall_command(line):
raise Fatal('expected EOF, got %r' % line) raise Fatal('firewall: expected command, got %r' % line)
else: else:
break break
finally: finally:
@ -224,8 +224,41 @@ def main(method_name, syslog):
debug1('firewall manager: undoing changes.\n') debug1('firewall manager: undoing changes.\n')
except: except:
pass pass
if port_v6:
method.setup_firewall(port_v6, 0, [], socket.AF_INET6, [], udp) try:
if port_v4: if len(subnets_v6) > 0 or len(nslist_v6) > 0:
method.setup_firewall(port_v4, 0, [], socket.AF_INET, [], udp) debug2('firewall manager: undoing IPv6 changes.\n')
restore_etc_hosts(port_v6 or port_v4) method.restore_firewall(port_v6, socket.AF_INET6, udp)
except:
try:
debug1("firewall manager: "
"Error trying to undo IPv6 firewall.\n")
for line in traceback.format_exc().splitlines():
debug1("---> %s\n" % line)
except:
pass
try:
if len(subnets_v4) > 0 or len(nslist_v4) > 0:
debug2('firewall manager: undoing IPv4 changes.\n')
method.restore_firewall(port_v4, socket.AF_INET, udp)
except:
try:
debug1("firewall manager: "
"Error trying to undo IPv4 firewall.\n")
for line in traceback.format_exc().splitlines():
debug1("firewall manager: ---> %s\n" % line)
except:
pass
try:
debug2('firewall manager: undoing /etc/hosts changes.\n')
restore_etc_hosts(port_v6 or port_v4)
except:
try:
debug1("firewall manager: "
"Error trying to undo /etc/hosts changes.\n")
for line in traceback.format_exc().splitlines():
debug1("firewall manager: ---> %s\n" % line)
except:
pass

View File

@ -7,9 +7,17 @@ verbose = 0
def log(s): def log(s):
global logprefix
try: try:
sys.stdout.flush() sys.stdout.flush()
sys.stderr.write(logprefix + s) if s.find("\n") != -1:
prefix = logprefix
s = s.rstrip("\n")
for line in s.split("\n"):
sys.stderr.write(prefix + line + "\n")
prefix = "---> "
else:
sys.stderr.write(logprefix + s)
sys.stderr.flush() sys.stderr.flush()
except IOError: except IOError:
# this could happen if stderr gets forcibly disconnected, eg. because # this could happen if stderr gets forcibly disconnected, eg. because

View File

@ -5,6 +5,7 @@ import select
import errno import errno
import os import os
import sys import sys
import platform
import subprocess as ssubprocess import subprocess as ssubprocess
import sshuttle.helpers as helpers import sshuttle.helpers as helpers
@ -35,8 +36,9 @@ def write_host_cache():
try: try:
f = open(tmpname, 'wb') f = open(tmpname, 'wb')
for name, ip in sorted(hostnames.items()): for name, ip in sorted(hostnames.items()):
f.write('%s,%s\n' % (name, ip)) f.write(('%s,%s\n' % (name, ip)).encode("ASCII"))
f.close() f.close()
os.chmod(tmpname, 0o600)
os.rename(tmpname, CACHEFILE) os.rename(tmpname, CACHEFILE)
finally: finally:
try: try:
@ -120,7 +122,7 @@ def _check_netstat():
argv = ['netstat', '-n'] argv = ['netstat', '-n']
try: try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
content = p.stdout.read() content = p.stdout.read().decode("ASCII")
p.wait() p.wait()
except OSError as e: except OSError as e:
log('%r failed: %r\n' % (argv, e)) log('%r failed: %r\n' % (argv, e))
@ -254,6 +256,9 @@ def hw_main(seed_hosts):
else: else:
helpers.logprefix = 'hostwatch: ' helpers.logprefix = 'hostwatch: '
debug1('Starting hostwatch with Python version %s\n'
% platform.python_version())
read_host_cache() read_host_cache()
_enqueue(_check_etc_hosts) _enqueue(_check_etc_hosts)

View File

@ -39,6 +39,7 @@ class BaseMethod(object):
result = Features() result = Features()
result.ipv6 = False result.ipv6 = False
result.udp = False result.udp = False
result.dns = True
return result return result
def get_tcp_dstip(self, sock): def get_tcp_dstip(self, sock):
@ -61,13 +62,20 @@ class BaseMethod(object):
def setup_udp_listener(self, udp_listener): def setup_udp_listener(self, udp_listener):
pass pass
def check_settings(self, udp, dns): def assert_features(self, features):
if udp: avail = self.get_supported_features()
Fatal("UDP support not supported with method %s.\n" % self.name) for key in ["udp", "dns", "ipv6"]:
if getattr(features, key) and not getattr(avail, key):
raise Fatal(
"Feature %s not supported with method %s.\n" %
(key, self.name))
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp): def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
raise NotImplementedError() raise NotImplementedError()
def restore_firewall(self, port, family, udp):
raise NotImplementedError()
def firewall_command(self, line): def firewall_command(self, line):
return False return False
@ -86,14 +94,12 @@ def get_method(method_name):
def get_auto_method(): def get_auto_method():
if _program_exists('ipfw'): if _program_exists('iptables'):
method_name = "ipfw"
elif _program_exists('iptables'):
method_name = "nat" method_name = "nat"
elif _program_exists('pfctl'): elif _program_exists('pfctl'):
method_name = "pf" method_name = "pf"
else: else:
raise Fatal( raise Fatal(
"can't find either ipfw, iptables or pfctl; check your PATH") "can't find either iptables or pfctl; check your PATH")
return get_method(method_name) return get_method(method_name)

View File

@ -1,237 +0,0 @@
import sys
import select
import socket
import struct
import subprocess as ssubprocess
from sshuttle.helpers import log, debug1, debug3, islocal, \
Fatal, family_to_string
from sshuttle.methods import BaseMethod
# python doesn't have a definition for this
IPPROTO_DIVERT = 254
def ipfw_rule_exists(n):
argv = ['ipfw', 'list']
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
found = False
for line in p.stdout:
if line.startswith('%05d ' % n):
if not ('ipttl 42' in line
or ('skipto %d' % (n + 1)) in line
or 'check-state' in line):
log('non-sshuttle ipfw rule: %r\n' % line.strip())
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
found = True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
return found
_oldctls = {}
def _fill_oldctls(prefix):
argv = ['sysctl', prefix]
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
for line in p.stdout:
assert(line[-1] == '\n')
(k, v) = line[:-1].split(': ', 1)
_oldctls[k] = v
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
if not line:
raise Fatal('%r returned no data' % (argv,))
def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv))
return ssubprocess.call(argv, stdout=open('/dev/null', 'w'))
_changedctls = []
def sysctl_set(name, val, permanent=False):
PREFIX = 'net.inet.ip'
assert(name.startswith(PREFIX + '.'))
val = str(val)
if not _oldctls:
_fill_oldctls(PREFIX)
if not (name in _oldctls):
debug1('>> No such sysctl: %r\n' % name)
return False
oldval = _oldctls[name]
if val != oldval:
rv = _sysctl_set(name, val)
if rv == 0 and permanent:
debug1('>> ...saving permanently in /etc/sysctl.conf\n')
f = open('/etc/sysctl.conf', 'a')
f.write('\n'
'# Added by sshuttle\n'
'%s=%s\n' % (name, val))
f.close()
else:
_changedctls.append(name)
return True
def _udp_unpack(p):
src = (socket.inet_ntoa(p[12:16]), struct.unpack('!H', p[20:22])[0])
dst = (socket.inet_ntoa(p[16:20]), struct.unpack('!H', p[22:24])[0])
return src, dst
def _udp_repack(p, src, dst):
addrs = socket.inet_aton(src[0]) + socket.inet_aton(dst[0])
ports = struct.pack('!HH', src[1], dst[1])
return p[:12] + addrs + ports + p[24:]
_real_dns_server = [None]
def _handle_diversion(divertsock, dnsport):
p, tag = divertsock.recvfrom(4096)
src, dst = _udp_unpack(p)
debug3('got diverted packet from %r to %r\n' % (src, dst))
if dst[1] == 53:
# outgoing DNS
debug3('...packet is a DNS request.\n')
_real_dns_server[0] = dst
dst = ('127.0.0.1', dnsport)
elif src[1] == dnsport:
if islocal(src[0], divertsock.family):
debug3('...packet is a DNS response.\n')
src = _real_dns_server[0]
else:
log('weird?! unexpected divert from %r to %r\n' % (src, dst))
assert(0)
newp = _udp_repack(p, src, dst)
divertsock.sendto(newp, tag)
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
class Method(BaseMethod):
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
# IPv6 not supported
if family not in [socket.AF_INET, ]:
raise Exception(
'Address family "%s" unsupported by ipfw method_name'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by ipfw method_name")
sport = str(port)
xsport = str(port + 1)
# cleanup any existing rules
if ipfw_rule_exists(port):
ipfw('delete', sport)
while _changedctls:
name = _changedctls.pop()
oldval = _oldctls[name]
_sysctl_set(name, oldval)
if subnets or dnsport:
sysctl_set('net.inet.ip.fw.enable', 1)
changed = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True)
if changed:
log("\n"
" WARNING: ONE-TIME NETWORK DISRUPTION:\n"
" =====================================\n"
"sshuttle has changed a MacOS kernel setting to work around\n"
"a bug in MacOS 10.6. This will cause your network to drop\n"
"within 5-10 minutes unless you restart your network\n"
"interface (change wireless networks or unplug/plug the\n"
"ethernet port) NOW, then restart sshuttle. The fix is\n"
"permanent; you only have to do this once.\n\n")
sys.exit(1)
ipfw('add', sport, 'check-state', 'ip',
'from', 'any', 'to', 'any')
if subnets:
# create new subnet entries
for f, swidth, sexclude, snet \
in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude:
ipfw('add', sport, 'skipto', xsport,
'tcp',
'from', 'any', 'to', '%s/%s' % (snet, swidth))
else:
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
'tcp',
'from', 'any', 'to', '%s/%s' % (snet, swidth),
'not', 'ipttl', '42', 'keep-state', 'setup')
# This part is much crazier than it is on Linux, because MacOS (at
# least 10.6, and probably other versions, and maybe FreeBSD too)
# doesn't correctly fixup the dstip/dstport for UDP packets when it
# puts them through a 'fwd' rule. It also doesn't fixup the
# srcip/srcport in the response packet. In Linux iptables, all that
# happens magically for us, so we just redirect the packets and relax.
#
# On MacOS, we have to fix the ports ourselves. For that, we use a
# 'divert' socket, which receives raw packets and lets us mangle them.
#
# Here's how it works. Let's say the local DNS server is 1.1.1.1:53,
# and the remote DNS server is 2.2.2.2:53, and the local transproxy
# port is 10.0.0.1:12300, and a client machine is making a request from
# 10.0.0.5:9999. We see a packet like this:
# 10.0.0.5:9999 -> 1.1.1.1:53
# Since the destip:port matches one of our local nameservers, it will
# match a 'fwd' rule, thus grabbing it on the local machine. However,
# the local kernel will then see a packet addressed to *:53 and not
# know what to do with it; there's nobody listening on port 53. Thus,
# we divert it, rewriting it into this:
# 10.0.0.5:9999 -> 10.0.0.1:12300
# This gets proxied out to the server, which sends it to 2.2.2.2:53,
# and the answer comes back, and the proxy sends it back out like this:
# 10.0.0.1:12300 -> 10.0.0.5:9999
# But that's wrong! The original machine expected an answer from
# 1.1.1.1:53, so we have to divert the *answer* and rewrite it:
# 1.1.1.1:53 -> 10.0.0.5:9999
#
# See? Easy stuff.
if dnsport:
divertsock = socket.socket(socket.AF_INET, socket.SOCK_RAW,
IPPROTO_DIVERT)
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
for f, ip in [i for i in nslist if i[0] == family]:
# relabel and then catch outgoing DNS requests
ipfw('add', sport, 'divert', sport,
'udp',
'from', 'any', 'to', '%s/32' % ip, '53',
'not', 'ipttl', '42')
# relabel DNS responses
ipfw('add', sport, 'divert', sport,
'udp',
'from', 'any', str(dnsport), 'to', 'any',
'not', 'ipttl', '42')
def do_wait():
while 1:
r, w, x = select.select([sys.stdin, divertsock], [], [])
if divertsock in r:
_handle_diversion(divertsock, dnsport)
if sys.stdin in r:
return
else:
do_wait = None
return do_wait

View File

@ -30,41 +30,60 @@ class Method(BaseMethod):
chain = 'sshuttle-%s' % port chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
self.restore_firewall(port, family, udp)
_ipt('-N', chain)
_ipt('-F', chain)
_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):
if sexclude:
_ipt('-A', chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp')
else:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp',
'--to-ports', str(port))
for f, ip in [i for i in nslist if i[0] == family]:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip,
'-p', 'udp',
'--dport', '53',
'--to-ports', str(dnsport))
def restore_firewall(self, port, family, udp):
# only ipv4 supported with NAT
if family != socket.AF_INET:
raise Exception(
'Address family "%s" unsupported by nat method_name'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by nat method_name")
table = "nat"
def _ipt(*args):
return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains # basic cleanup/setup of chains
if ipt_chain_exists(family, table, chain): if ipt_chain_exists(family, table, chain):
nonfatal(_ipt, '-D', 'OUTPUT', '-j', chain) nonfatal(_ipt, '-D', 'OUTPUT', '-j', chain)
nonfatal(_ipt, '-D', 'PREROUTING', '-j', chain) nonfatal(_ipt, '-D', 'PREROUTING', '-j', chain)
nonfatal(_ipt, '-F', chain) nonfatal(_ipt, '-F', chain)
_ipt('-X', chain) _ipt('-X', chain)
if subnets or dnsport:
_ipt('-N', chain)
_ipt('-F', chain)
_ipt('-I', 'OUTPUT', '1', '-j', chain)
_ipt('-I', 'PREROUTING', '1', '-j', chain)
if subnets:
# 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):
if sexclude:
_ipt('-A', chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp')
else:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet, swidth),
'-p', 'tcp',
'--to-ports', str(port))
if dnsport:
for f, ip in [i for i in nslist if i[0] == family]:
_ipt_ttl('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/32' % ip,
'-p', 'udp',
'--dport', '53',
'--to-ports', str(dnsport))

View File

@ -7,7 +7,7 @@ import subprocess as ssubprocess
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.helpers import debug1, debug2, 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
@ -24,67 +24,88 @@ def pfctl(args, stdin=None):
return o return o
_pf_context = {'started_by_sshuttle': False, 'Xtoken': ''} _pf_context = {'started_by_sshuttle': False, 'Xtoken': None}
# This are some classes and functions used to support pf in yosemite.
class pf_state_xport(Union):
_fields_ = [("port", c_uint16),
("call_id", c_uint16),
("spi", c_uint32)]
class pf_addr(Structure):
class _pfa(Union):
_fields_ = [("v4", c_uint32), # struct in_addr
("v6", c_uint32 * 4), # struct in6_addr
("addr8", c_uint8 * 16),
("addr16", c_uint16 * 8),
("addr32", c_uint32 * 4)]
_fields_ = [("pfa", _pfa)]
_anonymous_ = ("pfa",)
class pfioc_natlook(Structure):
_fields_ = [("saddr", pf_addr),
("daddr", pf_addr),
("rsaddr", pf_addr),
("rdaddr", pf_addr),
("sxport", pf_state_xport),
("dxport", pf_state_xport),
("rsxport", pf_state_xport),
("rdxport", pf_state_xport),
("af", c_uint8), # sa_family_t
("proto", c_uint8),
("proto_variant", c_uint8),
("direction", c_uint8)]
pfioc_rule = c_char * 3104 # sizeof(struct pfioc_rule)
pfioc_pooladdr = c_char * 1136 # sizeof(struct pfioc_pooladdr)
MAXPATHLEN = 1024
DIOCNATLOOK = ((0x40000000 | 0x80000000) | (
(sizeof(pfioc_natlook) & 0x1fff) << 16) | ((ord('D')) << 8) | (23))
DIOCCHANGERULE = ((0x40000000 | 0x80000000) | (
(sizeof(pfioc_rule) & 0x1fff) << 16) | ((ord('D')) << 8) | (26))
DIOCBEGINADDRS = ((0x40000000 | 0x80000000) | (
(sizeof(pfioc_pooladdr) & 0x1fff) << 16) | ((ord('D')) << 8) | (51))
PF_CHANGE_ADD_TAIL = 2
PF_CHANGE_GET_TICKET = 6
PF_PASS = 0
PF_RDR = 8
PF_OUT = 2
_pf_fd = None _pf_fd = None
class OsDefs(object):
def __init__(self, platform=None):
if platform is None:
platform = sys.platform
self.platform = platform
# This are some classes and functions used to support pf in yosemite.
if platform == 'darwin':
class pf_state_xport(Union):
_fields_ = [("port", c_uint16),
("call_id", c_uint16),
("spi", c_uint32)]
else:
class pf_state_xport(Union):
_fields_ = [("port", c_uint16),
("call_id", c_uint16)]
class pf_addr(Structure):
class _pfa(Union):
_fields_ = [("v4", c_uint32), # struct in_addr
("v6", c_uint32 * 4), # struct in6_addr
("addr8", c_uint8 * 16),
("addr16", c_uint16 * 8),
("addr32", c_uint32 * 4)]
_fields_ = [("pfa", _pfa)]
_anonymous_ = ("pfa",)
class pfioc_natlook(Structure):
_fields_ = [("saddr", pf_addr),
("daddr", pf_addr),
("rsaddr", pf_addr),
("rdaddr", pf_addr),
("sxport", pf_state_xport),
("dxport", pf_state_xport),
("rsxport", pf_state_xport),
("rdxport", pf_state_xport),
("af", c_uint8), # sa_family_t
("proto", c_uint8),
("proto_variant", c_uint8),
("direction", c_uint8)]
self.pfioc_natlook = pfioc_natlook
# sizeof(struct pfioc_rule)
self.pfioc_rule = c_char * \
(3104 if platform == 'darwin' else 3040)
# sizeof(struct pfioc_pooladdr)
self.pfioc_pooladdr = c_char * 1136
self.MAXPATHLEN = 1024
self.DIOCNATLOOK = (
(0x40000000 | 0x80000000) |
((sizeof(pfioc_natlook) & 0x1fff) << 16) |
((ord('D')) << 8) | (23))
self.DIOCCHANGERULE = (
(0x40000000 | 0x80000000) |
((sizeof(self.pfioc_rule) & 0x1fff) << 16) |
((ord('D')) << 8) | (26))
self.DIOCBEGINADDRS = (
(0x40000000 | 0x80000000) |
((sizeof(self.pfioc_pooladdr) & 0x1fff) << 16) |
((ord('D')) << 8) | (51))
self.PF_CHANGE_ADD_TAIL = 2
self.PF_CHANGE_GET_TICKET = 6
self.PF_PASS = 0
self.PF_RDR = 8
self.PF_OUT = 2
osdefs = OsDefs()
def pf_get_dev(): def pf_get_dev():
global _pf_fd global _pf_fd
if _pf_fd is None: if _pf_fd is None:
@ -103,16 +124,16 @@ def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port):
assert len(packed_src_ip) == len(packed_dst_ip) assert len(packed_src_ip) == len(packed_dst_ip)
length = len(packed_src_ip) length = len(packed_src_ip)
pnl = pfioc_natlook() pnl = osdefs.pfioc_natlook()
pnl.proto = proto pnl.proto = proto
pnl.direction = PF_OUT pnl.direction = osdefs.PF_OUT
pnl.af = family pnl.af = family
memmove(addressof(pnl.saddr), packed_src_ip, length) memmove(addressof(pnl.saddr), packed_src_ip, length)
pnl.sxport.port = socket.htons(src_port)
memmove(addressof(pnl.daddr), packed_dst_ip, length) memmove(addressof(pnl.daddr), packed_dst_ip, length)
pnl.sxport.port = socket.htons(src_port)
pnl.dxport.port = socket.htons(dst_port) pnl.dxport.port = socket.htons(dst_port)
ioctl(pf_get_dev(), DIOCNATLOOK, ioctl(pf_get_dev(), osdefs.DIOCNATLOOK,
(c_char * sizeof(pnl)).from_address(addressof(pnl))) (c_char * sizeof(pnl)).from_address(addressof(pnl)))
ip = socket.inet_ntop( ip = socket.inet_ntop(
@ -125,26 +146,26 @@ def pf_add_anchor_rule(type, name):
ACTION_OFFSET = 0 ACTION_OFFSET = 0
POOL_TICKET_OFFSET = 8 POOL_TICKET_OFFSET = 8
ANCHOR_CALL_OFFSET = 1040 ANCHOR_CALL_OFFSET = 1040
RULE_ACTION_OFFSET = 3068 RULE_ACTION_OFFSET = 3068 if osdefs.platform == 'darwin' else 2968
pr = pfioc_rule() pr = osdefs.pfioc_rule()
ppa = pfioc_pooladdr() ppa = osdefs.pfioc_pooladdr()
ioctl(pf_get_dev(), DIOCBEGINADDRS, ppa) ioctl(pf_get_dev(), osdefs.DIOCBEGINADDRS, ppa)
memmove(addressof(pr) + POOL_TICKET_OFFSET, ppa[4:8], 4) # pool_ticket memmove(addressof(pr) + POOL_TICKET_OFFSET, ppa[4:8], 4) # pool_ticket
memmove(addressof(pr) + ANCHOR_CALL_OFFSET, name, memmove(addressof(pr) + ANCHOR_CALL_OFFSET, name,
min(MAXPATHLEN, len(name))) # anchor_call = name min(osdefs.MAXPATHLEN, len(name))) # anchor_call = name
memmove(addressof(pr) + RULE_ACTION_OFFSET, memmove(addressof(pr) + RULE_ACTION_OFFSET,
struct.pack('I', type), 4) # rule.action = type struct.pack('I', type), 4) # rule.action = type
memmove(addressof(pr) + ACTION_OFFSET, struct.pack( memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
'I', PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET 'I', osdefs.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
ioctl(pf_get_dev(), DIOCCHANGERULE, pr) ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr)
memmove(addressof(pr) + ACTION_OFFSET, struct.pack( memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
'I', PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL 'I', osdefs.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
ioctl(pf_get_dev(), DIOCCHANGERULE, pr) ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr)
class Method(BaseMethod): class Method(BaseMethod):
@ -156,19 +177,20 @@ class Method(BaseMethod):
proxy = sock.getsockname() proxy = sock.getsockname()
argv = (sock.family, socket.IPPROTO_TCP, argv = (sock.family, socket.IPPROTO_TCP,
peer[0], peer[1], proxy[0], proxy[1]) peer[0].encode("ASCII"), peer[1],
pfile.write("QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % argv) proxy[0].encode("ASCII"), proxy[1])
out_line = b"QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % argv
pfile.write(out_line)
pfile.flush() pfile.flush()
line = pfile.readline() in_line = pfile.readline()
debug2("QUERY_PF_NAT %d,%d,%s,%d,%s,%d" % argv + ' > ' + line) debug2(out_line.decode("ASCII") + ' > ' + in_line.decode("ASCII"))
if line.startswith('QUERY_PF_NAT_SUCCESS '): if in_line.startswith(b'QUERY_PF_NAT_SUCCESS '):
(ip, port) = line[21:].split(',') (ip, port) = in_line[21:].split(b',')
return (ip, int(port)) return (ip.decode("ASCII"), int(port))
return sock.getsockname() return sock.getsockname()
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp): def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
global _pf_started_by_sshuttle
tables = [] tables = []
translating_rules = [] translating_rules = []
filtering_rules = [] filtering_rules = []
@ -180,57 +202,72 @@ class Method(BaseMethod):
if udp: if udp:
raise Exception("UDP not supported by pf method_name") raise Exception("UDP not supported by pf method_name")
if subnets: if len(subnets) > 0:
includes = [] includes = []
# 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 in sorted(
subnets, key=lambda s: (s[1], s[2]), reverse=True): subnets, key=lambda s: (s[1], s[2]), reverse=True):
includes.append("%s%s/%s" % includes.append(b"%s%s/%d" %
("!" if sexclude else "", snet, swidth)) (b"!" if sexclude else b"",
snet.encode("ASCII"),
swidth))
tables.append('table <forward_subnets> {%s}' % ','.join(includes)) tables.append(
b'table <forward_subnets> {%s}' % b','.join(includes))
translating_rules.append( translating_rules.append(
'rdr pass on lo0 proto tcp ' b'rdr pass on lo0 proto tcp '
'to <forward_subnets> -> 127.0.0.1 port %r' % port) b'to <forward_subnets> -> 127.0.0.1 port %r' % port)
filtering_rules.append( filtering_rules.append(
'pass out route-to lo0 inet proto tcp ' b'pass out route-to lo0 inet proto tcp '
'to <forward_subnets> keep state') b'to <forward_subnets> keep state')
if dnsport: if len(nslist) > 0:
tables.append('table <dns_servers> {%s}' % ','.join( tables.append(
[ns[1] for ns in nslist])) b'table <dns_servers> {%s}' %
translating_rules.append( b','.join([ns[1].encode("ASCII") for ns in nslist]))
'rdr pass on lo0 proto udp to ' translating_rules.append(
'<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport) b'rdr pass on lo0 proto udp to '
filtering_rules.append( b'<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
'pass out route-to lo0 inet proto udp to ' filtering_rules.append(
'<dns_servers> port 53 keep state') b'pass out route-to lo0 inet proto udp to '
b'<dns_servers> port 53 keep state')
rules = '\n'.join(tables + translating_rules + filtering_rules) \ rules = b'\n'.join(tables + translating_rules + filtering_rules) \
+ '\n' + b'\n'
assert isinstance(rules, bytes)
debug3("rules:\n" + rules.decode("ASCII"))
pf_status = pfctl('-s all')[0] pf_status = pfctl('-s all')[0]
if '\nrdr-anchor "sshuttle" all\n' not in pf_status: if b'\nrdr-anchor "sshuttle" all\n' not in pf_status:
pf_add_anchor_rule(PF_RDR, "sshuttle") pf_add_anchor_rule(osdefs.PF_RDR, b"sshuttle")
if '\nanchor "sshuttle" all\n' not in pf_status: if b'\nanchor "sshuttle" all\n' not in pf_status:
pf_add_anchor_rule(PF_PASS, "sshuttle") pf_add_anchor_rule(osdefs.PF_PASS, b"sshuttle")
pfctl('-a sshuttle -f /dev/stdin', rules) pfctl('-a sshuttle -f /dev/stdin', rules)
if sys.platform == "darwin": if osdefs.platform == "darwin":
o = pfctl('-E') o = pfctl('-E')
_pf_context['Xtoken'] = \ _pf_context['Xtoken'] = \
re.search(r'Token : (.+)', o[1]).group(1) re.search(b'Token : (.+)', o[1]).group(1)
elif 'INFO:\nStatus: Disabled' in pf_status: elif b'INFO:\nStatus: Disabled' in pf_status:
pfctl('-e') pfctl('-e')
_pf_context['started_by_sshuttle'] = True _pf_context['started_by_sshuttle'] = True
else:
pfctl('-a sshuttle -F all') def restore_firewall(self, port, family, udp):
if sys.platform == "darwin": if family != socket.AF_INET:
pfctl('-X %s' % _pf_context['Xtoken']) raise Exception(
elif _pf_context['started_by_sshuttle']: 'Address family "%s" unsupported by pf method_name'
pfctl('-d') % family_to_string(family))
if udp:
raise Exception("UDP not supported by pf method_name")
pfctl('-a sshuttle -F all')
if osdefs.platform == "darwin":
if _pf_context['Xtoken'] is not None:
pfctl('-X %s' % _pf_context['Xtoken'].decode("ASCII"))
elif _pf_context['started_by_sshuttle']:
pfctl('-d')
def firewall_command(self, line): def firewall_command(self, line):
if line.startswith('QUERY_PF_NAT '): if line.startswith('QUERY_PF_NAT '):

View File

@ -59,6 +59,7 @@ if recvmsg == "python":
ip = socket.inet_ntop(family, cmsg_data[start:start + length]) ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port) dstip = (ip, port)
break break
print("xxxxx", srcip, dstip)
return (srcip, dstip, data) return (srcip, dstip, data)
elif recvmsg == "socket_ext": elif recvmsg == "socket_ext":
def recv_udp(listener, bufsize): def recv_udp(listener, bufsize):
@ -105,7 +106,12 @@ class Method(BaseMethod):
def get_supported_features(self): def get_supported_features(self):
result = super(Method, self).get_supported_features() result = super(Method, self).get_supported_features()
result.ipv6 = True result.ipv6 = True
result.udp = True if recvmsg is None:
result.udp = False
result.dns = False
else:
result.udp = True
result.dns = True
return result return result
def get_tcp_dstip(self, sock): def get_tcp_dstip(self, sock):
@ -162,6 +168,91 @@ class Method(BaseMethod):
tproxy_chain = 'sshuttle-t-%s' % port tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port divert_chain = 'sshuttle-d-%s' % port
# basic cleanup/setup of chains
self.restore_firewall(port, family, udp)
_ipt('-N', mark_chain)
_ipt('-F', mark_chain)
_ipt('-N', divert_chain)
_ipt('-F', divert_chain)
_ipt('-N', tproxy_chain)
_ipt('-F', tproxy_chain)
_ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
_ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
_ipt('-A', divert_chain, '-j', 'ACCEPT')
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'tcp', '-p', 'tcp')
if udp:
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'udp', '-p', 'udp')
for f, ip in [i for i in nslist if i[0] == family]:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/32' % ip,
'-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):
if sexclude:
_ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp',
'--on-port', str(port))
if udp:
if sexclude:
_ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp',
'--on-port', str(port))
def restore_firewall(self, port, family, udp):
if family not in [socket.AF_INET, socket.AF_INET6]:
raise Exception(
'Address family "%s" unsupported by tproxy method'
% family_to_string(family))
table = "mangle"
def _ipt(*args):
return ipt(family, table, *args)
def _ipt_ttl(*args):
return ipt_ttl(family, table, *args)
mark_chain = 'sshuttle-m-%s' % port
tproxy_chain = 'sshuttle-t-%s' % port
divert_chain = 'sshuttle-d-%s' % port
# basic cleanup/setup of chains # basic cleanup/setup of chains
if ipt_chain_exists(family, table, mark_chain): if ipt_chain_exists(family, table, mark_chain):
_ipt('-D', 'OUTPUT', '-j', mark_chain) _ipt('-D', 'OUTPUT', '-j', mark_chain)
@ -176,81 +267,3 @@ class Method(BaseMethod):
if ipt_chain_exists(family, table, divert_chain): if ipt_chain_exists(family, table, divert_chain):
_ipt('-F', divert_chain) _ipt('-F', divert_chain)
_ipt('-X', divert_chain) _ipt('-X', divert_chain)
if subnets or dnsport:
_ipt('-N', mark_chain)
_ipt('-F', mark_chain)
_ipt('-N', divert_chain)
_ipt('-F', divert_chain)
_ipt('-N', tproxy_chain)
_ipt('-F', tproxy_chain)
_ipt('-I', 'OUTPUT', '1', '-j', mark_chain)
_ipt('-I', 'PREROUTING', '1', '-j', tproxy_chain)
_ipt('-A', divert_chain, '-j', 'MARK', '--set-mark', '1')
_ipt('-A', divert_chain, '-j', 'ACCEPT')
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'tcp', '-p', 'tcp')
if subnets and udp:
_ipt('-A', tproxy_chain, '-m', 'socket', '-j', divert_chain,
'-m', 'udp', '-p', 'udp')
if dnsport:
for f, ip in [i for i in nslist if i[0] == family]:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/32' % ip,
'-m', 'udp', '-p', 'udp', '--dport', '53',
'--on-port', str(dnsport))
if subnets:
for f, swidth, sexclude, snet \
in sorted(subnets, key=lambda s: s[1], reverse=True):
if sexclude:
_ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
else:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'tcp', '-p', 'tcp',
'--on-port', str(port))
if sexclude and udp:
_ipt('-A', mark_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
_ipt('-A', tproxy_chain, '-j', 'RETURN',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
elif udp:
_ipt('-A', mark_chain, '-j', 'MARK', '--set-mark', '1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp')
_ipt('-A', tproxy_chain, '-j', 'TPROXY',
'--tproxy-mark', '0x1/0x1',
'--dest', '%s/%s' % (snet, swidth),
'-m', 'udp', '-p', 'udp',
'--on-port', str(port))
def check_settings(self, udp, dns):
if udp and recvmsg is None:
Fatal("tproxy UDP support requires recvmsg function.\n")
if dns and recvmsg is None:
Fatal("tproxy DNS support requires recvmsg function.\n")
if udp:
debug1("tproxy UDP support enabled.\n")
if dns:
debug1("tproxy DNS support enabled.\n")

View File

@ -5,6 +5,7 @@ import traceback
import time import time
import sys import sys
import os import os
import platform
import sshuttle.ssnet as ssnet import sshuttle.ssnet as ssnet
import sshuttle.helpers as helpers import sshuttle.helpers as helpers
@ -16,22 +17,23 @@ from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, \
def _ipmatch(ipstr): def _ipmatch(ipstr):
if ipstr == 'default': if ipstr == b'default':
ipstr = '0.0.0.0/0' ipstr = b'0.0.0.0/0'
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) m = re.match(b'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if m: if m:
g = m.groups() g = m.groups()
ips = g[0] ips = g[0]
width = int(g[4] or 32) width = int(g[4] or 32)
if g[1] is None: if g[1] is None:
ips += '.0.0.0' ips += b'.0.0.0'
width = min(width, 8) width = min(width, 8)
elif g[2] is None: elif g[2] is None:
ips += '.0.0' ips += b'.0.0'
width = min(width, 16) width = min(width, 16)
elif g[3] is None: elif g[3] is None:
ips += '.0' ips += b'.0'
width = min(width, 24) width = min(width, 24)
ips = ips.decode("ASCII")
return (struct.unpack('!I', socket.inet_aton(ips))[0], width) return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
@ -56,11 +58,12 @@ def _shl(n, bits):
def _list_routes(): def _list_routes():
# FIXME: IPv4 only
argv = ['netstat', '-rn'] argv = ['netstat', '-rn']
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
routes = [] routes = []
for line in p.stdout: for line in p.stdout:
cols = re.split(r'\s+', line) cols = re.split(b'\s+', line)
ipw = _ipmatch(cols[0]) ipw = _ipmatch(cols[0])
if not ipw: if not ipw:
continue # some lines won't be parseable; never mind continue # some lines won't be parseable; never mind
@ -214,6 +217,9 @@ class UdpProxy(Handler):
def main(latency_control): def main(latency_control):
debug1('Starting server with Python version %s\n'
% platform.python_version())
if helpers.verbose >= 1: if helpers.verbose >= 1:
helpers.logprefix = ' s: ' helpers.logprefix = ' s: '
else: else:
@ -235,26 +241,26 @@ def main(latency_control):
socket.fromfd(sys.stdout.fileno(), socket.fromfd(sys.stdout.fileno(),
socket.AF_INET, socket.SOCK_STREAM)) socket.AF_INET, socket.SOCK_STREAM))
handlers.append(mux) handlers.append(mux)
routepkt = '' routepkt = b''
for r in routes: for r in routes:
routepkt += '%d,%s,%d\n' % r routepkt += b'%d,%s,%d\n' % (r[0], r[1].encode("ASCII"), r[2])
mux.send(0, ssnet.CMD_ROUTES, routepkt) mux.send(0, ssnet.CMD_ROUTES, routepkt)
hw = Hostwatch() hw = Hostwatch()
hw.leftover = '' hw.leftover = b''
def hostwatch_ready(sock): def hostwatch_ready(sock):
assert(hw.pid) assert(hw.pid)
content = hw.sock.recv(4096) content = hw.sock.recv(4096)
if content: if content:
lines = (hw.leftover + content).split('\n') lines = (hw.leftover + content).split(b'\n')
if lines[-1]: if lines[-1]:
# no terminating newline: entry isn't complete yet! # no terminating newline: entry isn't complete yet!
hw.leftover = lines.pop() hw.leftover = lines.pop()
lines.append('') lines.append('')
else: else:
hw.leftover = '' hw.leftover = b''
mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines)) mux.send(0, ssnet.CMD_HOST_LIST, b'\n'.join(lines))
else: else:
raise Fatal('hostwatch process died') raise Fatal('hostwatch process died')
@ -266,7 +272,7 @@ def main(latency_control):
mux.got_host_req = got_host_req mux.got_host_req = got_host_req
def new_channel(channel, data): def new_channel(channel, data):
(family, dstip, dstport) = data.split(',', 2) (family, dstip, dstport) = data.split(b',', 2)
family = int(family) family = int(family)
dstport = int(dstport) dstport = int(dstport)
outwrap = ssnet.connect_dst(family, dstip, dstport) outwrap = ssnet.connect_dst(family, dstip, dstport)
@ -324,14 +330,20 @@ def main(latency_control):
if dnshandlers: if dnshandlers:
now = time.time() now = time.time()
for channel, h in list(dnshandlers.items()): remove = []
for channel, h in dnshandlers.items():
if h.timeout < now or not h.ok: if h.timeout < now or not h.ok:
debug3('expiring dnsreqs channel=%d\n' % channel) debug3('expiring dnsreqs channel=%d\n' % channel)
del dnshandlers[channel] remove.append(channel)
h.ok = False h.ok = False
for channel in remove:
del dnshandlers[channel]
if udphandlers: if udphandlers:
for channel, h in list(udphandlers.items()): remove = []
for channel, h in udphandlers.items():
if not h.ok: if not h.ok:
debug3('expiring UDP channel=%d\n' % channel) debug3('expiring UDP channel=%d\n' % channel)
del udphandlers[channel] remove.append(channel)
h.ok = False h.ok = False
for channel in remove:
del udphandlers[channel]

View File

@ -91,7 +91,8 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
pyscript = r""" pyscript = r"""
import sys; import sys;
verbosity=%d; verbosity=%d;
exec compile(sys.stdin.read(%d), "assembler.py", "exec") stdin=getattr(sys.stdin,"buffer",sys.stdin);
exec(compile(stdin.read(%d), "assembler.py", "exec"))
""" % (helpers.verbose or 0, len(content)) """ % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip()) pyscript = re.sub(r'\s+', ' ', pyscript.strip())
@ -107,7 +108,7 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
if python: if python:
pycmd = "'%s' -c '%s'" % (python, pyscript) pycmd = "'%s' -c '%s'" % (python, pyscript)
else: else:
pycmd = ("P=python2; $P -V 2>/dev/null || P=python; " pycmd = ("P=python3.5; $P -V 2>/dev/null || P=python; "
"exec \"$P\" -c '%s'") % pyscript "exec \"$P\" -c '%s'") % pyscript
argv = (sshl + argv = (sshl +
portl + portl +

View File

@ -227,7 +227,7 @@ conflicts between client and server.
Unlike most VPNs, sshuttle forwards sessions, not packets. Unlike most VPNs, sshuttle forwards sessions, not packets.
That is, it uses kernel transparent proxying (`iptables That is, it uses kernel transparent proxying (`iptables
REDIRECT` rules on Linux, or `ipfw fwd` rules on BSD) to REDIRECT` rules on Linux) to
capture outgoing TCP sessions, then creates entirely capture outgoing TCP sessions, then creates entirely
separate TCP sessions out to the original destination at separate TCP sessions out to the original destination at
the other end of the tunnel. the other end of the tunnel.
@ -256,24 +256,6 @@ between the two separate streams, so a tcp-based tunnel is
fine. fine.
# BUGS
On MacOS 10.6 (at least up to 10.6.6), your network will
stop responding about 10 minutes after the first time you
start sshuttle, because of a MacOS kernel bug relating to
arp and the net.inet.ip.scopedroute sysctl. To fix it,
just switch your wireless off and on. Sshuttle makes the
kernel setting it changes permanent, so this won't happen
again, even after a reboot.
On MacOS, sshuttle will set the kernel boot flag
net.inet.ip.scopedroute to 0, which interferes with OS X
Internet Sharing and some VPN clients. To reset this flag,
you can remove any reference to net.inet.ip.scopedroute from
/Library/Preferences/SystemConfiguration/com.apple.Boot.plist
and reboot.
# SEE ALSO # SEE ALSO
`ssh`(1), `python`(1) `ssh`(1), `python`(1)

View File

@ -1,9 +1,5 @@
from mock import Mock, patch, call from mock import Mock, patch, call
import io import io
import os
import os.path
import shutil
import filecmp
import sshuttle.firewall import sshuttle.firewall
@ -19,27 +15,27 @@ NSLIST
10,2404:6800:4004:80c::33 10,2404:6800:4004:80c::33
PORTS 1024,1025,1026,1027 PORTS 1024,1025,1026,1027
GO 1 GO 1
HOST 1.2.3.3,existing
""") """)
stdout = Mock() stdout = Mock()
return stdin, stdout return stdin, stdout
@patch('sshuttle.firewall.HOSTSFILE', new='tmp/hosts') def test_rewrite_etc_hosts(tmpdir):
@patch('sshuttle.firewall.hostmap', new={ orig_hosts = tmpdir.join("hosts.orig")
'myhost': '1.2.3.4', orig_hosts.write("1.2.3.3 existing\n")
'myotherhost': '1.2.3.5',
})
def test_rewrite_etc_hosts():
if not os.path.isdir("tmp"):
os.mkdir("tmp")
with open("tmp/hosts.orig", "w") as f: new_hosts = tmpdir.join("hosts")
f.write("1.2.3.3 existing\n") orig_hosts.copy(new_hosts)
shutil.copyfile("tmp/hosts.orig", "tmp/hosts") hostmap = {
'myhost': '1.2.3.4',
'myotherhost': '1.2.3.5',
}
with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)):
sshuttle.firewall.rewrite_etc_hosts(hostmap, 10)
sshuttle.firewall.rewrite_etc_hosts(10) with new_hosts.open() as f:
with open("tmp/hosts") as f:
line = f.readline() line = f.readline()
s = line.split() s = line.split()
assert s == ['1.2.3.3', 'existing'] assert s == ['1.2.3.3', 'existing']
@ -57,39 +53,37 @@ def test_rewrite_etc_hosts():
line = f.readline() line = f.readline()
assert line == "" assert line == ""
sshuttle.firewall.restore_etc_hosts(10) with patch('sshuttle.firewall.HOSTSFILE', new=str(new_hosts)):
assert filecmp.cmp("tmp/hosts.orig", "tmp/hosts", shallow=False) is True sshuttle.firewall.restore_etc_hosts(10)
assert orig_hosts.computehash() == new_hosts.computehash()
@patch('sshuttle.firewall.HOSTSFILE', new='tmp/hosts') @patch('sshuttle.firewall.rewrite_etc_hosts')
@patch('sshuttle.firewall.setup_daemon') @patch('sshuttle.firewall.setup_daemon')
@patch('sshuttle.firewall.get_method') @patch('sshuttle.firewall.get_method')
def test_main(mock_get_method, mock_setup_daemon): def test_main(mock_get_method, mock_setup_daemon, mock_rewrite_etc_hosts):
stdin, stdout = setup_daemon() stdin, stdout = setup_daemon()
mock_setup_daemon.return_value = stdin, stdout mock_setup_daemon.return_value = stdin, stdout
if not os.path.isdir("tmp"): mock_get_method("not_auto").name = "test"
os.mkdir("tmp") mock_get_method.reset_mock()
sshuttle.firewall.main("test", False) sshuttle.firewall.main("not_auto", False)
with open("tmp/hosts") as f: assert mock_rewrite_etc_hosts.mock_calls == [
line = f.readline() call({'1.2.3.3': 'existing'}, 1024),
s = line.split() call({}, 1024),
assert s == ['1.2.3.3', 'existing'] ]
line = f.readline() assert stdout.mock_calls == [
assert line == ""
stdout.mock_calls == [
call.write('READY test\n'), call.write('READY test\n'),
call.flush(), call.flush(),
call.write('STARTED\n'), call.write('STARTED\n'),
call.flush() call.flush()
] ]
mock_setup_daemon.mock_calls == [call()] assert mock_setup_daemon.mock_calls == [call()]
mock_get_method.mock_calls == [ assert mock_get_method.mock_calls == [
call('test'), call('not_auto'),
call().setup_firewall( call().setup_firewall(
1024, 1026, 1024, 1026,
[(10, u'2404:6800:4004:80c::33')], [(10, u'2404:6800:4004:80c::33')],
@ -103,7 +97,6 @@ def test_main(mock_get_method, mock_setup_daemon):
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'), (2, 32, True, u'1.2.3.66')],
True), True),
call().setup_firewall()(), call().restore_firewall(1024, 10, True),
call().setup_firewall(1024, 0, [], 10, [], True), call().restore_firewall(1025, 2, True),
call().setup_firewall(1025, 0, [], 2, [], True),
] ]

View File

@ -11,12 +11,32 @@ import sshuttle.helpers
@patch('sshuttle.helpers.sys.stderr') @patch('sshuttle.helpers.sys.stderr')
def test_log(mock_stderr, mock_stdout): def test_log(mock_stderr, mock_stdout):
sshuttle.helpers.log("message") sshuttle.helpers.log("message")
sshuttle.helpers.log("abc")
sshuttle.helpers.log("message 1\n")
sshuttle.helpers.log("message 2\nline2\nline3\n")
sshuttle.helpers.log("message 3\nline2\nline3")
assert mock_stdout.mock_calls == [ assert mock_stdout.mock_calls == [
call.flush(), call.flush(),
call.flush(),
call.flush(),
call.flush(),
call.flush(),
] ]
assert mock_stderr.mock_calls == [ assert mock_stderr.mock_calls == [
call.write('prefix: message'), call.write('prefix: message'),
call.flush(), call.flush(),
call.write('prefix: abc'),
call.flush(),
call.write('prefix: message 1\n'),
call.flush(),
call.write('prefix: message 2\n'),
call.write('---> line2\n'),
call.write('---> line3\n'),
call.flush(),
call.write('prefix: message 3\n'),
call.write('---> line2\n'),
call.write('---> line3\n'),
call.flush(),
] ]

View File

@ -3,6 +3,7 @@ from mock import Mock, patch, call
import socket import socket
import struct import struct
from sshuttle.helpers import Fatal
from sshuttle.methods import get_method from sshuttle.methods import get_method
@ -11,6 +12,7 @@ def test_get_supported_features():
features = method.get_supported_features() features = method.get_supported_features()
assert not features.ipv6 assert not features.ipv6
assert not features.udp assert not features.udp
assert features.dns
def test_get_tcp_dstip(): def test_get_tcp_dstip():
@ -52,10 +54,18 @@ def test_setup_udp_listener():
assert listener.mock_calls == [] assert listener.mock_calls == []
def test_check_settings(): def test_assert_features():
method = get_method('nat') method = get_method('nat')
method.check_settings(True, True) features = method.get_supported_features()
method.check_settings(False, True) method.assert_features(features)
features.udp = True
with pytest.raises(Fatal):
method.assert_features(features)
features.ipv6 = True
with pytest.raises(Fatal):
method.assert_features(features)
def test_firewall_command(): def test_firewall_command():
@ -129,7 +139,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
mock_ipt_ttl.reset_mock() mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock() mock_ipt.reset_mock()
method.setup_firewall(1025, 0, [], 2, [], False) method.restore_firewall(1025, 2, 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')
] ]

View File

@ -3,6 +3,8 @@ from mock import Mock, patch, call, ANY
import socket import socket
from sshuttle.methods import get_method from sshuttle.methods import get_method
from sshuttle.helpers import Fatal
from sshuttle.methods.pf import OsDefs
def test_get_supported_features(): def test_get_supported_features():
@ -10,8 +12,10 @@ def test_get_supported_features():
features = method.get_supported_features() features = method.get_supported_features()
assert not features.ipv6 assert not features.ipv6
assert not features.udp assert not features.udp
assert features.dns
@patch('sshuttle.helpers.verbose', new=3)
def test_get_tcp_dstip(): def test_get_tcp_dstip():
sock = Mock() sock = Mock()
sock.getpeername.return_value = ("127.0.0.1", 1024) sock.getpeername.return_value = ("127.0.0.1", 1024)
@ -20,7 +24,7 @@ def test_get_tcp_dstip():
firewall = Mock() firewall = Mock()
firewall.pfile.readline.return_value = \ firewall.pfile.readline.return_value = \
"QUERY_PF_NAT_SUCCESS 127.0.0.3,1026\n" b"QUERY_PF_NAT_SUCCESS 127.0.0.3,1026\n"
method = get_method('pf') method = get_method('pf')
method.set_firewall(firewall) method.set_firewall(firewall)
@ -31,7 +35,7 @@ def test_get_tcp_dstip():
call.getsockname(), call.getsockname(),
] ]
assert firewall.mock_calls == [ assert firewall.mock_calls == [
call.pfile.write('QUERY_PF_NAT 2,6,127.0.0.1,1024,127.0.0.2,1025\n'), call.pfile.write(b'QUERY_PF_NAT 2,6,127.0.0.1,1024,127.0.0.2,1025\n'),
call.pfile.flush(), call.pfile.flush(),
call.pfile.readline() call.pfile.readline()
] ]
@ -67,16 +71,25 @@ def test_setup_udp_listener():
assert listener.mock_calls == [] assert listener.mock_calls == []
def test_check_settings(): def test_assert_features():
method = get_method('pf') method = get_method('pf')
method.check_settings(True, True) features = method.get_supported_features()
method.check_settings(False, True) method.assert_features(features)
features.udp = True
with pytest.raises(Fatal):
method.assert_features(features)
features.ipv6 = True
with pytest.raises(Fatal):
method.assert_features(features)
@patch('sshuttle.methods.pf.osdefs', OsDefs('darwin'))
@patch('sshuttle.methods.pf.sys.stdout') @patch('sshuttle.methods.pf.sys.stdout')
@patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev') @patch('sshuttle.methods.pf.pf_get_dev')
def test_firewall_command(mock_pf_get_dev, mock_ioctl, mock_stdout): def test_firewall_command_darwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
method = get_method('pf') method = get_method('pf')
assert not method.firewall_command("somthing") assert not method.firewall_command("somthing")
@ -87,7 +100,7 @@ def test_firewall_command(mock_pf_get_dev, mock_ioctl, mock_stdout):
assert mock_pf_get_dev.mock_calls == [call()] assert mock_pf_get_dev.mock_calls == [call()]
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 3226747927, ANY), call(mock_pf_get_dev(), 0xc0544417, ANY),
] ]
assert mock_stdout.mock_calls == [ assert mock_stdout.mock_calls == [
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'), call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
@ -95,13 +108,46 @@ def test_firewall_command(mock_pf_get_dev, mock_ioctl, mock_stdout):
] ]
# FIXME - test fails with platform=='darwin' due re.search not liking Mock @patch('sshuttle.methods.pf.osdefs', OsDefs('notdarwin'))
# objects. @patch('sshuttle.methods.pf.sys.stdout')
@patch('sshuttle.methods.pf.sys.platform', 'not_darwin') @patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev')
def test_firewall_command_notdarwin(mock_pf_get_dev, mock_ioctl, mock_stdout):
method = get_method('pf')
assert not method.firewall_command("somthing")
command = "QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % (
socket.AF_INET, socket.IPPROTO_TCP,
"127.0.0.1", 1025, "127.0.0.2", 1024)
assert method.firewall_command(command)
assert mock_pf_get_dev.mock_calls == [call()]
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xc04c4417, ANY),
]
assert mock_stdout.mock_calls == [
call.write('QUERY_PF_NAT_SUCCESS 0.0.0.0,0\n'),
call.flush(),
]
def pfctl(args, stdin=None):
if args == '-s all':
return (b'INFO:\nStatus: Disabled\nanother mary had a little lamb\n',
b'little lamb\n')
if args == '-E':
return (b'\n', b'Token : abcdefg\n')
return None
@patch('sshuttle.helpers.verbose', new=3)
@patch('sshuttle.methods.pf.osdefs', OsDefs('darwin'))
@patch('sshuttle.methods.pf.pfctl') @patch('sshuttle.methods.pf.pfctl')
@patch('sshuttle.methods.pf.ioctl') @patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev') @patch('sshuttle.methods.pf.pf_get_dev')
def test_setup_firewall(mock_pf_get_dev, mock_ioctl, mock_pfctl): def test_setup_firewall_darwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_pfctl.side_effect = pfctl
method = get_method('pf') method = get_method('pf')
assert method.name == 'pf' assert method.name == 'pf'
@ -138,23 +184,119 @@ def test_setup_firewall(mock_pf_get_dev, mock_ioctl, mock_pfctl):
[(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')], [(2, 24, False, u'1.2.3.0'), (2, 32, True, u'1.2.3.66')],
False) False)
assert mock_ioctl.mock_calls == [ assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 3295691827, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 3424666650, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 3424666650, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 3295691827, ANY), call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 3424666650, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 3424666650, ANY), call(mock_pf_get_dev(), 0xCC20441A, ANY),
]
assert mock_pfctl.mock_calls == [
call('-s all'),
call('-a sshuttle -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 proto tcp '
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 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 route-to lo0 inet proto udp '
b'to <dns_servers> port 53 keep state\n'),
call('-E'),
] ]
# FIXME - needs more work
# print(mock_pfctl.mock_calls)
# assert mock_pfctl.mock_calls == []
mock_pf_get_dev.reset_mock() mock_pf_get_dev.reset_mock()
mock_ioctl.reset_mock() mock_ioctl.reset_mock()
mock_pfctl.reset_mock() mock_pfctl.reset_mock()
method.setup_firewall(1025, 0, [], 2, [], False) method.restore_firewall(1025, 2, False)
assert mock_ioctl.mock_calls == [] assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [call('-a sshuttle -F all')] assert mock_pfctl.mock_calls == [
call('-a sshuttle -F all'),
call("-X abcdefg"),
]
mock_pf_get_dev.reset_mock()
mock_pfctl.reset_mock()
mock_ioctl.reset_mock()
@patch('sshuttle.helpers.verbose', new=3)
@patch('sshuttle.methods.pf.osdefs', OsDefs('notdarwin'))
@patch('sshuttle.methods.pf.pfctl')
@patch('sshuttle.methods.pf.ioctl')
@patch('sshuttle.methods.pf.pf_get_dev')
def test_setup_firewall_notdarwin(mock_pf_get_dev, mock_ioctl, mock_pfctl):
mock_pfctl.side_effect = pfctl
method = get_method('pf')
assert method.name == 'pf'
with pytest.raises(Exception) as excinfo:
method.setup_firewall(
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')],
True)
assert str(excinfo.value) \
== 'Address family "AF_INET6" unsupported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == []
with pytest.raises(Exception) as excinfo:
method.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')],
True)
assert str(excinfo.value) == 'UDP not supported by pf method_name'
assert mock_pf_get_dev.mock_calls == []
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == []
method.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')],
False)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
call(mock_pf_get_dev(), 0xCBE0441A, ANY),
]
assert mock_pfctl.mock_calls == [
call('-s all'),
call('-a sshuttle -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 proto tcp '
b'to <forward_subnets> -> 127.0.0.1 port 1025\n'
b'rdr pass on lo0 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 route-to lo0 inet proto udp '
b'to <dns_servers> port 53 keep state\n'),
call('-e'),
]
mock_pf_get_dev.reset_mock()
mock_ioctl.reset_mock()
mock_pfctl.reset_mock()
method.restore_firewall(1025, 2, False)
assert mock_ioctl.mock_calls == []
assert mock_pfctl.mock_calls == [
call('-a sshuttle -F all'),
call("-d"),
]
mock_pf_get_dev.reset_mock() mock_pf_get_dev.reset_mock()
mock_pfctl.reset_mock() mock_pfctl.reset_mock()
mock_ioctl.reset_mock() mock_ioctl.reset_mock()

View File

@ -3,11 +3,22 @@ from mock import Mock, patch, call
from sshuttle.methods import get_method from sshuttle.methods import get_method
def test_get_supported_features(): @patch("sshuttle.methods.tproxy.recvmsg")
def test_get_supported_features_recvmsg(mock_recvmsg):
method = get_method('tproxy') method = get_method('tproxy')
features = method.get_supported_features() features = method.get_supported_features()
assert features.ipv6 assert features.ipv6
assert features.udp assert features.udp
assert features.dns
@patch("sshuttle.methods.tproxy.recvmsg", None)
def test_get_supported_features_norecvmsg():
method = get_method('tproxy')
features = method.get_supported_features()
assert features.ipv6
assert not features.udp
assert not features.dns
def test_get_tcp_dstip(): def test_get_tcp_dstip():
@ -66,10 +77,10 @@ def test_setup_udp_listener():
] ]
def test_check_settings(): def test_assert_features():
method = get_method('tproxy') method = get_method('tproxy')
method.check_settings(True, True) features = method.get_supported_features()
method.check_settings(False, True) method.assert_features(features)
def test_firewall_command(): def test_firewall_command():
@ -160,7 +171,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
mock_ipt_ttl.reset_mock() mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock() mock_ipt.reset_mock()
method.setup_firewall(1025, 0, [], 10, [], True) method.restore_firewall(1025, 10, True)
assert mock_ipt_chain_exists.mock_calls == [ assert mock_ipt_chain_exists.mock_calls == [
call(10, 'mangle', 'sshuttle-m-1025'), call(10, 'mangle', 'sshuttle-m-1025'),
call(10, 'mangle', 'sshuttle-t-1025'), call(10, 'mangle', 'sshuttle-t-1025'),
@ -250,7 +261,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
mock_ipt_ttl.reset_mock() mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock() mock_ipt.reset_mock()
method.setup_firewall(1025, 0, [], 2, [], True) method.restore_firewall(1025, 2, 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'),
call(2, 'mangle', 'sshuttle-t-1025'), call(2, 'mangle', 'sshuttle-t-1025'),

16
tox.ini Normal file
View File

@ -0,0 +1,16 @@
[tox]
downloadcache = {toxworkdir}/cache/
envlist =
py27,
py35,
[testenv]
basepython =
py27: python2.7
py35: python3.5
commands =
py.test
deps =
pytest
mock
setuptools>=17.1