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