From a2fcb08a2d4622092eeffc44ae154896ec304a56 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Wed, 26 Jan 2011 02:00:19 -0800 Subject: [PATCH] Extremely basic, but functional, DNS proxying support (--dns option) Limitations: - uses a hardcoded DNS server IP on both client and server - never expires request/response objects, so leaks memory and sockets - works only with iptables, not with ipfw --- client.py | 42 ++++++++++++++++++++++++++++++++++++------ firewall.py | 19 ++++++++++++++----- main.py | 8 +++++--- server.py | 25 +++++++++++++++++++++++++ ssnet.py | 10 +++++++++- 5 files changed, 89 insertions(+), 15 deletions(-) diff --git a/client.py b/client.py index e584933..7872bae 100644 --- a/client.py +++ b/client.py @@ -111,14 +111,15 @@ def original_dst(sock): class FirewallClient: - def __init__(self, port, subnets_include, subnets_exclude): + def __init__(self, port, subnets_include, subnets_exclude, dnsport): self.port = port self.auto_nets = [] self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude + self.dnsport = dnsport argvbase = ([sys.argv[0]] + ['-v'] * (helpers.verbose or 0) + - ['--firewall', str(port)]) + ['--firewall', str(port), str(dnsport)]) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ @@ -190,7 +191,7 @@ class FirewallClient: def _main(listener, fw, ssh_cmd, remotename, python, latency_control, - seed_hosts, auto_nets, + dnslistener, seed_hosts, auto_nets, syslog, daemon): handlers = [] if helpers.verbose >= 1: @@ -292,6 +293,25 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control, handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) handlers.append(Handler([listener], onaccept)) + dnspeers = {} + def dns_done(chan, data): + peer = dnspeers.get(chan) + debug1('dns_done: channel=%r peer=%r\n' % (chan, peer)) + if peer: + del dnspeers[chan] + debug1('doing sendto %r\n' % (peer,)) + dnslistener.sendto(data, peer) + def ondns(): + pkt,peer = dnslistener.recvfrom(4096) + if pkt: + debug1('Got DNS request from %r: %d bytes\n' % (peer, len(pkt))) + chan = mux.next_channel() + dnspeers[chan] = peer + mux.send(chan, ssnet.CMD_DNS_REQ, pkt) + mux.channels[chan] = lambda cmd,data: dns_done(chan,data) + if dnslistener: + handlers.append(Handler([dnslistener], ondns)) + if seed_hosts != None: debug1('seed_hosts: %r\n' % seed_hosts) mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts)) @@ -307,7 +327,7 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control, mux.callback() -def main(listenip, ssh_cmd, remotename, python, latency_control, +def main(listenip, ssh_cmd, remotename, python, latency_control, dns, seed_hosts, auto_nets, subnets_include, subnets_exclude, syslog, daemon, pidfile): if syslog: @@ -319,6 +339,7 @@ def main(listenip, ssh_cmd, remotename, python, latency_control, log("%s\n" % e) return 5 debug1('Starting sshuttle proxy.\n') + listener = socket.socket() listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if listenip[1]: @@ -344,11 +365,20 @@ def main(listenip, ssh_cmd, remotename, python, latency_control, listenip = listener.getsockname() debug1('Listening on %r.\n' % (listenip,)) - fw = FirewallClient(listenip[1], subnets_include, subnets_exclude) + dnsport = 0 + dnslistener = None + if dns: + dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + dnslistener.bind((listenip[0], 0)) + dnsip = dnslistener.getsockname() + debug1('DNS listening on %r.\n' % (dnsip,)) + dnsport = dnsip[1] + + fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport) try: return _main(listener, fw, ssh_cmd, remotename, - python, latency_control, + python, latency_control, dnslistener, seed_hosts, auto_nets, syslog, daemon) finally: try: diff --git a/firewall.py b/firewall.py index b63bffa..8ec67bc 100644 --- a/firewall.py +++ b/firewall.py @@ -49,7 +49,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 @@ -80,6 +80,13 @@ def do_iptables(port, subnets): '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp', '--to-ports', str(port)) + + if dnsport: + ipt_ttl('-A', chain, '-j', 'REDIRECT', + '--dest', '192.168.42.1/32', + '-p', 'udp', + '--dport', '53', + '--to-ports', str(dnsport)) def ipfw_rule_exists(n): @@ -145,7 +152,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) @@ -235,9 +242,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 +300,7 @@ def main(port, syslog): try: if line: debug1('firewall manager: starting transproxy.\n') - do_it(port, subnets) + do_it(port, dnsport, subnets) sys.stdout.write('STARTED\n') try: @@ -319,5 +328,5 @@ def main(port, syslog): debug1('firewall manager: undoing changes.\n') except: pass - do_it(port, []) + do_it(port, 0, []) restore_etc_hosts(port) diff --git a/main.py b/main.py index 5597177..e76e596 100755 --- a/main.py +++ b/main.py @@ -54,6 +54,7 @@ sshuttle --hostwatch l,listen= transproxy to this ip address and port number [127.0.0.1:0] H,auto-hosts scan for remote hostnames and update local /etc/hosts N,auto-nets automatically determine subnets to route +dns capture local DNS requests and forward to the remote DNS server python= path to python interpreter on the remote server [python] r,remote= ssh hostname (and optional username) of remote sshuttle server x,exclude= exclude this subnet (can be used more than once) @@ -82,9 +83,9 @@ try: server.latency_control = opt.latency_control sys.exit(server.main()) elif opt.firewall: - if len(extra) != 1: - o.fatal('exactly one argument expected') - sys.exit(firewall.main(int(extra[0]), opt.syslog)) + if len(extra) != 2: + o.fatal('exactly two arguments expected') + sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog)) elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: @@ -111,6 +112,7 @@ try: remotename, opt.python, opt.latency_control, + opt.dns, sh, opt.auto_nets, parse_subnets(includes), diff --git a/server.py b/server.py index ae7a921..3395c9e 100644 --- a/server.py +++ b/server.py @@ -106,6 +106,23 @@ class Hostwatch: self.sock = None +class DnsProxy(Handler): + def __init__(self, mux, chan, request): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + Handler.__init__(self, [sock]) + self.sock = sock + self.mux = mux + self.chan = chan + self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) + self.sock.connect(('192.168.42.1', 53)) + self.sock.send(request) + + def callback(self): + data = self.sock.recv(4096) + debug2('dns response: %d bytes\n' % len(data)) + self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data) + + def main(): if helpers.verbose >= 1: helpers.logprefix = ' s: ' @@ -165,6 +182,14 @@ def main(): handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) mux.new_channel = new_channel + dnshandlers = {} + def dns_req(channel, data): + debug1('got dns request!\n') + h = DnsProxy(mux, channel, data) + handlers.append(h) + dnshandlers[channel] = h + mux.got_dns_req = dns_req + while mux.ok: if hw.pid: assert(hw.pid > 0) diff --git a/ssnet.py b/ssnet.py index 62fa378..554d870 100644 --- a/ssnet.py +++ b/ssnet.py @@ -21,6 +21,8 @@ CMD_DATA = 0x4206 CMD_ROUTES = 0x4207 CMD_HOST_REQ = 0x4208 CMD_HOST_LIST = 0x4209 +CMD_DNS_REQ = 0x420a +CMD_DNS_RESPONSE = 0x420b cmd_to_name = { CMD_EXIT: 'EXIT', @@ -33,6 +35,8 @@ cmd_to_name = { CMD_ROUTES: 'ROUTES', CMD_HOST_REQ: 'HOST_REQ', CMD_HOST_LIST: 'HOST_LIST', + CMD_DNS_REQ: 'DNS_REQ', + CMD_DNS_RESPONSE: 'DNS_RESPONSE', } @@ -281,7 +285,7 @@ class Mux(Handler): Handler.__init__(self, [rsock, wsock]) self.rsock = rsock self.wsock = wsock - self.new_channel = self.got_routes = None + self.new_channel = self.got_dns_req = self.got_routes = None self.got_host_req = self.got_host_list = None self.channels = {} self.chani = 0 @@ -343,6 +347,10 @@ class Mux(Handler): assert(not self.channels.get(channel)) if self.new_channel: self.new_channel(channel, data) + elif cmd == CMD_DNS_REQ: + assert(not self.channels.get(channel)) + if self.got_dns_req: + self.got_dns_req(channel, data) elif cmd == CMD_ROUTES: if self.got_routes: self.got_routes(data)