Networking

ARP (Address Resolution Protocol)

The shout-into-the-room that turns an IP into a MAC address

ARP (Address Resolution Protocol) maps a known IPv4 address to the unknown 48-bit MAC address on a local network by broadcasting a who-has request to every host; the owner replies with its hardware address, which the sender caches for minutes.

  • LayerBetween L2 and L3
  • Defined byRFC 826 (1982)
  • Request deliveryBroadcast
  • Reply deliveryUnicast
  • Frame typeEtherType 0x0806
  • Cache lifetimeseconds–hours

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

The problem ARP solves

Your laptop wants to send a packet to 192.168.1.20 on the same Wi-Fi network. It has the IP address. But the network card doesn't speak IP — it speaks Ethernet, and Ethernet frames are addressed by a 48-bit hardware identifier burned into every NIC: the MAC address, something like a4:83:e7:1c:9b:02. IP is layer 3; the wire is layer 2. To actually put the frame on the wire, the laptop needs the destination MAC, and right now it has no idea what it is.

ARP is the missing translation. The trick is almost comically blunt: the laptop shouts to the entire local segment — "Who has 192.168.1.20? Tell 192.168.1.10." Every host on the link receives that broadcast, but only the machine that actually owns 192.168.1.20 answers, with a unicast reply containing its MAC. The laptop stores the answer in a small table called the ARP cache so it never has to ask again for the next few minutes. That's the whole protocol — no clever data structure, no negotiation. ARP was specified by David Plummer in RFC 826 in November 1982, four pages long, and it has barely changed since.

The genius and the curse are the same property: ARP trusts whoever answers. There is no signature, no challenge, no proof of ownership. The protocol assumes the local segment is friendly. That assumption is what makes ARP spoofing the oldest trick in the LAN attacker's book.

The packet, field by field

An ARP message is a fixed 28-byte payload riding inside an Ethernet frame with EtherType 0x0806. Eight fields:

FieldBytesRequest valueReply value
Hardware type (HTYPE)21 (Ethernet)1
Protocol type (PTYPE)20x0800 (IPv4)0x0800
Hardware addr len (HLEN)166
Protocol addr len (PLEN)144
Operation (OPER)21 (request)2 (reply)
Sender MAC (SHA)6requester's MACowner's MAC
Sender IP (SPA)4requester's IPowner's IP
Target MAC (THA)600:00:00:00:00:00requester's MAC
Target IP (TPA)4the IP we're resolvingrequester's IP

The lifecycle is four steps:

  1. Cache check. Before sending anything, the kernel looks up the destination IP in the ARP cache. A hit costs a hash-table lookup — effectively free. A miss starts the resolution dance and the outbound packet is parked in a short queue.
  2. Broadcast request. The host builds an ARP request with the target IP it wants resolved and the target MAC zeroed out, wraps it in an Ethernet frame addressed to the broadcast MAC ff:ff:ff:ff:ff:ff, and sends it. Every NIC on the segment receives the frame and hands the ARP payload up.
  3. Unicast reply. Each host compares the target IP against its own. All but one discard it. The owner flips OPER to 2, swaps sender and target fields, fills in its own MAC, and sends a unicast reply straight back to the requester. As a side effect, the owner also caches the requester's IP→MAC mapping, since it learned it from the request — communication is almost always bidirectional.
  4. Cache and send. The requester stores the mapping with a timer and finally transmits the queued packet using the freshly learned MAC. Subsequent packets to that IP skip straight to step 1's cache hit until the entry expires.

Cost and complexity

ARP has no clever data structure to analyze — the interesting cost is the round trips and the broadcast amplification.

  • Cache hit: O(1) hash lookup, sub-microsecond. The overwhelmingly common case.
  • Cache miss: one round trip on the local segment. On gigabit Ethernet that's typically well under a millisecond, but on congested Wi-Fi a request and reply can take several milliseconds, during which the first packet of a connection stalls.
  • Broadcast cost: every request is processed by every host's CPU on the segment, even though only one cares. On a broadcast domain with n hosts, one resolution generates n frame-receptions. This is why a flat layer-2 network of thousands of hosts melts under ARP storms, and why VLANs and subnets exist — to keep broadcast domains small.
  • Failure path: if no host answers (the IP is dead or off the segment), the requester retransmits a few times — Linux defaults to 3 probes spaced ~1 second apart — then gives up and the queued packet is dropped. The application sees a timeout, not an instant error.

The hidden expense is cache invalidation. Too short a timer means constant re-resolution and broadcast traffic; too long means a stale entry sends frames to a host that has moved or died, blackholing traffic until the entry ages out. Every OS tunes this trade-off differently (see the comparison below).

