Build the mental model
A frame carries two pairs of addresses, and beginners reasonably ask why one pair is not enough. They answer different questions. The IP addresses are end-to-end: they name the original sender and final destination and normally do not change for the whole journey. The MAC addresses are hop-by-hop: they name only the two devices at either end of this physical link, and are rewritten at every router. When your laptop sends a packet to a server across the world, the destination IP is the server's but the destination MAC is your home router's — that router is the only device your laptop can physically hand a frame to.
A MAC address is 48 bits, written as six hex bytes. The first three bytes are the OUI, an organizationally unique identifier assigned to the manufacturer, so a MAC usually reveals the vendor. Two bits in the first byte carry meaning: the least significant bit marks a group (multicast or broadcast) address, and the next bit marks a locally administered rather than globally assigned address — which is what a randomized-MAC privacy feature sets.
ARP bridges the two worlds. Having decided a destination is on its own subnet, a host still does not know its MAC. It broadcasts an ARP request — "who has 192.168.1.20?" — to every machine on the LAN; only the owner replies, with a unicast containing its MAC. The result goes into the ARP cache with a short timeout, so the broadcast happens once rather than per packet — which is why the scheme scales. Because ARP carries no authentication, any host can answer for an address it does not own — the basis of ARP-poisoning attacks.
ARP: FINDING THE MAC THAT OWNS AN IP
------------------------------------
Host A wants to reach 192.168.1.20 but knows no MAC for it.
Host A switch Host B
192.168.1.10 | 192.168.1.20
aa:bb:cc:00:00:0a | dd:ee:ff:00:00:14
| | |
STEP 1 -- ARP REQUEST, broadcast: every host must look
| | |
|--- to ff:ff:ff:ff:ff:ff --------------------------->|
| "who has 192.168.1.20? tell 192.168.1.10" |
| | |
| +--> also delivered to |
| every other host |
| |
STEP 2 -- ARP REPLY, unicast: only Host A needs the answer
| |
|<--- to aa:bb:cc:00:00:0a ---------------------------|
| "192.168.1.20 is at dd:ee:ff:00:00:14" |
| |
STEP 3 -- Host A caches it, then sends the real frame
ARP cache on Host A
+---------------+---------------------+-----------+
| IP | MAC | expires |
+---------------+---------------------+-----------+
| 192.168.1.20 | dd:ee:ff:00:00:14 | ~60 s |
+---------------+---------------------+-----------+Connect it to a real scenario
Here is a symptom that looks like a routing problem but is not. A server is replaced with new hardware but keeps the same IP. Some clients reach it immediately; others hang for a minute and then start working. Nothing about routing changed — the old clients still have the old MAC in their ARP cache, so they keep framing packets to a NIC that no longer exists. When the cache entry expires, they ARP again, learn the new MAC, and recover. That is why `arp -d` or simply waiting is the fix, and why gratuitous ARP exists: the new machine broadcasts its own mapping so everyone's cache updates immediately instead of after a timeout.
The general procedure when a host on your own LAN is unreachable is to check the ARP cache before blaming anything higher up. If there is no entry at all, the ARP request is going unanswered — wrong subnet mask, host down, or a switch problem. If there is an entry but the MAC is wrong, or two different IPs share one MAC, suspect a duplicate address or ARP spoofing.
The code below parses MAC addresses written in the three common formats, splits out the OUI, and decodes the two flag bits in the first byte.
Try the working example
HEX_DIGITS = "0123456789abcdefABCDEF"
def normalize(raw):
"""Accept aa:bb:.., aa-bb-.. or aabb.ccdd.eeff and return 6 bytes."""
digits = "".join(c for c in raw if c in HEX_DIGITS)
if len(digits) != 12:
raise ValueError("not a 48-bit MAC address: %r" % raw)
return bytes.fromhex(digits)
def group(data, sep=":"):
return sep.join("%02x" % b for b in data)
def describe(raw):
mac = normalize(raw)
first = mac[0]
ig = first & 1 # bit 0 of the first byte
ul = (first >> 1) & 1 # bit 1 of the first byte
print("input :", raw)
print("normalized :", group(mac))
print("OUI (vendor) :", group(mac[:3]))
print("device part :", group(mac[3:]))
print("first byte :", format(first, "08b"))
if mac == b"\xff" * 6:
print(" special : broadcast (all 48 bits set)")
print(" I/G bit :", ig, "->", "group" if ig else "unicast")
print(" U/L bit :", ul, "->",
"locally administered" if ul else "globally unique (vendor)")
print()
for candidate in ("00:1A:2B:3C:4D:5E", "02-00-00-11-22-33",
"ffff.ffff.ffff"):
describe(candidate)
# The same physical NIC, written three ways -- all one address.
forms = ["00:1a:2b:3c:4d:5e", "00-1A-2B-3C-4D-5E", "001a.2b3c.4d5e"]
print("all three forms equal:", len({normalize(f) for f in forms}) == 1)
input : 00:1A:2B:3C:4D:5E
normalized : 00:1a:2b:3c:4d:5e
OUI (vendor) : 00:1a:2b
device part : 3c:4d:5e
first byte : 00000000
I/G bit : 0 -> unicast
U/L bit : 0 -> globally unique (vendor)
input : 02-00-00-11-22-33
normalized : 02:00:00:11:22:33
OUI (vendor) : 02:00:00
device part : 11:22:33
first byte : 00000010
I/G bit : 0 -> unicast
U/L bit : 1 -> locally administered
input : ffff.ffff.ffff
normalized : ff:ff:ff:ff:ff:ff
OUI (vendor) : ff:ff:ff
device part : ff:ff:ff
first byte : 11111111
special : broadcast (all 48 bits set)
I/G bit : 1 -> group
U/L bit : 1 -> locally administered
all three forms equal: True5-minute try-it
Extend describe() to also report whether a MAC is an IPv4 multicast MAC (one starting with 01:00:5e). Then feed it "00:1a:2b:3c:4d" (only five bytes), observe the ValueError, and explain why that length check matters.
One important caution
Assuming a MAC address is globally unique and permanent — locally administered addresses set the U/L bit, and modern phones and laptops randomize their MAC per Wi-Fi network, so MAC-based allowlists break
Expecting instant recovery after swapping hardware while keeping the IP — clients keep using the stale MAC until their ARP cache entry expires, so you need gratuitous ARP or a manual cache flush
RFC 826 — An Ethernet Address Resolution Protocol — Computer Networking