Example #1
0
    def test_parse(self):
        with self.assertRaisesRegex(ValueError, 'length'):
            DUID.parse(self.duid_bytes)

        length = len(self.duid_bytes)
        parsed_length, parsed_object = DUID.parse(self.duid_bytes, length=length)
        self.assertEqual(parsed_length, length)
        self.assertEqual(parsed_object, self.duid_object)
Example #2
0
 def test_parse_with_larger_buffer(self):
     offset = 50
     buffer = bytes(50 * [0]) + self.duid_bytes + bytes(50 * [0])
     length = len(self.duid_bytes)
     parsed_length, parsed_object = DUID.parse(buffer, offset=offset, length=length)
     self.assertEqual(parsed_length, length)
     self.assertEqual(parsed_object, self.duid_object)
Example #3
0
    def encode_duid(duid: DUID) -> str:
        """
        Encode DUID as a string.

        :param duid: The DUID object
        :return: The string representing the DUID
        """
        return codecs.encode(duid.save(), 'hex').decode('ascii')
Example #4
0
def determine_server_duid(config: configparser.ConfigParser):
    """
    Make sure we have a server DUID.

    :param config: The configuration
    """
    # Try to get the server DUID from the configuration
    config_duid = config['server']['duid']
    if config_duid.lower() not in ('', 'auto'):
        config_duid = config_duid.strip()
        try:
            duid = bytes.fromhex(config_duid.strip())
        except ValueError:
            logger.critical("Configured hex DUID contains invalid characters")
            sys.exit(1)

        # Check if we can parse this DUID
        length, duid = DUID.parse(duid, length=len(duid))
        if not isinstance(duid, DUID):
            logger.critical("Configured DUID is invalid")
            sys.exit(1)

        logger.debug("Using server DUID from configuration: {}".format(config_duid))

        config['server']['duid'] = codecs.encode(duid.save(), 'hex').decode('ascii')
        return

    # Use the first interface's MAC address as default
    if config:
        interface_names = [section_name.split(' ')[1] for section_name in config.sections()
                           if section_name.split(' ')[0] == 'interface']
        interface_names.sort()

        for interface_name in interface_names:
            link_addresses = netifaces.ifaddresses(interface_name).get(netifaces.AF_LINK, [])
            link_addresses = [link_address['addr'] for link_address in link_addresses if link_address.get('addr')]
            link_addresses.sort()

            for link_address in link_addresses:
                # Try to decode
                try:
                    ll_addr = bytes.fromhex(link_address.replace(':', ''))

                    duid = LinkLayerDUID(hardware_type=1, link_layer_address=ll_addr).save()

                    logger.debug("Using server DUID based on {} link address: "
                                 "{}".format(interface_name, codecs.encode(duid, 'hex').decode('ascii')))

                    config['server']['duid'] = codecs.encode(duid, 'hex').decode('ascii')
                    return
                except ValueError:
                    # Try the next one
                    pass

    # We didn't find a useful server DUID
    logger.critical("Cannot find a usable DUID")
    sys.exit(1)
Example #5
0
    def decode_duid(duid_str: str) -> DUID:
        """
        Decode DUID from a string.

        :param duid_str: The DUID string
        :return: The DUID object
        """
        duid_bytes = bytes.fromhex(duid_str)
        duid_len, duid = DUID.parse(duid_bytes, length=len(duid_bytes))
        return duid
Example #6
0
    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, option_len = self.parse_option_header(buffer, offset, length, min_length=2)

        duid_len, self.duid = DUID.parse(buffer, offset=offset + my_offset, length=option_len)
        my_offset += duid_len

        return my_offset
    def duid_ll(self):
        """
        Get the link-layer address stored in the DUID, if any

        :return: Link-layer address or None
        """

        # noinspection PyBroadException
        try:
            duid_str = self.duid
            if duid_str.startswith('0x'):
                duid_str = duid_str[2:]
            duid_bytes = bytes.fromhex(duid_str)
            length, duid = DUID.parse(duid_bytes, length=len(duid_bytes))

            if getattr(duid, 'hardware_type') == 1 and hasattr(duid, 'link_layer_address'):
                return EUI(int.from_bytes(duid.link_layer_address, byteorder='big'), dialect=mac_unix_expanded)
        except:
            pass

        return None
