9 from unittest
import SkipTest
10 from recursortests
import RecursorTest
12 FSTRM_CONTROL_ACCEPT
= 0x01
13 FSTRM_CONTROL_START
= 0x02
14 FSTRM_CONTROL_STOP
= 0x03
15 FSTRM_CONTROL_READY
= 0x04
16 FSTRM_CONTROL_FINISH
= 0x05
18 # Python2/3 compatibility hacks
20 from queue
import Queue
22 from Queue
import Queue
30 def checkDnstapBase(testinstance
, dnstap
, protocol
, initiator
, responder
, response_port
=53):
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'))
42 testinstance
.assertEqual(dnstap
.message
.socket_family
, dnstap_pb2
.INET
)
44 # The query address and port are from the the recursor, we don't know the port
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'))
49 testinstance
.assertTrue(dnstap
.message
.HasField('response_address'))
50 testinstance
.assertEqual(socket
.inet_ntop(socket
.AF_INET
, dnstap
.message
.response_address
), responder
)
51 testinstance
.assertTrue(dnstap
.message
.HasField('response_port'))
52 testinstance
.assertEqual(dnstap
.message
.response_port
, response_port
)
55 def checkDnstapQuery(testinstance
, dnstap
, protocol
, initiator
, responder
):
56 testinstance
.assertEqual(dnstap
.message
.type, dnstap_pb2
.Message
.RESOLVER_QUERY
)
57 checkDnstapBase(testinstance
, dnstap
, protocol
, initiator
, responder
)
59 testinstance
.assertTrue(dnstap
.message
.HasField('query_time_sec'))
60 testinstance
.assertTrue(dnstap
.message
.HasField('query_time_nsec'))
62 testinstance
.assertTrue(dnstap
.message
.HasField('query_message'))
64 # We cannot compare the incoming query with the outgoing one
65 # The IDs and some other fields will be different
67 #wire_message = dns.message.from_wire(dnstap.message.query_message)
68 #testinstance.assertEqual(wire_message, query)
70 def 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
)
74 testinstance
.assertTrue(dnstap
.message
.HasField('query_time_sec'))
75 testinstance
.assertTrue(dnstap
.message
.HasField('query_time_nsec'))
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
)
80 def 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
)
84 testinstance
.assertTrue(dnstap
.message
.HasField('query_time_sec'))
85 testinstance
.assertTrue(dnstap
.message
.HasField('query_time_nsec'))
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
)
90 testinstance
.assertTrue(dnstap
.message
.HasField('response_message'))
91 wire_message
= dns
.message
.from_wire(dnstap
.message
.response_message
)
93 def checkDnstapExtra(testinstance
, dnstap
, expected
):
94 testinstance
.assertTrue(dnstap
.HasField('extra'))
95 testinstance
.assertEqual(dnstap
.extra
, expected
)
98 def checkDnstapNoExtra(testinstance
, dnstap
):
99 testinstance
.assertFalse(dnstap
.HasField('extra'))
102 def checkDnstapResponse(testinstance
, dnstap
, protocol
, response
, initiator
, responder
):
103 testinstance
.assertEqual(dnstap
.message
.type, dnstap_pb2
.Message
.RESOLVER_RESPONSE
)
104 checkDnstapBase(testinstance
, dnstap
, protocol
, initiator
, responder
)
106 testinstance
.assertTrue(dnstap
.message
.HasField('query_time_sec'))
107 testinstance
.assertTrue(dnstap
.message
.HasField('query_time_nsec'))
109 testinstance
.assertTrue(dnstap
.message
.HasField('response_time_sec'))
110 testinstance
.assertTrue(dnstap
.message
.HasField('response_time_nsec'))
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
)
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
)
119 def fstrm_get_control_frame_type(data
):
120 (t
,) = struct
.unpack("!L", data
[0:4])
124 def fstrm_make_control_frame_reply(cft
):
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
132 elif cft
== FSTRM_CONTROL_START
:
135 raise Exception('unhandled control frame ' + cft
)
138 def fstrm_read_and_dispatch_control_frame(conn
):
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
)
145 reply
= fstrm_make_control_frame_reply(cft
)
151 def fstrm_handle_bidir_connection(conn
, on_data
):
157 (datalen
,) = struct
.unpack("!L", data
)
159 # control frame length follows
160 cft
= fstrm_read_and_dispatch_control_frame(conn
)
161 if cft
== FSTRM_CONTROL_STOP
:
165 data
= conn
.recv(datalen
)
173 class DNSTapServerParams(object):
174 def __init__(self
, path
):
179 DNSTapServerParameters
= DNSTapServerParams("/tmp/dnstap.sock")
182 class TestRecursorDNSTap(RecursorTest
):
184 def FrameStreamUnixListener(cls
, conn
, param
):
187 fstrm_handle_bidir_connection(conn
, lambda data
: \
188 param
.queue
.put(data
, True, timeout
=2.0))
189 except socket
.error
as e
:
190 if e
.errno
in (errno
.EBADF
, errno
.EPIPE
):
192 sys
.stderr
.write("Unexpected socket error %s\n" % str(e
))
194 except exception
as e
:
195 sys
.stderr
.write("Unexpected socket error %s\n" % str(e
))
200 def FrameStreamUnixListenerMain(cls
, param
):
201 sock
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
204 os
.remove(param
.path
)
207 sock
.bind(param
.path
)
209 except socket
.error
as e
:
210 sys
.stderr
.write("Error binding/listening in the framestream listener: %s\n" % str(e
))
212 DNSTapListeners
.append(sock
)
215 (conn
, addr
) = sock
.accept()
216 listener
= threading
.Thread(name
='DNSTap Worker', target
=cls
.FrameStreamUnixListener
, args
=[conn
, param
])
217 listener
.setDaemon(True)
219 except socket
.error
as e
:
220 if e
.errno
!= errno
.EBADF
:
221 sys
.stderr
.write("Socket error on accept: %s\n" % str(e
))
228 if os
.environ
.get("NODNSTAPTESTS") == "1":
229 raise SkipTest("Not Yet Supported")
233 cls
.startResponders()
235 listener
= threading
.Thread(name
='DNSTap Listener', target
=cls
.FrameStreamUnixListenerMain
, args
=[DNSTapServerParameters
])
236 listener
.setDaemon(True)
239 confdir
= os
.path
.join('configs', cls
._confdir
)
240 cls
.createConfigDir(confdir
)
242 cls
.generateRecursorConfig(confdir
)
243 cls
.startRecursor(confdir
, cls
._recursorPort
)
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)
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.
257 a 3600 IN A 192.0.2.42
258 tagged 3600 IN A 192.0.2.84
259 query-selected 3600 IN A 192.0.2.84
260 answer-selected 3600 IN A 192.0.2.84
261 types 3600 IN A 192.0.2.84
262 types 3600 IN AAAA 2001:DB8::1
263 types 3600 IN TXT "Lorem ipsum dolor sit amet"
264 types 3600 IN MX 10 a.example.
265 types 3600 IN SPF "v=spf1 -all"
266 types 3600 IN SRV 10 20 443 a.example.
267 cname 3600 IN CNAME a.example.
269 """.format(soa
=cls
._SOA
))
270 super(TestRecursorDNSTap
, cls
).generateRecursorConfig(confdir
)
273 def tearDownClass(cls
):
274 cls
.tearDownRecursor()
275 for listerner
in DNSTapListeners
:
278 class DNSTapDefaultTest(TestRecursorDNSTap
):
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.
285 _confdir
= 'DNSTapDefault'
286 _config_template
= """
287 auth-zones=example=configs/%s/example.zone""" % _confdir
288 _lua_config_file
= """
289 dnstapFrameStreamServer({"%s"})
290 """ % DNSTapServerParameters
.path
292 def getFirstDnstap(self
):
294 data
= DNSTapServerParameters
.queue
.get(True, timeout
=2.0)
297 self
.assertTrue(data
)
298 dnstap
= dnstap_pb2
.Dnstap()
299 dnstap
.ParseFromString(data
)
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
)
307 self
.assertNotEqual(res
, None)
309 # check the dnstap message corresponding to the UDP query
310 dnstap
= self
.getFirstDnstap()
312 checkDnstapQuery(self
, dnstap
, dnstap_pb2
.UDP
, '127.0.0.1', '127.0.0.8')
313 # We don't expect a response
314 checkDnstapNoExtra(self
, dnstap
)
316 class DNSTapLogNoQueriesTest(TestRecursorDNSTap
):
318 _confdir
= 'DNSTapLogNoQueries'
319 _config_template
= """
320 auth-zones=example=configs/%s/example.zone""" % _confdir
321 _lua_config_file
= """
322 dnstapFrameStreamServer({"%s"}, {logQueries=false})
323 """ % (DNSTapServerParameters
.path
)
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
)
330 self
.assertNotEqual(res
, None)
332 # We don't expect anything
333 self
.assertTrue(DNSTapServerParameters
.queue
.empty())
335 class DNSTapLogNODTest(TestRecursorDNSTap
):
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.
342 _confdir
= 'DNSTapLogNODQueries'
343 _config_template
= """
344 new-domain-tracking=yes
345 new-domain-history-dir=configs/%s/nod
346 unique-response-tracking=yes
347 unique-response-history-dir=configs/%s/udr
348 auth-zones=example=configs/%s/example.zone""" % (_confdir
, _confdir
, _confdir
)
349 _lua_config_file
= """
350 dnstapNODFrameStreamServer({"%s"})
351 """ % (DNSTapServerParameters
.path
)
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
)
360 def getFirstDnstap(self
):
362 data
= DNSTapServerParameters
.queue
.get(True, timeout
=2.0)
365 self
.assertTrue(data
)
366 dnstap
= dnstap_pb2
.Dnstap()
367 dnstap
.ParseFromString(data
)
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)
377 # check the dnstap message corresponding to the UDP query
378 dnstap
= self
.getFirstDnstap()
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
)
384 class DNSTapLogUDRTest(TestRecursorDNSTap
):
386 _confdir
= 'DNSTapLogUDRResponses'
387 _config_template
= """
388 new-domain-tracking=yes
389 new-domain-history-dir=configs/%s/nod
390 unique-response-tracking=yes
391 unique-response-history-dir=configs/%s/udr
392 auth-zones=example=configs/%s/example.zone""" % (_confdir
, _confdir
, _confdir
)
393 _lua_config_file
= """
394 dnstapNODFrameStreamServer({"%s"}, {logNODs=false, logUDRs=true})
395 """ % (DNSTapServerParameters
.path
)
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
)
404 def getFirstDnstap(self
):
406 data
= DNSTapServerParameters
.queue
.get(True, timeout
=2.0)
409 self
.assertTrue(data
)
410 dnstap
= dnstap_pb2
.Dnstap()
411 dnstap
.ParseFromString(data
)
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)
421 # check the dnstap message corresponding to the UDP query
422 dnstap
= self
.getFirstDnstap()
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
)
428 class DNSTapLogNODUDRTest(TestRecursorDNSTap
):
430 _confdir
= 'DNSTapLogNODUDRs'
431 _config_template
= """
432 new-domain-tracking=yes
433 new-domain-history-dir=configs/%s/nod
434 unique-response-tracking=yes
435 unique-response-history-dir=configs/%s/udr
436 auth-zones=example=configs/%s/example.zone""" % (_confdir
, _confdir
, _confdir
)
437 _lua_config_file
= """
438 dnstapNODFrameStreamServer({"%s"}, {logNODs=true, logUDRs=true})
439 """ % (DNSTapServerParameters
.path
)
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
)
448 def getFirstDnstap(self
):
450 data
= DNSTapServerParameters
.queue
.get(True, timeout
=2.0)
453 self
.assertTrue(data
)
454 dnstap
= dnstap_pb2
.Dnstap()
455 dnstap
.ParseFromString(data
)
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)
465 dnstap
= self
.getFirstDnstap()
466 checkDnstapUDR(self
, dnstap
, dnstap_pb2
.UDP
, '127.0.0.1', '127.0.0.1', 5300, name
)
468 dnstap
= self
.getFirstDnstap()
469 checkDnstapNOD(self
, dnstap
, dnstap_pb2
.UDP
, '127.0.0.1', '127.0.0.1', 5300, name
)
471 checkDnstapNoExtra(self
, dnstap
)