]>
Commit | Line | Data |
---|---|---|
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 | |
29 | draft-vandergaast-edns-client-subnet. | |
30 | ||
31 | The contained class supports both IPv4 and IPv6 addresses. | |
32 | Requirements: | |
33 | dnspython (http://www.dnspython.org/) | |
34 | """ | |
35 | from __future__ import print_function | |
36 | from __future__ import division | |
37 | ||
3d144e24 | 38 | import math |
9a0b88e8 RG |
39 | import socket |
40 | import struct | |
41 | import dns | |
42 | import dns.edns | |
43 | import dns.flags | |
44 | import dns.message | |
45 | import dns.query | |
46 | ||
47 | __author__ = "bhartvigsen@opendns.com (Brian Hartvigsen)" | |
48 | __version__ = "2.0.0" | |
49 | ||
50 | ASSIGNED_OPTION_CODE = 0x0008 | |
51 | DRAFT_OPTION_CODE = 0x50FA | |
52 | ||
53 | FAMILY_IPV4 = 1 | |
54 | FAMILY_IPV6 = 2 | |
55 | SUPPORTED_FAMILIES = (FAMILY_IPV4, FAMILY_IPV6) | |
56 | ||
57 | ||
58 | class 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 | ||
249 | dns.edns._type_to_class[DRAFT_OPTION_CODE] = ClientSubnetOption | |
250 | dns.edns._type_to_class[ASSIGNED_OPTION_CODE] = ClientSubnetOption | |
251 | ||
252 | if __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) |