Commit ac723694 authored by Brian May's avatar Brian May

Restructure code

Pull out firewall methods code into seperate files.

Fix problems starting with method=='auto'; we were making decisions
based on the method, before the method had been finalized by the
firewall.

Only very basic testing so far. What could go wrong?
parent bcd3205d
...@@ -149,13 +149,10 @@ helpers.verbose = opt.verbose ...@@ -149,13 +149,10 @@ helpers.verbose = opt.verbose
try: try:
if opt.firewall: if opt.firewall:
if len(extra) != 6: if len(extra) != 0:
o.fatal('exactly six arguments expected') o.fatal('exactly zero arguments expected')
port, dnsport = int(extra[0]), int(extra[1]) result = firewall.main(opt.method, opt.syslog)
nslist = [family_ip_tuple(ns) for ns in parse_list(opt.ns_hosts)] sys.exit(result)
sys.exit(firewall.main(int(extra[0]), int(extra[1]),
int(extra[2]), int(extra[3]), nslist,
extra[4], int(extra[5]), opt.syslog))
elif opt.hostwatch: elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra)) sys.exit(hostwatch.hw_main(extra))
else: else:
...@@ -183,23 +180,20 @@ try: ...@@ -183,23 +180,20 @@ try:
if opt.subnets: if opt.subnets:
includes = parse_subnet_file(opt.subnets) includes = parse_subnet_file(opt.subnets)
if not opt.method: if not opt.method:
method = "auto" method_name = "auto"
elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]: elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]:
method = opt.method method_name = opt.method
else: else:
o.fatal("method %s not supported" % opt.method) o.fatal("method_name %s not supported" % opt.method)
if not opt.listen: if not opt.listen:
if opt.method == "tproxy": ipport_v6 = "auto" # parse_ipport6('[::1]:0')
ipport_v6 = parse_ipport6('[::1]:0') ipport_v4 = "auto" # parse_ipport4('127.0.0.1:0')
else:
ipport_v6 = None
ipport_v4 = parse_ipport4('127.0.0.1:0')
else: else:
ipport_v6 = None ipport_v6 = None
ipport_v4 = None ipport_v4 = None
list = opt.listen.split(",") list = opt.listen.split(",")
for ip in list: for ip in list:
if '[' in ip and ']' in ip and opt.method == "tproxy": if '[' in ip and ']' in ip:
ipport_v6 = parse_ipport6(ip) ipport_v6 = parse_ipport6(ip)
else: else:
ipport_v4 = parse_ipport4(ip) ipport_v4 = parse_ipport4(ip)
...@@ -210,7 +204,7 @@ try: ...@@ -210,7 +204,7 @@ try:
opt.latency_control, opt.latency_control,
opt.dns, opt.dns,
nslist, nslist,
method, method_name,
sh, sh,
opt.auto_nets, opt.auto_nets,
parse_subnets(includes), parse_subnets(includes),
......
This diff is collapsed.
This diff is collapsed.
import socket
import subprocess as ssubprocess
from sshuttle.helpers import log, debug1, Fatal, family_to_string
def nonfatal(func, *args):
try:
func(*args)
except Fatal as e:
log('error: %s\n' % e)
def ipt_chain_exists(family, table, name):
if family == socket.AF_INET6:
cmd = 'ip6tables'
elif family == socket.AF_INET:
cmd = 'iptables'
else:
raise Exception('Unsupported family "%s"' % family_to_string(family))
argv = [cmd, '-t', table, '-nL']
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
for line in p.stdout:
if line.startswith(b'Chain %s ' % name.encode("ASCII")):
return True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def ipt(family, table, *args):
if family == socket.AF_INET6:
argv = ['ip6tables', '-t', table] + list(args)
elif family == socket.AF_INET:
argv = ['iptables', '-t', table] + list(args)
else:
raise Exception('Unsupported family "%s"' % family_to_string(family))
debug1('>> %s\n' % ' '.join(argv))
rv = ssubprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
_no_ttl_module = False
def ipt_ttl(family, *args):
global _no_ttl_module
if not _no_ttl_module:
# we avoid infinite loops by generating server-side connections
# with ttl 42. This makes the client side not recapture those
# connections, in case client == server.
try:
argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42']
ipt(family, *argsplus)
except Fatal:
ipt(family, *args)
# we only get here if the non-ttl attempt succeeds
log('sshuttle: warning: your iptables is missing '
'the ttl module.\n')
_no_ttl_module = True
else:
ipt(family, *args)
import os
import importlib
import socket
import struct
import errno
from sshuttle.helpers import Fatal, debug3
def original_dst(sock):
try:
SO_ORIGINAL_DST = 80
SOCKADDR_MIN = 16
sockaddr_in = sock.getsockopt(socket.SOL_IP,
SO_ORIGINAL_DST, SOCKADDR_MIN)
(proto, port, a, b, c, d) = struct.unpack('!HHBBBB', sockaddr_in[:8])
assert(socket.htons(proto) == socket.AF_INET)
ip = '%d.%d.%d.%d' % (a, b, c, d)
return (ip, port)
except socket.error as e:
if e.args[0] == errno.ENOPROTOOPT:
return sock.getsockname()
raise
class Features(object):
pass
class BaseMethod(object):
def __init__(self, name):
self.name = name
def get_supported_features(self):
result = Features()
result.ipv6 = False
result.udp = False
return result
def get_tcp_dstip(self, sock):
return original_dst(sock)
def recv_udp(self, udp_listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
data, srcip = udp_listener.recvfrom(bufsize)
return (srcip, None, data)
def send_udp(self, sock, srcip, dstip, data):
if srcip is not None:
Fatal("Method %s send_udp does not support setting srcip to %r"
% (self.name, srcip))
sock.sendto(data, dstip)
def setup_tcp_listener(self, tcp_listener):
pass
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 setup_firewall(self, port, dnsport, nslist, family, subnets, udp):
raise NotImplementedError()
def firewall_command(self, line):
return False
def _program_exists(name):
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
for p in paths:
fn = '%s/%s' % (p, name)
if os.path.exists(fn):
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
def get_method(method_name):
module = importlib.import_module("sshuttle.methods.%s" % method_name)
return module.Method(method_name)
def get_auto_method():
if _program_exists('ipfw'):
method_name = "ipfw"
elif _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")
return get_method(method_name)
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
import socket
from sshuttle.helpers import family_to_string
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists, nonfatal
from sshuttle.methods import BaseMethod
class Method(BaseMethod):
# We name the chain based on the transproxy port number so that it's
# possible to run multiple copies of sshuttle at the same time. Of course,
# the multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT").
def setup_firewall(self, port, dnsport, nslist, family, subnets, 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))
import os
import sys
import re
import socket
import struct
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
from sshuttle.methods import BaseMethod
def pfctl(args, stdin=None):
argv = ['pfctl'] + list(args.split(" "))
debug1('>> %s\n' % ' '.join(argv))
p = ssubprocess.Popen(argv, stdin=ssubprocess.PIPE,
stdout=ssubprocess.PIPE,
stderr=ssubprocess.PIPE)
o = p.communicate(stdin)
if p.returncode:
raise Fatal('%r returned %d' % (argv, p.returncode))
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_fd = None
def pf_get_dev():
global _pf_fd
if _pf_fd is None:
_pf_fd = os.open('/dev/pf', os.O_RDWR)
return _pf_fd
def pf_query_nat(family, proto, src_ip, src_port, dst_ip, dst_port):
[proto, family, src_port, dst_port] = [
int(v) for v in [proto, family, src_port, dst_port]]
length = 4 if family == socket.AF_INET else 16
pnl = pfioc_natlook()
pnl.proto = proto
pnl.direction = PF_OUT
pnl.af = family
memmove(addressof(pnl.saddr), socket.inet_pton(pnl.af, src_ip), length)
pnl.sxport.port = socket.htons(src_port)
memmove(addressof(pnl.daddr), socket.inet_pton(pnl.af, dst_ip), length)
pnl.dxport.port = socket.htons(dst_port)
ioctl(pf_get_dev(), DIOCNATLOOK, (
c_char * sizeof(pnl)).from_address(addressof(pnl)))
ip = socket.inet_ntop(
pnl.af, (c_char * length).from_address(addressof(pnl.rdaddr)))
port = socket.ntohs(pnl.rdxport.port)
return (ip, port)
def pf_add_anchor_rule(type, name):
ACTION_OFFSET = 0
POOL_TICKET_OFFSET = 8
ANCHOR_CALL_OFFSET = 1040
RULE_ACTION_OFFSET = 3068
pr = pfioc_rule()
ppa = pfioc_pooladdr()
ioctl(pf_get_dev(), 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
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)
memmove(addressof(pr) + ACTION_OFFSET, struct.pack(
'I', PF_CHANGE_ADD_TAIL), 4) # action = PF_CHANGE_ADD_TAIL
ioctl(pf_get_dev(), DIOCCHANGERULE, pr)
class Method(BaseMethod):
def get_tcp_dstip(self, sock):
# yuck
from sshuttle.client import firewall
peer = sock.getpeername()
proxy = sock.getsockname()
argv = (sock.family, socket.IPPROTO_TCP,
peer[0], peer[1], proxy[0], proxy[1])
firewall.pfile.write("QUERY_PF_NAT %r,%r,%s,%r,%s,%r\n" % argv)
firewall.pfile.flush()
line = firewall.pfile.readline()
debug2("QUERY_PF_NAT %r,%r,%s,%r,%s,%r" % argv + ' > ' + line)
if line.startswith('QUERY_PF_NAT_SUCCESS '):
(ip, port) = line[21:].split(',')
return (ip, 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 = []
if subnets:
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))
tables.append('table <forward_subnets> {%s}' % ','.join(includes))
translating_rules.append(
'rdr pass on lo0 proto tcp '
'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')
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')
rules = '\n'.join(tables + translating_rules + filtering_rules) \
+ '\n'
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")
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')
def firewall_command(self, line):
if line.startswith('QUERY_PF_NAT '):
try:
dst = pf_query_nat(*(line[13:].split(',')))
sys.stdout.write('QUERY_PF_NAT_SUCCESS %s,%r\n' % dst)
except IOError as e:
sys.stdout.write('QUERY_PF_NAT_FAILURE %s\n' % e)
sys.stdout.flush()
return True
else:
return False
import struct
from sshuttle.helpers import family_to_string
from sshuttle.linux import ipt, ipt_ttl, ipt_chain_exists
from sshuttle.methods import BaseMethod
from sshuttle.helpers import debug1, debug3, Fatal
recvmsg = None
try:
# try getting recvmsg from python
import socket as pythonsocket
getattr(pythonsocket.socket, "recvmsg")
socket = pythonsocket
recvmsg = "python"
except AttributeError:
# try getting recvmsg from socket_ext library
try:
import socket_ext
getattr(socket_ext.socket, "recvmsg")
socket = socket_ext
recvmsg = "socket_ext"
except ImportError:
import socket
IP_TRANSPARENT = 19
IP_ORIGDSTADDR = 20
IP_RECVORIGDSTADDR = IP_ORIGDSTADDR
SOL_IPV6 = 41
IPV6_ORIGDSTADDR = 74
IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR
if recvmsg == "python":
def recv_udp(listener, bufsize):
debug3('Accept UDP python using recvmsg.\n')
data, ancdata, msg_flags, srcip = listener.recvmsg(
4096, socket.CMSG_SPACE(24))
dstip = None
family = None
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET:
start = 4
length = 4
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port)
break
elif cmsg_level == SOL_IPV6 and cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET6:
start = 8
length = 16
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(family, cmsg_data[start:start + length])
dstip = (ip, port)
break
return (srcip, dstip, data)
elif recvmsg == "socket_ext":
def recv_udp(listener, bufsize):
debug3('Accept UDP using socket_ext recvmsg.\n')
srcip, data, adata, flags = listener.recvmsg(
(bufsize,), socket.CMSG_SPACE(24))
dstip = None
family = None
for a in adata:
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET:
start = 4
length = 4
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(
family, a.cmsg_data[start:start + length])
dstip = (ip, port)
break
elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
family, port = struct.unpack('=HH', a.cmsg_data[0:4])
port = socket.htons(port)
if family == socket.AF_INET6:
start = 8
length = 16
else:
raise Fatal("Unsupported socket type '%s'" % family)
ip = socket.inet_ntop(
family, a.cmsg_data[start:start + length])
dstip = (ip, port)
break
return (srcip, dstip, data[0])
else:
def recv_udp(listener, bufsize):
debug3('Accept UDP using recvfrom.\n')
data, srcip = listener.recvfrom(bufsize)
return (srcip, None, data)
class Method(BaseMethod):
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.ipv6 = True
result.udp = True
return result
def get_tcp_dstip(self, sock):
return sock.getsockname()
def recv_udp(self, udp_listener, bufsize):
srcip, dstip, data = recv_udp(udp_listener, bufsize)
if not dstip:
debug1(
"-- ignored UDP from %r: "
"couldn't determine destination IP address\n" % (srcip,))
return None
return None
def send_udp(self, sock, srcip, dstip, data):
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
sender.bind(srcip)
sender.sendto(data, dstip)
sender.close()
def setup_tcp_listener(self, tcp_listener):
tcp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
def setup_udp_listener(self, udp_listener):
udp_listener.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
if udp_listener.v4 is not None:
udp_listener.v4.setsockopt(
socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
if udp_listener.v6 is not None:
udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVORIGDSTADDR, 1)
def setup_firewall(self, port, dnsport, nslist, family, subnets, 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)
_ipt('-F', mark_chain)
_ipt('-X', mark_chain)
if ipt_chain_exists(family, table, tproxy_chain):
_ipt('-D', 'PREROUTING', '-j', tproxy_chain)
_ipt('-F', tproxy_chain)
_ipt('-X', tproxy_chain)
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 dns:
debug1("tproxy UDP support enabled.\n")
if udp:
debug1("tproxy DNS support enabled.\n")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment