"""
Classes and constants for the DUIDs defined in :rfc:`3315`
"""
from struct import pack, unpack_from
from dhcpkit.display_strings import hardware_types
from dhcpkit.protocol_element import ElementDataRepresentation, ProtocolElement
from dhcpkit.utils import normalise_hex
from typing import Union
# DUID type codes
DUID_LLT = 1
DUID_EN = 2
DUID_LL = 3
# This subclass remains abstract
# noinspection PyAbstractClass
[docs]class DUID(ProtocolElement):
"""
:rfc:`3315#section-9.1`
A DUID consists of a two-octet type code represented in network byte
order, followed by a variable number of octets that make up the
actual identifier. A DUID can be no more than 128 octets long (not
including the type code).
"""
# This needs to be overwritten in subclasses
duid_type = 0
def __hash__(self) -> int:
"""
Make DUIDs hashable.
:return: The hash value
"""
return hash(self.save())
[docs] @classmethod
def determine_class(cls, buffer: bytes, offset: int = 0) -> type:
"""
Return the appropriate subclass from the registry, or UnknownDUID if no subclass is registered.
:param buffer: The buffer to read data from
:param offset: The offset in the buffer where to start reading
:return: The best known class for this duid data
"""
from dhcpkit.ipv6.duid_registry import duid_registry
duid_type = unpack_from('!H', buffer, offset=offset)[0]
return duid_registry.get(duid_type, UnknownDUID)
[docs]class UnknownDUID(DUID):
"""
Container for raw DUID content for cases where we don't know how to decode the DUID.
"""
def __init__(self, duid_type: int = 0, duid_data: bytes = b''):
self.duid_type = duid_type
self.duid_data = duid_data
[docs] def load_from(self, buffer: bytes, offset: int = 0, length: int = None) -> int:
"""
Load the internal state of this object from the given buffer. The buffer may contain more data after the
structured element is parsed. This data is ignored.
:param buffer: The buffer to read data from
:param offset: The offset in the buffer where to start reading
:param length: The amount of data we are allowed to read from the buffer
:return: The number of bytes used from the buffer
"""
self.duid_type = unpack_from('!H', buffer, offset=offset)[0]
my_offset = self.parse_duid_header(buffer, offset, length)
duid_len = length - my_offset
self.duid_data = buffer[offset + my_offset:offset + my_offset + duid_len]
my_offset += duid_len
return my_offset
[docs] def save(self) -> Union[bytes, bytearray]:
"""
Save the internal state of this object as a buffer.
:return: The buffer with the data from this element
"""
return pack('!H', self.duid_type) + self.duid_data
[docs]class LinkLayerTimeDUID(DUID):
"""
:rfc:`3315#section-9.2`
This type of DUID consists of a two octet type field containing the
value 1, a two octet hardware type code, four octets containing a
time value, followed by link-layer address of any one network
interface that is connected to the DHCP device at the time that the
DUID is generated. The time value is the time that the DUID is
generated represented in seconds since midnight (UTC), January 1,
2000, modulo 2^32. The hardware type MUST be a valid hardware type
assigned by the IANA as described in :rfc:`826` [14]. Both the time and
the hardware type are stored in network byte order. The link-layer
address is stored in canonical form, as described in :rfc:`2464` [2].
The following diagram illustrates the format of a DUID-LLT:
.. code-block:: none
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 1 | hardware type (16 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time (32 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
. .
. link-layer address (variable length) .
. .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The choice of network interface can be completely arbitrary, as long
as that interface provides a globally unique link-layer address for
the link type, and the same DUID-LLT SHOULD be used in configuring
all network interfaces connected to the device, regardless of which
interface's link-layer address was used to generate the DUID-LLT.
Clients and servers using this type of DUID MUST store the DUID-LLT
in stable storage, and MUST continue to use this DUID-LLT even if the
network interface used to generate the DUID-LLT is removed. Clients
and servers that do not have any stable storage MUST NOT use this
type of DUID.
Clients and servers that use this DUID SHOULD attempt to configure
the time prior to generating the DUID, if that is possible, and MUST
use some sort of time source (for example, a real-time clock) in
generating the DUID, even if that time source could not be configured
prior to generating the DUID. The use of a time source makes it
unlikely that two identical DUID-LLTs will be generated if the
network interface is removed from the client and another client then
uses the same network interface to generate a DUID-LLT. A collision
between two DUID-LLTs is very unlikely even if the clocks have not
been configured prior to generating the DUID.
This method of DUID generation is recommended for all general purpose
computing devices such as desktop computers and laptop computers, and
also for devices such as printers, routers, and so on, that contain
some form of writable non-volatile storage.
Despite our best efforts, it is possible that this algorithm for
generating a DUID could result in a client identifier collision. A
DHCP client that generates a DUID-LLT using this mechanism MUST
provide an administrative interface that replaces the existing DUID
with a newly-generated DUID-LLT.
"""
duid_type = DUID_LLT
def __init__(self, hardware_type: int = 0, time: int = 0, link_layer_address: bytes = b''):
self.hardware_type = hardware_type
self.time = time
self.link_layer_address = link_layer_address
[docs] def display_hardware_type(self) -> ElementDataRepresentation:
"""
Nicer representation of hardware types
:return: Representation of hardware type
"""
display = hardware_types.get(self.hardware_type, 'Unknown')
return ElementDataRepresentation("{} ({})".format(display, self.hardware_type))
[docs] def display_link_layer_address(self) -> Union[ElementDataRepresentation, bytes]:
"""
Nicer representation of link-layer address if we know the hardware type
:return: Representation of link-layer address
"""
if self.hardware_type == 1:
return ElementDataRepresentation(normalise_hex(self.link_layer_address, include_colons=True))
else:
return self.link_layer_address
[docs] def validate(self):
"""
Validate that the contents of this object conform to protocol specs.
"""
if not isinstance(self.hardware_type, int) or not (0 <= self.hardware_type < 2 ** 16):
raise ValueError("Hardware type must be an unsigned 16 bit integer")
if not isinstance(self.time, int) or not (0 <= self.time < 2 ** 32):
raise ValueError("Time must be an unsigned 32 bit integer")
if not isinstance(self.link_layer_address, bytes):
raise ValueError("Link-layer address must be a sequence of bytes")
if len(self.link_layer_address) > 120:
raise ValueError("DUID-LLT link-layer address cannot be longer than 120 bytes")
[docs] def load_from(self, buffer: bytes, offset: int = 0, length: int = None) -> int:
"""
Load the internal state of this object from the given buffer. The buffer may contain more data after the
structured element is parsed. This data is ignored.
:param buffer: The buffer to read data from
:param offset: The offset in the buffer where to start reading
:param length: The amount of data we are allowed to read from the buffer
:return: The number of bytes used from the buffer
"""
my_offset = self.parse_duid_header(buffer, offset, length)
self.hardware_type, self.time = unpack_from('!HI', buffer, offset=offset + my_offset)
my_offset += 6
ll_len = length - my_offset
self.link_layer_address = buffer[offset + my_offset:offset + my_offset + ll_len]
my_offset += ll_len
return my_offset
[docs] def save(self) -> Union[bytes, bytearray]:
"""
Save the internal state of this object as a buffer.
:return: The buffer with the data from this element
"""
return pack('!HHI', self.duid_type, self.hardware_type, self.time) + self.link_layer_address
[docs]class EnterpriseDUID(DUID):
"""
:rfc:`3315#section-9.3`
This form of DUID is assigned by the vendor to the device. It
consists of the vendor's registered Private Enterprise Number as
maintained by IANA [6] followed by a unique identifier assigned by
the vendor. The following diagram summarizes the structure of a
DUID-EN:
.. code-block:: none
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 2 | enterprise-number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| enterprise-number (contd) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
. identifier .
. (variable length) .
. .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The source of the identifier is left up to the vendor defining it,
but each identifier part of each DUID-EN MUST be unique to the device
that is using it, and MUST be assigned to the device at the time it
is manufactured and stored in some form of non-volatile storage. The
generated DUID SHOULD be recorded in non-erasable storage. The
enterprise-number is the vendor's registered Private Enterprise
Number as maintained by IANA [6]. The enterprise-number is stored as
an unsigned 32 bit number.
An example DUID of this type might look like this:
.. code-block:: none
+---+---+---+---+---+---+---+---+
| 0 | 2 | 0 | 0 | 0 | 9| 12|192|
+---+---+---+---+---+---+---+---+
|132|221| 3 | 0 | 9 | 18|
+---+---+---+---+---+---+
This example includes the two-octet type of 2, the Enterprise Number
(9), followed by eight octets of identifier data
(0x0CC084D303000912).
"""
duid_type = DUID_EN
def __init__(self, enterprise_number: int = 0, identifier: bytes = b''):
self.enterprise_number = enterprise_number
self.identifier = identifier
[docs] def validate(self):
"""
Validate that the contents of this object conform to protocol specs.
"""
if not isinstance(self.enterprise_number, int) or not (0 <= self.enterprise_number < 2 ** 32):
raise ValueError("Enterprise number must be an unsigned 32 bit integer")
if not isinstance(self.identifier, bytes):
raise ValueError("Identifier must be a sequence of bytes")
if len(self.identifier) > 122:
raise ValueError("DUID-EN identifier cannot be longer than 122 bytes")
[docs] def load_from(self, buffer: bytes, offset: int = 0, length: int = None) -> int:
"""
Load the internal state of this object from the given buffer. The buffer may contain more data after the
structured element is parsed. This data is ignored.
:param buffer: The buffer to read data from
:param offset: The offset in the buffer where to start reading
:param length: The amount of data we are allowed to read from the buffer
:return: The number of bytes used from the buffer
"""
my_offset = self.parse_duid_header(buffer, offset, length)
self.enterprise_number = unpack_from('!I', buffer, offset=offset + my_offset)[0]
my_offset += 4
identifier_len = length - my_offset
self.identifier = buffer[offset + my_offset:offset + my_offset + identifier_len]
my_offset += identifier_len
return my_offset
[docs] def save(self) -> Union[bytes, bytearray]:
"""
Save the internal state of this object as a buffer.
:return: The buffer with the data from this element
"""
return pack('!HI', self.duid_type, self.enterprise_number) + self.identifier
[docs]class LinkLayerDUID(DUID):
"""
:rfc:`3315#section-9.4`
This type of DUID consists of two octets containing the DUID type 3,
a two octet network hardware type code, followed by the link-layer
address of any one network interface that is permanently connected to
the client or server device. For example, a host that has a network
interface implemented in a chip that is unlikely to be removed and
used elsewhere could use a DUID-LL. The hardware type MUST be a
valid hardware type assigned by the IANA, as described in :rfc:`826`
[14]. The hardware type is stored in network byte order. The
link-layer address is stored in canonical form, as described in
:rfc:`2464` [2]. The following diagram illustrates the format of a
DUID-LL:
.. code-block:: none
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 3 | hardware type (16 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
. .
. link-layer address (variable length) .
. .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The choice of network interface can be completely arbitrary, as long
as that interface provides a unique link-layer address and is
permanently attached to the device on which the DUID-LL is being
generated. The same DUID-LL SHOULD be used in configuring all
network interfaces connected to the device, regardless of which
interface's link-layer address was used to generate the DUID.
DUID-LL is recommended for devices that have a permanently-connected
network interface with a link-layer address, and do not have
nonvolatile, writable stable storage. DUID-LL MUST NOT be used by
DHCP clients or servers that cannot tell whether or not a network
interface is permanently attached to the device on which the DHCP
client is running.
"""
duid_type = DUID_LL
def __init__(self, hardware_type: int = 0, link_layer_address: bytes = b''):
self.hardware_type = hardware_type
self.link_layer_address = link_layer_address
[docs] def display_hardware_type(self) -> ElementDataRepresentation:
"""
Nicer representation of hardware types
:return: Representation of hardware type
"""
display = hardware_types.get(self.hardware_type, 'Unknown')
return ElementDataRepresentation("{} ({})".format(display, self.hardware_type))
[docs] def display_link_layer_address(self) -> Union[ElementDataRepresentation, bytes]:
"""
Nicer representation of link-layer address if we know the hardware type
:return: Representation of link-layer address
"""
if self.hardware_type == 1:
return ElementDataRepresentation(normalise_hex(self.link_layer_address, include_colons=True))
else:
return self.link_layer_address
[docs] def validate(self):
"""
Validate that the contents of this object conform to protocol specs.
"""
if not isinstance(self.hardware_type, int) or not (0 <= self.hardware_type < 2 ** 16):
raise ValueError("Hardware type must be an unsigned 16 bit integer")
if not isinstance(self.link_layer_address, bytes):
raise ValueError("Link-layer address must be a sequence of bytes")
if len(self.link_layer_address) > 124:
raise ValueError("DUID-LL link-layer address cannot be longer than 124 bytes")
[docs] def load_from(self, buffer: bytes, offset: int = 0, length: int = None) -> int:
"""
Load the internal state of this object from the given buffer. The buffer may contain more data after the
structured element is parsed. This data is ignored.
:param buffer: The buffer to read data from
:param offset: The offset in the buffer where to start reading
:param length: The amount of data we are allowed to read from the buffer
:return: The number of bytes used from the buffer
"""
my_offset = self.parse_duid_header(buffer, offset, length)
self.hardware_type = unpack_from('!H', buffer, offset=offset + my_offset)[0]
my_offset += 2
ll_len = length - my_offset
self.link_layer_address = buffer[offset + my_offset:offset + my_offset + ll_len]
my_offset += ll_len
return my_offset
[docs] def save(self) -> Union[bytes, bytearray]:
"""
Save the internal state of this object as a buffer.
:return: The buffer with the data from this element
"""
return pack('!HH', self.duid_type, self.hardware_type) + self.link_layer_address