]>
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 OM |
28 | |
29 | def checkDnstapBase(testinstance, dnstap, protocol, initiator): | |
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 OM |
42 | # |
43 | # We cannot check the query address and port since we only log outgoing queries via dnstap | |
44 | # | |
fc7f729f | 45 | #testinstance.assertTrue(dnstap.message.HasField('query_address')) |
4bfebc93 | 46 | #testinstance.assertEqual(socket.inet_ntop(socket.AF_INET, dnstap.message.query_address), initiator) |
fc7f729f | 47 | testinstance.assertTrue(dnstap.message.HasField('response_address')) |
4bfebc93 | 48 | testinstance.assertEqual(socket.inet_ntop(socket.AF_INET, dnstap.message.response_address), initiator) |
fc7f729f | 49 | testinstance.assertTrue(dnstap.message.HasField('response_port')) |
4bfebc93 | 50 | testinstance.assertEqual(dnstap.message.response_port, 53) |
fc7f729f OM |
51 | |
52 | ||
e593a398 | 53 | def checkDnstapQuery(testinstance, dnstap, protocol, initiator='127.0.0.1'): |
4bfebc93 | 54 | testinstance.assertEqual(dnstap.message.type, dnstap_pb2.Message.RESOLVER_QUERY) |
fc7f729f OM |
55 | checkDnstapBase(testinstance, dnstap, protocol, initiator) |
56 | ||
57 | testinstance.assertTrue(dnstap.message.HasField('query_time_sec')) | |
58 | testinstance.assertTrue(dnstap.message.HasField('query_time_nsec')) | |
59 | ||
60 | testinstance.assertTrue(dnstap.message.HasField('query_message')) | |
3f9f84c6 OM |
61 | # |
62 | # We cannot compare the incoming query with the outgoing one | |
63 | # The IDs and some other fields will be different | |
64 | # | |
e593a398 | 65 | #wire_message = dns.message.from_wire(dnstap.message.query_message) |
fc7f729f OM |
66 | #testinstance.assertEqual(wire_message, query) |
67 | ||
68 | ||
69 | def checkDnstapExtra(testinstance, dnstap, expected): | |
70 | testinstance.assertTrue(dnstap.HasField('extra')) | |
71 | testinstance.assertEqual(dnstap.extra, expected) | |
72 | ||
73 | ||
74 | def checkDnstapNoExtra(testinstance, dnstap): | |
75 | testinstance.assertFalse(dnstap.HasField('extra')) | |
76 | ||
77 | ||
78 | def checkDnstapResponse(testinstance, dnstap, protocol, response, initiator='127.0.0.1'): | |
4bfebc93 | 79 | testinstance.assertEqual(dnstap.message.type, dnstap_pb2.Message.RESOLVER_RESPONSE) |
fc7f729f OM |
80 | checkDnstapBase(testinstance, dnstap, protocol, initiator) |
81 | ||
82 | testinstance.assertTrue(dnstap.message.HasField('query_time_sec')) | |
83 | testinstance.assertTrue(dnstap.message.HasField('query_time_nsec')) | |
84 | ||
85 | testinstance.assertTrue(dnstap.message.HasField('response_time_sec')) | |
86 | testinstance.assertTrue(dnstap.message.HasField('response_time_nsec')) | |
87 | ||
88 | testinstance.assertTrue(dnstap.message.response_time_sec > dnstap.message.query_time_sec or \ | |
89 | dnstap.message.response_time_nsec > dnstap.message.query_time_nsec) | |
90 | ||
91 | testinstance.assertTrue(dnstap.message.HasField('response_message')) | |
92 | wire_message = dns.message.from_wire(dnstap.message.response_message) | |
93 | testinstance.assertEqual(wire_message, response) | |
94 | ||
95 | def fstrm_get_control_frame_type(data): | |
96 | (t,) = struct.unpack("!L", data[0:4]) | |
97 | return t | |
98 | ||
99 | ||
e593a398 | 100 | def fstrm_make_control_frame_reply(cft): |
fc7f729f OM |
101 | if cft == FSTRM_CONTROL_READY: |
102 | # Reply with ACCEPT frame and content-type | |
103 | contenttype = b'protobuf:dnstap.Dnstap' | |
104 | frame = struct.pack('!LLL', FSTRM_CONTROL_ACCEPT, 1, | |
105 | len(contenttype)) + contenttype | |
106 | buf = struct.pack("!LL", 0, len(frame)) + frame | |
107 | return buf | |
108 | elif cft == FSTRM_CONTROL_START: | |
109 | return None | |
110 | else: | |
111 | raise Exception('unhandled control frame ' + cft) | |
112 | ||
113 | ||
114 | def fstrm_read_and_dispatch_control_frame(conn): | |
115 | data = conn.recv(4) | |
116 | if not data: | |
117 | raise Exception('length of control frame payload could not be read') | |
118 | (datalen,) = struct.unpack("!L", data) | |
119 | data = conn.recv(datalen) | |
120 | cft = fstrm_get_control_frame_type(data) | |
e593a398 | 121 | reply = fstrm_make_control_frame_reply(cft) |
fc7f729f OM |
122 | if reply: |
123 | conn.send(reply) | |
124 | return cft | |
125 | ||
126 | ||
127 | def fstrm_handle_bidir_connection(conn, on_data): | |
128 | data = None | |
129 | while True: | |
130 | data = conn.recv(4) | |
131 | if not data: | |
132 | break | |
133 | (datalen,) = struct.unpack("!L", data) | |
134 | if datalen == 0: | |
135 | # control frame length follows | |
136 | cft = fstrm_read_and_dispatch_control_frame(conn) | |
137 | if cft == FSTRM_CONTROL_STOP: | |
138 | break | |
139 | else: | |
140 | # data frame | |
141 | data = conn.recv(datalen) | |
142 | if not data: | |
143 | break | |
144 | ||
145 | on_data(data) | |
146 | ||
147 | ||
148 | ||
e593a398 OM |
149 | class DNSTapServerParams(object): |
150 | def __init__(self, path): | |
151 | self.queue = Queue() | |
152 | self.path = path | |
fc7f729f OM |
153 | |
154 | ||
e593a398 | 155 | DNSTapServerParameters = DNSTapServerParams("/tmp/dnstap.sock") |
fc7f729f OM |
156 | DNSTapListeners = [] |
157 | ||
158 | class TestRecursorDNSTap(RecursorTest): | |
159 | @classmethod | |
160 | def FrameStreamUnixListener(cls, conn, param): | |
161 | while True: | |
162 | try: | |
163 | fstrm_handle_bidir_connection(conn, lambda data: \ | |
164 | param.queue.put(data, True, timeout=2.0)) | |
165 | except socket.error as e: | |
166 | if e.errno == 9: | |
167 | break | |
e593a398 | 168 | sys.stderr.write("Unexpected socket error %s\n" % str(e)) |
fc7f729f | 169 | sys.exit(1) |
e593a398 OM |
170 | except exception as e: |
171 | sys.stderr.write("Unexpected socket error %s\n" % str(e)) | |
172 | sys.exit(1) | |
173 | conn.close() | |
fc7f729f OM |
174 | |
175 | @classmethod | |
176 | def FrameStreamUnixListenerMain(cls, param): | |
e593a398 | 177 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
fc7f729f | 178 | try: |
e593a398 OM |
179 | try: |
180 | os.remove(param.path) | |
181 | except: | |
182 | pass | |
183 | sock.bind(param.path) | |
184 | sock.listen(100) | |
fc7f729f | 185 | except socket.error as e: |
e593a398 | 186 | sys.stderr.write("Error binding/listening in the framestream listener: %s\n" % str(e)) |
fc7f729f OM |
187 | sys.exit(1) |
188 | DNSTapListeners.append(sock) | |
fc7f729f | 189 | while True: |
e593a398 OM |
190 | try: |
191 | (conn, addr) = sock.accept() | |
192 | listener = threading.Thread(name='DNSTap Worker', target=cls.FrameStreamUnixListener, args=[conn, param]) | |
193 | listener.setDaemon(True) | |
194 | listener.start() | |
195 | except socket.error as e: | |
196 | if e.errno != 9: | |
197 | sys.stderr.write("Socket error on accept: %s\n" % str(e)) | |
198 | else: | |
199 | break | |
fc7f729f OM |
200 | sock.close() |
201 | ||
202 | @classmethod | |
203 | def setUpClass(cls): | |
e593a398 OM |
204 | if os.environ.get("NODNSTAPTESTS") == "1": |
205 | raise SkipTest("Not Yet Supported") | |
fc7f729f OM |
206 | |
207 | cls.setUpSockets() | |
208 | ||
209 | cls.startResponders() | |
210 | ||
211 | listener = threading.Thread(name='DNSTap Listener', target=cls.FrameStreamUnixListenerMain, args=[DNSTapServerParameters]) | |
212 | listener.setDaemon(True) | |
213 | listener.start() | |
214 | ||
fc7f729f OM |
215 | confdir = os.path.join('configs', cls._confdir) |
216 | cls.createConfigDir(confdir) | |
217 | ||
218 | cls.generateRecursorConfig(confdir) | |
219 | cls.startRecursor(confdir, cls._recursorPort) | |
220 | ||
221 | def setUp(self): | |
222 | # Make sure the queue is empty, in case | |
223 | # a previous test failed | |
224 | while not DNSTapServerParameters.queue.empty(): | |
225 | DNSTapServerParameters.queue.get(False) | |
226 | ||
227 | @classmethod | |
228 | def generateRecursorConfig(cls, confdir): | |
229 | authzonepath = os.path.join(confdir, 'example.zone') | |
230 | with open(authzonepath, 'w') as authzone: | |
231 | authzone.write("""$ORIGIN example. | |
232 | @ 3600 IN SOA {soa} | |
233 | a 3600 IN A 192.0.2.42 | |
234 | tagged 3600 IN A 192.0.2.84 | |
235 | query-selected 3600 IN A 192.0.2.84 | |
236 | answer-selected 3600 IN A 192.0.2.84 | |
237 | types 3600 IN A 192.0.2.84 | |
238 | types 3600 IN AAAA 2001:DB8::1 | |
239 | types 3600 IN TXT "Lorem ipsum dolor sit amet" | |
240 | types 3600 IN MX 10 a.example. | |
241 | types 3600 IN SPF "v=spf1 -all" | |
242 | types 3600 IN SRV 10 20 443 a.example. | |
243 | cname 3600 IN CNAME a.example. | |
244 | ||
245 | """.format(soa=cls._SOA)) | |
246 | super(TestRecursorDNSTap, cls).generateRecursorConfig(confdir) | |
247 | ||
248 | @classmethod | |
249 | def tearDownClass(cls): | |
250 | cls.tearDownRecursor() | |
251 | for listerner in DNSTapListeners: | |
252 | listerner.close() | |
253 | ||
254 | class DNSTapDefaultTest(TestRecursorDNSTap): | |
255 | """ | |
256 | This test makes sure that we correctly export outgoing queries over DNSTap. | |
257 | It must be improved and setup env so we can check for incoming responses, but makes sure for now | |
258 | that the recursor at least connects to the DNSTap server. | |
259 | """ | |
260 | ||
261 | _confdir = 'DNSTapDefault' | |
262 | _config_template = """ | |
263 | auth-zones=example=configs/%s/example.zone""" % _confdir | |
264 | _lua_config_file = """ | |
e593a398 OM |
265 | dnstapFrameStreamServer({"%s"}) |
266 | """ % DNSTapServerParameters.path | |
fc7f729f OM |
267 | |
268 | def getFirstDnstap(self): | |
e593a398 OM |
269 | try: |
270 | data = DNSTapServerParameters.queue.get(True, timeout=2.0) | |
271 | except: | |
272 | data = False | |
fc7f729f OM |
273 | self.assertTrue(data) |
274 | dnstap = dnstap_pb2.Dnstap() | |
275 | dnstap.ParseFromString(data) | |
276 | return dnstap | |
277 | ||
278 | def testA(self): | |
fc7f729f OM |
279 | name = 'www.example.org.' |
280 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
281 | query.flags |= dns.flags.RD | |
282 | res = self.sendUDPQuery(query) | |
4bfebc93 | 283 | self.assertNotEqual(res, None) |
e593a398 | 284 | |
fc7f729f OM |
285 | # check the dnstap message corresponding to the UDP query |
286 | dnstap = self.getFirstDnstap() | |
287 | ||
e593a398 | 288 | checkDnstapQuery(self, dnstap, dnstap_pb2.UDP, '127.0.0.8') |
fc7f729f OM |
289 | # We don't expect a response |
290 | checkDnstapNoExtra(self, dnstap) | |
291 | ||
292 | class DNSTapLogNoQueriesTest(TestRecursorDNSTap): | |
293 | """ | |
294 | This test makes sure that we correctly export outgoing queries over DNSTap. | |
295 | It must be improved and setup env so we can check for incoming responses, but makes sure for now | |
296 | that the recursor at least connects to the DNSTap server. | |
297 | """ | |
298 | ||
299 | _confdir = 'DNSTapLogNoQueries' | |
300 | _config_template = """ | |
301 | auth-zones=example=configs/%s/example.zone""" % _confdir | |
302 | _lua_config_file = """ | |
e593a398 OM |
303 | dnstapFrameStreamServer({"%s"}, {logQueries=false}) |
304 | """ % (DNSTapServerParameters.path) | |
fc7f729f OM |
305 | |
306 | def testA(self): | |
307 | name = 'www.example.org.' | |
308 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
309 | query.flags |= dns.flags.RD | |
310 | res = self.sendUDPQuery(query) | |
4bfebc93 | 311 | self.assertNotEqual(res, None) |
fc7f729f OM |
312 | |
313 | # We don't expect anything | |
314 | self.assertTrue(DNSTapServerParameters.queue.empty()) |