Skip to content

Commit 9522a21

Browse files
authored
bpo-32107 - Better merge of #4494 (#4576)
Improve UUID1 MAC address calculation and related tests. There are two bits in the MAC address that are relevant to UUID1. The first is the locally administered vs. universally administered bit (second least significant of the first octet). Physical network interfaces such as ethernet ports and wireless adapters will always be universally administered, but some interfaces --such as the interface that MacBook Pros communicate with their Touch Bars-- are locally administered. The former are guaranteed to be globally unique, while the latter are demonstrably *not* globally unique and are in fact the same on every MBP with a Touch Bar. With this bit is set, the MAC is locally administered; with it unset it is universally administered. The other bit is the multicast bit (least significant bit of the first octet). When no other MAC address can be found, RFC 4122 mandates that a random 48-bit number be generated. This randomly generated number *must* have the multicast bit set. The improvements in uuid.py include: * Preferentially return a universally administered MAC address, falling back to a locally administered address if none of the former can be found. * Improve several coding style issues, such as adding explicit returns of None, using a more readable bitmask pattern, and assuming that the ultimate fallback, random MAC generation will not fail (and propagating any exception there instead of swallowing them). Improvements in test_uuid.py include: * Always testing the calculated MAC for universal administration, unless explicitly disabled (i.e. for the random case), or implicitly disabled due to running in the Travis environment. Travis test machines have *no* universally administered MAC address at the time of this writing.
1 parent c975878 commit 9522a21

File tree

3 files changed

+93
-28
lines changed

3 files changed

+93
-28
lines changed

Lib/test/test_uuid.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -512,60 +512,69 @@ def test_find_mac(self):
512512

513513
self.assertEqual(mac, 0x1234567890ab)
514514

515-
def check_node(self, node, requires=None, network=False):
515+
def check_node(self, node, requires=None, *, check_bit=True):
516516
if requires and node is None:
517517
self.skipTest('requires ' + requires)
518518
hex = '%012x' % node
519519
if support.verbose >= 2:
520520
print(hex, end=' ')
521-
if network:
522-
# 47 bit will never be set in IEEE 802 addresses obtained
523-
# from network cards.
524-
self.assertFalse(node & 0x010000000000, hex)
521+
# The MAC address will be universally administered (i.e. the second
522+
# least significant bit of the first octet must be unset) for any
523+
# physical interface, such as an ethernet port or wireless adapter.
524+
# There are some cases where this won't be the case. Randomly
525+
# generated MACs may not be universally administered, but they must
526+
# have their multicast bit set, though this is tested in the
527+
# `test_random_getnode()` method specifically. Another case is the
528+
# Travis-CI case, which apparently only has locally administered MAC
529+
# addresses.
530+
if check_bit and not os.getenv('TRAVIS'):
531+
self.assertFalse(node & (1 << 41), '%012x' % node)
525532
self.assertTrue(0 < node < (1 << 48),
526533
"%s is not an RFC 4122 node ID" % hex)
527534

528535
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
529536
def test_ifconfig_getnode(self):
530537
node = self.uuid._ifconfig_getnode()
531-
self.check_node(node, 'ifconfig', True)
538+
self.check_node(node, 'ifconfig')
532539

533540
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
534541
def test_ip_getnode(self):
535542
node = self.uuid._ip_getnode()
536-
self.check_node(node, 'ip', True)
543+
self.check_node(node, 'ip')
537544

538545
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
539546
def test_arp_getnode(self):
540547
node = self.uuid._arp_getnode()
541-
self.check_node(node, 'arp', True)
548+
self.check_node(node, 'arp')
542549

543550
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
544551
def test_lanscan_getnode(self):
545552
node = self.uuid._lanscan_getnode()
546-
self.check_node(node, 'lanscan', True)
553+
self.check_node(node, 'lanscan')
547554

548555
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
549556
def test_netstat_getnode(self):
550557
node = self.uuid._netstat_getnode()
551-
self.check_node(node, 'netstat', True)
558+
self.check_node(node, 'netstat')
552559

553560
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
554561
def test_ipconfig_getnode(self):
555562
node = self.uuid._ipconfig_getnode()
556-
self.check_node(node, 'ipconfig', True)
563+
self.check_node(node, 'ipconfig')
557564

558565
@unittest.skipUnless(importable('win32wnet'), 'requires win32wnet')
559566
@unittest.skipUnless(importable('netbios'), 'requires netbios')
560567
def test_netbios_getnode(self):
561568
node = self.uuid._netbios_getnode()
562-
self.check_node(node, network=True)
569+
self.check_node(node)
563570

