]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.recursor-dnssec/test_RoutingTag.py
Merge pull request #13509 from rgacogne/ddist-teeaction-proxyprotocol
[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 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)