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:
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:

View File

@ -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)

View File

@ -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),

View File

@ -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)

View File

@ -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)