]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.recursor-dnssec/test_RoutingTag.py
Merge pull request #13478 from omoerbeek/rec-nsec3-iter-50
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / test_RoutingTag.py
CommitLineData
0cfe4767
OM
1import dns
2import os
3import socket
4import struct
5import threading
6import time
7import clientsubnetoption
8import subprocess
9from recursortests import RecursorTest
10from twisted.internet.protocol import DatagramProtocol
11from twisted.internet import reactor
12
13emptyECSText = 'No ECS received'
14nameECS = 'ecs-echo.example.'
15nameECSInvalidScope = 'invalid-scope.ecs-echo.example.'
16ttlECS = 60
17routingReactorRunning = False
18
19class RoutingTagTest(RecursorTest):
20 _config_template_default = """
21daemon=no
22trace=yes
23dont-query=
0cfe4767 24local-address=127.0.0.1
d2c1660a
OM
25packetcache-ttl=15
26packetcache-servfail-ttl=15
0cfe4767 27max-cache-ttl=600
d2c1660a 28threads=2
0cfe4767
OM
29loglevel=9
30disable-syslog=yes
9425e4ca
O
31log-common-errors=yes
32statistics-interval=0
33ecs-add-for=0.0.0.0/0
0cfe4767
OM
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
108class testRoutingTag(RoutingTagTest):
109 _confdir = 'RoutingTag'
110
111 _config_template = """
0cfe4767 112use-incoming-edns-subnet=yes
9425e4ca 113edns-subnet-allow-list=ecs-echo.example.
0cfe4767
OM
114forward-zones=ecs-echo.example=%s.24
115 """ % (os.environ['PREFIX'])
116 _lua_dns_script_file = """
117
118function 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
125end
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
495f88ad 136 # Now check a cache hit with the same routingTag (but no ECS)
0cfe4767
OM
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
c4d9cae9 146 # And see if a *no* tag does *not* hit the first one
0cfe4767
OM
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
1b54dc37 153 # And see if an unknown tag from the same subnet does hit the last
0cfe4767 154 self.setRoutingTag('baz')
1b54dc37
OM
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)
0cfe4767
OM
157 self.checkECSQueryHit(query, expected3)
158
1b54dc37
OM
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
15a04447 169 return # remove this line to peek at cache
0cfe4767
OM
170 rec_controlCmd = [os.environ['RECCONTROL'],
171 '--config-dir=%s' % 'configs/' + self._confdir,
f2e416cf 172 'dump-cache', 'x']
0cfe4767 173 try:
3d144e24 174 expected = b'dumped 7 records\n'
0cfe4767
OM
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
182class testRoutingTagFFI(RoutingTagTest):
183 _confdir = 'RoutingTagFFI'
184
185 _config_template = """
0cfe4767 186use-incoming-edns-subnet=yes
9425e4ca 187edns-subnet-allow-list=ecs-echo.example.
0cfe4767
OM
188forward-zones=ecs-echo.example=%s.24
189 """ % (os.environ['PREFIX'])
190 _lua_dns_script_file = """
191
192local ffi = require("ffi")
193ffi.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
200function 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
207end
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
495f88ad 217 # Now check a cache hit with the same routingTag (but no ECS)
0cfe4767
OM
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
c4d9cae9 227 # And see if a *no* tag does *not* hit the first one
0cfe4767
OM
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
1b54dc37 234 # And see if an unknown tag from the same subnet does hit the last
0cfe4767 235 self.setRoutingTag('baz')
1b54dc37
OM
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)
0cfe4767
OM
238 self.checkECSQueryHit(query, expected3)
239
1b54dc37
OM
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
0cfe4767
OM
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:
1b54dc37 255 expected = 'dumped 6 records\n'
0cfe4767
OM
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
263class 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:
3d144e24 305 response.use_edns(options = [ecso])
0cfe4767
OM
306
307 self.transport.write(response.to_wire(), address)