]>
Commit | Line | Data |
---|---|---|
fc7f729f OM |
1 | import os |
2 | import socket | |
3 | import struct | |
4 | import sys | |
5 | import threading | |
fc7f729f OM |
6 | import dns |
7 | import dnstap_pb2 | |
e593a398 OM |
8 | from nose import SkipTest |
9 | from recursortests import RecursorTest | |
fc7f729f OM |
10 | |
11 | FSTRM_CONTROL_ACCEPT = 0x01 | |
12 | FSTRM_CONTROL_START = 0x02 | |
13 | FSTRM_CONTROL_STOP = 0x03 | |
14 | FSTRM_CONTROL_READY = 0x04 | |
15 | FSTRM_CONTROL_FINISH = 0x05 | |
16 | ||
17 | # Python2/3 compatibility hacks | |
18 | try: | |
e593a398 | 19 | from queue import Queue |
fc7f729f | 20 | except ImportError: |
e593a398 | 21 | from Queue import Queue |
fc7f729f OM |
22 | |
23 | try: | |
e593a398 | 24 | range = xrange |
fc7f729f | 25 | except NameError: |
e593a398 | 26 | pass |
fc7f729f | 27 | |
fc7f729f | 28 | |
9489e2b5 | 29 | def checkDnstapBase(testinstance, dnstap, protocol, initiator, responder, response_port=53): |
fc7f729f OM |
30 | testinstance.assertTrue(dnstap) |
31 | testinstance.assertTrue(dnstap.HasField('identity')) | |
32 | #testinstance.assertEqual(dnstap.identity, b'a.server') | |
33 | testinstance.assertTrue(dnstap.HasField('version')) | |
34 | #testinstance.assertIn(b'dnsdist ', dnstap.version) | |
35 | testinstance.assertTrue(dnstap.HasField('type')) | |
36 | testinstance.assertEqual(dnstap.type, dnstap.MESSAGE) | |
37 | testinstance.assertTrue(dnstap.HasField('message')) | |
38 | testinstance.assertTrue(dnstap.message.HasField('socket_protocol')) | |
39 | testinstance.assertEqual(dnstap.message.socket_protocol, protocol) | |
40 | testinstance.assertTrue(dnstap.message.HasField('socket_family')) | |
4bfebc93 | 41 | testinstance.assertEqual(dnstap.message.socket_family, dnstap_pb2.INET) |
3f9f84c6 | 42 | # |
11927be3 | 43 | # The query address and port are from the the recursor, we don't know the port |
3f9f84c6 | 44 | # |
11927be3 OM |
45 | testinstance.assertTrue(dnstap.message.HasField('query_address')) |
46 | testinstance.assertEqual(socket.inet_ntop(socket.AF_INET, dnstap.message.query_address), initiator) | |
47 | testinstance.assertTrue(dnstap.message.HasField('query_port')) | |
fc7f729f | 48 | testinstance.assertTrue(dnstap.message.HasField('response_address')) |
11927be3 | 49 | testinstance.assertEqual(socket.inet_ntop(socket.AF_INET, dnstap.message.response_address), responder) |
fc7f729f | 50 | testinstance.assertTrue(dnstap.message.HasField('response_port')) |
9489e2b5 | 51 | testinstance.assertEqual(dnstap.message.response_port, response_port) |
fc7f729f OM |
52 | |
53 | ||
11927be3 | 54 | def checkDnstapQuery(testinstance, dnstap, protocol, initiator, responder): |
4bfebc93 | 55 | testinstance.assertEqual(dnstap.message.type, dnstap_pb2.Message.RESOLVER_QUERY) |
11927be3 | 56 | checkDnstapBase(testinstance, dnstap, protocol, initiator, responder) |
fc7f729f OM |
57 | |
58 | testinstance.assertTrue(dnstap.message.HasField('query_time_sec')) | |
59 | testinstance.assertTrue(dnstap.message.HasField('query_time_nsec')) | |
60 | ||
61 | testinstance.assertTrue(dnstap.message.HasField('query_message')) | |
3f9f84c6 OM |
62 | # |
63 | # We cannot compare the incoming query with the outgoing one | |
64 | # The IDs and some other fields will be different | |
65 | # | |
e593a398 | 66 | #wire_message = dns.message.from_wire(dnstap.message.query_message) |
fc7f729f OM |
67 | #testinstance.assertEqual(wire_message, query) |
68 | ||
9489e2b5 CHB |
69 | def checkDnstapNOD(testinstance, dnstap, protocol, initiator, responder, response_port, query_zone): |
70 | testinstance.assertEqual(dnstap.message.type, dnstap_pb2.Message.CLIENT_QUERY) | |
71 | checkDnstapBase(testinstance, dnstap, protocol, initiator, responder, response_port) | |
72 | ||
73 | testinstance.assertTrue(dnstap.message.HasField('query_time_sec')) | |
74 | testinstance.assertTrue(dnstap.message.HasField('query_time_nsec')) | |
75 | ||
76 | testinstance.assertTrue(dnstap.message.HasField('query_zone')) | |
77 | testinstance.assertEqual(dns.name.from_wire(dnstap.message.query_zone, 0)[0].to_text(), query_zone) | |
78 | ||
79 | def checkDnstapUDR(testinstance, dnstap, protocol, initiator, responder, response_port, query_zone): | |
80 | testinstance.assertEqual(dnstap.message.type, dnstap_pb2.Message.RESOLVER_RESPONSE) | |
81 | checkDnstapBase(testinstance, dnstap, protocol, initiator, responder, response_port) | |
82 | ||
83 | testinstance.assertTrue(dnstap.message.HasField('query_time_sec')) | |
84 | testinstance.assertTrue(dnstap.message.HasField('query_time_nsec')) | |
85 | ||
86 | testinstance.assertTrue(dnstap.message.HasField('query_zone')) | |
87 | testinstance.assertEqual(dns.name.from_wire(dnstap.message.query_zone, 0)[0].to_text(), query_zone) | |
88 | ||
89 | testinstance.assertTrue(dnstap.message.HasField('response_message')) | |
90 | wire_message = dns.message.from_wire(dnstap.message.response_message) | |
fc7f729f OM |
91 | |
92 | def checkDnstapExtra(testinstance, dnstap, expected): | |
93 | testinstance.assertTrue(dnstap.HasField('extra')) | |
94 | testinstance.assertEqual(dnstap.extra, expected) | |
95 | ||
96 | ||
97 | def checkDnstapNoExtra(testinstance, dnstap): | |
98 | testinstance.assertFalse(dnstap.HasField('extra')) | |
99 | ||
100 | ||
11927be3 | 101 | def checkDnstapResponse(testinstance, dnstap, protocol, response, initiator, responder): |
4bfebc93 | 102 | testinstance.assertEqual(dnstap.message.type, dnstap_pb2.Message.RESOLVER_RESPONSE) |
11927be3 | 103 | checkDnstapBase(testinstance, dnstap, protocol, initiator, responder) |
fc7f729f OM |
104 | |
105 | testinstance.assertTrue(dnstap.message.HasField('query_time_sec')) | |
106 | testinstance.assertTrue(dnstap.message.HasField('query_time_nsec')) | |
107 | ||
108 | testinstance.assertTrue(dnstap.message.HasField('response_time_sec')) | |
109 | testinstance.assertTrue(dnstap.message.HasField('response_time_nsec')) | |
110 | ||
111 | testinstance.assertTrue(dnstap.message.response_time_sec > dnstap.message.query_time_sec or \ | |
112 | dnstap.message.response_time_nsec > dnstap.message.query_time_nsec) | |
113 | ||
114 | testinstance.assertTrue(dnstap.message.HasField('response_message')) | |
115 | wire_message = dns.message.from_wire(dnstap.message.response_message) | |
116 | testinstance.assertEqual(wire_message, response) | |
117 | ||
118 | def fstrm_get_control_frame_type(data): | |
119 | (t,) = struct.unpack("!L", data[0:4]) | |
120 | return t | |
121 | ||
122 | ||
e593a398 | 123 | def fstrm_make_control_frame_reply(cft): |
fc7f729f OM |
124 | if cft == FSTRM_CONTROL_READY: |
125 | # Reply with ACCEPT frame and content-type | |
126 | contenttype = b'protobuf:dnstap.Dnstap' | |
127 | frame = struct.pack('!LLL', FSTRM_CONTROL_ACCEPT, 1, | |
128 | len(contenttype)) + contenttype | |
129 | buf = struct.pack("!LL", 0, len(frame)) + frame | |
130 | return buf | |
131 | elif cft == FSTRM_CONTROL_START: | |
132 | return None | |
133 | else: | |
134 | raise Exception('unhandled control frame ' + cft) | |
135 | ||
136 | ||
137 | def fstrm_read_and_dispatch_control_frame(conn): | |
138 | data = conn.recv(4) | |
139 | if not data: | |
140 | raise Exception('length of control frame payload could not be read') | |
141 | (datalen,) = struct.unpack("!L", data) | |
142 | data = conn.recv(datalen) | |
143 | cft = fstrm_get_control_frame_type(data) | |
e593a398 | 144 | reply = fstrm_make_control_frame_reply(cft) |
fc7f729f OM |
145 | if reply: |
146 | conn.send(reply) | |
147 | return cft | |
148 | ||
149 | ||
150 | def fstrm_handle_bidir_connection(conn, on_data): | |
151 | data = None | |
152 | while True: | |
153 | data = conn.recv(4) | |
154 | if not data: | |
155 | break | |
156 | (datalen,) = struct.unpack("!L", data) | |
157 | if datalen == 0: | |
158 | # control frame length follows | |
159 | cft = fstrm_read_and_dispatch_control_frame(conn) | |
160 | if cft == FSTRM_CONTROL_STOP: | |
161 | break | |
162 | else: | |
163 | # data frame | |
164 | data = conn.recv(datalen) | |
165 | if not data: | |
166 | break | |
167 | ||
168 | on_data(data) | |
169 | ||
170 | ||
171 | ||
e593a398 OM |
172 | class DNSTapServerParams(object): |
173 | def __init__(self, path): | |
174 | self.queue = Queue() | |
175 | self.path = path | |
fc7f729f OM |
176 | |
177 | ||
e593a398 | 178 | DNSTapServerParameters = DNSTapServerParams("/tmp/dnstap.sock") |
fc7f729f OM |
179 | DNSTapListeners = [] |
180 | ||
181 | class TestRecursorDNSTap(RecursorTest): | |
182 | @classmethod | |
183 | def FrameStreamUnixListener(cls, conn, param): | |
184 | while True: | |
185 | try: | |
186 | fstrm_handle_bidir_connection(conn, lambda data: \ | |
187 | param.queue.put(data, True, timeout=2.0)) | |
188 | except socket.error as e: | |
189 | if e.errno == 9: | |
190 | break | |
e593a398 | 191 | sys.stderr.write("Unexpected socket error %s\n" % str(e)) |
fc7f729f | 192 | sys.exit(1) |
e593a398 OM |
193 | except exception as e: |
194 | sys.stderr.write("Unexpected socket error %s\n" % str(e)) | |
9489e2b5 | 195 | sys.exit(1) |
e593a398 | 196 | conn.close() |
fc7f729f OM |
197 | |
198 | @classmethod | |
199 | def FrameStreamUnixListenerMain(cls, param): | |
e593a398 | 200 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
fc7f729f | 201 | try: |
e593a398 OM |
202 | try: |
203 | os.remove(param.path) | |
204 | except: | |
205 | pass | |
206 | sock.bind(param.path) | |
207 | sock.listen(100) | |
fc7f729f | 208 | except socket.error as e: |
e593a398 | 209 | sys.stderr.write("Error binding/listening in the framestream listener: %s\n" % str(e)) |
fc7f729f OM |
210 | sys.exit(1) |
211 | DNSTapListeners.append(sock) | |
fc7f729f | 212 | while True: |
e593a398 OM |
213 | try: |
214 | (conn, addr) = sock.accept() | |
215 | listener = threading.Thread(name='DNSTap Worker', target=cls.FrameStreamUnixListener, args=[conn, param]) | |
216 | listener.setDaemon(True) | |
217 | listener.start() | |
218 | except socket.error as e: | |
219 | if e.errno != 9: | |
220 | sys.stderr.write("Socket error on accept: %s\n" % str(e)) | |
221 | else: | |
222 | break | |
fc7f729f OM |
223 | sock.close() |
224 | ||
225 | @classmethod | |
226 | def setUpClass(cls): | |
e593a398 OM |
227 | if os.environ.get("NODNSTAPTESTS") == "1": |
228 | raise SkipTest("Not Yet Supported") | |
fc7f729f OM |
229 | |
230 | cls.setUpSockets() | |
231 | ||
232 | cls.startResponders() | |
233 | ||
234 | listener = threading.Thread(name='DNSTap Listener', target=cls.FrameStreamUnixListenerMain, args=[DNSTapServerParameters]) | |
235 | listener.setDaemon(True) | |
236 | listener.start() | |
237 | ||
fc7f729f OM |
238 | confdir = os.path.join('configs', cls._confdir) |
239 | cls.createConfigDir(confdir) | |
240 | ||
241 | cls.generateRecursorConfig(confdir) | |
242 | cls.startRecursor(confdir, cls._recursorPort) | |
243 | ||
244 | def setUp(self): | |
245 | # Make sure the queue is empty, in case | |
246 | # a previous test failed | |
247 | while not DNSTapServerParameters.queue.empty(): | |
248 | DNSTapServerParameters.queue.get(False) | |
249 | ||
250 | @classmethod | |
251 | def generateRecursorConfig(cls, confdir): | |
252 | authzonepath = os.path.join(confdir, 'example.zone') | |
253 | with open(authzonepath, 'w') as authzone: | |
254 | authzone.write("""$ORIGIN example. | |
255 | @ 3600 IN SOA {soa} | |
256 | a 3600 IN A 192.0.2.42 | |
257 | tagged 3600 IN A 192.0.2.84 | |
258 | query-selected 3600 IN A 192.0.2.84 | |
259 | answer-selected 3600 IN A 192.0.2.84 | |
260 | types 3600 IN A 192.0.2.84 | |
261 | types 3600 IN AAAA 2001:DB8::1 | |
262 | types 3600 IN TXT "Lorem ipsum dolor sit amet" | |
263 | types 3600 IN MX 10 a.example. | |
264 | types 3600 IN SPF "v=spf1 -all" | |
265 | types 3600 IN SRV 10 20 443 a.example. | |
266 | cname 3600 IN CNAME a.example. | |
267 | ||
268 | """.format(soa=cls._SOA)) | |
269 | super(TestRecursorDNSTap, cls).generateRecursorConfig(confdir) | |
270 | ||
271 | @classmethod | |
272 | def tearDownClass(cls): | |
273 | cls.tearDownRecursor() | |
274 | for listerner in DNSTapListeners: | |
275 | listerner.close() | |
276 | ||
277 | class DNSTapDefaultTest(TestRecursorDNSTap): | |
278 | """ | |
279 | This test makes sure that we correctly export outgoing queries over DNSTap. | |
280 | It must be improved and setup env so we can check for incoming responses, but makes sure for now | |
281 | that the recursor at least connects to the DNSTap server. | |
282 | """ | |
283 | ||
284 | _confdir = 'DNSTapDefault' | |
285 | _config_template = """ | |
286 | auth-zones=example=configs/%s/example.zone""" % _confdir | |
287 | _lua_config_file = """ | |
e593a398 OM |
288 | dnstapFrameStreamServer({"%s"}) |
289 | """ % DNSTapServerParameters.path | |
fc7f729f OM |
290 | |
291 | def getFirstDnstap(self): | |
e593a398 OM |
292 | try: |
293 | data = DNSTapServerParameters.queue.get(True, timeout=2.0) | |
294 | except: | |
295 | data = False | |
fc7f729f OM |
296 | self.assertTrue(data) |
297 | dnstap = dnstap_pb2.Dnstap() | |
298 | dnstap.ParseFromString(data) | |
299 | return dnstap | |
300 | ||
301 | def testA(self): | |
fc7f729f OM |
302 | name = 'www.example.org.' |
303 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
304 | query.flags |= dns.flags.RD | |
305 | res = self.sendUDPQuery(query) | |
4bfebc93 | 306 | self.assertNotEqual(res, None) |
9489e2b5 | 307 | |
fc7f729f OM |
308 | # check the dnstap message corresponding to the UDP query |
309 | dnstap = self.getFirstDnstap() | |
310 | ||
11927be3 | 311 | checkDnstapQuery(self, dnstap, dnstap_pb2.UDP, '127.0.0.1', '127.0.0.8') |
fc7f729f OM |
312 | # We don't expect a response |
313 | checkDnstapNoExtra(self, dnstap) | |
314 | ||
315 | class DNSTapLogNoQueriesTest(TestRecursorDNSTap): | |
fc7f729f OM |
316 | |
317 | _confdir = 'DNSTapLogNoQueries' | |
318 | _config_template = """ | |
319 | auth-zones=example=configs/%s/example.zone""" % _confdir | |
320 | _lua_config_file = """ | |
e593a398 OM |
321 | dnstapFrameStreamServer({"%s"}, {logQueries=false}) |
322 | """ % (DNSTapServerParameters.path) | |
fc7f729f OM |
323 | |
324 | def testA(self): | |
325 | name = 'www.example.org.' | |
326 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
327 | query.flags |= dns.flags.RD | |
328 | res = self.sendUDPQuery(query) | |
4bfebc93 | 329 | self.assertNotEqual(res, None) |
fc7f729f OM |
330 | |
331 | # We don't expect anything | |
332 | self.assertTrue(DNSTapServerParameters.queue.empty()) | |
9489e2b5 CHB |
333 | |
334 | class DNSTapLogNODTest(TestRecursorDNSTap): | |
335 | """ | |
336 | This test makes sure that we correctly export outgoing queries over DNSTap. | |
337 | It must be improved and setup env so we can check for incoming responses, but makes sure for now | |
338 | that the recursor at least connects to the DNSTap server. | |
339 | """ | |
340 | ||
341 | _confdir = 'DNSTapLogNODQueries' | |
342 | _config_template = """ | |
343 | new-domain-tracking=yes | |
344 | new-domain-history-dir=configs/%s/nod | |
345 | unique-response-tracking=yes | |
346 | unique-response-history-dir=configs/%s/udr | |
347 | auth-zones=example=configs/%s/example.zone""" % (_confdir, _confdir, _confdir) | |
348 | _lua_config_file = """ | |
349 | dnstapNODFrameStreamServer({"%s"}) | |
350 | """ % (DNSTapServerParameters.path) | |
351 | ||
352 | @classmethod | |
353 | def generateRecursorConfig(cls, confdir): | |
354 | for directory in ["nod", "udr"]: | |
355 | path = os.path.join('configs', cls._confdir, directory) | |
356 | cls.createConfigDir(path) | |
357 | super(DNSTapLogNODTest, cls).generateRecursorConfig(confdir) | |
358 | ||
359 | def getFirstDnstap(self): | |
360 | try: | |
361 | data = DNSTapServerParameters.queue.get(True, timeout=2.0) | |
362 | except: | |
363 | data = False | |
364 | self.assertTrue(data) | |
365 | dnstap = dnstap_pb2.Dnstap() | |
366 | dnstap.ParseFromString(data) | |
367 | return dnstap | |
368 | ||
369 | def testA(self): | |
370 | name = 'www.example.org.' | |
371 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
372 | query.flags |= dns.flags.RD | |
373 | res = self.sendUDPQuery(query) | |
374 | self.assertNotEqual(res, None) | |
375 | ||
376 | # check the dnstap message corresponding to the UDP query | |
377 | dnstap = self.getFirstDnstap() | |
378 | ||
379 | checkDnstapNOD(self, dnstap, dnstap_pb2.UDP, '127.0.0.1', '127.0.0.1', 5300, name) | |
380 | # We don't expect a response | |
381 | checkDnstapNoExtra(self, dnstap) | |
382 | ||
383 | class DNSTapLogUDRTest(TestRecursorDNSTap): | |
384 | ||
385 | _confdir = 'DNSTapLogUDRResponses' | |
386 | _config_template = """ | |
387 | new-domain-tracking=yes | |
388 | new-domain-history-dir=configs/%s/nod | |
389 | unique-response-tracking=yes | |
390 | unique-response-history-dir=configs/%s/udr | |
391 | auth-zones=example=configs/%s/example.zone""" % (_confdir, _confdir, _confdir) | |
392 | _lua_config_file = """ | |
393 | dnstapNODFrameStreamServer({"%s"}, {logNODs=false, logUDRs=true}) | |
394 | """ % (DNSTapServerParameters.path) | |
395 | ||
396 | @classmethod | |
397 | def generateRecursorConfig(cls, confdir): | |
398 | for directory in ["nod", "udr"]: | |
399 | path = os.path.join('configs', cls._confdir, directory) | |
400 | cls.createConfigDir(path) | |
401 | super(DNSTapLogUDRTest, cls).generateRecursorConfig(confdir) | |
402 | ||
403 | def getFirstDnstap(self): | |
404 | try: | |
405 | data = DNSTapServerParameters.queue.get(True, timeout=2.0) | |
406 | except: | |
407 | data = False | |
408 | self.assertTrue(data) | |
409 | dnstap = dnstap_pb2.Dnstap() | |
410 | dnstap.ParseFromString(data) | |
411 | return dnstap | |
412 | ||
413 | def testA(self): | |
414 | name = 'types.example.' | |
415 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
416 | query.flags |= dns.flags.RD | |
417 | res = self.sendUDPQuery(query) | |
418 | self.assertNotEqual(res, None) | |
419 | ||
420 | # check the dnstap message corresponding to the UDP query | |
421 | dnstap = self.getFirstDnstap() | |
422 | ||
423 | checkDnstapUDR(self, dnstap, dnstap_pb2.UDP, '127.0.0.1', '127.0.0.1', 5300, name) | |
424 | # We don't expect a rpasesponse | |
425 | checkDnstapNoExtra(self, dnstap) | |
426 | ||
427 | class DNSTapLogNODUDRTest(TestRecursorDNSTap): | |
428 | ||
429 | _confdir = 'DNSTapLogNODUDRs' | |
430 | _config_template = """ | |
431 | new-domain-tracking=yes | |
432 | new-domain-history-dir=configs/%s/nod | |
433 | unique-response-tracking=yes | |
434 | unique-response-history-dir=configs/%s/udr | |
435 | auth-zones=example=configs/%s/example.zone""" % (_confdir, _confdir, _confdir) | |
436 | _lua_config_file = """ | |
437 | dnstapNODFrameStreamServer({"%s"}, {logNODs=true, logUDRs=true}) | |
438 | """ % (DNSTapServerParameters.path) | |
439 | ||
440 | @classmethod | |
441 | def generateRecursorConfig(cls, confdir): | |
442 | for directory in ["nod", "udr"]: | |
443 | path = os.path.join('configs', cls._confdir, directory) | |
444 | cls.createConfigDir(path) | |
445 | super(DNSTapLogNODUDRTest, cls).generateRecursorConfig(confdir) | |
446 | ||
447 | def getFirstDnstap(self): | |
448 | try: | |
449 | data = DNSTapServerParameters.queue.get(True, timeout=2.0) | |
450 | except: | |
451 | data = False | |
452 | self.assertTrue(data) | |
453 | dnstap = dnstap_pb2.Dnstap() | |
454 | dnstap.ParseFromString(data) | |
455 | return dnstap | |
456 | ||
457 | def testA(self): | |
458 | name = 'types.example.' | |
459 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
460 | query.flags |= dns.flags.RD | |
461 | res = self.sendUDPQuery(query) | |
462 | self.assertNotEqual(res, None) | |
463 | ||
464 | dnstap = self.getFirstDnstap() | |
465 | checkDnstapUDR(self, dnstap, dnstap_pb2.UDP, '127.0.0.1', '127.0.0.1', 5300, name) | |
466 | ||
467 | dnstap = self.getFirstDnstap() | |
468 | checkDnstapNOD(self, dnstap, dnstap_pb2.UDP, '127.0.0.1', '127.0.0.1', 5300, name) | |
469 | ||
470 | checkDnstapNoExtra(self, dnstap) |