Networking

IPv6 Deep Dive: Link-Local, SLAAC, and Neighbor Discovery

If you’ve been writing IPv4 long enough to be comfortable with subnets, masks, and the magic-number trick, IPv6 is going to feel uncomfortable for about a week. The hex looks alien, the addresses are absurdly long, and there are three addresses on every interface where IPv4 had one. The good news: under the surface, IPv6 is doing the same job as IPv4 with a few of the warts removed. The bad news: NAT is gone, ARP is gone, broadcast is gone, and DHCP is optional. This lesson is about the things that genuinely change when you switch protocols, and the things that just look different but mean the same thing.

A screen showing rows and columns of cyan and green numbers
IPv6 has 2128 possible addresses — about 340 undecillion. That’s enough that we could give every grain of sand on Earth its own /64 subnet and still not run out. Photo: Tibe De Kort, Pexels.

This is lesson 14 of Networking from Scratch — the first deep-dive following the foundation tier. We covered IPv6 briefly in lesson 2 as “the longer addresses we’re slowly migrating to.” This article is the actual mechanics: how an address is built, how a host gets one without DHCP, how machines find each other on the wire when there’s no ARP, and the handful of tools that turn IPv6 from intimidating to debuggable.

Why IPv6 exists at all

IPv4 has 232 possible addresses — about 4.3 billion. That sounded like a lot in 1981. By the early 2010s every regional internet registry except AfriNIC had run out, and we’ve been duct-taping over the shortage with NAT, CGNAT, and a market in recycled address blocks ever since. IPv6 is the proper fix: 128-bit addresses, leaving us with 2128 — a number so large that it has no practical comparison except “every device, forever, with room to spare.”

But IPv6 isn’t just “IPv4 with bigger numbers.” It’s a redesigned protocol that fixes a few things people had been complaining about for years:

  • NAT becomes unnecessary. Every device can have a globally routable address again, the way the internet was originally meant to work. No more port-forwarding to expose a server.
  • The header is simpler. Optional fields and checksums got moved out of the main header into extension headers, which speeds up forwarding decisions on routers.
  • Address autoconfiguration is built in. A host can pick its own address from a router advertisement, no DHCP server required.
  • Broadcast is removed. Replaced with multicast (which is more efficient) and link-local communication.
  • ARP is replaced with Neighbor Discovery, which uses ICMPv6 instead of a separate L2 protocol.

The catch: the changes are deep enough that “IPv6 mode” is its own learning curve, even for experienced engineers. Most of what follows is about that curve.

The 128-bit address, written down

An IPv6 address is 128 bits. Written in full, it’s eight groups of four hexadecimal digits separated by colons:

2001:0db8:0000:acab:0000:ff00:0042:8329

Each group is 16 bits (four hex digits, since each hex digit is 4 bits). 8 groups × 16 bits = 128 bits. That’s the whole format, and you almost never write it that way because two compression rules dramatically shorten what most addresses look like in practice:

  1. Drop leading zeros within a group. 0db8 becomes db8; 0000 becomes 0; 0042 becomes 42. Trailing zeros stay.
  2. Replace one run of all-zero groups with ::. You can do this once per address. The receiving software fills in however many :0: groups are needed to make the total length 128 bits again.

So our example becomes:

2001:db8:0:acab:0:ff00:42:8329

It has two single-zero groups, but neither is a long enough run to be worth using :: for. If we instead had 2001:db8:0:0:0:0:0:1, we’d compress it to 2001:db8::1 — the most common form you’ll see for example addresses in documentation.

Why only one ::? Because if there were two, the receiver couldn’t tell how many zero groups each represented — the address would be ambiguous. With one, basic arithmetic recovers the missing groups (8 minus the count of groups you wrote).

Prefix length notation

Like IPv4, IPv6 networks are written with CIDR notation: /N where N is the prefix length in bits. The most common values:

Prefix Meaning
/128 A single host (the IPv6 equivalent of a /32)
/64 One subnet on a single link — the standard, almost universal choice
/56 or /48 What a residential or business customer typically gets allocated by an ISP
/32 What a regional ISP gets allocated by a registry

The first thing that surprises IPv4 people is the /64 default for subnets. With IPv4 you carve a network into the smallest subnets that fit your host count to conserve addresses. With IPv6 you don’t. A /64 has 264 addresses (eighteen quintillion) per subnet, which is more than enough — and several IPv6 features (notably SLAAC, below) require a /64. Don’t try to be clever with /96s and /112s. Use /64s.

The three addresses on every interface

This is the second thing that surprises IPv4 people. In IPv6, every interface that’s up has at least three active addresses simultaneously, each with a different scope:

Scope Prefix What it’s for
Link-local fe80::/10 The same physical wire only — never routed
Unique local (ULA) fc00::/7 Private, like RFC 1918 — routable internally but not on the public internet
Global unicast 2000::/3 Routable on the public internet — your real address

