]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.recursor-dnssec/test_RecDnstap.py
Merge pull request #12808 from omoerbeek/args-delint
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / test_RecDnstap.py
CommitLineData
fc7f729f
OM
1import os
2import socket
3import struct
4import sys
5import threading
fc7f729f
OM
6import dns
7import dnstap_pb2
e593a398
OM
8from nose import SkipTest
9from recursortests import RecursorTest
fc7f729f
OM
10
11FSTRM_CONTROL_ACCEPT = 0x01
12FSTRM_CONTROL_START = 0x02
13FSTRM_CONTROL_STOP = 0x03
14FSTRM_CONTROL_READY = 0x04
15FSTRM_CONTROL_FINISH = 0x05
16
17# Python2/3 compatibility hacks
18try:
e593a398 19 from queue import Queue
fc7f729f 20except ImportError:
e593a398 21 from Queue import Queue
fc7f729f
OM
22
23try:
e593a398 24 range = xrange
fc7f729f 25except NameError:
e593a398 26 pass
fc7f729f 27
fc7f729f 28
9489e2b5 29def 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 54def 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
69def 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
79def 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
92def checkDnstapExtra(testinstance, dnstap, expected):
93 testinstance.assertTrue(dnstap.HasField('extra'))
94 testinstance.assertEqual(dnstap.extra, expected)
95
96
97def checkDnstapNoExtra(testinstance, dnstap):
98 testinstance.assertFalse(dnstap.HasField('extra'))
99
100
11927be3 101def 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
118def fstrm_get_control_frame_type(data):
119 (t,) = struct.unpack("!L", data[0:4])
120 return t
121
122
e593a398 123def 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
137def 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
150def 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
172class DNSTapServerParams(object):
173 def __init__(self, path):
174 self.queue = Queue()
175 self.path = path
fc7f729f
OM
176
177
e593a398 178DNSTapServerParameters = DNSTapServerParams("/tmp/dnstap.sock")
fc7f729f
OM
179DNSTapListeners = []
180
181class 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}
256a 3600 IN A 192.0.2.42
257tagged 3600 IN A 192.0.2.84
258query-selected 3600 IN A 192.0.2.84
259answer-selected 3600 IN A 192.0.2.84
260types 3600 IN A 192.0.2.84
261types 3600 IN AAAA 2001:DB8::1
262types 3600 IN TXT "Lorem ipsum dolor sit amet"
263types 3600 IN MX 10 a.example.
264types 3600 IN SPF "v=spf1 -all"
265types 3600 IN SRV 10 20 443 a.example.
266cname 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
277class 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 = """
286auth-zones=example=configs/%s/example.zone""" % _confdir
287 _lua_config_file = """
e593a398
OM
288dnstapFrameStreamServer({"%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
315class DNSTapLogNoQueriesTest(TestRecursorDNSTap):
fc7f729f
OM
316
317 _confdir = 'DNSTapLogNoQueries'
318 _config_template = """
319auth-zones=example=configs/%s/example.zone""" % _confdir
320 _lua_config_file = """
e593a398
OM
321dnstapFrameStreamServer({"%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
334class 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 = """
343new-domain-tracking=yes
344new-domain-history-dir=configs/%s/nod
345unique-response-tracking=yes
346unique-response-history-dir=configs/%s/udr
347auth-zones=example=configs/%s/example.zone""" % (_confdir, _confdir, _confdir)
348 _lua_config_file = """
349dnstapNODFrameStreamServer({"%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
383class DNSTapLogUDRTest(TestRecursorDNSTap):
384
385 _confdir = 'DNSTapLogUDRResponses'
386 _config_template = """
387new-domain-tracking=yes
388new-domain-history-dir=configs/%s/nod
389unique-response-tracking=yes
390unique-response-history-dir=configs/%s/udr
391auth-zones=example=configs/%s/example.zone""" % (_confdir, _confdir, _confdir)
392 _lua_config_file = """
393dnstapNODFrameStreamServer({"%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
427class DNSTapLogNODUDRTest(TestRecursorDNSTap):
428
429 _confdir = 'DNSTapLogNODUDRs'
430 _config_template = """
431new-domain-tracking=yes
432new-domain-history-dir=configs/%s/nod
433unique-response-tracking=yes
434unique-response-history-dir=configs/%s/udr
435auth-zones=example=configs/%s/example.zone""" % (_confdir, _confdir, _confdir)
436 _lua_config_file = """
437dnstapNODFrameStreamServer({"%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)