1# mac.py - functions for handling MAC (Ethernet) addresses 2# 3# Copyright (C) 2018 Arthur de Jong 4# 5# This library is free software; you can redistribute it and/or 6# modify it under the terms of the GNU Lesser General Public 7# License as published by the Free Software Foundation; either 8# version 2.1 of the License, or (at your option) any later version. 9# 10# This library is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13# Lesser General Public License for more details. 14# 15# You should have received a copy of the GNU Lesser General Public 16# License along with this library; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 18# 02110-1301 USA 19 20"""MAC address (Media Access Control address). 21 22A media access control address (MAC address, sometimes Ethernet address) of a 23device is meant as a unique identifier within a network at the data link 24layer. 25 26More information: 27 28* https://en.wikipedia.org/wiki/MAC_address 29* https://en.wikipedia.org/wiki/Organizationally_unique_identifier 30* https://standards.ieee.org/faqs/regauth.html#2 31 32>>> validate('D0-50-99-84-A2-A0') 33'd0:50:99:84:a2:a0' 34>>> to_eui48('d0:50:99:84:a2:a0') 35'D0-50-99-84-A2-A0' 36>>> is_multicast('d0:50:99:84:a2:a0') 37False 38>>> str(get_manufacturer('d0:50:99:84:a2:a0')) 39'ASRock Incorporation' 40>>> get_oui('d0:50:99:84:a2:a0') 41'D05099' 42>>> get_iab('d0:50:99:84:a2:a0') 43'84A2A0' 44""" 45 46import re 47 48from stdnum import numdb 49from stdnum.exceptions import * 50from stdnum.util import clean 51 52 53_mac_re = re.compile('^([0-9a-f]{2}:){5}[0-9a-f]{2}$') 54 55 56def compact(number): 57 """Convert the MAC address to the minimal, consistent representation.""" 58 number = clean(number, ' ').strip().lower().replace('-', ':') 59 # zero-pad single-digit elements 60 return ':'.join('0' + n if len(n) == 1 else n for n in number.split(':')) 61 62 63def _lookup(number): 64 """Look up the manufacturer in the IEEE OUI registry.""" 65 number = compact(number).replace(':', '').upper() 66 info = numdb.get('oui').info(number) 67 try: 68 return ( 69 ''.join(n[0] for n in info[:-1]), 70 info[-2][1]['o'].replace('%', '"')) 71 except IndexError: 72 raise InvalidComponent() 73 74 75def get_manufacturer(number): 76 """Look up the manufacturer in the IEEE OUI registry.""" 77 return _lookup(number)[1] 78 79 80def get_oui(number): 81 """Return the OUI (organization unique ID) part of the address.""" 82 return _lookup(number)[0] 83 84 85def get_iab(number): 86 """Return the IAB (individual address block) part of the address.""" 87 number = compact(number).replace(':', '').upper() 88 return number[len(get_oui(number)):] 89 90 91def is_unicast(number): 92 """Check whether the number is a unicast address. 93 94 Unicast addresses are received by one node in a network (LAN).""" 95 number = compact(number) 96 return int(number[:2], 16) & 1 == 0 97 98 99def is_multicast(number): 100 """Check whether the number is a multicast address. 101 102 Multicast addresses are meant to be received by (potentially) multiple 103 nodes in a network (LAN).""" 104 return not is_unicast(number) 105 106 107def is_broadcast(number): 108 """Check whether the number is the broadcast address. 109 110 Broadcast addresses are meant to be received by all nodes in a network.""" 111 number = compact(number) 112 return number == 'ff:ff:ff:ff:ff:ff' 113 114 115def is_universally_administered(number): 116 """Check if the address is supposed to be assigned by the manufacturer.""" 117 number = compact(number) 118 return int(number[:2], 16) & 2 == 0 119 120 121def is_locally_administered(number): 122 """Check if the address is meant to be configured by an administrator.""" 123 return not is_universally_administered(number) 124 125 126def validate(number, validate_manufacturer=None): 127 """Check if the number provided is a valid MAC address. 128 129 The existence of the manufacturer is by default only checked for 130 universally administered addresses but can be explicitly set with the 131 `validate_manufacturer` argument. 132 """ 133 number = compact(number) 134 if len(number) != 17: 135 raise InvalidLength() 136 if not _mac_re.match(number): 137 raise InvalidFormat() 138 if validate_manufacturer is not False: 139 if validate_manufacturer or is_universally_administered(number): 140 get_manufacturer(number) 141 return number 142 143 144def is_valid(number, validate_manufacturer=None): 145 """Check if the number provided is a valid IBAN.""" 146 try: 147 return bool(validate(number, validate_manufacturer=validate_manufacturer)) 148 except ValidationError: 149 return False 150 151 152def to_eui48(number): 153 """Convert the MAC address to EUI-48 format.""" 154 return compact(number).upper().replace(':', '-') 155