Plus a solicited-node multicast address (ff02::1:ff__:____) that’s used by Neighbor Discovery, and the all-nodes (ff02::1) and all-routers (ff02::2) multicast groups. We’ll come back to those.

Link-local addresses (fe80::/10) — the always-on baseline

Every IPv6 interface gets a link-local address automatically the moment it comes up — before any router has been heard from, before SLAAC has run, before DHCP has happened. Link-local addresses always start with fe80:: and are followed by an interface identifier (more on that below). Two examples:

fe80::1                          (manually configured)
fe80::1234:5678:9abc:def0        (auto-generated)

Two important properties:

  • They never leave the wire. Routers are required to drop any packet with a link-local source or destination. They’re local-only by design.
  • The same address can exist on every interface. If you have three NICs, each can have fe80::1 with no conflict, because link-local scope is per-interface. To say which one you mean, append a zone identifier: fe80::1%eth0 on Linux, fe80::1%12 on Windows.

Link-local is what IPv6 uses internally for everything that has to work before the network is configured: Neighbor Discovery, Router Solicitations, Router Advertisements. Routing protocols like OSPFv3 and EIGRPv6 form their adjacencies over link-local addresses. You’ll see fe80:: all over your routing tables and you can mostly ignore it — it’s the protocol talking to itself.

Unique Local Addresses (fc00::/7) — the new RFC 1918

ULAs fill the same role 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 fill in IPv4: routable inside your organisation, not routable on the public internet. The official block is fc00::/7, though by spec you should generate your own /48 inside fd00::/8 using a pseudo-random algorithm so two organisations that merge don’t collide.

Whether to use ULAs at all is a matter of taste. The IPv6 design philosophy is “everything gets a global address, no NAT,” which makes ULAs feel redundant when ISPs hand out /48s for free. But many networks still want a stable internal address that doesn’t change if the ISP renumbers, and ULAs serve that need.

Global unicast (2000::/3) — the public face

The address that anyone on the internet can reach. Your ISP allocates a prefix (typically /48 or /56), you assign /64s to your internal links, and hosts pick up addresses inside those /64s either via SLAAC or DHCPv6. The first three bits of any global unicast address are 001, which is why they all start with 2 or 3 in hex. Anything starting with 2001: is a real address.

SLAAC: how a host gets an address with no DHCP server

This is the IPv6 superpower. Plug a host into a network that has an IPv6 router, and within a few seconds it has a working global unicast address — no DHCP server required. The mechanism is called SLAAC (Stateless Address Autoconfiguration) and it’s built into the protocol.

The choreography:

  1. Host comes up. It generates a link-local address (fe80::<interface-id>) and sends a Neighbor Solicitation to itself to make sure no one else is using that address (Duplicate Address Detection).
  2. Host sends a Router Solicitation to the all-routers multicast group ff02::2: “Any routers on this link, please introduce yourselves.”
  3. The router replies with a Router Advertisement to the all-nodes multicast group ff02::1. The RA contains the on-link prefix (typically a /64), the default router’s link-local address, and flags that tell the host how to fill in the rest.
  4. The host combines the prefix with its own interface identifier, runs Duplicate Address Detection again to make sure the resulting address is free, and starts using it.

That’s the whole process. The host never asks a DHCP server for an address. The router just announces what prefix to use, and every host fills in the back half itself.

The interface identifier

The lower 64 bits of a SLAAC address — the “interface ID” — is what the host generates locally. There are two main ways:

  • Modified EUI-64. Take the 48-bit MAC address, split it down the middle, insert fffe, and flip the seventh bit. So MAC 00:11:22:33:44:55 becomes 0211:22ff:fe33:4455. This was the original SLAAC scheme.
  • Random / privacy extensions (RFC 4941, now RFC 8981). Generate a random 64-bit interface ID, change it periodically. This is what every modern OS does by default, because EUI-64 leaks the MAC address (and therefore the device identity) to every server you connect to.

You’ll typically see two SLAAC global addresses on a modern host: one stable (used for incoming connections) and one temporary (used for outgoing connections, rotated daily-ish). That’s by design.

Neighbor Discovery: the protocol that replaced ARP

In IPv4, when a host needs to talk to another host on the same wire, it sends an ARP broadcast: “Who has 10.0.0.5? Tell me your MAC.” Every host on the wire has to look at the broadcast and decide whether it cares. This works but is wasteful.

IPv6 replaces ARP with Neighbor Discovery (ND), which runs on top of ICMPv6 and uses multicast instead of broadcast. The four ICMPv6 message types you’ll see most:

Type Name What it does
133 Router Solicitation (RS) Host asks “any routers here?”
134 Router Advertisement (RA) Router announces prefix + flags
135 Neighbor Solicitation (NS) “What’s the MAC for IPv6 X?” (replaces ARP request)
136 Neighbor Advertisement (NA) “The MAC for X is Y” (replaces ARP reply)

