]>
Commit | Line | Data |
---|---|---|
fa980c59 RG |
1 | import clientsubnetoption |
2 | import cookiesoption | |
3 | import dns | |
4 | import os | |
5 | import requests | |
628fcdb9 | 6 | import subprocess |
fa980c59 RG |
7 | |
8 | from recursortests import RecursorTest | |
9 | ||
10 | class PacketCacheRecursorTest(RecursorTest): | |
11 | ||
5b4650e2 PL |
12 | _auth_zones = { |
13 | '8': {'threads': 1, | |
14 | 'zones': ['ROOT']} | |
15 | } | |
16 | ||
fa980c59 RG |
17 | _confdir = 'PacketCache' |
18 | _wsPort = 8042 | |
19 | _wsTimeout = 2 | |
20 | _wsPassword = 'secretpassword' | |
21 | _apiKey = 'secretapikey' | |
22 | _config_template = """ | |
628fcdb9 O |
23 | packetcache-ttl=10 |
24 | packetcache-servfail-ttl=5 | |
fa980c59 RG |
25 | auth-zones=example=configs/%s/example.zone |
26 | webserver=yes | |
27 | webserver-port=%d | |
28 | webserver-address=127.0.0.1 | |
29 | webserver-password=%s | |
30 | api-key=%s | |
31 | """ % (_confdir, _wsPort, _wsPassword, _apiKey) | |
32 | ||
33 | @classmethod | |
34 | def generateRecursorConfig(cls, confdir): | |
35 | authzonepath = os.path.join(confdir, 'example.zone') | |
36 | with open(authzonepath, 'w') as authzone: | |
37 | authzone.write("""$ORIGIN example. | |
38 | @ 3600 IN SOA {soa} | |
39 | a 3600 IN A 192.0.2.42 | |
40 | b 3600 IN A 192.0.2.42 | |
41 | c 3600 IN A 192.0.2.42 | |
42 | d 3600 IN A 192.0.2.42 | |
43 | e 3600 IN A 192.0.2.42 | |
628fcdb9 | 44 | f 3600 IN CNAME f ; CNAME loop: dirty trick to get a ServFail in an authzone |
fa980c59 RG |
45 | """.format(soa=cls._SOA)) |
46 | super(PacketCacheRecursorTest, cls).generateRecursorConfig(confdir) | |
47 | ||
fa980c59 RG |
48 | def checkPacketCacheMetrics(self, expectedHits, expectedMisses): |
49 | headers = {'x-api-key': self._apiKey} | |
50 | url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/statistics' | |
51 | r = requests.get(url, headers=headers, timeout=self._wsTimeout) | |
52 | self.assertTrue(r) | |
4bfebc93 | 53 | self.assertEqual(r.status_code, 200) |
fa980c59 RG |
54 | self.assertTrue(r.json()) |
55 | content = r.json() | |
56 | foundHits = False | |
57 | foundMisses = True | |
58 | for entry in content: | |
59 | if entry['name'] == 'packetcache-hits': | |
60 | foundHits = True | |
4bfebc93 | 61 | self.assertEqual(int(entry['value']), expectedHits) |
fa980c59 RG |
62 | elif entry['name'] == 'packetcache-misses': |
63 | foundMisses = True | |
4bfebc93 | 64 | self.assertEqual(int(entry['value']), expectedMisses) |
fa980c59 RG |
65 | |
66 | self.assertTrue(foundHits) | |
67 | self.assertTrue(foundMisses) | |
68 | ||
69 | def testPacketCache(self): | |
70 | # first query, no cookie | |
71 | qname = 'a.example.' | |
72 | query = dns.message.make_query(qname, 'A', want_dnssec=True) | |
73 | expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.42') | |
74 | ||
75 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
76 | sender = getattr(self, method) | |
77 | res = sender(query) | |
78 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
79 | self.assertRRsetInAnswer(res, expected) | |
80 | ||
282b5da9 | 81 | self.checkPacketCacheMetrics(0, 2) |
fa980c59 RG |
82 | |
83 | # we should get a hit over UDP this time | |
84 | res = self.sendUDPQuery(query) | |
85 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
86 | self.assertRRsetInAnswer(res, expected) | |
282b5da9 O |
87 | self.checkPacketCacheMetrics(1, 2) |
88 | ||
89 | # we should get a hit over TCP this time | |
90 | res = self.sendTCPQuery(query) | |
91 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
92 | self.assertRRsetInAnswer(res, expected) | |
93 | self.checkPacketCacheMetrics(2, 2) | |
fa980c59 RG |
94 | |
95 | eco1 = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef') | |
96 | eco2 = cookiesoption.CookiesOption(b'deadc0de', b'deadc0de') | |
97 | ecso1 = clientsubnetoption.ClientSubnetOption('192.0.2.1', 32) | |
98 | ecso2 = clientsubnetoption.ClientSubnetOption('192.0.2.2', 32) | |
99 | ||
100 | # we add a cookie, should not match anymore | |
101 | query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco1]) | |
102 | res = self.sendUDPQuery(query) | |
103 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
104 | self.assertRRsetInAnswer(res, expected) | |
282b5da9 | 105 | self.checkPacketCacheMetrics(2, 3) |
fa980c59 RG |
106 | |
107 | # same cookie, should match | |
108 | query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco1]) | |
109 | res = self.sendUDPQuery(query) | |
110 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
111 | self.assertRRsetInAnswer(res, expected) | |
282b5da9 | 112 | self.checkPacketCacheMetrics(3, 3) |
fa980c59 RG |
113 | |
114 | # different cookie, should still match | |
115 | query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco2]) | |
116 | res = self.sendUDPQuery(query) | |
117 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
118 | self.assertRRsetInAnswer(res, expected) | |
282b5da9 | 119 | self.checkPacketCacheMetrics(4, 3) |
fa980c59 RG |
120 | |
121 | # first cookie but with an ECS option, should not match | |
122 | query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco1, ecso1]) | |
123 | res = self.sendUDPQuery(query) | |
124 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
125 | self.assertRRsetInAnswer(res, expected) | |
282b5da9 | 126 | self.checkPacketCacheMetrics(4, 4) |
fa980c59 RG |
127 | |
128 | # different cookie but same ECS option, should match | |
129 | query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco2, ecso1]) | |
130 | res = self.sendUDPQuery(query) | |
131 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
132 | self.assertRRsetInAnswer(res, expected) | |
282b5da9 | 133 | self.checkPacketCacheMetrics(5, 4) |
fa980c59 RG |
134 | |
135 | # first cookie but different ECS option, should still match (we ignore EDNS Client Subnet | |
136 | # in the recursor's packet cache, but ECS-specific responses are not cached | |
137 | query = dns.message.make_query(qname, 'A', want_dnssec=True, options=[eco1, ecso2]) | |
138 | res = self.sendUDPQuery(query) | |
139 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
140 | self.assertRRsetInAnswer(res, expected) | |
282b5da9 | 141 | self.checkPacketCacheMetrics(6, 4) |
628fcdb9 O |
142 | |
143 | # NXDomain should get default packetcache TTL (10) | |
144 | query = dns.message.make_query('nxdomain.example.', 'A', want_dnssec=True) | |
145 | res = self.sendUDPQuery(query) | |
146 | self.assertRcodeEqual(res, dns.rcode.NXDOMAIN) | |
147 | self.checkPacketCacheMetrics(6, 5) | |
148 | ||
149 | # NoData should get default packetcache TTL (10) | |
150 | query = dns.message.make_query('a.example.', 'AAAA', want_dnssec=True) | |
151 | res = self.sendUDPQuery(query) | |
152 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
153 | self.checkPacketCacheMetrics(6, 6) | |
154 | ||
155 | # ServFail should get ServFail TTL (5) | |
156 | query = dns.message.make_query('f.example.', 'A', want_dnssec=True) | |
157 | res = self.sendUDPQuery(query) | |
158 | self.assertRcodeEqual(res, dns.rcode.SERVFAIL) | |
159 | self.checkPacketCacheMetrics(6, 7) | |
160 | ||
161 | # We peek into the cache to check TTLs and allow TTLs te be one lower than inserted since the clock might have ticked | |
162 | rec_controlCmd = [os.environ['RECCONTROL'], | |
163 | '--config-dir=%s' % 'configs/' + self._confdir, | |
164 | 'dump-cache', '-'] | |
165 | try: | |
166 | ret = subprocess.check_output(rec_controlCmd, stderr=subprocess.STDOUT) | |
167 | self.assertTrue((b"a.example. 10 A ; tag 0 udp\n" in ret) or (b"a.example. 9 A ; tag 0 udp\n" in ret)) | |
168 | self.assertTrue((b"nxdomain.example. 10 A ; tag 0 udp\n" in ret) or (b"nxdomain.example. 9 A ; tag 0 udp\n" in ret)) | |
169 | self.assertTrue((b"a.example. 10 AAAA ; tag 0 udp\n" in ret) or (b"a.example. 9 AAAA ; tag 0 udp\n" in ret)) | |
170 | self.assertTrue((b"f.example. 5 A ; tag 0 udp\n" in ret) or (b"f.example. 4 A ; tag 0 udp\n" in ret)) | |
171 | ||
172 | except subprocess.CalledProcessError as e: | |
173 | print(e.output) | |
174 | raise | |
175 |