Skip to content
This repository was archived by the owner on Dec 18, 2023. It is now read-only.

Commit 0bf0351

Browse files
committed
Merge branch 'dns'
* dns: dns on MacOS: use divert sockets instead of 'fwd' rules. client.py: do DNS listener on the same port as the TCP listener. Move client._islocal() to helpers.islocal() in preparation for sharing. dns: add support for MacOS (but it doesn't work...) Oops, dns_done() crashed if the request had already been timed out. dns: trim DNS channel handlers after a response, or after a timeout. dns: extract 'nameserver' lines from /etc/resolv.conf Extremely basic, but functional, DNS proxying support (--dns option)
2 parents e7a1989 + 9731680 commit 0bf0351

File tree

6 files changed

+256
-41
lines changed

6 files changed

+256
-41
lines changed

client.py

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
1-
import struct, socket, select, errno, re, signal
1+
import struct, socket, select, errno, re, signal, time
22
import compat.ssubprocess as ssubprocess
33
import helpers, ssnet, ssh, ssyslog
44
from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
55
from helpers import *
66

77
_extra_fd = os.open('/dev/null', os.O_RDONLY)
88

9-
def _islocal(ip):
10-
sock = socket.socket()
11-
try:
12-
try:
13-
sock.bind((ip, 0))
14-
except socket.error, e:
15-
if e.args[0] == errno.EADDRNOTAVAIL:
16-
return False # not a local IP
17-
else:
18-
raise
19-
finally:
20-
sock.close()
21-
return True # it's a local IP, or there would have been an error
22-
23-
249
def got_signal(signum, frame):
2510
log('exiting on signal %d\n' % signum)
2611
sys.exit(1)
@@ -111,14 +96,15 @@ def original_dst(sock):
11196

11297

11398
class FirewallClient:
114-
def __init__(self, port, subnets_include, subnets_exclude):
99+
def __init__(self, port, subnets_include, subnets_exclude, dnsport):
115100
self.port = port
116101
self.auto_nets = []
117102
self.subnets_include = subnets_include
118103
self.subnets_exclude = subnets_exclude
104+
self.dnsport = dnsport
119105
argvbase = ([sys.argv[0]] +
120106
['-v'] * (helpers.verbose or 0) +
121-
['--firewall', str(port)])
107+
['--firewall', str(port), str(dnsport)])
122108
if ssyslog._p:
123109
argvbase += ['--syslog']
124110
argv_tries = [
@@ -190,7 +176,7 @@ def done(self):
190176

191177

192178
def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
193-
seed_hosts, auto_nets,
179+
dnslistener, seed_hosts, auto_nets,
194180
syslog, daemon):
195181
handlers = []
196182
if helpers.verbose >= 1:
@@ -282,7 +268,7 @@ def onaccept():
282268
dstip = original_dst(sock)
283269
debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1],
284270
dstip[0],dstip[1]))
285-
if dstip[1] == listener.getsockname()[1] and _islocal(dstip[0]):
271+
if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]):
286272
debug1("-- ignored: that's my address!\n")
287273
sock.close()
288274
return
@@ -292,6 +278,30 @@ def onaccept():
292278
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
293279
handlers.append(Handler([listener], onaccept))
294280

281+
dnsreqs = {}
282+
def dns_done(chan, data):
283+
peer,timeout = dnsreqs.get(chan) or (None,None)
284+
debug3('dns_done: channel=%r peer=%r\n' % (chan, peer))
285+
if peer:
286+
del dnsreqs[chan]
287+
debug3('doing sendto %r\n' % (peer,))
288+
dnslistener.sendto(data, peer)
289+
def ondns():
290+
pkt,peer = dnslistener.recvfrom(4096)
291+
now = time.time()
292+
if pkt:
293+
debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt)))
294+
chan = mux.next_channel()
295+
dnsreqs[chan] = peer,now+30
296+
mux.send(chan, ssnet.CMD_DNS_REQ, pkt)
297+
mux.channels[chan] = lambda cmd,data: dns_done(chan,data)
298+
for chan,(peer,timeout) in dnsreqs.items():
299+
if timeout < now:
300+
del dnsreqs[chan]
301+
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
302+
if dnslistener:
303+
handlers.append(Handler([dnslistener], ondns))
304+
295305
if seed_hosts != None:
296306
debug1('seed_hosts: %r\n' % seed_hosts)
297307
mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts))
@@ -307,7 +317,7 @@ def onaccept():
307317
mux.callback()
308318