Example #8
0
    def parse_csv_file(csv_filename: str) -> [(str, Assignment)]:
        """
        Read the assignments from the file specified in the configuration

        :param csv_filename: The filename of the CSV file
        :return: An list of identifiers and their assignment
        """

        logger.debug("Loading assignments from {}".format(csv_filename))

        with open(csv_filename) as csv_file:
            # Auto-detect the CSV dialect
            sniffer = csv.Sniffer()
            sample = csv_file.read(10240)
            dialect = sniffer.sniff(sample)

            # If there is no header: assume that the columns are 'id', 'address' and 'prefix' in that order
            csv_has_header = sniffer.has_header(sample)
            fieldnames = ["id", "address", "prefix"] if not csv_has_header else None

            # Restart and parse
            csv_file.seek(0)
            reader = csv.DictReader(csv_file, dialect=dialect, fieldnames=fieldnames)

            # First line is column headings
            line = 1
            for row in reader:
                line += 1
                try:
                    address_str = row["address"].strip().split(";")
                    addresses = [IPv6Address(address) for address in address_str]
                    logger.debug("Loaded addresses {!r}".format(addresses))

                    prefix_str = row["prefix"].strip()
                    prefix = prefix_str and IPv6Network(prefix_str) or None

                    # Validate and normalise id input
                    row_id = row["id"]

                    if row_id.startswith("duid:"):
                        duid_hex = row_id.split(":", 1)[1]
                        duid_bytes = codecs.decode(duid_hex, "hex")
                        length, duid = DUID.parse(duid_bytes, length=len(duid_bytes))
                        duid_hex = codecs.encode(duid.save(), "hex").decode("ascii")
                        row_id = "duid:{}".format(duid_hex)

                    elif row_id.startswith("interface-id:"):
                        interface_id_hex = row_id.split(":", 1)[1]
                        interface_id = codecs.decode(interface_id_hex, "hex")
                        interface_id_hex = codecs.encode(interface_id, "hex").decode("ascii")
                        row_id = "interface_id:{}".format(interface_id_hex)

                    elif row_id.startswith("interface-id-str:"):
                        interface_id = row_id.split(":", 1)[1]
                        interface_id_hex = codecs.encode(interface_id.encode("ascii"), "hex").decode("ascii")
                        row_id = "interface_id:{}".format(interface_id_hex)

                    elif row_id.startswith("remote-id:") or row_id.startswith("remote-id-str:"):
                        remote_id_data = row_id.split(":", 1)[1]
                        try:
                            enterprise_id, remote_id = remote_id_data.split(":", 1)
                            enterprise_id = int(enterprise_id)
                            if row_id.startswith("remote-id:"):
                                remote_id = codecs.decode(remote_id, "hex")
                            else:
                                remote_id = remote_id.encode("ascii")

                            row_id = "remote-id:{}:{}".format(
                                enterprise_id, codecs.encode(remote_id, "hex").decode("ascii")
                            )
                        except ValueError:
                            raise ValueError(
                                "Remote-ID must be formatted as 'remote-id:<enterprise>:<remote-id-hex>', "
                                "for example: 'remote-id:9:0123456789abcdef"
                            )

                    else:
                        raise ValueError(
                            "The id must start with duid: or interface-id: followed by a hex-encoded "
                            "value, interface-id-str: followed by an ascii string, remote-id: followed by "
                            "an enterprise-id, a colon and a hex-encoded value or remote-id-str: followed"
                            "by an enterprise-id, a colon and an ascii string"
                        )

                    # Store the normalised id
                    logger.debug("Loaded assignment for {}".format(row_id))
                    yield row_id, Assignment(address=addresses, prefix=prefix)

                except KeyError:
                    raise configparser.Error("Assignment CSV must have columns 'id', 'address' and 'prefix'")
                except ValueError as e:
                    logger.error("Ignoring line {} with invalid value: {}".format(line, e))
