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
This commit is contained in:
Avery Pennarun 2011-01-26 02:00:19 -08:00
parent e7a19890aa
commit a2fcb08a2d
5 changed files with 89 additions and 15 deletions

View File

@ -111,14 +111,15 @@ def original_dst(sock):
class FirewallClient: class FirewallClient:
def __init__(self, port, subnets_include, subnets_exclude): def __init__(self, port, subnets_include, subnets_exclude, dnsport):
self.port = port self.port = port
self.auto_nets = [] self.auto_nets = []
self.subnets_include = subnets_include self.subnets_include = subnets_include
self.subnets_exclude = subnets_exclude self.subnets_exclude = subnets_exclude
self.dnsport = dnsport
argvbase = ([sys.argv[0]] + argvbase = ([sys.argv[0]] +
['-v'] * (helpers.verbose or 0) + ['-v'] * (helpers.verbose or 0) +
['--firewall', str(port)]) ['--firewall', str(port), str(dnsport)])
if ssyslog._p: if ssyslog._p:
argvbase += ['--syslog'] argvbase += ['--syslog']
argv_tries = [ argv_tries = [
@ -190,7 +191,7 @@ class FirewallClient:
def _main(listener, fw, ssh_cmd, remotename, python, latency_control, def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
seed_hosts, auto_nets, dnslistener, seed_hosts, auto_nets,
syslog, daemon): syslog, daemon):
handlers = [] handlers = []
if helpers.verbose >= 1: 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(Proxy(SockWrapper(sock, sock), outwrap))
handlers.append(Handler([listener], onaccept)) 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: if seed_hosts != None:
debug1('seed_hosts: %r\n' % seed_hosts) debug1('seed_hosts: %r\n' % seed_hosts)
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(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() 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, seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile): subnets_include, subnets_exclude, syslog, daemon, pidfile):
if syslog: if syslog:
@ -319,6 +339,7 @@ def main(listenip, ssh_cmd, remotename, python, latency_control,
log("%s\n" % e) log("%s\n" % e)
return 5 return 5
debug1('Starting sshuttle proxy.\n') debug1('Starting sshuttle proxy.\n')
listener = socket.socket() listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if listenip[1]: if listenip[1]:
@ -344,11 +365,20 @@ def main(listenip, ssh_cmd, remotename, python, latency_control,
listenip = listener.getsockname() listenip = listener.getsockname()
debug1('Listening on %r.\n' % (listenip,)) 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: try:
return _main(listener, fw, ssh_cmd, remotename, return _main(listener, fw, ssh_cmd, remotename,
python, latency_control, python, latency_control, dnslistener,
seed_hosts, auto_nets, syslog, daemon) seed_hosts, auto_nets, syslog, daemon)
finally: finally:
try: try:

View File

@ -49,7 +49,7 @@ def ipt_ttl(*args):
# multiple copies shouldn't have overlapping subnets, or only the most- # multiple copies shouldn't have overlapping subnets, or only the most-
# recently-started one will win (because we use "-I OUTPUT 1" instead of # recently-started one will win (because we use "-I OUTPUT 1" instead of
# "-A OUTPUT"). # "-A OUTPUT").
def do_iptables(port, subnets): def do_iptables(port, dnsport, subnets):
chain = 'sshuttle-%s' % port chain = 'sshuttle-%s' % port
# basic cleanup/setup of chains # basic cleanup/setup of chains
@ -80,6 +80,13 @@ def do_iptables(port, subnets):
'--dest', '%s/%s' % (snet,swidth), '--dest', '%s/%s' % (snet,swidth),
'-p', 'tcp', '-p', 'tcp',
'--to-ports', str(port)) '--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): def ipfw_rule_exists(n):
@ -145,7 +152,7 @@ def ipfw(*args):
raise Fatal('%r returned %d' % (argv, rv)) raise Fatal('%r returned %d' % (argv, rv))
def do_ipfw(port, subnets): def do_ipfw(port, dnsport, subnets):
sport = str(port) sport = str(port)
xsport = str(port+1) 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 # 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 # supercede it in the transproxy list, at least, so the leftover rules
# are hopefully harmless. # are hopefully harmless.
def main(port, syslog): def main(port, dnsport, syslog):
assert(port > 0) assert(port > 0)
assert(port <= 65535) assert(port <= 65535)
assert(dnsport >= 0)
assert(dnsport <= 65535)
if os.getuid() != 0: if os.getuid() != 0:
raise Fatal('you must be root (or enable su/sudo) to set the firewall') raise Fatal('you must be root (or enable su/sudo) to set the firewall')
@ -291,7 +300,7 @@ def main(port, syslog):
try: try:
if line: if line:
debug1('firewall manager: starting transproxy.\n') debug1('firewall manager: starting transproxy.\n')
do_it(port, subnets) do_it(port, dnsport, subnets)
sys.stdout.write('STARTED\n') sys.stdout.write('STARTED\n')
try: try:
@ -319,5 +328,5 @@ def main(port, syslog):
debug1('firewall manager: undoing changes.\n') debug1('firewall manager: undoing changes.\n')
except: except:
pass pass
do_it(port, []) do_it(port, 0, [])
restore_etc_hosts(port) restore_etc_hosts(port)

View File

@ -54,6 +54,7 @@ sshuttle --hostwatch
l,listen= transproxy to this ip address and port number [127.0.0.1:0] 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 H,auto-hosts scan for remote hostnames and update local /etc/hosts
N,auto-nets automatically determine subnets to route 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] python= path to python interpreter on the remote server [python]
r,remote= ssh hostname (and optional username) of remote sshuttle server r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once) x,exclude= exclude this subnet (can be used more than once)
@ -82,9 +83,9 @@ try:
server.latency_control = opt.latency_control server.latency_control = opt.latency_control
sys.exit(server.main()) sys.exit(server.main())
elif opt.firewall: elif opt.firewall:
if len(extra) != 1: if len(extra) != 2:
o.fatal('exactly one argument expected') o.fatal('exactly two arguments expected')
sys.exit(firewall.main(int(extra[0]), opt.syslog)) sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog))
elif opt.hostwatch: elif opt.hostwatch:
sys.exit(hostwatch.hw_main(extra)) sys.exit(hostwatch.hw_main(extra))
else: else:
@ -111,6 +112,7 @@ try:
remotename, remotename,
opt.python, opt.python,
opt.latency_control, opt.latency_control,
opt.dns,
sh, sh,
opt.auto_nets, opt.auto_nets,
parse_subnets(includes), parse_subnets(includes),