564571
def test_random_getnode(self):
565572
node = self.uuid._random_getnode()
566-
# Least significant bit of first octet must be set.
567-
self.assertTrue(node & 0x010000000000, '%012x' % node)
568-
self.check_node(node)
573+
# The multicast bit, i.e. the least significant bit of first octet,
574+
# must be set for randomly generated MAC addresses. See RFC 4122,
575+
# $4.1.6.
576+
self.assertTrue(node & (1 << 40), '%012x' % node)
577+
self.check_node(node, check_bit=False)
569578

570579
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
571580
def test_unix_getnode(self):
@@ -575,13 +584,17 @@ def test_unix_getnode(self):
575584
node = self.uuid._unix_getnode()
576585
except TypeError:
577586
self.skipTest('requires uuid_generate_time')
578-
self.check_node(node, 'unix')
587+
# Since we don't know the provenance of the MAC address, don't check
588+
# whether it is locally or universally administered.
589+
self.check_node(node, 'unix', check_bit=False)
579590

580591
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
581592
@unittest.skipUnless(importable('ctypes'), 'requires ctypes')
582593
def test_windll_getnode(self):
583594
node = self.uuid._windll_getnode()
584-
self.check_node(node)
595+
# Since we don't know the provenance of the MAC address, don't check
596+
# whether it is locally or universally administered.
597+
self.check_node(node, check_bit=False)
585598

586599

587600
class TestInternalsWithoutExtModule(BaseTestInternals, unittest.TestCase):

Lib/uuid.py

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,29 @@ def _popen(command, *args):
342342
env=env)
343343
return proc
344344

345+
# For MAC (a.k.a. IEEE 802, or EUI-48) addresses, the second least significant
346+
# bit of the first octet signifies whether the MAC address is universally (0)
347+
# or locally (1) administered. Network cards from hardware manufacturers will
348+
# always be universally administered to guarantee global uniqueness of the MAC
349+
# address, but any particular machine may have other interfaces which are
350+
# locally administered. An example of the latter is the bridge interface to
351+
# the Touch Bar on MacBook Pros.
352+
#
353+
# This bit works out to be the 42nd bit counting from 1 being the least
354+
# significant, or 1<<41. We'll skip over any locally administered MAC
355+
# addresses, as it makes no sense to use those in UUID calculation.
356+
#
357+
# See https://en.wikipedia.org/wiki/MAC_address#Universal_vs._local
358+
359+
def _is_universal(mac):
360+
return not (mac & (1 << 41))
361+
345362
def _find_mac(command, args, hw_identifiers, get_index):
363+
first_local_mac = None
346364
try:
347365
proc = _popen(command, *args.split())
348366
if not proc:
349-
return
367+
return None
350368
with proc:
351369
for line in proc.stdout:
352370
words = line.lower().rstrip().split()
@@ -355,8 +373,9 @@ def _find_mac(command, args, hw_identifiers, get_index):
355373
try:
356374
word = words[get_index(i)]
357375
mac = int(word.replace(b':', b''), 16)
358-
if mac:
376+
if _is_universal(mac):
359377
return mac
378+
first_local_mac = first_local_mac or mac
360379
except (ValueError, IndexError):
361380
# Virtual interfaces, such as those provided by
362381
# VPNs, do not have a colon-delimited MAC address
@@ -366,6 +385,7 @@ def _find_mac(command, args, hw_identifiers, get_index):
366385
pass
367386
except OSError:
368387
pass
388+
return first_local_mac or None
369389

370390
def _ifconfig_getnode():
371391
"""Get the hardware address on Unix by running ifconfig."""
@@ -375,13 +395,15 @@ def _ifconfig_getnode():
375395
mac = _find_mac('ifconfig', args, keywords, lambda i: i+1)
376396
if mac:
377397
return mac
398+
return None
378399

379400
def _ip_getnode():
380401
"""Get the hardware address on Unix by running ip."""
381402
# This works on Linux with iproute2.
382403
mac = _find_mac('ip', 'link list', [b'link/ether'], lambda i: i+1)
383404
if mac:
384405
return mac
406+
return None
385407

386408
def _arp_getnode():
387409
"""Get the hardware address on Unix by running arp."""
@@ -404,8 +426,10 @@ def _arp_getnode():
404426
# This works on Linux, FreeBSD and NetBSD
405427
mac = _find_mac('arp', '-an', [os.fsencode('(%s)' % ip_addr)],
406428
lambda i: i+2)
429+
# Return None instead of 0.
407430
if mac:
408431
return mac
432+
return None
409433

410434
def _lanscan_getnode():
411435
"""Get the hardware address on Unix by running lanscan."""
@@ -415,32 +439,36 @@ def _lanscan_getnode():
415439
def _netstat_getnode():
416440
"""Get the hardware address on Unix by running netstat."""
417441
# This might work on AIX, Tru64 UNIX.
442+
first_local_mac = None
418443
try:
419444
proc = _popen('netstat', '-ia')
420445
if not proc:
421-
return
446+
return None
422447
with proc:
423448
words = proc.stdout.readline().rstrip().split()
424449
try:
425450
i = words.index(b'Address')
426451
except ValueError:
427-
return
452+
return None
428453
for line in proc.stdout:
429454
try:
430455
words = line.rstrip().split()
431456
word = words[i]
432457
if len(word) == 17 and word.count(b':') == 5:
433458
mac = int(word.replace(b':', b''), 16)
434-
if mac:
459+
if _is_universal(mac):
435460
return mac
461+
first_local_mac = first_local_mac or mac
436462
except (ValueError, IndexError):
437463
pass
438464
except OSError:
439465
pass
466+
return first_local_mac or None
440467

441468
def _ipconfig_getnode():
442469
"""Get the hardware address on Windows by running ipconfig.exe."""
443470
import os, re
471+
first_local_mac = None
444472
dirs = ['', r'c:\windows\system32', r'c:\winnt\system32']
445473
try:
446474
import ctypes
@@ -458,18 +486,23 @@ def _ipconfig_getnode():
458486
for line in pipe:
459487
value = line.split(':')[-1].strip().lower()
460488
if re.match('([0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value):
461-
return int(value.replace('-', ''), 16)
489+
mac = int(value.replace('-', ''), 16)
490+
if _is_universal(mac):
491+
return mac
492+
first_local_mac = first_local_mac or mac
493+
return first_local_mac or None
462494

463495
def _netbios_getnode():
464496
"""Get the hardware address on Windows using NetBIOS calls.
465497
See http://support.microsoft.com/kb/118623 for details."""
466498
import win32wnet, netbios
499+
first_local_mac = None
467500
ncb = netbios.NCB()
468501
ncb.Command = netbios.NCBENUM
469502
ncb.Buffer = adapters = netbios.LANA_ENUM()
470503
adapters._pack()
471504
if win32wnet.Netbios(ncb) != 0:
472-
return
505+
return None
473506
adapters._unpack()
474507
for i in range(adapters.length):
475508
ncb.Reset()
@@ -488,7 +521,11 @@ def _netbios_getnode():
488521
bytes = status.adapter_address[:6]
489522
if len(bytes) != 6:
490523
continue
491-
return int.from_bytes(bytes, 'big')
524+
mac = int.from_bytes(bytes, 'big')
525+
if _is_universal(mac):
526+
return mac
527+
first_local_mac = first_local_mac or mac
528+
return first_local_mac or None
492529

493530

494531
_generate_time_safe = _UuidCreate = None
@@ -601,9 +638,19 @@ def _windll_getnode():
601638
return UUID(bytes=bytes_(_buffer.raw)).node
602639

603640
def _random_getnode():
604-
"""Get a random node ID, with eighth bit set as suggested by RFC 4122."""
641+
"""Get a random node ID."""
642+
# RFC 4122, $4.1.6 says "For systems with no IEEE address, a randomly or
643+
# pseudo-randomly generated value may be used; see Section 4.5. The
644+
# multicast bit must be set in such addresses, in order that they will
645+
# never conflict with addresses obtained from network cards."
646+
#
647+
# The "multicast bit" of a MAC address is defined to be "the least
648+
# significant bit of the first octet". This works out to be the 41st bit
649+
# counting from 1 being the least significant bit, or 1<<40.
650+
#
651+
# See https://en.wikipedia.org/wiki/MAC_address#Unicast_vs._multicast
605652
import random
606-
return random.getrandbits(48) | 0x010000000000
653+
return random.getrandbits(48) | (1 << 40)
607654

608655

609656
_node = None
@@ -626,13 +673,14 @@ def getnode():
626673
getters = [_unix_getnode, _ifconfig_getnode, _ip_getnode,
627674
_arp_getnode, _lanscan_getnode, _netstat_getnode]
628675

629-
for getter in getters + [_random_getnode]:
676+
for getter in getters:
630677
try:
631678
_node = getter()
632679
except:
633680
continue
634681
if _node is not None:
635682
return _node
683+
return _random_getnode()
636684

637685

638686
_last_timestamp = None
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Improve the private ``*_getnode()`` methods for UUID1 such that universally
2+
administered MAC addresses are preferred over locally administered MAC
3+
addresses. If only the latter is available, the first such one is returned.
4+
Improve the related tests and fix some bugs there as well.

0 commit comments

Comments
 (0)