diff options
Diffstat (limited to 'Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py')
-rw-r--r-- | Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py new file mode 100644 index 0000000..044ac52 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py @@ -0,0 +1,304 @@ +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)) + + +# 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. 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('-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 = 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 ('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, 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: + 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') + + # 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') + + +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, syslog): + 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) + + 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, 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, []) + restore_etc_hosts(port) |