# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
|
|
|
# Copyright (C) 2009-2017 Nominum, Inc.
|
|
#
|
|
# Permission to use, copy, modify, and distribute this software and its
|
|
# documentation for any purpose with or without fee is hereby granted,
|
|
# provided that the above copyright notice and this permission notice
|
|
# appear in all copies.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
|
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
|
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
|
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
"""EDNS Options"""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import math
|
|
import struct
|
|
|
|
import dns.inet
|
|
|
|
#: NSID
|
|
NSID = 3
|
|
#: DAU
|
|
DAU = 5
|
|
#: DHU
|
|
DHU = 6
|
|
#: N3U
|
|
N3U = 7
|
|
#: ECS (client-subnet)
|
|
ECS = 8
|
|
#: EXPIRE
|
|
EXPIRE = 9
|
|
#: COOKIE
|
|
COOKIE = 10
|
|
#: KEEPALIVE
|
|
KEEPALIVE = 11
|
|
#: PADDING
|
|
PADDING = 12
|
|
#: CHAIN
|
|
CHAIN = 13
|
|
|
|
class Option(object):
|
|
|
|
"""Base class for all EDNS option types."""
|
|
|
|
def __init__(self, otype):
|
|
"""Initialize an option.
|
|
|
|
*otype*, an ``int``, is the option type.
|
|
"""
|
|
self.otype = otype
|
|
|
|
def to_wire(self, file):
|
|
"""Convert an option to wire format.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def from_wire(cls, otype, wire, current, olen):
|
|
"""Build an EDNS option object from wire format.
|
|
|
|
*otype*, an ``int``, is the option type.
|
|
|
|
*wire*, a ``binary``, is the wire-format message.
|
|
|
|
*current*, an ``int``, is the offset in *wire* of the beginning
|
|
of the rdata.
|
|
|
|
*olen*, an ``int``, is the length of the wire-format option data
|
|
|
|
Returns a ``dns.edns.Option``.
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
def _cmp(self, other):
|
|
"""Compare an EDNS option with another option of the same type.
|
|
|
|
Returns < 0 if < *other*, 0 if == *other*, and > 0 if > *other*.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, Option):
|
|
return False
|
|
if self.otype != other.otype:
|
|
return False
|
|
return self._cmp(other) == 0
|
|
|
|
def __ne__(self, other):
|
|
if not isinstance(other, Option):
|
|
return False
|
|
if self.otype != other.otype:
|
|
return False
|
|
return self._cmp(other) != 0
|
|
|
|
def __lt__(self, other):
|
|
if not isinstance(other, Option) or \
|
|
self.otype != other.otype:
|
|
return NotImplemented
|
|
return self._cmp(other) < 0
|
|
|
|
def __le__(self, other):
|
|
if not isinstance(other, Option) or \
|
|
self.otype != other.otype:
|
|
return NotImplemented
|
|
return self._cmp(other) <= 0
|
|
|
|
def __ge__(self, other):
|
|
if not isinstance(other, Option) or \
|
|
self.otype != other.otype:
|
|
return NotImplemented
|
|
return self._cmp(other) >= 0
|
|
|
|
def __gt__(self, other):
|
|
if not isinstance(other, Option) or \
|
|
self.otype != other.otype:
|
|
return NotImplemented
|
|
return self._cmp(other) > 0
|
|
|
|
|
|
class GenericOption(Option):
|
|
|
|
"""Generic Option Class
|
|
|
|
This class is used for EDNS option types for which we have no better
|
|
implementation.
|
|
"""
|
|
|
|
def __init__(self, otype, data):
|
|
super(GenericOption, self).__init__(otype)
|
|
self.data = data
|
|
|
|
def to_wire(self, file):
|
|
file.write(self.data)
|
|
|
|
def to_text(self):
|
|
return "Generic %d" % self.otype
|
|
|
|
@classmethod
|
|
def from_wire(cls, otype, wire, current, olen):
|
|
return cls(otype, wire[current: current + olen])
|
|
|
|
def _cmp(self, other):
|
|
if self.data == other.data:
|
|
return 0
|
|
if self.data > other.data:
|
|
return 1
|
|
return -1
|
|
|
|
|
|
class ECSOption(Option):
|
|
"""EDNS Client Subnet (ECS, RFC7871)"""
|
|
|
|
def __init__(self, address, srclen=None, scopelen=0):
|
|
"""*address*, a ``text``, is the client address information.
|
|
|
|
*srclen*, an ``int``, the source prefix length, which is the
|
|
leftmost number of bits of the address to be used for the
|
|
lookup. The default is 24 for IPv4 and 56 for IPv6.
|
|
|
|
*scopelen*, an ``int``, the scope prefix length. This value
|
|
must be 0 in queries, and should be set in responses.
|
|
"""
|
|
|
|
super(ECSOption, self).__init__(ECS)
|
|
af = dns.inet.af_for_address(address)
|
|
|
|
if af == dns.inet.AF_INET6:
|
|
self.family = 2
|
|
if srclen is None:
|
|
srclen = 56
|
|
elif af == dns.inet.AF_INET:
|
|
self.family = 1
|
|
if srclen is None:
|
|
srclen = 24
|
|
else:
|
|
raise ValueError('Bad ip family')
|
|
|
|
self.address = address
|
|
self.srclen = srclen
|
|
self.scopelen = scopelen
|
|
|
|
addrdata = dns.inet.inet_pton(af, address)
|
|
nbytes = int(math.ceil(srclen/8.0))
|
|
|
|
# Truncate to srclen and pad to the end of the last octet needed
|
|
# See RFC section 6
|
|
self.addrdata = addrdata[:nbytes]
|
|
nbits = srclen % 8
|
|
if nbits != 0:
|
|
last = struct.pack('B', ord(self.addrdata[-1:]) & (0xff << nbits))
|
|
self.addrdata = self.addrdata[:-1] + last
|
|
|
|
def to_text(self):
|
|
return "ECS {}/{} scope/{}".format(self.address, self.srclen,
|
|
self.scopelen)
|
|
|
|
def to_wire(self, file):
|
|
file.write(struct.pack('!H', self.family))
|
|
file.write(struct.pack('!BB', self.srclen, self.scopelen))
|
|
file.write(self.addrdata)
|
|
|
|
@classmethod
|
|
def from_wire(cls, otype, wire, cur, olen):
|
|
family, src, scope = struct.unpack('!HBB', wire[cur:cur+4])
|
|
cur += 4
|
|
|
|
addrlen = int(math.ceil(src/8.0))
|
|
|
|
if family == 1:
|
|
af = dns.inet.AF_INET
|
|
pad = 4 - addrlen
|
|
elif family == 2:
|
|
af = dns.inet.AF_INET6
|
|
pad = 16 - addrlen
|
|
else:
|
|
raise ValueError('unsupported family')
|
|
|
|
addr = dns.inet.inet_ntop(af, wire[cur:cur+addrlen] + b'\x00' * pad)
|
|
return cls(addr, src, scope)
|
|
|
|
def _cmp(self, other):
|
|
if self.addrdata == other.addrdata:
|
|
return 0
|
|
if self.addrdata > other.addrdata:
|
|
return 1
|
|
return -1
|
|
|
|
_type_to_class = {
|
|
ECS: ECSOption
|
|
}
|
|
|
|
def get_option_class(otype):
|
|
"""Return the class for the specified option type.
|
|
|
|
The GenericOption class is used if a more specific class is not
|
|
known.
|
|
"""
|
|
|
|
cls = _type_to_class.get(otype)
|
|
if cls is None:
|
|
cls = GenericOption
|
|
return cls
|
|
|
|
|
|
def option_from_wire(otype, wire, current, olen):
|
|
"""Build an EDNS option object from wire format.
|
|
|
|
*otype*, an ``int``, is the option type.
|
|
|
|
*wire*, a ``binary``, is the wire-format message.
|
|
|
|
*current*, an ``int``, is the offset in *wire* of the beginning
|
|
of the rdata.
|
|
|
|
*olen*, an ``int``, is the length of the wire-format option data
|
|
|
|
Returns an instance of a subclass of ``dns.edns.Option``.
|
|
"""
|
|
|
|
cls = get_option_class(otype)
|
|
return cls.from_wire(otype, wire, current, olen)
|