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