oldpy back to sync with master and hostwatch fixes

Ported fixes from #55 so they are compatible with older python versions.
This commit is contained in:
vieira 2016-01-06 23:38:54 +00:00
commit 1bab128cc5
22 changed files with 813 additions and 688 deletions

View File

@ -43,17 +43,9 @@ Client side Requirements
| | | * IPv6 TCP + |
| | | * 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. |
+-------+--------+------------+-----------------------------------------------+
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
------------------------
@ -80,25 +72,42 @@ later.
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
done once after booting up::
- The following commands need to be run first as root. This only needs to be
done once after booting up::
ip route add local default dev lo table 100
ip rule add fwmark 1 lookup 100
ip -6 route add local default dev lo table 100
ip -6 rule add fwmark 1 lookup 100
ip route add local default dev lo table 100
ip rule add fwmark 1 lookup 100
ip -6 route add local default dev lo table 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
connection does not have IPv6 support, no IPv6 routes will exist, IPv6
packets will not be generated and sshuttle cannot intercept them. Add some
dummy routes to external interfaces. Make sure they get removed however
after sshuttle exits.
- You may need to exclude the IP address of the server you are connecting to.
Otherwise sshuttle may attempt to intercept the ssh packets, which will not
work. Use the `--exclude` parameter for this.
- 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

2
run
View File

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

View File

@ -6,6 +6,7 @@ import sshuttle.options as options
import sshuttle.client as client
import sshuttle.firewall as firewall
import sshuttle.hostwatch as hostwatch
import sshuttle.ssyslog as ssyslog
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
dns capture local DNS requests and forward to the remote DNS server
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
r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once)
@ -181,7 +182,7 @@ try:
includes = parse_subnet_file(opt.subnets)
if not opt.method:
method_name = "auto"
elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]:
elif opt.method in ["auto", "nat", "tproxy", "pf"]:
method_name = opt.method
else:
o.fatal("method_name %s not supported" % opt.method)
@ -197,6 +198,9 @@ try:
ipport_v6 = parse_ipport6(ip)
else:
ipport_v4 = parse_ipport4(ip)
if opt.syslog:
ssyslog.start_syslog()
ssyslog.stderr_to_syslog()
return_code = client.main(ipport_v6, ipport_v4,
opt.ssh_cmd,
remotename,
@ -209,7 +213,7 @@ try:
opt.auto_nets,
parse_subnets(includes),
parse_subnets(excludes),
opt.syslog, opt.daemon, opt.pidfile)
opt.daemon, opt.pidfile)
if return_code == 0:
log('Normal exit code, exiting...')

View File

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

View File

