]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.recursor-dnssec/test_RoutingTag.py
Make sure we can install unsigned packages.
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / test_RoutingTag.py
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 ecs-add-for=0.0.0.0/0
25 local-address=127.0.0.1
26 packetcache-ttl=0
27 packetcache-servfail-ttl=0
28 max-cache-ttl=600
29 threads=1
30 loglevel=9
31 disable-syslog=yes
32 """
33
34 def sendECSQuery(self, query, expected, expectedFirstTTL=None):
35 res = self.sendUDPQuery(query)
36
37 self.assertRcodeEqual(res, dns.rcode.NOERROR)
38 self.assertRRsetInAnswer(res, expected)
39 # this will break if you are not looking for the first RR, sorry!
40 if expectedFirstTTL is not None:
41 self.assertEqual(res.answer[0].ttl, expectedFirstTTL)
42 else:
43 expectedFirstTTL = res.answer[0].ttl
44
45 # wait one second, check that the TTL has been
46 # decreased indicating a cache hit
47 time.sleep(1)
48
49 res = self.sendUDPQuery(query)
50
51 self.assertRcodeEqual(res, dns.rcode.NOERROR)
52 self.assertRRsetInAnswer(res, expected)
53 self.assertLess(res.answer[0].ttl, expectedFirstTTL)
54
55 def checkECSQueryHit(self, query, expected):
56 res = self.sendUDPQuery(query)
57
58 self.assertRcodeEqual(res, dns.rcode.NOERROR)
59 self.assertRRsetInAnswer(res, expected)
60 # this will break if you are not looking for the first RR, sorry!
61 self.assertLess(res.answer[0].ttl, ttlECS)
62
63 def setRoutingTag(self, tag):
64 # This value is picked up by the gettag()
65 file = open('tagfile', 'w')
66 if tag:
67 file.write(tag)
68 file.close();
69
70 @classmethod
71 def startResponders(cls):
72 global routingReactorRunning
73 print("Launching responders..")
74
75 address = cls._PREFIX + '.24'
76 port = 53
77
78 if not routingReactorRunning:
79 reactor.listenUDP(port, UDPRoutingResponder(), interface=address)
80 routingReactorRunning = True
81
82 if not reactor.running:
83 cls._UDPResponder = threading.Thread(name='UDP Routing Responder', target=reactor.run, args=(False,))
84 cls._UDPResponder.setDaemon(True)
85 cls._UDPResponder.start()
86
87 @classmethod
88 def setUpClass(cls):
89 cls.setUpSockets()
90
91 cls.startResponders()
92
93 confdir = os.path.join('configs', cls._confdir)
94 cls.createConfigDir(confdir)
95
96 cls.generateRecursorConfig(confdir)
97 cls.startRecursor(confdir, cls._recursorPort)
98
99 print("Launching tests..")
100
101 @classmethod
102 def tearDownClass(cls):
103 cls.tearDownRecursor()
104 os.unlink('tagfile')
105
106 class testRoutingTag(RoutingTagTest):
107 _confdir = 'RoutingTag'
108
109 _config_template = """
110 log-common-errors=yes
111 use-incoming-edns-subnet=yes
112 edns-subnet-whitelist=ecs-echo.example.
113 forward-zones=ecs-echo.example=%s.24
114 """ % (os.environ['PREFIX'])
115 _lua_dns_script_file = """
116
117 function gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp, proxyProtocolValues)
118 local rtag
119 for line in io.lines('tagfile') do
120 rtag = line
121 break
122 end
123 return 0, nil, nil, nil, nil, nil, rtag
124 end
125 """
126
127 def testSendECS(self):
128 # First send an ECS query with routingTag
129 self.setRoutingTag('foo')
130 expected1 = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '192.0.2.0/24')
131 ecso = clientsubnetoption.ClientSubnetOption('192.0.2.1', 32)
132 query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
133 self.sendECSQuery(query, expected1)
134
135 # Now check a cache hit with the same routingTag (but no ECS)
136 query = dns.message.make_query(nameECS, 'TXT', 'IN')
137 self.checkECSQueryHit(query, expected1)
138
139 expected2 = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '127.0.0.0/24')
140 # And see if a different tag does *not* hit the first one
141 self.setRoutingTag('bar')
142 query = dns.message.make_query(nameECS, 'TXT', 'IN')
143 self.sendECSQuery(query, expected2)
144
145 # And see if a *no* tag does *not* hit the first one
146 expected3 = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '192.0.3.0/24')
147 self.setRoutingTag(None)
148 ecso = clientsubnetoption.ClientSubnetOption('192.0.3.1', 32)
149 query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
150 self.sendECSQuery(query, expected3)
151
152 # And see if an unknown tag from the same subnet does hit the last
153 self.setRoutingTag('baz')
154 ecso = clientsubnetoption.ClientSubnetOption('192.0.3.2', 32)
155 query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
156 self.checkECSQueryHit(query, expected3)
157
158 # And a no tag and no subnet query does hit the general case
159 self.setRoutingTag(None)
160 query = dns.message.make_query(nameECS, 'TXT', 'IN')
161 self.sendECSQuery(query, expected2)
162
163 # And a unknown tag and no subnet query does hit the general case
164 self.setRoutingTag('bag')
165 query = dns.message.make_query(nameECS, 'TXT', 'IN')
166 self.sendECSQuery(query, expected2)
167
168 #return # remove this line to peek at cache
169 rec_controlCmd = [os.environ['RECCONTROL'],
170 '--config-dir=%s' % 'configs/' + self._confdir,
171 'dump-cache x']
172 try:
173 expected = 'dumped 6 records\n'
174 ret = subprocess.check_output(rec_controlCmd, stderr=subprocess.STDOUT)
175 self.assertEqual(ret, expected)
176
177 except subprocess.CalledProcessError as e:
178 print(e.output)
179 raise
180
181 class testRoutingTagFFI(RoutingTagTest):
182 _confdir = 'RoutingTagFFI'
183
184 _config_template = """
185 log-common-errors=yes
186 use-incoming-edns-subnet=yes
187 edns-subnet-whitelist=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.options = [ecso]
306
307 self.transport.write(response.to_wire(), address)