sshuttle/firewall.py

203 lines
6.2 KiB
Python
Raw Normal View History

import subprocess, re
import helpers
from helpers import *
def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL']
p = subprocess.Popen(argv, stdout = subprocess.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 = subprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
# 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, 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:
ipt('-N', chain)
ipt('-F', chain)
ipt('-I', 'OUTPUT', '1', '-j', chain)
ipt('-I', 'PREROUTING', '1', '-j', chain)
# create new subnet entries
for snet,swidth in subnets:
ipt('-A', chain, '-j', 'REDIRECT',
'--dest', '%s/%s' % (snet,swidth),
'-p', 'tcp',
'--to-ports', str(port),
'-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops
)
def ipfw_rule_exists(n):
argv = ['ipfw', 'list']
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
for line in p.stdout:
if line.startswith('%05d ' % n):
if line[5:].find('ipttl 42') < 0:
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
return True
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def sysctl_get(name):
argv = ['sysctl', '-n', name]
p = subprocess.Popen(argv, stdout = subprocess.PIPE)
line = p.stdout.readline()
rv = p.wait()
if rv:
raise Fatal('%r returned %d' % (argv, rv))
if not line:
raise Fatal('%r returned no data' % (argv,))
assert(line[-1] == '\n')
return line[:-1]
def _sysctl_set(name, val):
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv, stdout = open('/dev/null', 'w'))
_oldctls = []
def sysctl_set(name, val):
oldval = sysctl_get(name)
if str(val) != str(oldval):
_oldctls.append((name, oldval))
return _sysctl_set(name, val)
def ipfw(*args):
argv = ['ipfw', '-q'] + list(args)
debug1('>> %s\n' % ' '.join(argv))
rv = subprocess.call(argv)
if rv:
raise Fatal('%r returned %d' % (argv, rv))
def do_ipfw(port, subnets):
sport = str(port)
# cleanup any existing rules
if ipfw_rule_exists(port):
ipfw('del', sport)
while _oldctls:
(name,oldval) = _oldctls.pop()
_sysctl_set(name, oldval)
if subnets:
sysctl_set('net.inet.ip.fw.enable', 1)
sysctl_set('net.inet.ip.forwarding', 1)
# create new subnet entries
for snet,swidth in subnets:
ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port,
'log', 'tcp',
'from', 'any', 'to', '%s/%s' % (snet,swidth),
'not', 'ipttl', '42')
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)
# 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, subnets):
assert(port > 0)
assert(port <= 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)
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
if line != 'GO\n':
raise Fatal('firewall: expected GO but got %r' % line)
try:
if line:
debug1('firewall manager: starting transproxy.\n')
do_it(port, subnets)
sys.stdout.write('STARTED\n')
try:
sys.stdout.flush()
# 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 sys.stdin.readline(128):
pass
except IOError:
# the parent process died for some reason; he's surely been loud
# enough, so no reason to report another error
return
finally:
try:
debug1('firewall manager: undoing changes.\n')
except:
pass
do_it(port, [])