@ -14,7 +14,7 @@ import platform
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
resolvconf_nameservers
from sshuttle.methods import get_method
from sshuttle.methods import get_method, Features
_extra_fd = os.open('/dev/null', os.O_RDONLY)
@ -67,7 +67,7 @@ def daemonize():
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
try:
os.write(outfd, '%d\n' % os.getpid())
os.write(outfd, b'%d\n' % os.getpid())
finally:
os.close(outfd)
os.chdir("/")
@ -81,8 +81,6 @@ def daemonize():
os.dup2(si.fileno(), 1)
si.close()
ssyslog.stderr_to_syslog()
def daemon_cleanup():
try:
@ -277,18 +275,25 @@ udp_by_src = {}
def expire_connections(now, mux):
remove = []
for chan, timeout in dnsreqs.items():
if timeout < now:
debug3('expiring dnsreqs channel=%d\n' % chan)
remove.append(chan)
del mux.channels[chan]
del dnsreqs[chan]
for chan in remove:
del dnsreqs[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
remove = []
for peer, (chan, timeout) in udp_by_src.items():
if timeout < now:
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 udp_by_src[peer]
for peer in remove:
del udp_by_src[peer]
debug3('Remaining UDP channels: %d\n' % len(udp_by_src))
@ -330,7 +335,7 @@ def onaccept_tcp(listener, method, mux, handlers):
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))
debug3('doing send from %r to %r\n' % (srcip, dstip,))
method.send_udp(sock, srcip, dstip, data)
@ -349,10 +354,10 @@ def onaccept_udp(listener, method, mux, handlers):
chan = mux.next_channel()
mux.channels[chan] = lambda cmd, data: udp_done(
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
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)
expire_connections(now, mux)
@ -382,8 +387,7 @@ def ondns(listener, method, mux, handlers):
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control,
dns_listener, seed_hosts, auto_nets,
syslog, daemon):
dns_listener, seed_hosts, auto_nets, daemon):
debug1('Starting client with Python version %s\n'
% platform.python_version())
@ -433,21 +437,26 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
if initstring != expected:
raise Fatal('expected server init string %r; got %r'
% (expected, initstring))
debug1('connected.\n')
print('Connected.')
log('Connected.\n')
sys.stdout.flush()
if daemon:
daemonize()
log('daemonizing (%s).\n' % _pidname)
elif syslog:
debug1('switching to syslog.\n')
ssyslog.stderr_to_syslog()
def onroutes(routestr):
if auto_nets:
for line in routestr.strip().split('\n'):
(family, ip, width) = line.split(',', 2)
fw.auto_nets.append((int(family), ip, int(width)))
for line in routestr.strip().split(b'\n'):
(family, ip, width) = line.split(b',', 2)
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
# up intercepting the ssh connection!
@ -493,10 +502,8 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
def main(listenip_v6, listenip_v4,
ssh_cmd, remotename, python, latency_control, dns, nslist,
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:
try:
check_daemon(pidfile)
@ -507,19 +514,45 @@ def main(listenip_v6, listenip_v4,
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 features.ipv6:
if avail.ipv6:
listenip_v6 = ('::1', 0)
else:
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":
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 both ports given, no need to search for a spare port
ports = [0, ]
@ -538,7 +571,7 @@ def main(listenip_v6, listenip_v4,
tcp_listener = MultiListener()
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if udp:
if required.udp:
udp_listener = MultiListener(socket.SOCK_DGRAM)
udp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
else:
@ -586,10 +619,7 @@ def main(listenip_v6, listenip_v4,
udp_listener.print_listening("UDP redirector")
bound = False
if dns or nslist:
if dns:
nslist += resolvconf_nameservers()
dns = True
if required.dns:
# search for spare port for DNS
debug2('Binding DNS:')
ports = range(12300, 9000, -1)
@ -630,22 +660,45 @@ def main(listenip_v6, listenip_v4,
dnsport_v4 = 0
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)
if udp_listener:
fw.method.setup_udp_listener(udp_listener)
if dns_listener:
fw.method.setup_udp_listener(dns_listener)
# start the firewall
fw.setup(subnets_include, subnets_exclude, nslist,
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
udp)
required.udp)
# start the client process
try:
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
python, latency_control, dns_listener,
seed_hosts, auto_nets, syslog,
daemon)
seed_hosts, auto_nets, daemon)
finally:
try:
if daemon:

View File

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

@ -17,9 +17,17 @@ else:
return s
def log(s):
global logprefix
try:
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()
except IOError:
# this could happen if stderr gets forcibly disconnected, eg. because

View File

@ -9,7 +9,7 @@ import platform
import subprocess as ssubprocess
import sshuttle.helpers as helpers
from sshuttle.helpers import log, debug1, debug2, debug3
from sshuttle.helpers import b, log, debug1, debug2, debug3
POLL_TIME = 60 * 15
NETSTAT_POLL_TIME = 30
@ -37,7 +37,7 @@ def write_host_cache():
try:
f = open(tmpname, 'wb')
for name, ip in sorted(hostnames.items()):
f.write('%s,%s\n' % (name, ip))
f.write(b('%s,%s\n' % (name, ip)))
f.close()
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
os.rename(tmpname, CACHEFILE)
@ -124,7 +124,7 @@ def _check_netstat():
argv = ['netstat', '-n']
try:
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
content = p.stdout.read()
content = p.stdout.read().decode("ASCII")
p.wait()
except OSError:
_, e = sys.exc_info()[:2]

View File

@ -39,6 +39,7 @@ class BaseMethod(object):
result = Features()
result.ipv6 = False
result.udp = False
result.dns = True
return result
def get_tcp_dstip(self, sock):
@ -61,13 +62,20 @@ class BaseMethod(object):
def setup_udp_listener(self, udp_listener):
pass
def check_settings(self, udp, dns):
if udp:
Fatal("UDP support not supported with method %s.\n" % self.name)
def assert_features(self, features):
avail = self.get_supported_features()
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):
raise NotImplementedError()
def restore_firewall(self, port, family, udp):
raise NotImplementedError()
def firewall_command(self, line):
return False
@ -86,14 +94,12 @@ def get_method(method_name):
def get_auto_method():
if _program_exists('ipfw'):
method_name = "ipfw"
elif _program_exists('iptables'):
if _program_exists('iptables'):
method_name = "nat"
elif _program_exists('pfctl'):
method_name = "pf"
else:
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)

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
# 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
if ipt_chain_exists(family, table, chain):
nonfatal(_ipt, '-D', 'OUTPUT', '-j', chain)
nonfatal(_ipt, '-D', 'PREROUTING', '-j', chain)
nonfatal(_ipt, '-F', 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 ctypes import c_char, c_uint8, c_uint16, c_uint32, Union, Structure, \
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
@ -24,67 +24,88 @@ def pfctl(args, stdin=None):
return o
_pf_context = {'started_by_sshuttle': False, 'Xtoken': ''}
# 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_context = {'started_by_sshuttle': False, 'Xtoken': 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():
global _pf_fd
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)
length = len(packed_src_ip)
pnl = pfioc_natlook()
pnl = osdefs.pfioc_natlook()
pnl.proto = proto
pnl.direction = PF_OUT
pnl.direction = osdefs.PF_OUT
pnl.af = family
memmove(addressof(pnl.saddr), packed_src_ip, length)
pnl.sxport.port = socket.htons(src_port)
memmove(addressof(pnl.daddr), packed_dst_ip, length)
pnl.sxport.port = socket.htons(src_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)))
ip = socket.inet_ntop(
@ -125,26 +146,26 @@ def pf_add_anchor_rule(type, name):
ACTION_OFFSET = 0
POOL_TICKET_OFFSET = 8
ANCHOR_CALL_OFFSET = 1040
RULE_ACTION_OFFSET = 3068
RULE_ACTION_OFFSET = 3068 if osdefs.platform == 'darwin' else 2968
pr = pfioc_rule()
ppa = pfioc_pooladdr()
pr = osdefs.pfioc_rule()
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) + 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,
struct.pack('I', type), 4) # rule.action = type
memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
'I', PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
'I', osdefs.PF_CHANGE_GET_TICKET), 4) # action = PF_CHANGE_GET_TICKET
ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr)
memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
'I', PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
'I', osdefs.PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
ioctl(pf_get_dev(), osdefs.DIOCCHANGERULE, pr)
class Method(BaseMethod):
@ -156,19 +177,20 @@ class Method(BaseMethod):
proxy = sock.getsockname()
argv = (sock.family, socket.IPPROTO_TCP,
peer[0], peer[1], proxy[0], proxy[1])
pfile.write("QUERY_PF_NAT %d,%d,%s,%d,%s,%d\n" % argv)
peer[0].encode("ASCII"), peer[1],
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()
line = pfile.readline()
debug2("QUERY_PF_NAT %d,%d,%s,%d,%s,%d" % argv + ' > ' + line)
if line.startswith('QUERY_PF_NAT_SUCCESS '):
(ip, port) = line[21:].split(',')
return (ip, int(port))
in_line = pfile.readline()
debug2(out_line.decode("ASCII") + ' > ' + in_line.decode("ASCII"))
if in_line.startswith(b'QUERY_PF_NAT_SUCCESS '):
(ip, port) = in_line[21:].split(b',')
return (ip.decode("ASCII"), int(port))
return sock.getsockname()
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
global _pf_started_by_sshuttle
tables = []
translating_rules = []
filtering_rules = []
@ -180,57 +202,72 @@ class Method(BaseMethod):
if udp:
raise Exception("UDP not supported by pf method_name")
if subnets:
if len(subnets) > 0:
includes = []
# If a given subnet is both included and excluded, list the
# exclusion first; the table will ignore the second, opposite
# definition
for f, swidth, sexclude, snet in sorted(
subnets, key=lambda s: (s[1], s[2]), reverse=True):
includes.append("%s%s/%s" %
("!" if sexclude else "", snet, swidth))
includes.append(b"%s%s/%d" %
(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(
'rdr pass on lo0 proto tcp '
'to <forward_subnets> -> 127.0.0.1 port %r' % port)
b'rdr pass on lo0 proto tcp '
b'to <forward_subnets> -> 127.0.0.1 port %r' % port)
filtering_rules.append(
'pass out route-to lo0 inet proto tcp '
'to <forward_subnets> keep state')
b'pass out route-to lo0 inet proto tcp '
b'to <forward_subnets> keep state')
if dnsport:
tables.append('table <dns_servers> {%s}' % ','.join(
[ns[1] for ns in nslist]))
translating_rules.append(
'rdr pass on lo0 proto udp to '
'<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
filtering_rules.append(
'pass out route-to lo0 inet proto udp to '
'<dns_servers> port 53 keep state')
if len(nslist) > 0:
tables.append(
b'table <dns_servers> {%s}' %
b','.join([ns[1].encode("ASCII") for ns in nslist]))
translating_rules.append(
b'rdr pass on lo0 proto udp to '
b'<dns_servers> port 53 -> 127.0.0.1 port %r' % dnsport)
filtering_rules.append(
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) \
+ '\n'
rules = b'\n'.join(tables + translating_rules + filtering_rules) \
+ b'\n'
assert isinstance(rules, bytes)
debug3("rules:\n" + rules.decode("ASCII"))
pf_status = pfctl('-s all')[0]
if '\nrdr-anchor "sshuttle" all\n' not in pf_status:
pf_add_anchor_rule(PF_RDR, "sshuttle")
if '\nanchor "sshuttle" all\n' not in pf_status:
pf_add_anchor_rule(PF_PASS, "sshuttle")
pf_status = pfctl('-s all')[0]
if b'\nrdr-anchor "sshuttle" all\n' not in pf_status:
pf_add_anchor_rule(osdefs.PF_RDR, b"sshuttle")
if b'\nanchor "sshuttle" all\n' not in pf_status:
pf_add_anchor_rule(osdefs.PF_PASS, b"sshuttle")
pfctl('-a sshuttle -f /dev/stdin', rules)
if sys.platform == "darwin":
o = pfctl('-E')
_pf_context['Xtoken'] = \
re.search(r'Token : (.+)', o[1]).group(1)
elif 'INFO:\nStatus: Disabled' in pf_status:
pfctl('-e')
_pf_context['started_by_sshuttle'] = True
else:
pfctl('-a sshuttle -F all')
if sys.platform == "darwin":
pfctl('-X %s' % _pf_context['Xtoken'])
elif _pf_context['started_by_sshuttle']:
pfctl('-d')
pfctl('-a sshuttle -f /dev/stdin', rules)
if osdefs.platform == "darwin":
o = pfctl('-E')
_pf_context['Xtoken'] = \
re.search(b'Token : (.+)', o[1]).group(1)
elif b'INFO:\nStatus: Disabled' in pf_status:
pfctl('-e')
_pf_context['started_by_sshuttle'] = True
def restore_firewall(self, port, family, udp):
if family != socket.AF_INET:
raise Exception(
'Address family "%s" unsupported by pf method_name'
% 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):
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])
dstip = (ip, port)
break
print("xxxxx", srcip, dstip)
return (srcip, dstip, data)
elif recvmsg == "socket_ext":
def recv_udp(listener, bufsize):
@ -105,7 +106,12 @@ class Method(BaseMethod):
def get_supported_features(self):
result = super(Method, self).get_supported_features()
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
def get_tcp_dstip(self, sock):
@ -162,6 +168,91 @@ class Method(BaseMethod):
tproxy_chain = 'sshuttle-t-%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
if ipt_chain_exists(family, table, mark_chain):
_ipt('-D', 'OUTPUT', '-j', mark_chain)
@ -176,81 +267,3 @@ class Method(BaseMethod):
if ipt_chain_exists(family, table, divert_chain):
_ipt('-F', 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

@ -17,22 +17,23 @@ from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \
def _ipmatch(ipstr):
if ipstr == 'default':
ipstr = '0.0.0.0/0'
m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if ipstr == b('default'):
ipstr = b('0.0.0.0/0')
m = re.match(b('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$'), ipstr)
if m:
g = m.groups()
ips = g[0]
width = int(g[4] or 32)
if g[1] is None:
ips += '.0.0.0'
ips += b('.0.0.0')
width = min(width, 8)
elif g[2] is None:
ips += '.0.0'
ips += b('.0.0')
width = min(width, 16)
elif g[3] is None:
ips += '.0'
ips += b('.0')
width = min(width, 24)
ips = ips.decode("ASCII")
return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
@ -57,11 +58,12 @@ def _shl(n, bits):
def _list_routes():
# FIXME: IPv4 only
argv = ['netstat', '-rn']
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
routes = []
for line in p.stdout:
cols = re.split(r'\s+', line.decode("ASCII"))
cols = re.split(b('\s+'), line)
ipw = _ipmatch(cols[0])
if not ipw:
continue # some lines won't be parseable; never mind
@ -249,20 +251,20 @@ def main(latency_control):
mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
hw = Hostwatch()
hw.leftover = ''
hw.leftover = b('')
def hostwatch_ready(sock):
assert(hw.pid)
content = hw.sock.recv(4096)
if content:
lines = (hw.leftover + content).split('\n')
lines = (hw.leftover + content).split(b('\n'))
if lines[-1]:
# no terminating newline: entry isn't complete yet!
hw.leftover = lines.pop()
lines.append('')
else:
hw.leftover = ''
mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines))
hw.leftover = b('')
mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
else:
raise Fatal('hostwatch process died')
@ -274,7 +276,7 @@ def main(latency_control):
mux.got_host_req = got_host_req
def new_channel(channel, data):
(family, dstip, dstport) = data.decode("ASCII").split(',', 2)
(family, dstip, dstport) = data.split(b(','), 2)
family = int(family)
dstport = int(dstport)
outwrap = ssnet.connect_dst(family, dstip, dstport)
@ -332,14 +334,20 @@ def main(latency_control):
if dnshandlers:
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:
debug3('expiring dnsreqs channel=%d\n' % channel)
del dnshandlers[channel]
remove.append(channel)
h.ok = False
for channel in remove:
del dnshandlers[channel]
if udphandlers:
for channel, h in list(udphandlers.items()):
remove = []
for channel, h in udphandlers.items():
if not h.ok:
debug3('expiring UDP channel=%d\n' % channel)
del udphandlers[channel]
remove.append(channel)
h.ok = False
for channel in remove:
del udphandlers[channel]