309319

310-
def main(listenip, ssh_cmd, remotename, python, latency_control,
320+
def main(listenip, ssh_cmd, remotename, python, latency_control, dns,
311321
seed_hosts, auto_nets,
312322
subnets_include, subnets_exclude, syslog, daemon, pidfile):
313323
if syslog:
@@ -319,8 +329,7 @@ def main(listenip, ssh_cmd, remotename, python, latency_control,
319329
log("%s\n" % e)
320330
return 5
321331
debug1('Starting sshuttle proxy.\n')
322-
listener = socket.socket()
323-
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
332+
324333
if listenip[1]:
325334
ports = [listenip[1]]
326335
else:
@@ -330,8 +339,13 @@ def main(listenip, ssh_cmd, remotename, python, latency_control,
330339
debug2('Binding:')
331340
for port in ports:
332341
debug2(' %d' % port)
342+
listener = socket.socket()
343+
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
344+
dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
345+
dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
333346
try:
334347
listener.bind((listenip[0], port))
348+
dnslistener.bind((listenip[0], port))
335349
bound = True
336350
break
337351
except socket.error, e:
@@ -344,11 +358,20 @@ def main(listenip, ssh_cmd, remotename, python, latency_control,
344358
listenip = listener.getsockname()
345359
debug1('Listening on %r.\n' % (listenip,))
346360

347-
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude)
361+
if dns:
362+
dnsip = dnslistener.getsockname()
363+
debug1('DNS listening on %r.\n' % (dnsip,))
364+
dnsport = dnsip[1]
365+
else:
366+
dnsport = 0
367+
dnslistener = None
368+
dnslistener.bind((listenip[0], 0))
369+
370+
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport)
348371

349372
try:
350373
return _main(listener, fw, ssh_cmd, remotename,
351-
python, latency_control,
374+
python, latency_control, dnslistener,
352375
seed_hosts, auto_nets, syslog, daemon)
353376
finally:
354377
try:

firewall.py

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import re, errno
1+
import re, errno, socket, select, struct
22
import compat.ssubprocess as ssubprocess
33
import helpers, ssyslog
44
from helpers import *
55

6+
# python doesn't have a definition for this
7+
IPPROTO_DIVERT = 254
8+
69

710
def ipt_chain_exists(name):
811
argv = ['iptables', '-t', 'nat', '-nL']
@@ -49,7 +52,7 @@ def ipt_ttl(*args):
4952
# multiple copies shouldn't have overlapping subnets, or only the most-
5053
# recently-started one will win (because we use "-I OUTPUT 1" instead of
5154
# "-A OUTPUT").
52-
def do_iptables(port, subnets):
55+
def do_iptables(port, dnsport, subnets):
5356
chain = 'sshuttle-%s' % port
5457

5558
# basic cleanup/setup of chains
@@ -59,12 +62,13 @@ def do_iptables(port, subnets):
5962
ipt('-F', chain)
6063
ipt('-X', chain)
6164

62-
if subnets:
65+
if subnets or dnsport:
6366
ipt('-N', chain)
6467
ipt('-F', chain)
6568
ipt('-I', 'OUTPUT', '1', '-j', chain)
6669
ipt('-I', 'PREROUTING', '1', '-j', chain)
6770

71+
if subnets:
6872
# create new subnet entries. Note that we're sorting in a very
6973
# particular order: we need to go from most-specific (largest swidth)
7074
# to least-specific, and at any given level of specificity, we want
@@ -80,6 +84,15 @@ def do_iptables(port, subnets):
8084
'--dest', '%s/%s' % (snet,swidth),
8185
'-p', 'tcp',
8286
'--to-ports', str(port))
87+
88+
if dnsport:
89+
nslist = resolvconf_nameservers()
90+
for ip in nslist:
91+
ipt_ttl('-A', chain, '-j', 'REDIRECT',
92+
'--dest', '%s/32' % ip,
93+
'-p', 'udp',
94+
'--dport', '53',
95+
'--to-ports', str(dnsport))
8396

8497

8598
def ipfw_rule_exists(n):
@@ -88,7 +101,7 @@ def ipfw_rule_exists(n):
88101
found = False
89102
for line in p.stdout:
90103
if line.startswith('%05d ' % n):
91-
if not ('ipttl 42 setup keep-state' in line
104+
if not ('ipttl 42' in line
92105
or ('skipto %d' % (n+1)) in line
93106
or 'check-state' in line):
94107
log('non-sshuttle ipfw rule: %r\n' % line.strip())
@@ -135,6 +148,39 @@ def sysctl_set(name, val):
135148
if val != oldval:
136149
_changedctls.append(name)
137150
return _sysctl_set(name, val)
151+
152+
153+
def _udp_unpack(p):
154+
src = (socket.inet_ntoa(p[12:16]), struct.unpack('!H', p[20:22])[0])
155+
dst = (socket.inet_ntoa(p[16:20]), struct.unpack('!H', p[22:24])[0])
156+
return src, dst
157+
158+
159+
def _udp_repack(p, src, dst):
160+
addrs = socket.inet_aton(src[0]) + socket.inet_aton(dst[0])
161+
ports = struct.pack('!HH', src[1], dst[1])
162+
return p[:12] + addrs + ports + p[24:]
163+
164+
165+
_real_dns_server = [None]
166+
def _handle_diversion(divertsock, dnsport):
167+
p,tag = divertsock.recvfrom(4096)
168+
src,dst = _udp_unpack(p)
169+
debug3('got diverted packet from %r to %r\n' % (src, dst))
170+
if dst[1] == 53:
171+
# outgoing DNS
172+
debug3('...packet is a DNS request.\n')
173+
_real_dns_server[0] = dst
174+
dst = ('127.0.0.1', dnsport)
175+
elif src[1] == dnsport:
176+
if islocal(src[0]):
177+
debug3('...packet is a DNS response.\n')
178+
src = _real_dns_server[0]
179+
else:
180+
log('weird?! unexpected divert from %r to %r\n' % (src, dst))
181+
assert(0)
182+
newp = _udp_repack(p, src, dst)
183+
divertsock.sendto(newp, tag)
138184

139185

140186
def ipfw(*args):
@@ -145,7 +191,7 @@ def ipfw(*args):
145191
raise Fatal('%r returned %d' % (argv, rv))
146192

147193

148-
def do_ipfw(port, subnets):
194+
def do_ipfw(port, dnsport, subnets):
149195
sport = str(port)
150196
xsport = str(port+1)
151197

@@ -158,13 +204,14 @@ def do_ipfw(port, subnets):
158204
oldval = _oldctls[name]
159205
_sysctl_set(name, oldval)
160206

161-
if subnets:
207+
if subnets or dnsport:
162208
sysctl_set('net.inet.ip.fw.enable', 1)
163209
sysctl_set('net.inet.ip.scopedroute', 0)
164210

165211
ipfw('add', sport, 'check-state', 'ip',
166212
'from', 'any', 'to', 'any')
167-
213+
214+
if subnets:
168215
# create new subnet entries
169216
for swidth,sexclude,snet in sorted(subnets, reverse=True):
170217
if sexclude:
@@ -177,6 +224,65 @@ def do_ipfw(port, subnets):
177224
'from', 'any', 'to', '%s/%s' % (snet,swidth),
178225
'not', 'ipttl', '42', 'keep-state', 'setup')
179226

227+
# This part is much crazier than it is on Linux, because MacOS (at least
228+
# 10.6, and probably other versions, and maybe FreeBSD too) doesn't
229+
# correctly fixup the dstip/dstport for UDP packets when it puts them
230+
# through a 'fwd' rule. It also doesn't fixup the srcip/srcport in the
231+
# response packet. In Linux iptables, all that happens magically for us,
232+
# so we just redirect the packets and relax.
233+
#
234+
# On MacOS, we have to fix the ports ourselves. For that, we use a
235+
# 'divert' socket, which receives raw packets and lets us mangle them.
236+
#
237+
# Here's how it works. Let's say the local DNS server is 1.1.1.1:53,
238+
# and the remote DNS server is 2.2.2.2:53, and the local transproxy port
239+
# is 10.0.0.1:12300, and a client machine is making a request from
240+
# 10.0.0.5:9999. We see a packet like this:
241+
# 10.0.0.5:9999 -> 1.1.1.1:53
242+
# Since the destip:port matches one of our local nameservers, it will
243+
# match a 'fwd' rule, thus grabbing it on the local machine. However,
244+
# the local kernel will then see a packet addressed to *:53 and
245+
# not know what to do with it; there's nobody listening on port 53. Thus,
246+
# we divert it, rewriting it into this:
247+
# 10.0.0.5:9999 -> 10.0.0.1:12300
248+
# This gets proxied out to the server, which sends it to 2.2.2.2:53,
249+
# and the answer comes back, and the proxy sends it back out like this:
250+
# 10.0.0.1:12300 -> 10.0.0.5:9999
251+
# But that's wrong! The original machine expected an answer from
252+
# 1.1.1.1:53, so we have to divert the *answer* and rewrite it:
253+
# 1.1.1.1:53 -> 10.0.0.5:9999
254+
#
255+
# See? Easy stuff.
256+
if dnsport:
257+
divertsock = socket.socket(socket.AF_INET, socket.SOCK_RAW,
258+
IPPROTO_DIVERT)
259+
divertsock.bind(('0.0.0.0', port)) # IP field is ignored
260+
261+
nslist = resolvconf_nameservers()
262+
for ip in nslist:
263+
# relabel and then catch outgoing DNS requests
264+
ipfw('add', sport, 'divert', sport,
265+
'log', 'udp',
266+
'from', 'any', 'to', '%s/32' % ip, '53',
267+
'not', 'ipttl', '42')
268+
# relabel DNS responses
269+
ipfw('add', sport, 'divert', sport,
270+
'log', 'udp',
271+
'from', 'any', str(dnsport), 'to', 'any',
272+
'not', 'ipttl', '42')
273+
274+
def do_wait():
275+
while 1:
276+
r,w,x = select.select([sys.stdin, divertsock], [], [])
277+
if divertsock in r:
278+
_handle_diversion(divertsock, dnsport)
279+
if sys.stdin in r:
280+
return
281+
else:
282+
do_wait = None
283+
284+
return do_wait
285+
180286

181287
def program_exists(name):
182288
paths = (os.getenv('PATH') or os.defpath).split(os.pathsep)
@@ -185,6 +291,7 @@ def program_exists(name):
185291
if os.path.exists(fn):
186292
return not os.path.isdir(fn) and os.access(fn, os.X_OK)
187293

294+
188295
hostmap = {}
189296
def rewrite_etc_hosts(port):
190297
HOSTSFILE='/etc/hosts'
@@ -235,9 +342,11 @@ def restore_etc_hosts(port):
235342
# exit. In case that fails, it's not the end of the world; future runs will
236343
# supercede it in the transproxy list, at least, so the leftover rules
237344
# are hopefully harmless.
238-
def main(port, syslog):
345+
def main(port, dnsport, syslog):
239346
assert(port > 0)
240347
assert(port <= 65535)
348+
assert(dnsport >= 0)
349+
assert(dnsport <= 65535)
241350

242351
if os.getuid() != 0:
243352
raise Fatal('you must be root (or enable su/sudo) to set the firewall')
@@ -291,7 +400,7 @@ def main(port, syslog):
291400
try:
292401
if line:
293402
debug1('firewall manager: starting transproxy.\n')
294-
do_it(port, subnets)
403+
do_wait = do_it(port, dnsport, subnets)
295404
sys.stdout.write('STARTED\n')
296405

297406
try:
@@ -305,6 +414,7 @@ def main(port, syslog):
305414
# to stay running so that we don't need a *second* password
306415
# authentication at shutdown time - that cleanup is important!
307416
while 1:
417+
if do_wait: do_wait()
308418
line = sys.stdin.readline(128)
309419
if line.startswith('HOST '):
310420
(name,ip) = line[5:].strip().split(',', 1)
@@ -319,5 +429,5 @@ def main(port, syslog):
319429
debug1('firewall manager: undoing changes.\n')
320430
except:
321431
pass
322-
do_it(port, [])
432+
do_it(port, 0, [])
323433
restore_etc_hosts(port)

0 commit comments

Comments
 (0)