Example #9
0
    def parse_csv_file(csv_filename: str) -> List[Tuple[str, Assignment]]:
        """
        Read the assignments from the file specified in the configuration

        :param csv_filename: The filename of the CSV file
        :return: An list of identifiers and their assignment
        """

        logger.debug("Loading assignments from {}".format(csv_filename))

        with open(csv_filename) as csv_file:
            # Auto-detect the CSV dialect
            sniffer = csv.Sniffer()
            sample = csv_file.read(10240)
            dialect = sniffer.sniff(sample)

            # Restart and parse
            csv_file.seek(0)
            reader = csv.DictReader(csv_file, dialect=dialect)

            # First line is column headings
            for row in reader:
                try:
                    address_str = row['address'].strip()
                    address = address_str and IPv6Address(address_str) or None

                    prefix_str = row['prefix'].strip()
                    prefix = prefix_str and IPv6Network(prefix_str) or None

                    # Validate and normalise id input
                    row_id = row['id']

                    if row_id.startswith('duid:'):
                        duid_hex = row_id.split(':', 1)[1]
                        duid_bytes = codecs.decode(duid_hex, 'hex')
                        length, duid = DUID.parse(duid_bytes, length=len(duid_bytes))
                        duid_hex = codecs.encode(duid.save(), 'hex').decode('ascii')
                        row_id = 'duid:{}'.format(duid_hex)

                    elif row_id.startswith('interface-id:'):
                        interface_id_hex = row_id.split(':', 1)[1]
                        interface_id_hex = normalise_hex(interface_id_hex)
                        interface_id = codecs.decode(interface_id_hex, 'hex')
                        interface_id_hex = codecs.encode(interface_id, 'hex').decode('ascii')
                        row_id = 'interface_id:{}'.format(interface_id_hex)

                    elif row_id.startswith('interface-id-str:'):
                        interface_id = row_id.split(':', 1)[1]
                        interface_id_hex = codecs.encode(interface_id.encode('ascii'), 'hex').decode('ascii')
                        row_id = 'interface_id:{}'.format(interface_id_hex)

                    elif row_id.startswith('remote-id:') or row_id.startswith('remote-id-str:'):
                        remote_id_data = row_id.split(':', 1)[1]
                        try:
                            enterprise_id, remote_id = remote_id_data.split(':', 1)
                            enterprise_id = int(enterprise_id)
                            if row_id.startswith('remote-id:'):
                                remote_id = normalise_hex(remote_id)
                                remote_id = codecs.decode(remote_id, 'hex')
                            else:
                                remote_id = remote_id.encode('ascii')

                            row_id = 'remote-id:{}:{}'.format(enterprise_id,
                                                              codecs.encode(remote_id, 'hex').decode('ascii'))
                        except ValueError:
                            raise ValueError("Remote-ID must be formatted as 'remote-id:<enterprise>:<remote-id-hex>', "
                                             "for example: 'remote-id:9:0123456789abcdef")

                    else:
                        raise ValueError("The id must start with duid: or interface-id: followed by a hex-encoded "
                                         "value, interface-id-str: followed by an ascii string, remote-id: followed by "
                                         "an enterprise-id, a colon and a hex-encoded value or remote-id-str: followed"
                                         "by an enterprise-id, a colon and an ascii string")

                    # Store the normalised id
                    logger.debug("Loaded assignment for {}".format(row_id))
                    yield row_id, Assignment(address=address, prefix=prefix)

                except KeyError:
                    raise ValueError("Assignment CSV must have columns 'id', 'address' and 'prefix'")
                except ValueError as e:
                    logger.error("Ignoring {} line {} with invalid value: {}".format(csv_file, reader.line_num, e))