Chapter 8: When Addresses Are No Longer a Scarce Resource
Chapter Prelude: Historical Baggage and a New Starting Point
There is a number that haunts the world of network engineering like a ghost: $2^{32}$.
If you have even a passing familiarity with networking, this number needs no explanation—it is the theoretical total of IPv4 addresses. In the real world, however, inefficient address allocation and historical fragmentation led to the official depletion of the IPv4 address pool in 2011. Since then, the internet has been like a patient on dialysis—kept alive by NAT (Network Address Translation).
But NAT is fundamentally a hack. It breaks the original end-to-end design philosophy of the internet, complicates P2P communication, and makes protocol design clumsy.
The IPv6 we discuss in this chapter is not simply a matter of stretching addresses from 32 bits to 128 bits. It stands on the shoulders of the IPv4 giant (or veteran, rather), incorporating three decades of operational internet experience into a fundamental restructure.
You will find that the kernel code for IPv6 is full of familiar shadows—variable names often just gain a "v6", and function names simply swap a prefix. But this similarity is deceptive. Beneath this familiarity, IPv6 underwent radical底层 surgery: it discarded the legacy options that had to be kept in IPv4, redefined the packet header structure, and fundamentally changed how hosts connect to a network.
In this chapter, we are not just learning "how to configure IPv6"—we are understanding how the kernel rethinks routing, multicast, and autoconfiguration under this new protocol. We will dive into the kernel source code to see how Linux achieves flexibility through extension headers and manages multicast group membership through the MLD protocol.
Only by understanding these things can you truly grasp the pulse of the next-generation internet protocol within the operating system.
8.1 IPv6: More Than Just Longer
When you enter the kernel's IPv6 subsystem, you will find a domain that is still growing wild.
To be honest, reading this code gives a sense of déjà vu—as if you are looking at a remaster of the IPv4 code. This feeling is normal. Over the past decade, IPv6 has gained many exciting features, but quite a few were designed to maintain compatibility or improve the transition experience. Examples include ICMPv6 sockets, IPv6 multicast routing, and even IPv6 NAT—something the original designers swore would never be used.
The most subtle case is IPsec (IP Security). In the initial IPv6 design drafts, IPsec was mandatory—every IPv6 node had to support encryption. In the real world, operating systems ultimately implemented IPsec in IPv4 as well, while IPv6 retained the flexibility to treat it as "optional."
This tells us one thing: engineering practice is always more complex than theoretical design.
However, when we truly dive into the kernel and strip away that familiar shell, we find that IPv6 is far more than just a patch on top of IPv4. Developers used decades of hindsight to make quite a few corrections to the protocol stack.
- Header simplification: Routers no longer handle fragmentation; all the dirty work is offloaded to the source host.
- Extension headers: A chained structure replaces IPv4's rigid options fields, making protocol extension flexible.
- Autoconfiguration: Hosts can finally obtain a globally routable address without relying on a DHCP server.
Our journey will cover these key points: extension headers, the MLD protocol, autoconfiguration, and how the kernel handles sending and receiving IPv6 packets.
Before diving into the mechanisms, we need to understand how the "house numbers" are written. After all, in the world of IPv6, the scale of the address space has gone from "a grain of sand" to "every atom on Earth."
If you were impressed by the IPv4 struct inet_sock, then the in6_addr and ipv6hdr you are about to see will feel familiar—but note that the semantics of the addresses have fundamentally changed.
Ready? Let's break down the most famous concept first.
IPv6 Addresses: From House Numbers to Coordinates
In the IPv4 era, we were used to thinking of addresses as 32-bit integers. In IPv6, an address is a 128-bit beast. Just printing one out requires eight groups of colon-separated hexadecimal numbers.
What does the kernel use to hold this giant?
It lives in include/uapi/linux/in6.h, defined very straightforwardly:
struct in6_addr {
union {
__u8 u6_addr8[16];
__be16 u6_addr16[8];
__be32 u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};
This is a textbook use of a union. You will notice that to accommodate different operational scenarios, the kernel slices this 128-bit space into three views:
u6_addr8: Used when you need byte-level operations (like memory copies).u6_addr16: Used when you need to handle 16-bit segments.u6_addr32: Used when you need bitwise operations or masking.
This design is common in kernel networking code—for performance, programmers always want to operate directly on word sizes rather than doing shift operations.
With the data structure in place, the next step is classification.
The Three Faces of Addresses
IPv6 address type classification is much cleaner than IPv4, but beginners often get stuck on one thing: unicast, anycast, and multicast.
These are not just differences in name—they determine where a packet ultimately goes.
1. Unicast The most common case. One address maps to one interface (NIC). A packet sent to a unicast address has exactly one destination—that single interface.
This is like sending a letter home, with the address precise down to the house number.
2. Anycast This is a very clever concept introduced by IPv6. A single address is assigned to a group of interfaces (typically distributed across different routers or nodes). When you send a packet to an anycast address, routing protocols measure the distance and deliver the packet to the nearest interface.
You can think of anycast as a "chain store." You are looking for the "McDonald's" brand (the anycast address), but you will only walk into the one nearest to you (the nearest router).
3. Multicast
Addresses starting with ff. This is the evolved version of IPv4 multicast. A packet sent to a multicast address is received by all members of that group.
This is like a radio station—as long as you tune to that frequency, everyone receives the same signal.
Special Address Spaces
Beyond these three types, IPv6 reserves large "special zones" for specific purposes. If you see these prefixes in a packet capture tool, you should now know what they are doing:
Link-local Address
Prefix: fe80::/64
This is IPv6's most important "lifeline."
Even if you haven't configured any IP address or connected to a DHCP server, as long as your NIC has IPv6 enabled, it will automatically generate an address starting with fe80.
Its rule is strict: This address is only valid on the local link (the same Ethernet cable or Wi-Fi network).
- Routers will never forward a packet whose source or destination address is a Link-local address.
- It is the foundation of the Neighbor Discovery Protocol (NDP) and autoconfiguration.
Analogy callback: Remember the "chain store" mentioned earlier? A Link-local address is like the intercom system inside that store. You can only use it to talk to people in the same store. Want to call the store on the next street? No way.
Global Unicast Address
This is IPv6's "regular army." It is globally routable and typically consists of three parts, as defined in RFC 3587:
- Global Routing Prefix: Assigned by your ISP, identifying your approximate location on the internet.
- Subnet ID: Your identifier for subdividing the local LAN.
- Interface ID: Identifies the specific NIC interface.
This is what actually replaces 192.168.x.x or 8.8.8.8.
Special Multicast Addresses
Within the multicast category, there are a few addresses that kernel developers must know by heart, because they are hardcoded into the protocol stack logic.
Solicited-Node Multicast Address
This is an absolutely brilliant design optimization in IPv6.
In the IPv4 era, if you wanted to use ARP to ask "Who has 192.168.1.1?", you had to broadcast to everyone on the entire subnet. This was fine when there were only a few devices, but what if there were 10,000 devices on the subnet? Every single one would be woken up by this inane question.
IPv6 solves this efficiency problem with the Solicited-Node address.
The format is: ff02:0:0:0:0:1:ffXX:XXXX.
The first 104 bits are fixed, and only the last 24 bits are taken from the target unicast/anycast address.
What does this mean? It means each device only needs to listen to a very small number of multicast addresses (usually just one or two). When you want to find the MAC address corresponding to a specific unicast address, you don't need to shout to the entire network—you just call out to that specific Solicited-Node multicast address. The noise is drastically reduced.
Another Link-local callback: Remember
fe80? It is not only the starting point for autoconfiguration but also the foundation of Solicited-Node multicast—all of these "whispers" take place within the scope of link-local addresses.
Predefined Global Multicast Addresses
There are also some reserved multicast addresses, such as:
ff02::1: The all-nodes address. Both routers and hosts must listen.ff02::2: The all-routers address. Only routers listen; hosts ignore it.
This is why, in the Linux kernel source code, you will see calls like ipv6_dev_mc_inc() during interface initialization—it forcibly adds the NIC to these mandatory listening groups at startup.
⚠️ Pitfall Warning Don't assume Link-local addresses can just be ignored. When configuring firewalls or routing rules, many people habitually focus only on Global addresses, only to find that the Neighbor Discovery Protocol breaks and pings fail. The reason is simple: you blocked the traffic for
fe80::/10. This is like cutting the phone line in your house and then wondering why the delivery driver (the router) can't reach you.
The IPv6 Header: A Streamlined Skeleton
Let's take a look at what an IPv6 packet looks like in the kernel's eyes. Here is the definition in include/uapi/linux/ipv6.h:
struct ipv6hdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 priority:4,
version:4;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u8 version:4,
priority:4;
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 flow_lbl[3];
__be16 payload_len;
__u8 nexthdr;
__u8 hop_limit;
struct in6_addr saddr;
struct in6_addr daddr;
};
Compare this with struct iphdr (IPv4), and you will notice several obvious changes:
- Version: Still 4 bits, with a value of 6.
- Priority: Later redefined as Traffic Class, used for QoS.
- Flow Label (20 bit):
flow_lbl. This is a new field in IPv6, used to tag packet flows so that routers can identify packets belonging to the same TCP connection and apply the same handling (fast forwarding without inspecting upper-layer headers). - Payload Len: The payload length. Note that this no longer includes the header itself.
- Next Header: This is the most critical change. It replaces the IPv4 Protocol field, but it is more flexible.
- Hop Limit: Equivalent to IPv4's TTL, but renamed to be more accurate.
Why is nexthdr the core?
In IPv4, if you wanted to add a security option, you had to stuff it into the IPv4 header's Options field, and routers had to painstakingly parse the options just to figure out the next hop. This was slow.
IPv6 says: Don't overcomplicate things.
nexthdr directly points to the type of the next "header." It could be TCP (6), UDP (17), or the Extension Header we are about to discuss.
Extension Headers: The Wisdom of Chained Processing
This is one of the parts of IPv6 design with the most "engineering aesthetic."
The IPv6 header is fixed at 40 bytes. What if you need additional functionality? For example:
- You need to fragment.
- You need to specify a mandatory route.
- You need to encrypt (IPsec).
IPv6's answer: make these features into independent "extension headers," strung together one after another.
The first byte of every extension header is the Next Header field, pointing to the type of the next header. The Next Header of the last extension header points to the upper-layer protocol (such as TCP).
[ IPv6 Header (NextHdr=Routing) ]
-> [ Routing Header (NextHdr=TCP) ]
-> [ TCP Segment ]
This design brings two huge benefits:
- Routers have it easy: With the exception of a very few extension headers (like Hop-by-Hop), routers don't need to parse these intermediate headers at all—they just skip over them. This significantly boosts forwarding speed.
- Infinite extensibility: Theoretically, you can chain headers of any length.
Several common extension headers:
1. Hop-by-Hop Options Header
- Next Header: Usually points to TCP/UDP or another extension header.
- Characteristic: The only extension header that must be processed by every router along the path.
- Use case: Often used for Router Alert (telling routers "Hey, don't just forward this, look at it") or Jumbo Payload. Because it forces processing by all routers, the performance overhead is large, so it is rarely used.
2. Routing Header
- Analogy: Similar to IPv4's Loose Source Route.
- Function: Specifies the intermediate router addresses that the packet must pass through.
- Type 0 deprecated: The early Routing Header Type 0 was deprecated by RFC 5095 due to security issues (it could be used for reflection attacks). The current Routing Header Type 2 is primarily used in Mobile IPv6.
3. Fragment Header
This is the easiest place to trip up in IPv6.
Major change: In IPv6, intermediate routers will never fragment. If a router receives a packet larger than the MTU, it will not kindly chop it up for you like IPv4 would. Instead, it drops it directly and sends back an ICMPv6 "Packet Too Big" message.
Fragmentation must be done by the source host. The Fragment Header contains the fragment offset, identifier, and other information, similar to IPv4, but it exists as an independent extension header.
⚠️ Pitfall Warning: MTU issues are the number-one killer of IPv6 network failures. Many people switch from IPv4 to IPv6 and find that large packets are always dropped while small packets go through. Why? Because they configured the wrong MTU on intermediate routers, or they disabled ICMPv6 in the firewall (causing the "Packet Too Big" message to never come back). The source host thinks the path is clear and keeps sending large packets, which are all silently dropped halfway. Never block ICMPv6—it is the breathing passage for IPv6 to function properly.
4. Destination Options Header
This header is interesting. It is only for the final destination to see. If there is a Routing Header in between, it can appear before the Routing Header; if no fragmentation is needed, it sits directly after the IPv6 header. It is typically used to carry parameters that only the destination host cares about.
Section Echo
In this section, we built a basic view of IPv6: from that massive in6_addr struct, to the three primary communication modes (unicast, anycast, multicast), to the extension header mechanism linked together by nexthdr.
It may look like just a reshuffling of fields, but the kernel's design philosophy shifts here: IPv6 assumes routers are smart, but also lazy—they only do the most basic forwarding, leaving complex logic to the endpoints. This return to the "end-to-end" principle is the most essential advancement of IPv6 over the heavily patched IPv4.
But to truly get these addresses and headers running, the kernel needs to do one more thing: routing.
In the next section, we will dive into the kernel's routing subsystem and see how Linux finds the next hop in that vast 128-bit space through fib6_lookup().