ARP vs the neighbors

ARP (IPv4)NDP (IPv6)RARPDNSProxy ARP
MapsIP → MACIP → MACMAC → IPname → IPIP → MAC (on behalf of another host)
LayerL2.5 (over Ethernet)L3 (ICMPv6)L2.5L7 (over UDP/TCP)L2.5
Discovery methodBroadcastMulticast (solicited-node)BroadcastUnicast to resolverRouter answers for off-link IP
ScopeLocal segmentLocal linkLocal segmentGlobalLocal segment
AuthenticationNoneOptional (SEND/RFC 3971)NoneOptional (DNSSEC)None
StatusUniversal on IPv4 LANsMandatory on IPv6Obsolete (replaced by DHCP)UniversalDiscouraged, still common

The headline shift is broadcast → multicast. ARP wakes every host on the segment; IPv6's NDP sends a Neighbor Solicitation to a solicited-node multicast group derived from the last 24 bits of the target IP, so on average only a handful of hosts (often just one) ever process the message. Same job, far less collateral noise — and NDP can be authenticated, which ARP fundamentally cannot.

What the numbers actually say

  • 28 bytes of ARP payload, padded to Ethernet's 46-byte minimum, in a 64-byte minimum frame. ARP is one of the smallest protocols you'll meet.
  • 248 ≈ 281 trillion possible MAC addresses — the 48-bit space ARP resolves into. The first 24 bits are the vendor OUI; the lower 24 identify the device.
  • Linux default reachable time: 30 seconds base, randomized to 15–45 s to avoid synchronized re-resolution storms across hosts that booted together. Cisco IOS, by contrast, holds entries for 4 hours by default — a 480× difference that surprises people debugging failover.
  • 3 retransmissions is the typical request retry count before a host declares the neighbor unreachable; the failed packet is dropped and the socket reports a timeout.
  • One poisoned cache entry is all an attacker needs. A single forged reply claiming the gateway's IP redirects every off-subnet packet through the attacker — the canonical man-in-the-middle, and it costs one 64-byte frame.

JavaScript implementation

A faithful model of the resolver: an ARP cache with TTL expiry, a request/reply state machine, and a packet queue that drains once resolution completes. Network I/O is stubbed as a broadcast bus the hosts share.

const BROADCAST = "ff:ff:ff:ff:ff:ff";
const REACHABLE_MS = 30_000;            // Linux-style base lifetime

class Host {
  constructor(ip, mac, bus) {
    this.ip = ip; this.mac = mac; this.bus = bus;
    this.cache = new Map();             // ip -> { mac, expires }
    this.pending = new Map();           // ip -> [packet, ...] queued during resolution
    bus.attach(this);
  }

  // Look up a MAC, resolving via ARP on a miss.
  lookup(targetIp) {
    const hit = this.cache.get(targetIp);
    if (hit && hit.expires > Date.now()) return hit.mac;   // O(1) cache hit
    this.cache.delete(targetIp);                            // expire stale entry
    return null;                                            // caller must wait for ARP
  }

  send(targetIp, payload) {
    const mac = this.lookup(targetIp);
    if (mac) { this.bus.unicast(mac, { from: this.mac, payload }); return; }
    // Miss: queue the packet and broadcast a who-has request.
    if (!this.pending.has(targetIp)) {
      this.pending.set(targetIp, []);
      this.bus.broadcast({
        oper: 1, senderMac: this.mac, senderIp: this.ip,
        targetMac: "00:00:00:00:00:00", targetIp,
      });
    }
    this.pending.get(targetIp).push(payload);
  }

  // Called by the bus for every ARP frame on the segment.
  onArp(pkt) {
    // Opportunistic learning: we now know the sender's IP -> MAC.
    this.cache.set(pkt.senderIp, { mac: pkt.senderMac, expires: Date.now() + REACHABLE_MS });

    if (pkt.oper === 1 && pkt.targetIp === this.ip) {
      // It's a request for us — reply by unicast.
      this.bus.unicastArp(pkt.senderMac, {
        oper: 2, senderMac: this.mac, senderIp: this.ip,
        targetMac: pkt.senderMac, targetIp: pkt.senderIp,
      });
    } else if (pkt.oper === 2 && pkt.targetIp === this.ip) {
      // It's a reply to us — flush the queued packets.
      const queued = this.pending.get(pkt.senderIp) || [];
      this.pending.delete(pkt.senderIp);
      for (const payload of queued) {
        this.bus.unicast(pkt.senderMac, { from: this.mac, payload });
      }
    }
  }
}

