mirror of
https://github.com/sshuttle/sshuttle.git
synced 2024-11-25 09:23:48 +01:00
ebfc3703ec
...because stupid MacOS ipfw 'fwd' rules don't work quite right with udp. It can intercept packets bound for remote hosts, but it doesn't correctly rewrite the port number from its original to the new socket, so it gets dropped by the local kernel anyway. That is, a packet to 1.2.3.4:53 should be redirected to, say, 127.0.0.1:9999, the local DNS listener socket. But instead, it gets sent to 127.0.0.1:53, which nobody is listening on, so it gets eaten. Sigh.
347 lines
11 KiB
Python
347 lines
11 KiB
Python
import re, errno
|
|
import compat.ssubprocess as ssubprocess
|
|
import helpers, ssyslog
|
|
from helpers import *
|
|
|
|
|
|
def ipt_chain_exists(name):
|
|
argv = ['iptables', '-t', 'nat', '-nL']
|
|
p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE)
|
|
for line in p.stdout:
|
|
if line.startswith('Chain %s ' % name):
|
|
return True
|
|
rv = p.wait()
|
|
if rv:
|
|
raise Fatal('%r returned %d' % (argv, rv))
|
|
|
|
|
|
def ipt(*args):
|
|
argv = ['iptables', '-t', 'nat'] + list(args)
|
|
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(*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(*argsplus)
|
|
except Fatal:
|
|
ipt(*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(*args)
|
|
|
|
|
|
|
|
# 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 do_iptables(port, dnsport, subnets):
|
|
chain = 'sshuttle-%s' % port
|
|
|
|
# basic cleanup/setup of chains
|
|
if ipt_chain_exists(chain):
|
|
ipt('-D', 'OUTPUT', '-j', chain)
|
|
ipt('-D', 'PREROUTING', '-j', chain)
|
|
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 swidth,sexclude,snet in sorted(subnets, 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:
|
|
nslist = resolvconf_nameservers()
|
|
for ip in nslist:
|
|
ipt_ttl('-A', chain, '-j', 'REDIRECT',
|
|
'--dest', '%s/32' % ip,
|
|
'-p', 'udp',
|
|
'--dport', '53',
|
|
'--to-ports', str(dnsport))
|
|
|
|
|
|
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 setup keep-state' in line
|
|
or 'ipttl 42 keep-state' 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))
|
|
rv = ssubprocess.call(argv, stdout = open('/dev/null', 'w'))
|
|
|
|
|
|
_changedctls = []
|
|
def sysctl_set(name, val):
|
|
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
|
|
oldval = _oldctls[name]
|
|
if val != oldval:
|
|
_changedctls.append(name)
|
|
return _sysctl_set(name, val)
|
|
|
|
|
|
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))
|
|
|
|
|
|
def do_ipfw(port, dnsport, subnets):
|
|
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)
|
|
sysctl_set('net.inet.ip.scopedroute', 0)
|
|
|
|
ipfw('add', sport, 'check-state', 'ip',
|
|
'from', 'any', 'to', 'any')
|
|
|
|
if subnets:
|
|
# create new subnet entries
|
|
for swidth,sexclude,snet in sorted(subnets, reverse=True):
|
|
if sexclude:
|
|
ipfw('add', sport, 'skipto', xsport,
|
|
'log', 'tcp',
|
|
'from', 'any', 'to', '%s/%s' % (snet,swidth))
|
|
else:
|
|
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
|
|
'log', 'tcp',
|
|
'from', 'any', 'to', '%s/%s' % (snet,swidth),
|
|
'not', 'ipttl', '42', 'keep-state', 'setup')
|
|
|
|
if dnsport:
|
|
nslist = resolvconf_nameservers()
|
|
for ip in nslist:
|
|
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % dnsport,
|
|
'log', 'udp',
|
|
'from', 'any', 'to', '%s/32' % ip, '53',
|
|
'not', 'ipttl', '42', 'keep-state')
|
|
|
|
|
|
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)
|
|
|
|
|
|
hostmap = {}
|
|
def rewrite_etc_hosts(port):
|
|
HOSTSFILE='/etc/hosts'
|
|
BAKFILE='%s.sbak' % HOSTSFILE
|
|
APPEND='# sshuttle-firewall-%d AUTOCREATED' % port
|
|
old_content = ''
|
|
st = None
|
|
try:
|
|
old_content = open(HOSTSFILE).read()
|
|
st = os.stat(HOSTSFILE)
|
|
except IOError, e:
|
|
if e.errno == errno.ENOENT:
|
|
pass
|
|
else:
|
|
raise
|
|
if old_content.strip() and not os.path.exists(BAKFILE):
|
|
os.link(HOSTSFILE, BAKFILE)
|
|
tmpname = "%s.%d.tmp" % (HOSTSFILE, port)
|
|
f = open(tmpname, 'w')
|
|
for line in old_content.rstrip().split('\n'):
|
|
if line.find(APPEND) >= 0:
|
|
continue
|
|
f.write('%s\n' % line)
|
|
for (name,ip) in sorted(hostmap.items()):
|
|
f.write('%-30s %s\n' % ('%s %s' % (ip,name), APPEND))
|
|
f.close()
|
|
|
|
if st:
|
|
os.chown(tmpname, st.st_uid, st.st_gid)
|
|
os.chmod(tmpname, st.st_mode)
|
|
else:
|
|
os.chown(tmpname, 0, 0)
|
|
os.chmod(tmpname, 0644)
|
|
os.rename(tmpname, HOSTSFILE)
|
|
|
|
|
|
def restore_etc_hosts(port):
|
|
global hostmap
|
|
hostmap = {}
|
|
rewrite_etc_hosts(port)
|
|
|
|
|
|
# This is some voodoo for setting up the kernel's transparent
|
|
# proxying stuff. If subnets is empty, we just delete our sshuttle rules;
|
|
# otherwise we delete it, then make them from scratch.
|
|
#
|
|
# This code is supposed to clean up after itself by deleting its rules on
|
|
# exit. In case that fails, it's not the end of the world; future runs will
|
|
# supercede it in the transproxy list, at least, so the leftover rules
|
|
# are hopefully harmless.
|
|
def main(port, dnsport, syslog):
|
|
assert(port > 0)
|
|
assert(port <= 65535)
|
|
assert(dnsport >= 0)
|
|
assert(dnsport <= 65535)
|
|
|
|
if os.getuid() != 0:
|
|
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
|
|
|
|
if program_exists('ipfw'):
|
|
do_it = do_ipfw
|
|
elif program_exists('iptables'):
|
|
do_it = do_iptables
|
|
else:
|
|
raise Fatal("can't find either ipfw or iptables; check your PATH")
|
|
|
|
# because of limitations of the 'su' command, the *real* stdin/stdout
|
|
# are both attached to stdout initially. Clone stdout into stdin so we
|
|
# can read from it.
|
|
os.dup2(1, 0)
|
|
|
|
if syslog:
|
|
ssyslog.start_syslog()
|
|
ssyslog.stderr_to_syslog()
|
|
|
|
debug1('firewall manager ready.\n')
|
|
sys.stdout.write('READY\n')
|
|
sys.stdout.flush()
|
|
|
|
# ctrl-c shouldn't be passed along to me. When the main sshuttle dies,
|
|
# I'll die automatically.
|
|
os.setsid()
|
|
|
|
# we wait until we get some input before creating the rules. That way,
|
|
# sshuttle can launch us as early as possible (and get sudo password
|
|
# authentication as early in the startup process as possible).
|
|
line = sys.stdin.readline(128)
|
|
if not line:
|
|
return # parent died; nothing to do
|
|
|
|
subnets = []
|
|
if line != 'ROUTES\n':
|
|
raise Fatal('firewall: expected ROUTES but got %r' % line)
|
|
while 1:
|
|
line = sys.stdin.readline(128)
|
|
if not line:
|
|
raise Fatal('firewall: expected route but got %r' % line)
|
|
elif line == 'GO\n':
|
|
break
|
|
try:
|
|
(width,exclude,ip) = line.strip().split(',', 2)
|
|
except:
|
|
raise Fatal('firewall: expected route or GO but got %r' % line)
|
|
subnets.append((int(width), bool(int(exclude)), ip))
|
|
|
|
try:
|
|
if line:
|
|
debug1('firewall manager: starting transproxy.\n')
|
|
do_it(port, dnsport, subnets)
|
|
sys.stdout.write('STARTED\n')
|
|
|
|
try:
|
|
sys.stdout.flush()
|
|
except IOError:
|
|
# the parent process died for some reason; he's surely been loud
|
|
# enough, so no reason to report another error
|
|
return
|
|
|
|
# Now we wait until EOF or any other kind of exception. We need
|
|
# to stay running so that we don't need a *second* password
|
|
# authentication at shutdown time - that cleanup is important!
|
|
while 1:
|
|
line = sys.stdin.readline(128)
|
|
if line.startswith('HOST '):
|
|
(name,ip) = line[5:].strip().split(',', 1)
|
|
hostmap[name] = ip
|
|
rewrite_etc_hosts(port)
|
|
elif line:
|
|
raise Fatal('expected EOF, got %r' % line)
|
|
else:
|
|
break
|
|
finally:
|
|
try:
|
|
debug1('firewall manager: undoing changes.\n')
|
|
except:
|
|
pass
|
|
do_it(port, 0, [])
|
|
restore_etc_hosts(port)
|