]>
Commit | Line | Data |
---|---|---|
1 | import dns | |
2 | import os | |
3 | import socket | |
4 | import struct | |
5 | import threading | |
6 | import time | |
7 | import clientsubnetoption | |
8 | import subprocess | |
9 | from recursortests import RecursorTest | |
10 | from twisted.internet.protocol import DatagramProtocol | |
11 | from twisted.internet import reactor | |
12 | ||
13 | emptyECSText = 'No ECS received' | |
14 | nameECS = 'ecs-echo.example.' | |
15 | nameECSInvalidScope = 'invalid-scope.ecs-echo.example.' | |
16 | ttlECS = 60 | |
17 | routingReactorRunning = False | |
18 | ||
19 | class RoutingTagTest(RecursorTest): | |
20 | _config_template_default = """ | |
21 | daemon=no | |
22 | trace=yes | |
23 | dont-query= | |
24 | local-address=127.0.0.1 | |
25 | packetcache-ttl=15 | |
26 | packetcache-servfail-ttl=15 | |
27 | max-cache-ttl=600 | |
28 | threads=2 | |
29 | loglevel=9 | |
30 | disable-syslog=yes | |
31 | log-common-errors=yes | |
32 | statistics-interval=0 | |
33 | ecs-add-for=0.0.0.0/0 | |
34 | """ | |
35 | ||
36 | def sendECSQuery(self, query, expected, expectedFirstTTL=None): | |
37 | res = self.sendUDPQuery(query) | |
38 | ||
39 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
40 | self.assertRRsetInAnswer(res, expected) | |
41 | # this will break if you are not looking for the first RR, sorry! | |
42 | if expectedFirstTTL is not None: | |
43 | self.assertEqual(res.answer[0].ttl, expectedFirstTTL) | |
44 | else: | |
45 | expectedFirstTTL = res.answer[0].ttl | |
46 | ||
47 | # wait one second, check that the TTL has been | |
48 | # decreased indicating a cache hit | |
49 | time.sleep(1) | |
50 | ||
51 | res = self.sendUDPQuery(query) | |
52 | ||
53 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
54 | self.assertRRsetInAnswer(res, expected) | |
55 | self.assertLess(res.answer[0].ttl, expectedFirstTTL) | |
56 | ||
57 | def checkECSQueryHit(self, query, expected): | |
58 | res = self.sendUDPQuery(query) | |
59 | ||
60 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
61 | self.assertRRsetInAnswer(res, expected) | |
62 | # this will break if you are not looking for the first RR, sorry! | |
63 | self.assertLess(res.answer[0].ttl, ttlECS) | |
64 | ||
65 | def setRoutingTag(self, tag): | |
66 | # This value is picked up by the gettag() | |
67 | file = open('tagfile', 'w') | |
68 | if tag: | |
69 | file.write(tag) | |
70 | file.close(); | |
71 | ||
72 | @classmethod | |
73 | def startResponders(cls): | |
74 | global routingReactorRunning | |
75 | print("Launching responders..") | |
76 | ||
77 | address = cls._PREFIX + '.24' | |
78 | port = 53 | |
79 | ||
80 | if not routingReactorRunning: | |
81 | reactor.listenUDP(port, UDPRoutingResponder(), interface=address) | |
82 | routingReactorRunning = True | |
83 | ||
84 | if not reactor.running: | |
85 | cls._UDPResponder = threading.Thread(name='UDP Routing Responder', target=reactor.run, args=(False,)) | |
86 | cls._UDPResponder.setDaemon(True) | |
87 | cls._UDPResponder.start() | |
88 | ||
89 | @classmethod | |
90 | def setUpClass(cls): | |
91 | cls.setUpSockets() | |
92 | ||
93 | cls.startResponders() | |
94 | ||
95 | confdir = os.path.join('configs', cls._confdir) | |
96 | cls.createConfigDir(confdir) | |
97 | ||
98 | cls.generateRecursorConfig(confdir) | |
99 | cls.startRecursor(confdir, cls._recursorPort) | |
100 | ||
101 | print("Launching tests..") | |
102 | ||
103 | @classmethod | |
104 | def tearDownClass(cls): | |
105 | cls.tearDownRecursor() | |
106 | os.unlink('tagfile') | |
107 | ||
108 | class testRoutingTag(RoutingTagTest): | |
109 | _confdir = 'RoutingTag' | |
110 | ||
111 | _config_template = """ | |
112 | use-incoming-edns-subnet=yes | |
113 | edns-subnet-allow-list=ecs-echo.example. | |
114 | forward-zones=ecs-echo.example=%s.24 | |
115 | """ % (os.environ['PREFIX']) | |
116 | _lua_dns_script_file = """ | |
117 | ||
118 | function gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp, proxyProtocolValues) | |
119 | local rtag | |
120 | for line in io.lines('tagfile') do | |
121 | rtag = line | |
122 | break | |
123 | end | |
124 | return 0, nil, nil, nil, nil, nil, rtag | |
125 | end | |
126 | """ | |
127 | ||
128 | def testSendECS(self): | |
129 | # First send an ECS query with routingTag | |
130 | self.setRoutingTag('foo') | |
131 | expected1 = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '192.0.2.0/24') | |
132 | ecso = clientsubnetoption.ClientSubnetOption('192.0.2.1', 32) | |
133 | query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512) | |
134 | self.sendECSQuery(query, expected1) | |
135 | ||
136 | # Now check a cache hit with the same routingTag (but no ECS) | |
137 | query = dns.message.make_query(nameECS, 'TXT', 'IN') | |
138 | self.checkECSQueryHit(query, expected1) | |
139 | ||
140 | expected2 = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '127.0.0.0/24') | |
141 | # And see if a different tag does *not* hit the first one | |
142 | self.setRoutingTag('bar') | |
143 | query = dns.message.make_query(nameECS, 'TXT', 'IN') | |
144 | self.sendECSQuery(query, expected2) | |
145 | ||
146 | # And see if a *no* tag does *not* hit the first one | |
147 | expected3 = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '192.0.3.0/24') | |
148 | self.setRoutingTag(None) | |
149 | ecso = clientsubnetoption.ClientSubnetOption('192.0.3.1', 32) | |
150 | query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512) | |
151 | self.sendECSQuery(query, expected3) | |
152 | ||
153 | # And see if an unknown tag from the same subnet does hit the last | |
154 | self.setRoutingTag('baz') | |
155 | ecso = clientsubnetoption.ClientSubnetOption('192.0.3.2', 32) | |
156 | query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512) | |
157 | self.checkECSQueryHit(query, expected3) | |
158 | ||
159 | # And a no tag and no subnet query does hit the general case | |
160 | self.setRoutingTag(None) | |
161 | query = dns.message.make_query(nameECS, 'TXT', 'IN') | |
162 | self.sendECSQuery(query, expected2) | |
163 | ||
164 | # And a unknown tag and no subnet query does hit the general case | |
165 | self.setRoutingTag('bag') | |
166 | query = dns.message.make_query(nameECS, 'TXT', 'IN') | |
167 | self.sendECSQuery(query, expected2) | |
168 | ||
169 | return # remove this line to peek at cache | |
170 | rec_controlCmd = [os.environ['RECCONTROL'], | |
171 | '--config-dir=%s' % 'configs/' + self._confdir, | |
172 | 'dump-cache', 'x'] | |
173 | try: | |
174 | expected = b'dumped 7 records\n' | |
175 | ret = subprocess.check_output(rec_controlCmd, stderr=subprocess.STDOUT) | |
176 | self.assertEqual(ret, expected) | |
177 | ||
178 | except subprocess.CalledProcessError as e: | |
179 | print(e.output) | |
180 | raise | |
181 | ||
182 | class testRoutingTagFFI(RoutingTagTest): | |
183 | _confdir = 'RoutingTagFFI' | |
184 | ||
185 | _config_template = """ | |
186 | use-incoming-edns-subnet=yes | |
187 | edns-subnet-allow-list=ecs-echo.example. | |
188 | forward-zones=ecs-echo.example=%s.24 | |
189 | """ % (os.environ['PREFIX']) | |
190 | _lua_dns_script_file = """ | |
191 | ||
192 | local ffi = require("ffi") | |
193 | ffi.cdef[[ | |
194 | typedef struct pdns_ffi_param pdns_ffi_param_t; | |
195 | ||
196 | const char* pdns_ffi_param_get_qname(pdns_ffi_param_t* ref); | |
197 | void pdns_ffi_param_set_routingtag(pdns_ffi_param_t* ref, const char* rtag); | |
198 | ]] | |
199 | ||
200 | function gettag_ffi(obj) | |
201 | for line in io.lines('tagfile') do | |
202 | local rtag = ffi.string(line) | |
203 | ffi.C.pdns_ffi_param_set_routingtag(obj, rtag) | |
204 | break | |
205 | end | |
206 | return 0 | |
207 | end | |
208 | """ | |
209 | def testSendECS(self): | |
210 | # First send an ECS query with routingTag | |
211 | self.setRoutingTag('foo') | |
212 | expected1 = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '192.0.2.0/24') | |
213 | ecso = clientsubnetoption.ClientSubnetOption('192.0.2.1', 32) | |
214 | query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512) | |
215 | self.sendECSQuery(query, expected1) | |
216 | ||
217 | # Now check a cache hit with the same routingTag (but no ECS) | |
218 | query = dns.message.make_query(nameECS, 'TXT', 'IN') | |
219 | self.checkECSQueryHit(query, expected1) | |
220 | ||
221 | expected2 = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '127.0.0.0/24') | |
222 | # And see if a different tag does *not* hit the first one | |
223 | self.setRoutingTag('bar') | |
224 | query = dns.message.make_query(nameECS, 'TXT', 'IN') | |
225 | self.sendECSQuery(query, expected2) | |
226 | ||
227 | # And see if a *no* tag does *not* hit the first one | |
228 | expected3 = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '192.0.3.0/24') | |
229 | self.setRoutingTag(None) | |
230 | ecso = clientsubnetoption.ClientSubnetOption('192.0.3.1', 32) | |
231 | query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512) | |
232 | self.sendECSQuery(query, expected3) | |
233 | ||
234 | # And see if an unknown tag from the same subnet does hit the last | |
235 | self.setRoutingTag('baz') | |
236 | ecso = clientsubnetoption.ClientSubnetOption('192.0.3.2', 32) | |
237 | query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512) | |
238 | self.checkECSQueryHit(query, expected3) | |
239 | ||
240 | # And a no tag and no subnet query does hit the general case | |
241 | self.setRoutingTag(None) | |
242 | query = dns.message.make_query(nameECS, 'TXT', 'IN') | |
243 | self.sendECSQuery(query, expected2) | |
244 | ||
245 | # And a unknown tag and no subnet query does hit the general case | |
246 | self.setRoutingTag('bag') | |
247 | query = dns.message.make_query(nameECS, 'TXT', 'IN') | |
248 | self.sendECSQuery(query, expected2) | |
249 | ||
250 | return #remove this line to peek at cache | |
251 | rec_controlCmd = [os.environ['RECCONTROL'], | |
252 | '--config-dir=%s' % 'configs/' + self._confdir, | |
253 | 'dump-cache y'] | |
254 | try: | |
255 | expected = 'dumped 6 records\n' | |
256 | ret = subprocess.check_output(rec_controlCmd, stderr=subprocess.STDOUT) | |
257 | self.assertEqual(ret, expected) | |
258 | ||
259 | except subprocess.CalledProcessError as e: | |
260 | print(e.output) | |
261 | raise | |
262 | ||
263 | class UDPRoutingResponder(DatagramProtocol): | |
264 | @staticmethod | |
265 | def ipToStr(option): | |
266 | if option.family == clientsubnetoption.FAMILY_IPV4: | |
267 | ip = socket.inet_ntop(socket.AF_INET, struct.pack('!L', option.ip)) | |
268 | elif option.family == clientsubnetoption.FAMILY_IPV6: | |
269 | ip = socket.inet_ntop(socket.AF_INET6, | |
270 | struct.pack('!QQ', | |
271 | option.ip >> 64, | |
272 | option.ip & (2 ** 64 - 1))) | |
273 | return ip | |
274 | ||
275 | def datagramReceived(self, datagram, address): | |
276 | request = dns.message.from_wire(datagram) | |
277 | ||
278 | response = dns.message.make_response(request) | |
279 | response.flags |= dns.flags.AA | |
280 | ecso = None | |
281 | ||
282 | if (request.question[0].name == dns.name.from_text(nameECS) or request.question[0].name == dns.name.from_text(nameECSInvalidScope)) and request.question[0].rdtype == dns.rdatatype.TXT: | |
283 | ||
284 | text = emptyECSText | |
285 | for option in request.options: | |
286 | if option.otype == clientsubnetoption.ASSIGNED_OPTION_CODE and isinstance(option, clientsubnetoption.ClientSubnetOption): | |
287 | text = self.ipToStr(option) + '/' + str(option.mask) | |
288 | ||
289 | # Send a scope more specific than the received source for nameECSInvalidScope | |
290 | if request.question[0].name == dns.name.from_text(nameECSInvalidScope): | |
291 | ecso = clientsubnetoption.ClientSubnetOption("192.0.42.42", 32, 32) | |
292 | else: | |
293 | ecso = clientsubnetoption.ClientSubnetOption(self.ipToStr(option), option.mask, option.mask) | |
294 | ||
295 | answer = dns.rrset.from_text(request.question[0].name, ttlECS, dns.rdataclass.IN, 'TXT', text) | |
296 | response.answer.append(answer) | |
297 | ||
298 | elif request.question[0].name == dns.name.from_text(nameECS) and request.question[0].rdtype == dns.rdatatype.NS: | |
299 | answer = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'NS', 'ns1.ecs-echo.example.') | |
300 | response.answer.append(answer) | |
301 | additional = dns.rrset.from_text('ns1.ecs-echo.example.', 15, dns.rdataclass.IN, 'A', os.environ['PREFIX'] + '.24') | |
302 | response.additional.append(additional) | |
303 | ||
304 | if ecso: | |
305 | response.use_edns(options = [ecso]) | |
306 | ||
307 | self.transport.write(response.to_wire(), address) |