MAC generator
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)
Please register or sign in to comment