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.1and10.0.0.2). - With the ARP cache cleared,
node-apingsnode-b. -
node-abroadcasts an ARP request — "who has10.0.0.2? tell10.0.0.1" — toff:ff:ff:ff:ff:ff. -
node-bsends a unicast ARP reply — "10.0.0.2is ataa:...:b2". -
node-a's ARP table now maps10.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
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
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— bundlesip,ping, andtcpdump(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
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/arp-24
2. Deploy
sudo containerlab deploy -t arp-24.clab.yml
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
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
4. Look at the ARP table
docker exec clab-arp-24-node-a ip neigh show dev eth1
10.0.0.2 lladdr aa:c1:ab:97:55:b2 REACHABLE
Expected Output
-
ping 10.0.0.2succeeds. - The capture shows
Request who-has 10.0.0.2 tell 10.0.0.1(destinationff:ff:ff:ff:ff:ff) andReply 10.0.0.2 is-at <MAC>. -
ip neighshows10.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-adoesn't knownode-b's MAC. So it addresses the frame toff:ff:ff:ff:ff:ff(broadcast) and asks everyone: "who has10.0.0.2? tell10.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 tonode-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
If you used labctl.sh run arp-24, the script runs destroy for you at the end.
Check Your Understanding
- Besides an IP address, what do you need to deliver a frame on the same link, and why?
- Are the ARP request and reply broadcast or unicast, respectively? Why is that possible?
- What do "who-has" and "tell" in an ARP request mean?
- What goes into the ARP cache, and why cache at all?
- When sending to a host on another segment (across a router), whose MAC does ARP resolve?
- Explain the difference between ARP (IPv4) and NDP (IPv6) in terms of how they ask.
References
- RFC 826: An Ethernet Address Resolution Protocol
- RFC 1122: Requirements for Internet Hosts — Communication Layers
- RFC 5227: IPv4 Address Conflict Detection
- RFC 5737: IPv4 Address Blocks Reserved for Documentation
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(withtcpdump)
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
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
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
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)