View File

@ -106,6 +106,23 @@ class Hostwatch:
self.sock = None 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(): def main():
if helpers.verbose >= 1: if helpers.verbose >= 1:
helpers.logprefix = ' s: ' helpers.logprefix = ' s: '
@ -165,6 +182,14 @@ def main():
handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
mux.new_channel = new_channel 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: while mux.ok:
if hw.pid: if hw.pid:
assert(hw.pid > 0) assert(hw.pid > 0)

View File

@ -21,6 +21,8 @@ CMD_DATA = 0x4206
CMD_ROUTES = 0x4207 CMD_ROUTES = 0x4207
CMD_HOST_REQ = 0x4208 CMD_HOST_REQ = 0x4208
CMD_HOST_LIST = 0x4209 CMD_HOST_LIST = 0x4209
CMD_DNS_REQ = 0x420a
CMD_DNS_RESPONSE = 0x420b
cmd_to_name = { cmd_to_name = {
CMD_EXIT: 'EXIT', CMD_EXIT: 'EXIT',
@ -33,6 +35,8 @@ cmd_to_name = {
CMD_ROUTES: 'ROUTES', CMD_ROUTES: 'ROUTES',
CMD_HOST_REQ: 'HOST_REQ', CMD_HOST_REQ: 'HOST_REQ',
CMD_HOST_LIST: 'HOST_LIST', 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]) Handler.__init__(self, [rsock, wsock])
self.rsock = rsock self.rsock = rsock
self.wsock = wsock 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.got_host_req = self.got_host_list = None
self.channels = {} self.channels = {}
self.chani = 0 self.chani = 0
@ -343,6 +347,10 @@ class Mux(Handler):
assert(not self.channels.get(channel)) assert(not self.channels.get(channel))
if self.new_channel: if self.new_channel:
self.new_channel(channel, data) 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: elif cmd == CMD_ROUTES:
if self.got_routes: if self.got_routes:
self.got_routes(data) self.got_routes(data)