Skip to main content

ch08_3

8.3 IPv6 Header

In the previous section, we solved the "where to" problem — we got the address and joined the Solicited-Node multicast group. Now, it's time to tackle the "how to get there" problem. But before we actually take a step, we need to understand what an IPv6 packet looks like.

This isn't just a matter of looking at a diagram and filling in the blanks. The IPv6 header design wasn't merely about accommodating those 128-bit extra-long addresses; it was a major "surgery" to shed the historical baggage of IPv4. Some features were removed, some were moved out, and others took on forms you might not expect.

Removing the Safety Net

Every IPv6 packet opens with the IPv6 header. This header has a fixed length of 40 bytes.

Pause for a moment and appreciate that word "fixed." In the IPv4 era, the header length was variable (because of Options), so an IHL (Internet Header Length) field was mandatory to tell the kernel "how long this header is." With IPv6, that field is gone. 40 bytes, set in stone.

There's an even more radical change: the IPv6 header has no Checksum.

In IPv4, every time a packet passed through a router, the TTL was decremented by 1, and the router had to faithfully recalculate the checksum for the entire header. Back when CPUs were single-core and clocked at a few hundred megahertz, this was a non-trivial overhead. IPv6 simply chopped this overhead away: the checksum responsibility was offloaded to Layer 2 (the link layer, e.g., CRC) and Layer 4 (the transport layer, e.g., TCP/UDP checksums).

This has a direct consequence: when a router forwards an IPv6 packet, modifying the hop_limit (the equivalent of IPv4's TTL) does not require recalculating a checksum. On a pure software router, this translates to a very real performance boost.

Of course, there's a side effect: in IPv4, UDP could set its checksum to 0 to mean "no checksum." But in IPv6, except for a very small number of special tunneling scenarios (RFC 6935), UDP must have checksums enabled. Because if the data gets corrupted in transit, there's no safety net to catch it.

Header Structure: The Split Traffic Class

Let's look at the definition in the kernel. There's a subtle detail here that's easy to miss on the first read:

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;
};

(include/uapi/linux/ipv6.h)

Notice the first byte. The standard document (RFC 2460) specifies 4 bits of Version + 8 bits of Traffic Class + 20 bits of Flow Label.

But in Linux's implementation, this first byte is split into priority:4 and version:4. Where did the remaining 4 bits of Traffic Class go? They were stuffed into the first bit of the flow_lbl array.

In other words, it takes the priority in the code plus the upper 4 bits of flow_lbl[0] to piece together a complete 8-bit Traffic Class. This is a classic bitfield operation done to save memory width.

Let's break down each field in this structure and see what role it plays in the IPv6 world:

  • version (4 bit): Must be 6. Nothing more to say — it's the protocol's ID card.

  • priority (4 bit) / Traffic Class (8 bit): This is the Traffic Class. Although RFC 2460 defined this field, it didn't specify what each value means — that part was left for the DiffServ (QoS) mechanisms to define. The kernel code keeping the priority name actually has a bit of a legacy flavor to it.

  • flow_lbl (20 bit): Flow Label. This is a field that was marked as "experimental" back in the RFC 2460 days and didn't get a clear specification until RFC 6437 in 2011. Its purpose is to tag a series of packets belonging to the same "flow." When a router sees this label, it can theoretically skip looking up the 5-tuple and do fast forwarding directly based on the label. RFC 6437 even suggests using it to detect address spoofing. However, in today's general internet, you rarely see it deployed at scale.

  • payload_len (16 bit): Payload Length. This does not include the fixed 40-byte header. 16 bits means the maximum length is 65,535 bytes. If this is exceeded (e.g., with Jumbo Frames), this field must be set to 0, and the Jumbo Payload option in the Hop-by-Hop extension header takes over. We'll dive into this in the next section.

  • nexthdr (8 bit): Next Header. This is an extremely critical design. If there are no extension headers, it's the upper-layer protocol number: IPPROTO_TCP (6), IPPROTO_UDP (17), and so on. If there are extension headers, it points to the type of the extension header immediately following the IPv6 header. It's like the next pointer in a linked list node.

  • hop_limit (8 bit): Hop Limit. Decremented by 1 at each router. When it hits 0, the packet is dropped, and an ICMPv6 Time Exceeded message is sent back. This is the mechanism that prevents packets from getting lost and looping forever in the network.

  • saddr / daddr (128 bit): Source Address and Destination Address. There's a small detail here: if a Routing Header (routing extension header) is used, the daddr might not be the final destination, but rather the "next hop" along the path. It's like writing a shipping label where the recipient isn't your friend, but some transit station.

Saying Goodbye to IPv4

You'll notice that a lot of IPv4's baggage is missing here: no Header Length, no Checksum, no Options.

IPv4's Options mechanism was criticized for a long time due to performance issues (causing variable-length headers that are hard to accelerate in hardware). IPv6 completely replaced it with Extension Headers — a more elegant and flexible "chained" approach.

But we won't rush into the details of extension headers just yet. For now, staring at this fixed 40-byte structure, you should be able to feel a shift in design philosophy: from a complex variable-length structure, back to a fixed, fast, and predictable simple skeleton. For the purity of these 40 bytes, IPv6 was even willing to push the "checksum" responsibility onto others.

In the next section, we'll attach the "limbs" to this simple skeleton — those functionally diverse, end-to-end linked Extension Headers. Then you'll discover how the tiny nexthdr field single-handedly supports the extensibility of the entire network stack.