DEV Community

pathvector-dev
pathvector-dev

Posted on • Originally published at blog.pathvector.dev

ARP Explained: How IPv4 Turns an IP Address into a MAC by Shouting at Everyone

Originally published at https://blog.pathvector.dev/protocol-lab-arp-24/ — part of the free Protocol Lab series.

This post is part of Protocol Lab, a free, hands-on series for learning networking protocols by building and breaking them in a container lab. All the lab material — topologies, configs, and scripts — lives in the repo: github.com/pathvector-studio/protocol-lab.

In Lab #23, we watched IPv6 Neighbor Discovery find a neighbor's MAC address. This lab shows the IPv4 original that NDP replaced: ARP (Address Resolution Protocol). Same job — turn an IP address into the MAC address needed to actually deliver a frame — but ARP does it by broadcasting to everyone on the link.

Reading guide: rfc-notes/arp-address-resolution.md

Prerequisites: Lab 23: IPv6 Neighbor Discovery, TCP Lab 07

Expected time: 35–50 minutes.

The Goal

You will watch the resolution happen, step by step:

  • Two IPv4 nodes share a link (10.0.0.1 and 10.0.0.2).
  • With the ARP cache cleared, node-a pings node-b.
  • node-a broadcasts an ARP request — "who has 10.0.0.2? tell 10.0.0.1" — to ff:ff:ff:ff:ff:ff.
  • node-b sends a unicast ARP reply — "10.0.0.2 is at aa:...:b2".
  • node-a's ARP table now maps 10.0.0.2 → MAC.

By the end, you should be able to complete this comparison with Lab 23:

ARP (this lab, IPv4) NDP (Lab 23, IPv6)
"who has X?" sent to broadcast (everyone) solicited-node multicast (few)
Protocol its own EtherType 0x0806 ICMPv6
Request / Reply ARP request / reply Neighbor Solicitation / Advertisement
Cache ARP table (ip neigh) neighbor cache (ip -6 neigh)

What You Will Learn

  • Why an IP address alone cannot deliver a frame on a link — you need the MAC.
  • How ARP resolves IP → MAC with a broadcast request and a unicast reply.
  • What the ARP request and reply carry ("who-has", "is-at").
  • Where the ARP cache lives (ip neigh) and why it exists.
  • Why broadcast is simple but less efficient than NDP's multicast.

This lab does not cover:

  • Gratuitous ARP, ARP probe/announce (RFC 5227), or proxy ARP.
  • ARP spoofing / security.
  • RARP or the historical BOOTP relationship.

Where to Read in the RFCs

RFC Section What to focus on
RFC 826 whole document ARP's packet format and resolution algorithm (it's short)
RFC 826 "Packet format" hardware/protocol type, op (1=request, 2=reply)
RFC 1122 2.3.2 How the ARP cache is handled (host requirements)

The Big Picture

Two nodes with IPv4 addresses are connected by a single link.

node-a 10.0.0.1/24 ==== eth1/eth1 ==== node-b 10.0.0.2/24
Enter fullscreen mode Exit fullscreen mode

Clear node-a's ARP cache and ping, and ARP resolution kicks in.

sequenceDiagram
  participant A as node-a (10.0.0.1)
  participant BC as broadcast<br/>ff:ff:ff:ff:ff:ff
  participant B as node-b (10.0.0.2)

  Note over A: cache cleared; wants 10.0.0.2's MAC
  A->>BC: ARP request "who has 10.0.0.2? tell 10.0.0.1"
  Note over B: that's me
  B->>A: ARP reply "10.0.0.2 is-at aa:...:b2" (unicast)
  Note over A: ARP table: 10.0.0.2 -> aa:...:b2
Enter fullscreen mode Exit fullscreen mode

10.0.0.0/24 is a local, closed range.

Note: Everything here uses local address space, so nothing in this lab touches the real internet.

What You Need

Recommended environment:

  • Linux / WSL2 / a Linux VM
  • Docker
  • containerlab

Images used:

  • nicolaka/netshoot:latest — bundles ip, ping, and tcpdump (with ARP decoding).

No additional images are required.

Running the Lab

The quick path, which deploys, verifies, and tears down for you:

./scripts/labctl.sh run arp-24
Enter fullscreen mode Exit fullscreen mode

Or step through it manually:

1. Move into the working directory

cd protocol-lab/examples/arp-24
Enter fullscreen mode Exit fullscreen mode

2. Deploy

sudo containerlab deploy -t arp-24.clab.yml
Enter fullscreen mode Exit fullscreen mode

3. Clear the ARP cache and observe the resolution

docker exec clab-arp-24-node-a ip neigh flush all
docker exec -d clab-arp-24-node-a tcpdump -i eth1 -n -e "arp"
docker exec clab-arp-24-node-a ping -c2 10.0.0.2
docker exec clab-arp-24-node-a pkill -INT tcpdump
docker exec clab-arp-24-node-a tcpdump -n -e -vv -r /tmp/arp.pcap
Enter fullscreen mode Exit fullscreen mode

What to look for:

aa:...:e2 > ff:ff:ff:ff:ff:ff, ARP, Request who-has 10.0.0.2 tell 10.0.0.1
aa:...:b2 > aa:...:e2, ARP, Reply 10.0.0.2 is-at aa:...:b2
Enter fullscreen mode Exit fullscreen mode

4. Look at the ARP table

