summaryrefslogtreecommitdiffstats
path: root/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py
diff options
context:
space:
mode:
Diffstat (limited to 'Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py')
-rw-r--r--Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py304
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)