The clever part: instead of broadcasting to every host on the wire, an NS goes to the solicited-node multicast group derived from the target’s address. The address is ff02::1:ff__:____, where the last 24 bits come from the target’s interface ID. Any host whose address ends in those 24 bits joins that multicast group; everyone else ignores the packet at the NIC level. Same outcome as ARP, much less traffic that uninvolved hosts have to process.

Multicast replaces broadcast

IPv6 has no broadcast. The all-nodes multicast group ff02::1 reaches every host on the link, which is the closest equivalent, but you almost never send to it directly. Instead you use the more specific multicast groups baked into the protocol:

Group Members
ff02::1 All nodes on the link
ff02::2 All routers on the link
ff02::5 All OSPFv3 routers
ff02::6 All OSPFv3 designated routers
ff02::1:ff__:____ The solicited-node group for one specific host

The benefit is mostly efficiency — broadcast wakes up every host’s CPU; multicast can be filtered at the NIC for hosts that aren’t in the group. On a busy network with thousands of hosts, this matters.

Verifying it on a real machine

Three commands cover almost everything you’ll need to debug:

Listing addresses

Linux (any modern distro):

ip -6 addr show

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP
    inet6 2001:db8:1234::a1b2/64 scope global temporary dynamic
       valid_lft 86391sec preferred_lft 14391sec
    inet6 2001:db8:1234::1/64 scope global dynamic mngtmpaddr
       valid_lft 86391sec preferred_lft 14391sec
    inet6 fe80::a00:27ff:fe11:2233/64 scope link
       valid_lft forever preferred_lft forever

Windows:

ipconfig /all

You should see at least one fe80:: link-local address and (on a network with IPv6 enabled) at least one global address starting with 2 or 3. Often two globals: a stable one and a temporary privacy address.

Pinging

Linux: ping -6 hostname or ping6 hostname. Windows: ping -6 hostname. To ping a link-local address, append the zone:

ping -6 fe80::1%eth0      # Linux
ping -6 fe80::1%12        # Windows

Listing the neighbor cache

The IPv6 equivalent of arp -a:

ip -6 neigh show          # Linux
netsh interface ipv6 show neighbors    # Windows

Output is a list of (IPv6 address, MAC address, state) entries — STALE, REACHABLE, DELAY, or PROBE. Same idea as the ARP cache, but with a richer state machine.

The dual-stack reality

Twenty-five years after the IPv6 spec was finalised, most networks still run both protocols simultaneously. The reasons are practical:

  • Some legacy applications and devices still don’t do IPv6 properly — especially older industrial gear, embedded devices, and some VoIP systems.
  • Many enterprise firewalls and monitoring tools were retrofitted with IPv6 support and the IPv6 paths get less testing than the IPv4 paths.
  • Your internet uplink probably hands you both, and there’s no benefit to disabling IPv4 if any of your traffic goes to IPv4-only destinations (which is still most of them by traffic share, despite IPv6 carrying the majority of mobile traffic in some regions).

The pragmatic stance: run dual-stack, don’t disable IPv6. Disabling IPv6 on an interface is a known anti-pattern — it breaks features that assume v6 is available (Windows Updates, some mDNS-based discovery, Microsoft Teams in some configurations) and rarely fixes whatever someone thought it would fix. If a host has an IPv6 problem, debug the problem; don’t turn off the protocol.

What you can now answer

  • How long is an IPv6 address? — 128 bits, written as 8 groups of 4 hex digits, often compressed with ::.
  • What’s a /64 and why is it the default? — A single subnet of 264 addresses; SLAAC depends on it.
  • What does fe80:: mean? — A link-local address, scoped to one wire, never routed.
  • How does a host get an IPv6 address without DHCP? — SLAAC: it asks the router for the prefix and fills in the host part itself.
  • What replaces ARP? — Neighbor Discovery using ICMPv6 NS/NA messages and solicited-node multicast.
  • What replaces broadcast? — Multicast, with specific groups like ff02::1 (all nodes) and ff02::2 (all routers).
  • Why does an interface have multiple addresses? — Different scopes (link-local + global) and modern privacy extensions add a temporary global on top of the stable one.

What’s next

The next deep-dive lessons cover NAT in detail (the trick that’s kept IPv4 alive longer than it should have, and why IPv6 designed it out), then VLANs and trunking (slicing one physical wire into many logical ones), Wi-Fi tuning (channel planning, roaming, the things lesson 6 only touched on), and network troubleshooting tools (ping/traceroute/mtr/tcpdump/wireshark in earnest). After that come three hands-on labs: building a home network in software, capturing packets, and standing up a VPC in AWS.

Leave a Reply