class Bus {
  constructor() { this.hosts = []; }
  attach(h) { this.hosts.push(h); }
  broadcast(arpPkt) { for (const h of this.hosts) if (h.mac !== arpPkt.senderMac) h.onArp(arpPkt); }
  unicastArp(mac, arpPkt) { for (const h of this.hosts) if (h.mac === mac) h.onArp(arpPkt); }
  unicast(mac, frame) { /* deliver data frame to the host owning `mac` */ }
}

// Demo: A resolves B by who-has broadcast, then sends.
const bus = new Bus();
const A = new Host("192.168.1.10", "aa:aa:aa:aa:aa:aa", bus);
const B = new Host("192.168.1.20", "bb:bb:bb:bb:bb:bb", bus);
A.send("192.168.1.20", "hello");   // miss -> broadcast -> B replies -> queued packet flushed

Two details worth flagging. First, onArp caches the sender's mapping on every frame it sees, request or reply — this opportunistic learning is exactly how a real stack populates its cache without asking, and exactly the hole that ARP spoofing exploits. Second, the pending queue is what prevents a burst of who-has broadcasts: only the first packet to an unresolved IP triggers a request; the rest wait.

Python implementation

The same logic, plus an explicit retransmission loop and TTL sweep that a production stack would run.

import time

BROADCAST = "ff:ff:ff:ff:ff:ff"
REACHABLE = 30.0          # seconds (Linux base_reachable_time)
MAX_PROBES = 3

class Host:
    def __init__(self, ip, mac, bus):
        self.ip, self.mac, self.bus = ip, mac, bus
        self.cache = {}       # ip -> (mac, expires)
        self.pending = {}     # ip -> list of payloads
        bus.attach(self)

    def lookup(self, ip):
        entry = self.cache.get(ip)
        if entry and entry[1] > time.time():
            return entry[0]                 # O(1) hit
        self.cache.pop(ip, None)            # drop stale
        return None

    def send(self, target_ip, payload):
        mac = self.lookup(target_ip)
        if mac:
            self.bus.unicast(mac, (self.mac, payload))
            return
        if target_ip not in self.pending:
            self.pending[target_ip] = []
            self._who_has(target_ip)        # first packet triggers the request
        self.pending[target_ip].append(payload)

    def _who_has(self, target_ip):
        self.bus.broadcast(dict(
            oper=1, sha=self.mac, spa=self.ip,
            tha="00:00:00:00:00:00", tpa=target_ip))

    def on_arp(self, pkt):
        # Opportunistic learning from any ARP frame.
        self.cache[pkt["spa"]] = (pkt["sha"], time.time() + REACHABLE)

        if pkt["oper"] == 1 and pkt["tpa"] == self.ip:          # request for us
            self.bus.unicast_arp(pkt["sha"], dict(
                oper=2, sha=self.mac, spa=self.ip,
                tha=pkt["sha"], tpa=pkt["spa"]))
        elif pkt["oper"] == 2 and pkt["tpa"] == self.ip:        # reply to us
            for payload in self.pending.pop(pkt["spa"], []):
                self.bus.unicast(pkt["sha"], (self.mac, payload))

    def sweep(self):
        """Periodic TTL eviction — run on a timer in a real stack."""
        now = time.time()
        self.cache = {ip: e for ip, e in self.cache.items() if e[1] > now}

class Bus:
    def __init__(self): self.hosts = []
    def attach(self, h): self.hosts.append(h)
    def broadcast(self, pkt):
        for h in self.hosts:
            if h.mac != pkt["sha"]:
                h.on_arp(pkt)
    def unicast_arp(self, mac, pkt):
        for h in self.hosts:
            if h.mac == mac:
                h.on_arp(pkt)
    def unicast(self, mac, frame):
        pass  # deliver data frame

bus = Bus()
a = Host("192.168.1.10", "aa:aa:aa:aa:aa:aa", bus)
b = Host("192.168.1.20", "bb:bb:bb:bb:bb:bb", bus)
a.send("192.168.1.20", b"hello")   # who-has 192.168.1.20 -> reply -> flush

Note the asymmetry between resolving and being resolved: send drives the active path (broadcast, queue, wait), while on_arp handles both being asked (reply) and getting an answer (flush). A real implementation adds the retransmission timer around _who_has — re-probe up to MAX_PROBES times, then drop the queued packets and surface a "host unreachable" error.

Variants worth knowing

Gratuitous ARP. A host broadcasts an ARP for its own IP that nobody asked about. It detects duplicate-IP conflicts at boot, pre-warms neighbors' caches, and — most importantly — announces that an IP has moved to a new MAC. When a high-availability cluster fails over a virtual IP, a gratuitous ARP is what tells the switch and every peer to redirect traffic to the new active node within milliseconds.

