]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.recursor-dnssec/clientsubnetoption.py
Merge pull request #13819 from omoerbeek/rec-ta
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / clientsubnetoption.py
CommitLineData
9a0b88e8
RG
1#!/usr/bin/env python
2#
3# Copyright (c) 2012 OpenDNS, Inc.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8# * Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10# * Redistributions in binary form must reproduce the above copyright
11# notice, this list of conditions and the following disclaimer in the
12# documentation and/or other materials provided with the distribution.
13# * Neither the name of the OpenDNS nor the names of its contributors may be
14# used to endorse or promote products derived from this software without
15# specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20# DISCLAIMED. IN NO EVENT SHALL OPENDNS BE LIABLE FOR ANY DIRECT, INDIRECT,
21# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
23# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
26# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28""" Class to implement draft-ietf-dnsop-edns-client-subnet (previously known as
29draft-vandergaast-edns-client-subnet.
30
31The contained class supports both IPv4 and IPv6 addresses.
32Requirements:
33 dnspython (http://www.dnspython.org/)
34"""
35from __future__ import print_function
36from __future__ import division
37
3d144e24 38import math
9a0b88e8
RG
39import socket
40import struct
41import dns
42import dns.edns
43import dns.flags
44import dns.message
45import dns.query
46
47__author__ = "bhartvigsen@opendns.com (Brian Hartvigsen)"
48__version__ = "2.0.0"
49
50ASSIGNED_OPTION_CODE = 0x0008
51DRAFT_OPTION_CODE = 0x50FA
52
53FAMILY_IPV4 = 1
54FAMILY_IPV6 = 2
55SUPPORTED_FAMILIES = (FAMILY_IPV4, FAMILY_IPV6)
56
57
58class ClientSubnetOption(dns.edns.Option):
59 """Implementation of draft-vandergaast-edns-client-subnet-01.
60
61 Attributes:
62 family: An integer indicating which address family is being sent
63 ip: IP address in integer notation
64 mask: An integer representing the number of relevant bits being sent
65 scope: An integer representing the number of significant bits used by
66 the authoritative server.
67 """
68
69 def __init__(self, ip, bits=24, scope=0, option=ASSIGNED_OPTION_CODE):
70 super(ClientSubnetOption, self).__init__(option)
71
72 n = None
73 f = None
74
75 for family in (socket.AF_INET, socket.AF_INET6):
76 try:
77 n = socket.inet_pton(family, ip)
78 if family == socket.AF_INET6:
79 f = FAMILY_IPV6
80 hi, lo = struct.unpack('!QQ', n)
81 ip = hi << 64 | lo
82 elif family == socket.AF_INET:
83 f = FAMILY_IPV4
84 ip = struct.unpack('!L', n)[0]
85 except Exception:
86 pass
87
88 if n is None:
89 raise Exception("%s is an invalid ip" % ip)
90
91 self.family = f
92 self.ip = ip
93 self.mask = bits
94 self.scope = scope
95 self.option = option
96
97 if self.family == FAMILY_IPV4 and self.mask > 32:
98 raise Exception("32 bits is the max for IPv4 (%d)" % bits)
99 if self.family == FAMILY_IPV6 and self.mask > 128:
100 raise Exception("128 bits is the max for IPv6 (%d)" % bits)
101
102 def calculate_ip(self):
103 """Calculates the relevant ip address based on the network mask.
104
105 Calculates the relevant bits of the IP address based on network mask.
106 Sizes up to the nearest octet for use with wire format.
107
108 Returns:
109 An integer of only the significant bits sized up to the nearest
110 octect.
111 """
112
113 if self.family == FAMILY_IPV4:
114 bits = 32
115 elif self.family == FAMILY_IPV6:
116 bits = 128
117
118 ip = self.ip >> bits - self.mask
119
120 if (self.mask % 8 != 0):
121 ip = ip << 8 - (self.mask % 8)
122
123 return ip
124
125 def is_draft(self):
126 """" Determines whether this instance is using the draft option code """
127 return self.option == DRAFT_OPTION_CODE
128
3d144e24 129 def to_wire(self, file=None):
9a0b88e8
RG
130 """Create EDNS packet as defined in draft-vandergaast-edns-client-subnet-01."""
131
132 ip = self.calculate_ip()
133
134 mask_bits = self.mask
135 if mask_bits % 8 != 0:
136 mask_bits += 8 - (self.mask % 8)
137
138 if self.family == FAMILY_IPV4:
139 test = struct.pack("!L", ip)
140 elif self.family == FAMILY_IPV6:
141 test = struct.pack("!QQ", ip >> 64, ip & (2 ** 64 - 1))
142 test = test[-(mask_bits // 8):]
143
144 format = "!HBB%ds" % (mask_bits // 8)
145 data = struct.pack(format, self.family, self.mask, self.scope, test)
3d144e24
PD
146 if file:
147 file.write(data)
148 else:
149 return data
9a0b88e8
RG
150
151 def from_wire(cls, otype, wire, current, olen):
152 """Read EDNS packet as defined in draft-vandergaast-edns-client-subnet-01.
153
154 Returns:
155 An instance of ClientSubnetOption based on the ENDS packet
156 """
157
158 data = wire[current:current + olen]
159 (family, mask, scope) = struct.unpack("!HBB", data[:4])
160
161 c_mask = mask
162 if mask % 8 != 0:
163 c_mask += 8 - (mask % 8)
164
165 ip = struct.unpack_from("!%ds" % (c_mask // 8), data, 4)[0]
166
167 if (family == FAMILY_IPV4):
168 ip = ip + b'\0' * ((32 - c_mask) // 8)
169 ip = socket.inet_ntop(socket.AF_INET, ip)
170 elif (family == FAMILY_IPV6):
171 ip = ip + b'\0' * ((128 - c_mask) // 8)
172 ip = socket.inet_ntop(socket.AF_INET6, ip)
173 else:
174 raise Exception("Returned a family other then IPv4 or IPv6")
175
176 return cls(ip, mask, scope, otype)
177
178 from_wire = classmethod(from_wire)
179
3d144e24
PD
180 # needed in 2.0.0..
181 @classmethod
182 def from_wire_parser(cls, otype, parser):
183 family, src, scope = parser.get_struct('!HBB')
184 addrlen = int(math.ceil(src / 8.0))
185 prefix = parser.get_bytes(addrlen)
186 if family == 1:
187 pad = 4 - addrlen
188 addr = dns.ipv4.inet_ntoa(prefix + b'\x00' * pad)
189 elif family == 2:
190 pad = 16 - addrlen
191 addr = dns.ipv6.inet_ntoa(prefix + b'\x00' * pad)
192 else:
193 raise ValueError('unsupported family')
194
195 return cls(addr, src, scope, otype)
196
9a0b88e8
RG
197 def __repr__(self):
198 if self.family == FAMILY_IPV4:
199 ip = socket.inet_ntop(socket.AF_INET, struct.pack('!L', self.ip))
200 elif self.family == FAMILY_IPV6:
201 ip = socket.inet_ntop(socket.AF_INET6,
202 struct.pack('!QQ',
203 self.ip >> 64,
204 self.ip & (2 ** 64 - 1)))
205
206 return "%s(%s, %s, %s)" % (
207 self.__class__.__name__,
208 ip,
209 self.mask,
210 self.scope
211 )
212
3d144e24
PD
213 def to_text(self):
214 return self.__repr__()
215
9a0b88e8
RG
216 def __eq__(self, other):
217 """Rich comparison method for equality.
218
219 Two ClientSubnetOptions are equal if their relevant ip bits, mask, and
220 family are identical. We ignore scope since generally we want to
221 compare questions to responses and that bit is only relevant when
222 determining caching behavior.
223
224 Returns:
225 boolean
226 """
227
228 if not isinstance(other, ClientSubnetOption):
229 return False
230 if self.calculate_ip() != other.calculate_ip():
231 return False
232 if self.mask != other.mask:
233 return False
234 if self.family != other.family:
235 return False
236 return True
237
238 def __ne__(self, other):
239 """Rich comparison method for inequality.
240
241 See notes for __eq__()
242
243 Returns:
244 boolean
245 """
246 return not self.__eq__(other)
247
248
249dns.edns._type_to_class[DRAFT_OPTION_CODE] = ClientSubnetOption
250dns.edns._type_to_class[ASSIGNED_OPTION_CODE] = ClientSubnetOption
251
252if __name__ == "__main__":
253 import argparse
254 import sys
255
256 def CheckForClientSubnetOption(addr, args, option_code=ASSIGNED_OPTION_CODE):
257 print("Testing for edns-clientsubnet using option code", hex(option_code), file=sys.stderr)
258 cso = ClientSubnetOption(args.subnet, args.mask, option=option_code)
259 message = dns.message.make_query(args.rr, args.type)
260 # Tested authoritative servers seem to use the last code in cases
261 # where they support both. We make the official code last to allow
262 # us to check for support of both draft and official
263 message.use_edns(options=[cso])
264
265 try:
266 r = dns.query.udp(message, addr, timeout=args.timeout)
267 if r.flags & dns.flags.TC:
268 r = dns.query.tcp(message, addr, timeout=args.timeout)
269 except dns.exception.Timeout:
270 print("Timeout: No answer received from %s\n" % args.nameserver, file=sys.stderr)
271 sys.exit(3)
272
273 error = False
274 found = False
275 for options in r.options:
276 # Have not run into anyone who passes back both codes yet
277 # but just in case, we want to check all possible options
278 if isinstance(options, ClientSubnetOption):
279 found = True
280 print("Found ClientSubnetOption...", end=None, file=sys.stderr)
281 if not cso.family == options.family:
282 error = True
283 print("\nFailed: returned family (%d) is different from the passed family (%d)" % (options.family, cso.family), file=sys.stderr)
284 if not cso.calculate_ip() == options.calculate_ip():
285 error = True
286 print("\nFailed: returned ip (%s) is different from the passed ip (%s)." % (options.calculate_ip(), cso.calculate_ip()), file=sys.stderr)
287 if not options.mask == cso.mask:
288 error = True
289 print("\nFailed: returned mask bits (%d) is different from the passed mask bits (%d)" % (options.mask, cso.mask), file=sys.stderr)
290 if not options.scope != 0:
291 print("\nWarning: scope indicates edns-clientsubnet data is not used", file=sys.stderr)
292 if options.is_draft():
293 print("\nWarning: detected support for edns-clientsubnet draft code", file=sys.stderr)
294
295 if found and not error:
296 print("Success", file=sys.stderr)
297 elif found:
298 print("Failed: See error messages above", file=sys.stderr)
299 else:
300 print("Failed: No ClientSubnetOption returned", file=sys.stderr)
301
302 parser = argparse.ArgumentParser(description='draft-vandergaast-edns-client-subnet-01 tester')
303 parser.add_argument('nameserver', help='The nameserver to test')
304 parser.add_argument('rr', help='DNS record that should return an EDNS enabled response')
305 parser.add_argument('-s', '--subnet', help='Specifies an IP to pass as the client subnet.', default='192.0.2.0')
306 parser.add_argument('-m', '--mask', type=int, help='CIDR mask to use for subnet')
307 parser.add_argument('--timeout', type=int, help='Set the timeout for query to TIMEOUT seconds, default=10', default=10)
308 parser.add_argument('-t', '--type', help='DNS query type, default=A', default='A')
309 args = parser.parse_args()
310
311 if not args.mask:
312 if ':' in args.subnet:
313 args.mask = 48
314 else:
315 args.mask = 24
316
317 try:
318 addr = socket.gethostbyname(args.nameserver)
319 except socket.gaierror:
320 print("Unable to resolve %s\n" % args.nameserver, file=sys.stderr)
321 sys.exit(3)
322
323 CheckForClientSubnetOption(addr, args, DRAFT_OPTION_CODE)
324 print("", file=sys.stderr)
325 CheckForClientSubnetOption(addr, args, ASSIGNED_OPTION_CODE)