IPv6 Killed ARP: Watching Neighbor Discovery Resolve a MAC Address, Packet by Packet
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.
To send a packet to a neighbor on the same link, a host needs that neighbor's MAC address. On IPv4 that job is ARP. On IPv6 it is Neighbor Discovery (NDP) — the same idea, but done with ICMPv6 and multicast instead of broadcast. In this lab we watch the resolution happen on the wire.
Reading guide: rfc-notes/ndp-neighbor-discovery.md
Prerequisite: TCP Lab 07: Handshake and Teardown (reading captures)
Expected time: 40–55 minutes.
The Goal
Two IPv6 nodes share a link (2001:db8:23::1 and ::2), and we trigger address resolution from scratch:
- With the neighbor cache cleared,
node-apingsnode-b. node-asends a Neighbor Solicitation (ICMPv6 type 135) tonode-b's solicited-node multicast address — "who has2001:db8:23::2?"node-breplies with a Neighbor Advertisement (type 136) carrying its MAC.node-a's neighbor table now maps2001:db8:23::2 → MAC.
By the end, you should be able to compare ARP and NDP:
| ARP (IPv4) | NDP (IPv6) | |
|---|---|---|
| Carried by | its own EtherType | ICMPv6 |
| "who has X?" sent to | broadcast (everyone) | solicited-node multicast (few) |
| Request / Reply | ARP request / reply | Neighbor Solicitation / Advertisement |
| Cache | ARP table | neighbor cache |
What You Will Learn
- That IPv6 replaces ARP with Neighbor Discovery, built on ICMPv6.
- What a Neighbor Solicitation and Neighbor Advertisement are.
- What a solicited-node multicast address is (
ff02::1:ffXX:XXXX) and why it is more targeted than broadcast. - Where the neighbor cache is (
ip -6 neigh) and what states it has. - That NDP also does other jobs (router discovery, DAD) — here we focus on address resolution.
This lab does not cover:
- Router Advertisement / SLAAC auto-configuration (needs a router advertising a prefix).
- Duplicate Address Detection (DAD) in depth, or Secure Neighbor Discovery (SEND).
- IPv6 addressing/subnetting beyond what the lab uses.
Where to Read in the RFCs
| Reference | What to focus on |
|---|---|
| RFC 4861 §4.3–4.4 | Message formats for Neighbor Solicitation / Advertisement |
| RFC 4861 §7.2 | The address-resolution flow (NS → NA, caching) |
| RFC 4291 §2.7.1 | How a solicited-node multicast address is constructed |
| RFC 4861 §7.3 | Neighbor cache states (INCOMPLETE / REACHABLE / STALE) |
The Big Picture
Two IPv6 nodes are connected by a single link.
node-a 2001:db8:23::1/64 ==== eth1/eth1 ==== node-b 2001:db8:23::2/64
Clear node-a's neighbor cache and ping — NDP address resolution kicks in.
sequenceDiagram
participant A as node-a (::1)
participant M as solicited-node multicast<br/>ff02::1:ff00:2
participant B as node-b (::2)
Note over A: cache cleared; wants ::2's MAC
A->>M: Neighbor Solicitation "who has 2001:db8:23::2?"
Note over B: B listens on ff02::1:ff00:2
B->>A: Neighbor Advertisement "::2 is at aa:...:cb"
Note over A: neighbor cache: ::2 -> aa:...:cb (REACHABLE)
Note:
2001:db8::/32is the IPv6 documentation prefix (RFC 3849), so nothing in this lab touches the real internet.
What You Need
Recommended environment:
- Linux / WSL2 / a Linux VM (IPv6 enabled — the default on any recent Linux)
- Docker
- containerlab
Images used:
nicolaka/netshoot:latest— bundlesip,ping6, andtcpdump(with ICMPv6 decoding).
No additional images are required.
Running the Lab
The quick path, which deploys, verifies, and tears down for you:
./scripts/labctl.sh run ndp-23
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/ndp-23
2. Deploy
sudo containerlab deploy -t ndp-23.clab.yml
docker exec clab-ndp-23-node-a ip -6 addr show eth1 # 2001:db8:23::1 plus link-local fe80::
3. Clear the neighbor cache and watch the resolution
docker exec clab-ndp-23-node-a ip -6 neigh flush all
docker exec -d clab-ndp-23-node-a tcpdump -i eth1 -n -e "icmp6"
docker exec clab-ndp-23-node-a ping6 -c2 2001:db8:23::2
docker exec clab-ndp-23-node-a pkill -INT tcpdump
docker exec clab-ndp-23-node-a tcpdump -n -e -vv -r /tmp/ndp.pcap
What to look for:
... > 33:33:ff:00:00:02 ... 2001:db8:23::1 > ff02::1:ff00:2: ICMP6, neighbor solicitation, who has 2001:db8:23::2
... 2001:db8:23::2 > 2001:db8:23::1: ICMP6, neighbor advertisement, tgt is 2001:db8:23::2, Flags [solicited, override]
4. Inspect the neighbor cache
docker exec clab-ndp-23-node-a ip -6 neigh show dev eth1
2001:db8:23::2 lladdr aa:c1:ab:36:6f:cb REACHABLE
::2 is now in the cache with its MAC. From here on the cache is used — no solicitation on every packet.
Expected Output
ping6 2001:db8:23::2succeeds.- The capture shows a
neighbor solicitation(sent toff02::1:ff00:2) and aneighbor advertisement(carrying the MAC). ip -6 neighshows2001:db8:23::2 lladdr <MAC> REACHABLE.
Why It Works
An IP address identifies which host, but actually delivering a frame on the same link requires a MAC address. This "IP → MAC" resolution is ARP's job on IPv4 and NDP's job on IPv6.
- It rides on ICMPv6. NDP is not a standalone protocol; it is a set of ICMPv6 messages (types 133–137). Neighbor Solicitation = 135, Neighbor Advertisement = 136. That makes it a natural part of IPv6 itself.
- Solicited-node multicast. ARP broadcasts to everyone. NDP aims tighter: from the low-order 24 bits of the target address
2001:db8:23::2it forms the solicited-node multicast addressff02::1:ff00:2and sends the NS there. Only the very few hosts whose low 24 bits match are listening on that group — so unrelated hosts never get woken up. More efficient than broadcast. - NS → NA. The target (
node-b) listens on its own solicited-node multicast address, and on receiving the NS replies with a Neighbor Advertisement carrying its MAC.Flags [solicited, override]means "this answers a request / it may overwrite an existing cache entry." - The neighbor cache. The learned "IP → MAC" mapping is cached (
ip -6 neigh). Entries move through states such as INCOMPLETE (resolving) → REACHABLE (recently confirmed) → STALE (unused for a while). Subsequent traffic uses the cache instead of soliciting every time. - NDP's other jobs. Beyond address resolution, NDP also handles router discovery (RS/RA), prefix advertisement / SLAAC, and Duplicate Address Detection (DAD — sending an NS for your own address at assignment time to check for conflicts). This lab focuses on resolution, but it's all the same ICMPv6 framework.
The key insight: IPv6 does "IP → MAC" resolution with ICMPv6 and a tightly targeted multicast (solicited-node) — smarter than ARP's broadcast.
Common Pitfalls
- Treating NDP as something totally different from ARP. The role is identical (IP → MAC); only the mechanism changed to ICMPv6 + multicast.
- Assuming it's broadcast. IPv6 has no broadcast. It uses solicited-node multicast.
- How the solicited-node multicast address is built. Append the low-order 24 bits of the target address to
ff02::1:ff00:0/104. - Forgetting link-local. Every IPv6 interface also carries a
fe80::link-local address, and NDP works over link-local too. - Cache states. REACHABLE, STALE, and friends. Traffic can start even from STALE; the entry is re-confirmed when needed.
- DAD's NS. The moment an address is assigned, an NS also goes out (duplicate detection). It can show up in your capture mixed in with the ping-triggered NS.
Cleanup
sudo containerlab destroy -t ndp-23.clab.yml --cleanup
If you used labctl.sh run ndp-23, the script runs destroy for you at the end.
Check Your Understanding
- Besides the IP address, what does a host need to deliver an IP packet on the same link?
- What is IPv6's equivalent of IPv4's ARP? What does it ride on?
- What address is a Neighbor Solicitation sent to? How does that differ from broadcast?
- How is a solicited-node multicast address constructed?
- What goes into the neighbor cache? What states does it have?
- Besides address resolution, what other jobs does NDP do? Name two.
References
- RFC 4861: Neighbor Discovery for IP version 6 (IPv6)
- RFC 4291: IP Version 6 Addressing Architecture
- RFC 4862: IPv6 Stateless Address Autoconfiguration
- RFC 3849: IPv6 Address Prefix 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(ping6,tcpdump)
Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run ndp-23 performed deploy → verify → destroy, and verification.json returned "status": "verified".
Neighbor Solicitation → Advertisement (ARP, the IPv6 way)
$ docker exec clab-ndp-23-node-a tcpdump -n -e -vv -r ndp.pcap
aa:c1:ab:f1:f4:6f > 33:33:ff:00:00:02 ... 2001:db8:23::1 > ff02::1:ff00:2:
ICMP6, neighbor solicitation, who has 2001:db8:23::2
aa:c1:ab:36:6f:cb > aa:c1:ab:f1:f4:6f ... 2001:db8:23::2 > 2001:db8:23::1:
ICMP6, neighbor advertisement, tgt is 2001:db8:23::2, Flags [solicited, override]
node-a sends the NS to node-b's solicited-node multicast address (ff02::1:ff00:2, which is 33:33:ff:00:00:02 at layer 2), and node-b answers with an NA carrying its MAC. The difference from ARP: instead of broadcast, this is a multicast that reaches only the hosts whose low 24 bits match.
The neighbor cache after resolution
$ docker exec clab-ndp-23-node-a ip -6 neigh show dev eth1
2001:db8:23::2 lladdr aa:c1:ab:36:6f:cb REACHABLE
2001:db8:23::2 is in the cache with its MAC, state REACHABLE. Subsequent traffic uses the cache — no solicitation each time. IPv6 does its "IP → MAC" resolution with ICMPv6 and a targeted multicast: the successor to ARP.
Cleanup
containerlab destroy -t ndp-23.clab.yml --cleanup
That's Neighbor Discovery: same job as ARP, but carried by ICMPv6 and aimed at a solicited-node multicast group instead of shouting at the whole link.
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 look at what happens when a router joins the link — Router Advertisements and how IPv6 hosts configure themselves with SLAAC.