1
- import re , errno
1
+ import re , errno , socket , select , struct
2
2
import compat .ssubprocess as ssubprocess
3
3
import helpers , ssyslog
4
4
from helpers import *
5
5
6
+ # python doesn't have a definition for this
7
+ IPPROTO_DIVERT = 254
8
+
6
9
7
10
def ipt_chain_exists (name ):
8
11
argv = ['iptables' , '-t' , 'nat' , '-nL' ]
@@ -49,7 +52,7 @@ def ipt_ttl(*args):
49
52
# multiple copies shouldn't have overlapping subnets, or only the most-
50
53
# recently-started one will win (because we use "-I OUTPUT 1" instead of
51
54
# "-A OUTPUT").
52
- def do_iptables (port , subnets ):
55
+ def do_iptables (port , dnsport , subnets ):
53
56
chain = 'sshuttle-%s' % port
54
57
55
58
# basic cleanup/setup of chains
@@ -59,12 +62,13 @@ def do_iptables(port, subnets):
59
62
ipt ('-F' , chain )
60
63
ipt ('-X' , chain )
61
64
62
- if subnets :
65
+ if subnets or dnsport :
63
66
ipt ('-N' , chain )
64
67
ipt ('-F' , chain )
65
68
ipt ('-I' , 'OUTPUT' , '1' , '-j' , chain )
66
69
ipt ('-I' , 'PREROUTING' , '1' , '-j' , chain )
67
70
71
+ if subnets :
68
72
# create new subnet entries. Note that we're sorting in a very
69
73
# particular order: we need to go from most-specific (largest swidth)
70
74
# to least-specific, and at any given level of specificity, we want
@@ -80,6 +84,15 @@ def do_iptables(port, subnets):
80
84
'--dest' , '%s/%s' % (snet ,swidth ),
81
85
'-p' , 'tcp' ,
82
86
'--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 ))
83
96
84
97
85
98
def ipfw_rule_exists (n ):
@@ -88,7 +101,7 @@ def ipfw_rule_exists(n):
88
101
found = False
89
102
for line in p .stdout :
90
103
if line .startswith ('%05d ' % n ):
91
- if not ('ipttl 42 setup keep-state ' in line
104
+ if not ('ipttl 42' in line
92
105
or ('skipto %d' % (n + 1 )) in line
93
106
or 'check-state' in line ):
94
107
log ('non-sshuttle ipfw rule: %r\n ' % line .strip ())
@@ -135,6 +148,39 @@ def sysctl_set(name, val):
135
148
if val != oldval :
136
149
_changedctls .append (name )
137
150
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 )
138
184
139
185
140
186
def ipfw (* args ):
@@ -145,7 +191,7 @@ def ipfw(*args):
145
191
raise Fatal ('%r returned %d' % (argv , rv ))
146
192
147
193
148
- def do_ipfw (port , subnets ):
194
+ def do_ipfw (port , dnsport , subnets ):
149
195
sport = str (port )
150
196
xsport = str (port + 1 )
151
197
@@ -158,13 +204,14 @@ def do_ipfw(port, subnets):
158
204
oldval = _oldctls [name ]
159
205
_sysctl_set (name , oldval )
160
206
161
- if subnets :
207
+ if subnets or dnsport :
162
208
sysctl_set ('net.inet.ip.fw.enable' , 1 )
163
209
sysctl_set ('net.inet.ip.scopedroute' , 0 )
164
210
165
211
ipfw ('add' , sport , 'check-state' , 'ip' ,
166
212
'from' , 'any' , 'to' , 'any' )
167
-
213
+
214
+ if subnets :
168
215
# create new subnet entries
169
216
for swidth ,sexclude ,snet in sorted (subnets , reverse = True ):
170
217
if sexclude :
@@ -177,6 +224,65 @@ def do_ipfw(port, subnets):
177
224
'from' , 'any' , 'to' , '%s/%s' % (snet ,swidth ),
178
225
'not' , 'ipttl' , '42' , 'keep-state' , 'setup' )
179
226
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
+
180
286
181
287
def program_exists (name ):
182
288
paths = (os .getenv ('PATH' ) or os .defpath ).split (os .pathsep )
@@ -185,6 +291,7 @@ def program_exists(name):
185
291
if os .path .exists (fn ):
186
292
return not os .path .isdir (fn ) and os .access (fn , os .X_OK )
187
293
294
+
188
295
hostmap = {}
189
296
def rewrite_etc_hosts (port ):
190
297
HOSTSFILE = '/etc/hosts'
@@ -235,9 +342,11 @@ def restore_etc_hosts(port):
235
342
# exit. In case that fails, it's not the end of the world; future runs will
236
343
# supercede it in the transproxy list, at least, so the leftover rules
237
344
# are hopefully harmless.
238
- def main (port , syslog ):
345
+ def main (port , dnsport , syslog ):
239
346
assert (port > 0 )
240
347
assert (port <= 65535 )
348
+ assert (dnsport >= 0 )
349
+ assert (dnsport <= 65535 )
241
350
242
351
if os .getuid () != 0 :
243
352
raise Fatal ('you must be root (or enable su/sudo) to set the firewall' )
@@ -291,7 +400,7 @@ def main(port, syslog):
291
400
try :
292
401
if line :
293
402
debug1 ('firewall manager: starting transproxy.\n ' )
294
- do_it (port , subnets )
403
+ do_wait = do_it (port , dnsport , subnets )
295
404
sys .stdout .write ('STARTED\n ' )
296
405
297
406
try :
@@ -305,6 +414,7 @@ def main(port, syslog):
305
414
# to stay running so that we don't need a *second* password
306
415
# authentication at shutdown time - that cleanup is important!
307
416
while 1 :
417
+ if do_wait : do_wait ()
308
418
line = sys .stdin .readline (128 )
309
419
if line .startswith ('HOST ' ):
310
420
(name ,ip ) = line [5 :].strip ().split (',' , 1 )
@@ -319,5 +429,5 @@ def main(port, syslog):
319
429
debug1 ('firewall manager: undoing changes.\n ' )
320
430
except :
321
431
pass
322
- do_it (port , [])
432
+ do_it (port , 0 , [])
323
433
restore_etc_hosts (port )
0 commit comments