docker exec clab-arp-24-node-a ip neigh show dev eth1
Enter fullscreen mode Exit fullscreen mode
10.0.0.2 lladdr aa:c1:ab:97:55:b2 REACHABLE
Enter fullscreen mode Exit fullscreen mode

Expected Output

  • ping 10.0.0.2 succeeds.
  • The capture shows Request who-has 10.0.0.2 tell 10.0.0.1 (destination ff:ff:ff:ff:ff:ff) and Reply 10.0.0.2 is-at <MAC>.
  • ip neigh shows 10.0.0.2 lladdr <MAC> REACHABLE.

Why It Works

To deliver a frame on the same link, you need the destination's MAC address. An IP address identifies which host, but Ethernet delivers frames by MAC. In IPv4, ARP is the mechanism that resolves this "IP → MAC" mapping.

  • Broadcast request. node-a doesn't know node-b's MAC. So it addresses the frame to ff:ff:ff:ff:ff:ff (broadcast) and asks everyone: "who has 10.0.0.2? tell 10.0.0.1". Every host on the link receives it.
  • Unicast reply. Only the matching host, node-b, answers: "that's me, here's my MAC." Since the request carries the sender's (node-a's) IP and MAC, the reply can be unicast straight back to node-a — no need to answer everyone.
  • ARP cache. The learned "IP → MAC" mapping is cached (ip neigh). Subsequent traffic uses the cache instead of broadcasting every time. Entries are re-verified once they get stale.
  • Difference from NDP (Lab 23). The role is exactly the same: IP → MAC. The difference is how you ask. ARP broadcasts and wakes up everyone on the link. NDP sends only to the target's solicited-node multicast address, so unrelated hosts are never disturbed. This efficiency (and design cleanup) is why IPv6 abolished broadcast and standardized on multicast.

The key insight: Ethernet delivers by MAC, so you need a mechanism to look up a MAC from an IP. IPv4 does it with broadcast ARP; IPv6 does it with multicast NDP.

Common Pitfalls

  • Thinking an IP address alone gets the packet there. Delivery on a link is by MAC. ARP is what looks it up.
  • Thinking both request and reply are broadcast. The request is broadcast; the reply is unicast.
  • Thinking ARP works across routers. ARP only works within a single link (L2). For a host on another segment, you ARP for the gateway's MAC instead.
  • Forgetting the cache. Once resolved, the mapping is cached — hosts don't ask every time.
  • Thinking NDP does a different job. The job is the same. Only the means differ (broadcast vs multicast, its own EtherType vs ICMPv6).

Cleanup

sudo containerlab destroy -t arp-24.clab.yml --cleanup
Enter fullscreen mode Exit fullscreen mode

If you used labctl.sh run arp-24, the script runs destroy for you at the end.

Check Your Understanding

  1. Besides an IP address, what do you need to deliver a frame on the same link, and why?
  2. Are the ARP request and reply broadcast or unicast, respectively? Why is that possible?
  3. What do "who-has" and "tell" in an ARP request mean?
  4. What goes into the ARP cache, and why cache at all?
  5. When sending to a host on another segment (across a router), whose MAC does ARP resolve?
  6. Explain the difference between ARP (IPv4) and NDP (IPv6) in terms of how they ask.

References

Verified Run Log (2026-07-07)

This lab has been confirmed reproducible on real hardware.

Environment:

  • Ubuntu 26.04 LTS (kernel 7.0.0-27-generic, x86_64)
  • Docker 29.1.3
  • containerlab 0.77.0
  • node-a / node-b: nicolaka/netshoot:latest (with tcpdump)

Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run arp-24 performed deploy → verify → destroy, and verification.json returned "status": "verified".

ARP request (broadcast) → reply (unicast)

$ docker exec clab-arp-24-node-a tcpdump -n -e -vv -r arp.pcap
aa:c1:ab:10:4e:e2 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806) ...
    Request who-has 10.0.0.2 tell 10.0.0.1, length 28
aa:c1:ab:97:55:b2 > aa:c1:ab:10:4e:e2, ethertype ARP (0x0806) ...
    Reply 10.0.0.2 is-at aa:c1:ab:97:55:b2, length 28
Enter fullscreen mode Exit fullscreen mode

The request goes to ff:ff:ff:ff:ff:ff (broadcast): "who has 10.0.0.2? tell 10.0.0.1". Only the matching host, node-b, replies — by unicast — with "10.0.0.2 is at aa:c1:ab:97:55:b2".

The ARP table after resolution

$ docker exec clab-arp-24-node-a ip neigh show dev eth1
10.0.0.2 lladdr aa:c1:ab:97:55:b2 REACHABLE
Enter fullscreen mode Exit fullscreen mode

10.0.0.2 enters the cache with its MAC, marked REACHABLE. From here on, this cache is used.

Put side by side with Lab 23 (NDP), the role is identical — "IP → MAC" — and the difference is how you ask: ARP broadcasts to everyone, NDP multicasts to just the relevant host. That's exactly why IPv6 got rid of broadcast.

Cleanup

containerlab destroy -t arp-24.clab.yml --cleanup
Enter fullscreen mode Exit fullscreen mode

That's ARP: when IPv4 needs a neighbor's MAC, it simply asks the whole link and caches the answer — the simple broadcast original that IPv6's multicast NDP later refined.

Explore the full Protocol Lab series here: github.com/pathvector-studio/protocol-lab. If these labs are useful to you, please ⭐ star the repo on GitHub — it genuinely helps others find the project.

Next up, we'll keep climbing the stack — see what happens after the link layer knows where to deliver.

Top comments (0)