View File

@ -89,10 +89,10 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
b"\n")
pyscript = r"""
import sys, os;
import sys;
verbosity=%d;
sys.stdin = os.fdopen(0, "rb");
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))
pyscript = re.sub(r'\s+', ' ', pyscript.strip())
@ -108,7 +108,7 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
if python:
pycmd = "'%s' -c '%s'" % (python, pyscript)
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
argv = (sshl +
portl +

View File

@ -227,7 +227,7 @@ conflicts between client and server.
Unlike most VPNs, sshuttle forwards sessions, not packets.
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
separate TCP sessions out to the original destination at
the other end of the tunnel.
@ -256,24 +256,6 @@ between the two separate streams, so a tcp-based tunnel is
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
`ssh`(1), `python`(1)

View File

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

View File

@ -11,12 +11,32 @@ import sshuttle.helpers
@patch('sshuttle.helpers.sys.stderr')
def test_log(mock_stderr, mock_stdout):
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 == [
call.flush(),
call.flush(),
call.flush(),
call.flush(),
call.flush(),
]
assert mock_stderr.mock_calls == [
call.write('prefix: message'),
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 struct
from sshuttle.helpers import Fatal
from sshuttle.methods import get_method
@ -11,6 +12,7 @@ def test_get_supported_features():
features = method.get_supported_features()
assert not features.ipv6
assert not features.udp
assert features.dns
def test_get_tcp_dstip():
@ -52,10 +54,18 @@ def test_setup_udp_listener():
assert listener.mock_calls == []
def test_check_settings():
def test_assert_features():
method = get_method('nat')
method.check_settings(True, True)
method.check_settings(False, True)
features = method.get_supported_features()
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():
@ -129,7 +139,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
mock_ipt_ttl.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 == [
call(2, 'nat', 'sshuttle-1025')
]

View File

@ -3,6 +3,8 @@ from mock import Mock, patch, call, ANY
import socket
from sshuttle.methods import get_method
from sshuttle.helpers import Fatal
from sshuttle.methods.pf import OsDefs
def test_get_supported_features():
@ -10,8 +12,10 @@ def test_get_supported_features():
features = method.get_supported_features()
assert not features.ipv6
assert not features.udp
assert features.dns
@patch('sshuttle.helpers.verbose', new=3)
def test_get_tcp_dstip():
sock = Mock()
sock.getpeername.return_value = ("127.0.0.1", 1024)
@ -20,7 +24,7 @@ def test_get_tcp_dstip():
firewall = Mock()
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.set_firewall(firewall)
@ -31,7 +35,7 @@ def test_get_tcp_dstip():
call.getsockname(),
]
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.readline()
]
@ -67,16 +71,25 @@ def test_setup_udp_listener():
assert listener.mock_calls == []
def test_check_settings():
def test_assert_features():
method = get_method('pf')
method.check_settings(True, True)
method.check_settings(False, True)
features = method.get_supported_features()
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.ioctl')
@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')
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_ioctl.mock_calls == [
call(mock_pf_get_dev(), 3226747927, ANY),
call(mock_pf_get_dev(), 0xc0544417, ANY),
]
assert mock_stdout.mock_calls == [
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
# objects.
@patch('sshuttle.methods.pf.sys.platform', 'not_darwin')
@patch('sshuttle.methods.pf.osdefs', OsDefs('notdarwin'))
@patch('sshuttle.methods.pf.sys.stdout')
@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.ioctl')
@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')
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')],
False)
assert mock_ioctl.mock_calls == [
call(mock_pf_get_dev(), 3295691827, ANY),
call(mock_pf_get_dev(), 3424666650, ANY),
call(mock_pf_get_dev(), 3424666650, ANY),
call(mock_pf_get_dev(), 3295691827, ANY),
call(mock_pf_get_dev(), 3424666650, ANY),
call(mock_pf_get_dev(), 3424666650, ANY),
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 0xCC20441A, ANY),
call(mock_pf_get_dev(), 0xC4704433, ANY),
call(mock_pf_get_dev(), 0xCC20441A, 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_ioctl.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_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_pfctl.reset_mock()
mock_ioctl.reset_mock()

View File

@ -3,11 +3,22 @@ from mock import Mock, patch, call
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')
features = method.get_supported_features()
assert features.ipv6
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():
@ -66,10 +77,10 @@ def test_setup_udp_listener():
]
def test_check_settings():
def test_assert_features():
method = get_method('tproxy')
method.check_settings(True, True)
method.check_settings(False, True)
features = method.get_supported_features()
method.assert_features(features)
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.reset_mock()
method.setup_firewall(1025, 0, [], 10, [], True)
method.restore_firewall(1025, 10, True)
assert mock_ipt_chain_exists.mock_calls == [
call(10, 'mangle', 'sshuttle-m-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.reset_mock()
method.setup_firewall(1025, 0, [], 2, [], True)
method.restore_firewall(1025, 2, True)
assert mock_ipt_chain_exists.mock_calls == [
call(2, 'mangle', 'sshuttle-m-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