The purpose of IPv6 neighbor discovery is to take a known IPv6 address on an attached subnet and obtain the corresponding MAC address. That enables you to encapsulate a L3 IPv6 packet in a L2 frame and send it on the L2 medium.
This is very similar to IPv4 ARP, where you take a known IPv4 address on an attached subnet, and obtain the corresponding MAC address.
The trick is, you need to get your information by sending an L2 frame. You need to have a valid L2 destination address for that frame. But you don't already have the L2 unicast destination for the frame.
Lets look at the IPv4 ARP example:
Here is an IPv4 ARP for 192.168.0.19 from 192.168.0.10:
192.168.0.10 = c0a8 000a
192.168.0.19 = c0a8 0013
My ethernet = 685b 3589 0a04
[iMac:~] droot% sudo tcpdump arp -x
tcpdump: data link type PKTAP
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on pktap, link-type PKTAP (Apple DLT_PKTAP), capture size 262144 bytes
18:58:01.528290 ARP, Request who-has 192.168.0.19 tell 192.168.0.10, length 28
0x0000: ffff ffff ffff 685b 3589 0a04 0806 0001
0x0010: 0800 0604 0001 685b 3589 0a04 c0a8 000a
0x0020: 0000 0000 0000 c0a8 0013
In the case above, in order to guarantee that the frame would be decoded by the destination, which has an UNKNOWN L2 MAC address, ARP sends the L2 ethernet frame to destination ff.ff.ff.ff.ff.ff, the ethernet broadcast address.
This ARP protocol, designed in 1982 (RFC 826) is inefficient. Every ARP request results in an interrupt on every host on the subnet.
The designers of IPv6 wanted to do better. They wanted only the intended destination to "listen" for the neighbor discovery packet. They would have loved to make the destination of the ethernet frame the IPv6 destination.
One problem: The destination MAC address is 48 bits. The IPv6 address is 128 bits.
Here's an IPv6 neighbor discovery/solicitation request for link-local address fe80::aaaa:aaaa:aaaa:aaaa from fe80::c0e:acdb:30a3:482c
[iMac:~] droot% sudo tcpdump -x -v icmp6 && ip6 == 135
tcpdump: data link type PKTAP
tcpdump: listening on pktap, link-type PKTAP (Apple DLT_PKTAP), capture size 262144 bytes
19:33:58.748554 IP6 (hlim 255, next-header ICMPv6 (58) payload length: 32) imac.local > ff02::1:ffaa:aaaa: [icmp6 sum ok] ICMP6, neighbor solicitation, length 32, who has fe80::aaaa:aaaa:aaaa:aaaa
source link-address option (1), length 8 (1): 68:5b:35:89:0a:04
0x0000: 3333 ffaa aaaa 685b 3589 0a04 86dd 6000
0x0010: 0000 0020 3aff fe80 0000 0000 0000 0c0e
0x0020: acdb 30a3 482c ff02 0000 0000 0000 0000
0x0030: 0001 ffaa aaaa 8700 4cfc 0000 0000 fe80
0x0040: 0000 0000 0000 aaaa aaaa aaaa aaaa 0101
0x0050: 685b 3589 0a04
The first improvement is that this is actually an ICMPv6 packet destined for IPv6 multicast address ff02::1:ffaa:aaaa. We don't need to define a separate ARP-like protocol for each layer-2 technology. We use L3-multicast and define a L3-multicast to L2-multicast protocol. IPv6 neighbor discovery uses the ICMPv6 message type 135.
We cannot use the IPv6 unicast destination as the neighbor discovery destination, because we don't know how to encode the 128-bit IPv6 unicast address as a 48-bit ethernet unicast address.
But we can define a rule to pick a specific L3 multicast desination from our L3 unicast destination. From RFC4291 https://www.rfc-editor.org/rfc/rfc4291#section-2.7.1
Solicited-Node multicast address are computed as a function of a
node's unicast and anycast addresses. A Solicited-Node multicast
address is formed by taking the low-order 24 bits of an address
(unicast or anycast) and appending those bits to the prefix
FF02:0:0:0:0:1:FF00::/104 resulting in a multicast address in the
So our IPv6 unicast request for fe80::aaaa:aaaa:aaaa:aaaa becomes an ICMPv6 neighbor solicitation request for ff02::1:ffaa:aaaa which is an IPv6 multicast address.
We have a rule for IPv6 L3 multicast addresses: they can be transmitted on the wire with an L2 destination of 33:33:xx:xx:xx:xx where the last 32 bits of the L3 address are translated into the last 32 bits of the L2 address!
That results in this destination mac address:
fe80::aaaa:aaaa:aaaa:aaaa -> ff02::1:ffaa:aaaa -> 3333.ffaa.aaaa
What's cool about this is that this does not result in an interrupt for every host on the net. Only the hosts that are listening for L2 multicasts for 3333.ffaa.aaaa. Assuming the last 24 bits are "random" (not true), that would be 1 out of 16 million (2^24) hosts.
There is some complexity, but only because we have some rules:
- A rule to translate an IPv6 L3 unicast address into an IPv6 L3 multicast address for neighbor solicitation
- A rule to translate an IPv6 L3 multicast address into an L2 multicast address for transmission.
We cannot do this:
fe80::aaaa:aaaa:aaaa:aaaa (IPv6 unicast)
-> fe80::aaaa:aaaa:aaaa:aaaa (ICMPv6 multicast?)
We just don't know how to translate the IPv6 unicast address into an L2 address. Since we are going from 128 bits to 48 bits, at some point we have to use a many->one mapping. In IPv4 ARP we use ethernet broadcast (all->one). In IPv6 we use L3 multicast/L2 multicast (many->one).