Skip to content
Snippets Groups Projects

MAC generator

  • Clone with SSH
  • Clone with HTTPS
  • Embed
  • Share
    The snippet can be accessed without any authentication.
    Authored by Nik | Klampfradler

    This script generates MAC addresses, either random or reproducible as a hash function of the hostname. It can also verify against an LDAP directory whether a generated MAC address collides with an already known one.

    Intended for use with e.g. Ansible of other machinery when automatically creating virtual machines.

    macgen.py 5.82 KiB
    #!/usr/bin/python3
    # -
    # Copyright 2019 Dominik George <d.george@tarent.de>
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to
    # deal in the Software without restriction, including without limitation the
    # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    # sell copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    # IN THE SOFTWARE.
    
    import argparse
    import binascii
    import random
    import re
    from urllib.parse import urlparse
    
    try:
        import ldap3
    except ImportError:
        # LDAP support is optional
        pass
    
    # Initial seed used for first iteration of MAC generation
    PEARSON_SEED = 1990
    
    
    def normalise_hostname(hostname: str) -> str:
        """Convert a hostname string to lower case and verify it fulfills the
        LDH character set (lower-case, digits and hyphen).
        """
    
        hostname_norm = hostname.lower()
    
        if re.match(r"^[a-z0-9-]{1,63}$", hostname_norm):
            return hostname_norm
        else:
            raise ValueError("Hostnames must be 1 to 63 LDH characters.")
    
    
    def pearson8(message: str, seed: int = PEARSON_SEED) -> int:
        """Calculate the 8-bit Pearson hash of a message."""
    
        table = list(range(0, 256))
        random.Random(seed).shuffle(table)
    
        pearson = len(message) % 256
    
        for char in message:
            pearson = table[pearson ^ ord(char)]
    
        return pearson
    
    
    def mac_split(hex_string: str) -> str:
        """Separate the octets of a MAC address hex string by a colon."""
    
        return ':'.join(a+b for a, b in zip(hex_string[::2], hex_string[1::2]))
    
    
    def mac_from_hostname(hostname: str, pearson_seed: int = PEARSON_SEED) -> str:
        """Generate a MAC address fulfilling the following criteria:
    
         * Be in one of the private use areas
         * Be reproducible (same hostname -> same MAC address)
         * Be sufficiently collision-free
    
        The generated MAC address takes the following format:
    
         ab:cc:cc:cc:cc:dd
    
         ab -> Length of the hostname, seperated in modulo 16 (a)
               and divided by 16 (b mapped to "2", "4", "6", "a") parts
         cc -> CRC32 checksum of the hostname
         dd -> 8-bit Pearson hash of the hostname
    
        The way ab is calculated ensures that the result is a byte in
        one of the private-use MAC areas; dd and ab together enlarge the
        uniqueness that a CRC32 alone does not guarantee.
        """
    
        hostname_norm = normalise_hostname(hostname)
    
        hostname_len = "%01x" % (len(hostname_norm) % 16) + \
            ["2", "4", "6", "a"][int(len(hostname_norm) / 16)]
        hostname_crc = "%08x" % binascii.crc32(hostname_norm.encode('ascii'))
        hostname_pearson = "%02x" % pearson8(hostname_norm, pearson_seed)
    
        hex_string = hostname_len + hostname_crc + hostname_pearson
        return mac_split(hex_string)
    
    
    def mac_random(**kwargs):
        """Return a pseudo-random MAC address within one of the private ranges."""
    
        return ':'.join(['5%01x' % random.choice([2, 4, 6, 10])] + ['%02x' % random.randint(0, 255) for _ in range(0, 5)])
    
    
    def ldap_find_mac(mac: str, hostname: str, ldap_uri: str, schema: str = 'isc'):
        """Check whether a MAC address is registered in an LDAP directory.
    
        This function returns True if the provided MAC address is
        already known in LDAP, but for a different hostname.
    
        It returns False if the MAC address is either not registered, or
        registered for the same hostname.
        """
    
        parsed_uri = urlparse(ldap_uri)
    
        server = ldap3.Server(parsed_uri.hostname, port=parsed_uri.port or 389,
                              use_ssl=parsed_uri.scheme == 'ldaps')
        conn = ldap3.Connection(server, auto_bind=True)
    
        if schema == 'isc':
            query = '(&(dhcpHWAddress=ethernet %s)(!(cn=%s)))' % (mac, hostname)
    
        return conn.search(parsed_uri.path.strip('/'), query)
    
    
    if __name__ == '__main__':
        parser = argparse.ArgumentParser(
            description='Generate random or reproducible MAC address')
    
        parser.add_argument(
            '-r', '--random', action='store_true', help='Generate random MAC address instead of reproducible')
        parser.add_argument('-L', '--ldap-uri',
                            help='Query an LDAP directory for used MAC addresses')
        parser.add_argument('-S', '--ldap-schema', choices=['isc'], default='isc',
                            help='LDAP schema to use with -L')
        parser.add_argument('hostname',
                            help='Hostname (without domain) for the target host')
    
        args = parser.parse_args()
    
        if args.ldap_uri and 'ldap3' not in globals():
            print('LDAP checking enabled, but python3-ldap is not available.')
            exit(1)
    
        if args.random:
            gen_func = mac_random
        else:
            gen_func = mac_from_hostname
    
        mac_taken = True
        pearson_seed = PEARSON_SEED
    
        while mac_taken:
            mac = gen_func(hostname=args.hostname, pearson_seed=pearson_seed)
    
            if args.ldap_uri:
                mac_taken = ldap_find_mac(
                    mac, args.hostname, args.ldap_uri, args.ldap_schema)
            else:
                # Assume MAC is free if verification is not enabled
                mac_taken = False
    
            # If MAC was already taken, try again with a new pearson seed
            # This has nothing to say for the random generator
            pearson_seed += 1
    
        print(mac)
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment