Skip to content

Commit 23df2d1

Browse files
authored
bpo-32107 - Improve MAC address calculation and fix test_uuid.py (#4600)
``uuid.getnode()`` now preferentially returns universally administered MAC addresses if available, over locally administered MAC addresses. This makes a better guarantee for global uniqueness of UUIDs returned from ``uuid.uuid1()``. If only locally administered MAC addresses are available, the first such one found is returned. Also improve internal code style by being explicit about ``return None`` rather than falling off the end of the function. Improve the test robustness.
1 parent 71bd588 commit 23df2d1

File tree

4 files changed

+89
-29
lines changed

4 files changed

+89
-29
lines changed

Doc/library/uuid.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,18 @@ The :mod:`uuid` module defines the following functions:
156156

157157
Get the hardware address as a 48-bit positive integer. The first time this
158158
runs, it may launch a separate program, which could be quite slow. If all
159-
attempts to obtain the hardware address fail, we choose a random 48-bit number
160-
with its eighth bit set to 1 as recommended in RFC 4122. "Hardware address"
161-
means the MAC address of a network interface, and on a machine with multiple
162-
network interfaces the MAC address of any one of them may be returned.
159+
attempts to obtain the hardware address fail, we choose a random 48-bit
160+
number with the multicast bit (least significant bit of the first octet)
161+
set to 1 as recommended in RFC 4122. "Hardware address" means the MAC
162+
address of a network interface. On a machine with multiple network
163+
interfaces, universally administered MAC addresses (i.e. where the second
164+
least significant bit of the first octet is *unset*) will be preferred over
165+
locally administered MAC addresses, but with no other ordering guarantees.
166+
167+
.. versionchanged:: 3.7
168+
Universally administered MAC addresses are preferred over locally
169+
administered MAC addresses, since the former are guaranteed to be
170+
globally unique, while the latter are not.
163171

164172
.. index:: single: getnode
165173

Lib/test/test_uuid.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -512,59 +512,57 @@ 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):
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)
525521
self.assertTrue(0 < node < (1 << 48),
526522
"%s is not an RFC 4122 node ID" % hex)
527523

528524
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
529525
def test_ifconfig_getnode(self):
530526
node = self.uuid._ifconfig_getnode()
531-
self.check_node(node, 'ifconfig', True)
527+
self.check_node(node, 'ifconfig')
532528

533529
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
534530
def test_ip_getnode(self):
535531
node = self.uuid._ip_getnode()
536-
self.check_node(node, 'ip', True)
532+
self.check_node(node, 'ip')
537533

538534
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
539535
def test_arp_getnode(self):
540536
node = self.uuid._arp_getnode()
541-
self.check_node(node, 'arp', True)
537+
self.check_node(node, 'arp')
542538

543539
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
544540
def test_lanscan_getnode(self):
545541
node = self.uuid._lanscan_getnode()
546-
self.check_node(node, 'lanscan', True)
542+
self.check_node(node, 'lanscan')
547543

548544
@unittest.skipUnless(os.name == 'posix', 'requires Posix')
549545
def test_netstat_getnode(self):
550546
node = self.uuid._netstat_getnode()
551-
self.check_node(node, 'netstat', True)
547+
self.check_node(node, 'netstat')
552548

553549
@unittest.skipUnless(os.name == 'nt', 'requires Windows')
554550
def test_ipconfig_getnode(self):
555551
node = self.uuid._ipconfig_getnode()
556-
self.check_node(node, 'ipconfig', True)
552+
self.check_node(node, 'ipconfig')
557553

558554
@unittest.skipUnless(importable('win32wnet'), 'requires win32wnet')
559555
@unittest.skipUnless(importable('netbios'), 'requires netbios')
560556
def test_netbios_getnode(self):
561557
node = self.uuid._netbios_getnode()
562-
self.check_node(node, network=True)
558+
self.check_node(node)
563559

564560
def test_random_getnode(self):
565561
node = self.uuid._random_getnode()
566-
# Least significant bit of first octet must be set.
567-
self.assertTrue(node & 0x010000000000, '%012x' % node)
562+
# The multicast bit, i.e. the least significant bit of first octet,
563+
# must be set for randomly generated MAC addresses. See RFC 4122,
564+
# $4.1.6.
565+
self.assertTrue(node & (1 << 40), '%012x' % node)
568566
self.check_node(node)
569567

570568
@unittest.skipUnless(os.name == 'posix', 'requires Posix')

Lib/uuid.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,30 @@ 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 prefer universally administered MAC addresses
355+
# over locally administered ones since the former are globally unique, but
356+
# we'll return the first of the latter found if that's all the machine has.
357+
#
358+
# See https://en.wikipedia.org/wiki/MAC_address#Universal_vs._local
359+
360+
def _is_universal(mac):
361+
return not (mac & (1 << 41))
362+
345363
def _find_mac(command, args, hw_identifiers, get_index):
364+
first_local_mac = None
346365
try:
347366
proc = _popen(command, *args.split())
348367
if not proc:
349-
return
368+
return None
350369
with proc:
351370
for line in proc.stdout:
352371
words = line.lower().rstrip().split()
@@ -355,8 +374,9 @@ def _find_mac(command, args, hw_identifiers, get_index):
355374
try:
356375
word = words[get_index(i)]
357376
mac = int(word.replace(b':', b''), 16)
358-
if mac:
377+
if _is_universal(mac):
359378
return mac
379+
first_local_mac = first_local_mac or mac
360380
except (ValueError, IndexError):
361381
# Virtual interfaces, such as those provided by
362382
# VPNs, do not have a colon-delimited MAC address
@@ -366,6 +386,7 @@ def _find_mac(command, args, hw_identifiers, get_index):
366386
pass
367387
except OSError:
368388
pass
389+
return first_local_mac or None
369390

370391
def _ifconfig_getnode():
371392
"""Get the hardware address on Unix by running ifconfig."""
@@ -375,13 +396,15 @@ def _ifconfig_getnode():
375396
mac = _find_mac('ifconfig', args, keywords, lambda i: i+1)
376397
if mac:
377398
return mac
399+
return None
378400

379401
def _ip_getnode():
380402
"""Get the hardware address on Unix by running ip."""
381403
# This works on Linux with iproute2.
382404
mac = _find_mac('ip', 'link list', [b'link/ether'], lambda i: i+1)
383405
if mac:
384406
return mac
407+
return None
385408

386409
def _arp_getnode():
387410
"""Get the hardware address on Unix by running arp."""
@@ -404,8 +427,10 @@ def _arp_getnode():
404427
# This works on Linux, FreeBSD and NetBSD
405428
mac = _find_mac('arp', '-an', [os.fsencode('(%s)' % ip_addr)],
406429
lambda i: i+2)
430+
# Return None instead of 0.
407431
if mac:
408432
return mac
433+
return None
409434

410435
def _lanscan_getnode():
411436
"""Get the hardware address on Unix by running lanscan."""
@@ -415,32 +440,36 @@ def _lanscan_getnode():
415440
def _netstat_getnode():
416441
"""Get the hardware address on Unix by running netstat."""
417442
# This might work on AIX, Tru64 UNIX.
443+
first_local_mac = None
418444
try:
419445
proc = _popen('netstat', '-ia')
420446
if not proc:
421-
return
447+
return None
422448
with proc:
423449
words = proc.stdout.readline().rstrip().split()
424450
try:
425451
i = words.index(b'Address')
426452
except ValueError:
427-
return
453+
return None
428454
for line in proc.stdout:
429455
try:
430456
words = line.rstrip().split()
431457
word = words[i]
432458
if len(word) == 17 and word.count(b':') == 5:
433459
mac = int(word.replace(b':', b''), 16)
434-
if mac:
460+
if _is_universal(mac):
435461
return mac
462+
first_local_mac = first_local_mac or mac
436463
except (ValueError, IndexError):
437464
pass
438465
except OSError:
439466
pass
467+
return first_local_mac or None
440468

441469
def _ipconfig_getnode():
442470
"""Get the hardware address on Windows by running ipconfig.exe."""
443471
import os, re
472+
first_local_mac = None
444473
dirs = ['', r'c:\windows\system32', r'c:\winnt\system32']
445474
try:
446475
import ctypes
@@ -458,18 +487,23 @@ def _ipconfig_getnode():
458487
for line in pipe:
459488
value = line.split(':')[-1].strip().lower()
460489
if re.match('([0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value):
461-
return int(value.replace('-', ''), 16)
490+
mac = int(value.replace('-', ''), 16)
491+
if _is_universal(mac):
492+
return mac
493+
first_local_mac = first_local_mac or mac
494+
return first_local_mac or None
462495

463496
def _netbios_getnode():
464497
"""Get the hardware address on Windows using NetBIOS calls.
465498
See http://support.microsoft.com/kb/118623 for details."""
466499
import win32wnet, netbios
500+
first_local_mac = None
467501
ncb = netbios.NCB()
468502
ncb.Command = netbios.NCBENUM
469503
ncb.Buffer = adapters = netbios.LANA_ENUM()
470504
adapters._pack()
471505
if win32wnet.Netbios(ncb) != 0:
472-
return
506+
return None
473507
adapters._unpack()
474508
for i in range(adapters.length):
475509
ncb.Reset()
@@ -488,7 +522,11 @@ def _netbios_getnode():
488522
bytes = status.adapter_address[:6]
489523
if len(bytes) != 6:
490524
continue
491-
return int.from_bytes(bytes, 'big')
525+
mac = int.from_bytes(bytes, 'big')
526+
if _is_universal(mac):
527+
return mac
528+
first_local_mac = first_local_mac or mac
529+
return first_local_mac or None
492530

493531

494532
_generate_time_safe = _UuidCreate = None
@@ -601,9 +639,19 @@ def _windll_getnode():
601639
return UUID(bytes=bytes_(_buffer.raw)).node
602640

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

608656

609657
_node = None
@@ -626,13 +674,14 @@ def getnode():
626674
getters = [_unix_getnode, _ifconfig_getnode, _ip_getnode,
627675
_arp_getnode, _lanscan_getnode, _netstat_getnode]
628676

629-
for getter in getters + [_random_getnode]:
677+
for getter in getters:
630678
try:
631679
_node = getter()
632680
except:
633681
continue
634682
if _node is not None:
635683
return _node
684+
return _random_getnode()
636685

637686

638687
_last_timestamp = None
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``uuid.getnode()`` now preferentially returns universally administered MAC
2+
addresses if available, over locally administered MAC addresses. This makes a
3+
better guarantee for global uniqueness of UUIDs returned from
4+
``uuid.uuid1()``. If only locally administered MAC addresses are available,
5+
the first such one found is returned.

0 commit comments

Comments
 (0)