summaryrefslogtreecommitdiffstats
path: root/firewall.py
diff options
context:
space:
mode:
Diffstat (limited to 'firewall.py')
-rw-r--r--firewall.py130
1 files changed, 120 insertions, 10 deletions
diff --git a/firewall.py b/firewall.py
index b63bffa..c7557ed 100644
--- a/firewall.py
+++ b/firewall.py
@@ -1,8 +1,11 @@
-import re, errno
+import re, errno, socket, select, struct
import compat.ssubprocess as ssubprocess
import helpers, ssyslog
from helpers import *
+# python doesn't have a definition for this
+IPPROTO_DIVERT = 254
+
def ipt_chain_exists(name):
argv = ['iptables', '-t', 'nat', '-nL']
@@ -49,7 +52,7 @@ def ipt_ttl(*args):
# 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):
+def do_iptables(port, dnsport, subnets):
chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains
@@ -59,12 +62,13 @@ def do_iptables(port, subnets):
ipt('-F', chain)
ipt('-X', chain)
- if subnets:
+ 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
@@ -80,6 +84,15 @@ def do_iptables(port, subnets):
'--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):
@@ -88,7 +101,7 @@ def ipfw_rule_exists(n):
found = False
for line in p.stdout:
if line.startswith('%05d ' % n):
- if not ('ipttl 42 setup keep-state' in line
+ 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())
@@ -135,6 +148,39 @@ def sysctl_set(name, val):
if val != oldval:
_changedctls.append(name)
return _sysctl_set(name, val)
+
+
+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]):
+ 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):
@@ -145,7 +191,7 @@ def ipfw(*args):
raise Fatal('%r returned %d' % (argv, rv))
-def do_ipfw(port, subnets):
+def do_ipfw(port, dnsport, subnets):
sport = str(port)
xsport = str(port+1)
@@ -158,13 +204,14 @@ def do_ipfw(port, subnets):
oldval = _oldctls[name]
_sysctl_set(name, oldval)
- if subnets:
+ 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:
@@ -177,6 +224,65 @@ def do_ipfw(port, subnets):
'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
+
+ nslist = resolvconf_nameservers()
+ for ip in nslist:
+ # relabel and then catch outgoing DNS requests
+ ipfw('add', sport, 'divert', sport,
+ 'log', 'udp',
+ 'from', 'any', 'to', '%s/32' % ip, '53',
+ 'not', 'ipttl', '42')
+ # relabel DNS responses
+ ipfw('add', sport, 'divert', sport,
+ 'log', '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
+
def program_exists(name):
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
@@ -185,6 +291,7 @@ def program_exists(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'
@@ -235,9 +342,11 @@ def restore_etc_hosts(port):
# 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):
+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')
@@ -291,7 +400,7 @@ def main(port, syslog):
try:
if line:
debug1('firewall manager: starting transproxy.\n')
- do_it(port, subnets)
+ do_wait = do_it(port, dnsport, subnets)
sys.stdout.write('STARTED\n')
try:
@@ -305,6 +414,7 @@ def main(port, syslog):
# to stay running so that we don't need a *second* password
# authentication at shutdown time - that cleanup is important!
while 1:
+ if do_wait: do_wait()
line = sys.stdin.readline(128)
if line.startswith('HOST '):
(name,ip) = line[5:].strip().split(',', 1)
@@ -319,5 +429,5 @@ def main(port, syslog):
debug1('firewall manager: undoing changes.\n')
except:
pass
- do_it(port, [])
+ do_it(port, 0, [])
restore_etc_hosts(port)