Proxy ARP. A router answers an ARP request on behalf of a host that lives on a different segment, lending its own MAC. It lets a misconfigured host that thinks everything is on-link still reach off-link destinations. Clever, but it hides subnet boundaries and is discouraged in modern designs.

ARP probe and announcement (RFC 5227, ACD). The IPv4 Address Conflict Detection ritual: before claiming an address, a host sends probes with the sender IP set to 0.0.0.0 (so it doesn't pollute caches if the address is taken), waits, and only then sends an announcement. This is the machinery behind "another device on the network is using your IP" warnings.

RARP (Reverse ARP). The mirror — map a known MAC to an unknown IP, used by diskless workstations in the 1980s to learn their own address at boot. Completely superseded by BOOTP and then DHCP, but worth knowing as the historical opposite of ARP.

Dynamic ARP Inspection (DAI). Not a protocol variant but a switch feature: the switch validates every ARP frame against a trusted IP-to-MAC binding table (built from DHCP snooping) and drops forgeries. It's the standard defense against ARP spoofing on enterprise gear.

Common bugs and edge cases

  • Trusting the reply. The number-one operational hazard. ARP has no authentication; a host that caches every reply it sees will happily believe an attacker. If you need integrity, you need DAI, static entries, or IPv6 SEND — ARP alone cannot give it to you.
  • Stale cache after a NIC swap or failover. Replace a server's network card (new MAC, same IP) and peers keep sending to the dead MAC until their entry ages out — minutes of blackholed traffic. The fix is a gratuitous ARP from the new card.
  • Forgetting opportunistic learning. A naive implementation only caches replies, then re-broadcasts to talk back to a host that just messaged it. Real stacks cache the sender of every ARP frame, request included.
  • Broadcast storms on flat networks. Put 4,000 hosts in one broadcast domain and ARP traffic alone can saturate CPUs. The cure is structural — smaller subnets and VLANs — not protocol tuning.
  • Mismatched cache timers across failover. A 4-hour Cisco timer plus a virtual IP that failed over means traffic blackholes for hours unless a gratuitous ARP forces an update. Always emit a gratuitous ARP on failover; never rely on natural expiry.
  • Cross-subnet confusion. ARP only resolves IPs on the local segment. For an off-subnet destination the host must ARP for the gateway's IP, not the destination's. Resolving the wrong one is a classic beginner mistake when hand-rolling a stack.

Frequently asked questions

Why does ARP exist if the packet already has the IP address?

Routers and switches on the local link don't read IP addresses — Ethernet frames are delivered by 48-bit MAC address. IP lives in layer 3; the wire is layer 2. ARP is the glue that translates the layer-3 destination into the layer-2 address the NIC must put in the frame header to actually deliver it on the local segment.

What's the difference between an ARP request and an ARP reply?

The request is a broadcast — destination MAC ff:ff:ff:ff:ff:ff — so every host on the segment sees it, but only the owner of the target IP answers. The reply is a unicast sent straight back to the requester's MAC. Request floods the whole segment; reply is point-to-point.

How long does an ARP cache entry live?

It depends on the OS. Linux uses an adaptive timer (base_reachable_time defaults to 30 seconds, randomized to 15–45 s) and revalidates entries that are still in use. Windows uses 15–45 seconds for fresh entries and up to 10 minutes for in-use ones. Cisco IOS defaults to a flat 4 hours. There is no value in the protocol itself — RFC 826 leaves expiry entirely to the implementation.

What is a gratuitous ARP?

A host broadcasting an ARP for its own IP, unprompted. It serves three purposes: detecting duplicate IPs at boot, pre-populating neighbors' caches, and announcing a MAC change when a failover or virtual IP moves to a new machine. It's an answer nobody asked the question for.

Why is ARP a security risk?

ARP has no authentication. Any host can reply to any request, or send an unsolicited reply, claiming to own an IP it doesn't. An attacker who poisons the gateway's MAC in your cache becomes a silent man-in-the-middle for all your traffic. Mitigations live outside ARP itself: Dynamic ARP Inspection on the switch, static entries, or moving to IPv6's authenticated SEND.

Does IPv6 use ARP?

No. IPv6 replaces ARP with the Neighbor Discovery Protocol (NDP), built on ICMPv6. NDP uses multicast Neighbor Solicitation messages instead of broadcast, so it bothers far fewer hosts, and it can be hardened with Secure Neighbor Discovery (SEND). The role is identical — find the link-layer address for an IP — but the mechanism is different.