Skip to main content

8.9 Quick Reference & Kernel Fragments

Throughout this book, we often say: "Don't memorize—understand the mechanisms." But before you dive deep into the kernel code, having an accurate "map" in hand is essential. This section is that map.

In the previous eight sections, we broke down IPv6 reception, transmission, routing, multicast, and autoconfiguration. Now, we need to gather the function call relationships scattered across the net/ipv6/ directory. This isn't your boring API manual that just lists function prototypes—I'll try to explain the "context" behind these functions so you know exactly where they sit in the grand scheme of the entire flow.

Finally, we'll organize a few key constant tables and point out some easily overlooked "special corners."


Core Methods—The IPv6 Kernel Toolbox

Let's start with the kernel methods we encounter most often in the code. Some are responsible for manipulating data structures, while others trigger critical protocol actions.

1. Address Checks and Operations

Often, when you get an struct in6_addr *, the first thing you want to know is whether it's a "valid" or "special" address.

bool ipv6_addr_any(const struct in6_addr *a)

This is the most basic check. It determines whether the given address is the all-zeros address (::).

Why does this matter? When you initialize a socket or haven't bound to a specific address yet, the kernel often uses the all-zeros address to represent "any address" or "unspecified." If you pass an all-zeros address during a route lookup, the routing subsystem will either crash or match a default route, depending on the context. So, asking "Are you any?" first is usually a lifesaver.

bool ipv6_addr_equal(const struct in6_addr *a1, const struct in6_addr *a2)

Directly compares two in6_addr structures for equality. This is slightly faster than memcmp and has clearer semantics—it explicitly tells anyone reading the code: this is an IPv6 address comparison.

static inline void ipv6_addr_set(struct in6_addr *addr, __be32 w1, __be32 w2, __be32 w3, __be32 w4)

This is a "manual transmission" function. An in6_addr is essentially a 128-bit chunk of data (usually four 32-bit integers). If you want to manually assemble an address—for example, piecing together a link-local prefix—this function is used to stuff those four u32 values in.

⚠️ Pitfall Warning: Pay attention to byte order. The w1 through w4 here are in network byte order (__be32). If you fill them directly with local integers, it usually works fine on a PC (because x86 is little-endian), but when porting to certain big-endian architectures or embedded boards, you'll end up with a reversed address.

bool ipv6_addr_is_multicast(const struct in6_addr *addr)

Determines whether it's a multicast address. The implementation is very straightforward: it checks if the first 8 bits are 0xFF. If so, it returns true. Don't write (addr->s6_addr[0] == 0xff) yourself—use this macro or function. Not only does it keep the code clean, but if the standard ever changes (however unlikely), the kernel only needs to be updated in one place.


2. Packet Anatomy—Extracting IPv6 Information from an SKB

Once a packet arrives at the protocol stack, it's stuffed into an struct sk_buff (SKB). You need to "extract" the IPv6 header from the SKB.

struct ipv6hdr *ipv6_hdr(const struct sk_buff *skb)

This is the most common "get header" operation. It returns a pointer to the IPv6 header within the SKB.

Note: This function assumes the SKB's network layer header pointer (skb->network_header) has been correctly set. In Netfilter hooks or early stages of the receive path, this is usually safe. But if you get an SKB in an odd corner (like a driver callback), the pointer might not be set yet. Before using this, it's best to confirm that skb->network_header is valid.

bool ipv6_ext_hdr(u8 nexthdr)

As we discussed earlier, the nexthdr field in IPv6 can be either an upper-layer protocol (TCP/UDP) or the next extension header. This function helps you make that determination: if the value of nexthdr is a known extension header type (like Hop-by-Hop, Routing, Fragment, etc.), it returns true.

This is extremely useful when traversing the extension header chain—you need to know when to stop (when you hit a transport layer protocol) and when to keep going (when you hit an extension header).


3. Protocol Registration and Dispatch—Making the Kernel Recognize Your Protocol

If you want to write a new protocol (like building a layer on top of UDP yourself), or write a Netfilter module to precisely match a certain extension header, you'll run into these.

int inet6_add_protocol(const struct inet6_protocol *prot, unsigned char protocol)

This is the core function for registering a protocol handler with the kernel.

  • If you're writing TCPv6, you'd set protocol to IPPROTO_TCP.
  • If you're writing ICMPv6, it's IPPROTO_ICMPV6.
  • But interestingly, the Fragment extension header is also registered through this function (with a value of NEXTHDR_FRAGMENT).

After calling this function, the kernel fills your handler function pointer into the global inet6_protos array. Later, when the IPv6 core code finishes parsing the header and finds that nexthdr matches your registered value, it will directly call your function. This is a classic "publish-subscribe" pattern.


4. MLD and Multicast—When a Host Wants to Join

We spent a lot of time on MLDv2 earlier. The kernel logic responsible for joining a network interface to a multicast group or sending MLD reports outward lives in these functions.

bool ipv6_is_mld(struct sk_buff *skb, int nexthdr, int offset)

This is a very handy helper function. It checks for you: is this packet ICMPv6? And is the ICMPv6 type MLD-related? It checks for one of the following four types—returning true if any match:

  • ICMPV6_MGM_QUERY (MLD Query)
  • ICMPV6_MGM_REPORT (MLDv1 Report)
  • ICMPV6_MGM_REDUCTION (Leave Multicast)
  • ICMPV6_MLD2_REPORT (MLDv2 Report)

int ipv6_dev_mc_inc(struct net_device *dev, const struct in6_addr *addr)

This is the underlying implementation for "making the NIC listen to multicast."

  • When you call setsockopt(..., IPV6_ADD_MEMBERSHIP, ...) in user space, the kernel eventually ends up here (or at its socket variant).
  • It does two things:
    1. Sets up hardware-level filtering on the NIC so it receives frames destined for this multicast address.
    2. Maintains a software list in the kernel's inet6_dev structure, recording which groups this interface belongs to.

int ipv6_sock_mc_join(struct sock *sk, int ifindex, const struct in6_addr *addr)

This is the kernel entry point for user space's IPV6_JOIN_GROUP. It not only calls the aforementioned _inc to update the hardware, but also hangs the socket on that group's member list, so the kernel knows who to copy multicast packets to when they arrive.


5. Reception and Forwarding—A Packet's Journey

int ipv6_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)

This is the "main entrance" of the IPv6 protocol stack. When a driver receives an IPv6 packet (Ethernet type 0x86DD) and hands it to the kernel, netif_receive_skb() ultimately throws the packet to ipv6_rcv.

Its main job is "security screening":

  1. Is the version number 6?
  2. Is the packet length less than the basic IPv6 header length?
  3. If there's a problem, it drops the packet and sends an ICMPv6 Parameter Problem.
  4. If everything checks out, it calls ip6_rcv_finish() to proceed to the next step (route lookup).

int ip6_forward(struct sk_buff *skb)

This is the core of the forwarding path. A packet only reaches here when the machine is configured as a router (/proc/sys/net/ipv6/conf/all/forwarding = 1).

It does a few critical things:

  • Hop Limit check: If the TTL decrements to 0, it sends an ICMPv6 Time Exceeded and drops the packet.
  • PMTU check: If the packet is too large and exceeds the next hop's MTU, it sends an ICMPv6 Packet Too Big and drops the packet (IPv6 intermediate routers never fragment).
  • Send ICMPv6 Redirect: If it notices the source host made a poor routing choice (e.g., going through another interface on the local subnet would be closer), it calls ndisc_send_redirect() to notify the source host.

6. Route Lookup—Asking for Directions

void ip6_route_input(struct sk_buff *skb)

This is the "input route lookup." When a packet is destined for the local machine or needs to be forwarded by it, the kernel needs to know: "Where exactly should this packet go?" It calls the underlying fib6_lookup() to search the FIB (Forwarding Information Base) table and attaches the result (a dst_entry) to the SKB.

Note: The lookup result isn't just "local delivery" or "forward"—for forwarded packets, it also determines what the next hop's MAC address is.

struct dst_entry *ip6_route_output(struct net *net, const struct sock *sk, struct flowi6 *fl6)

This is the "output route lookup." When the local machine (like Apache or your client program) needs to send a packet, it asks the kernel: "I want to connect to this IP—which interface should it go out from? Who is the next hop?" It also calls fib6_lookup(), but in the context of the transmit path.


Macros and Constant Tables—The Common Language of the Protocol

Kernel code is full of magic numbers. To keep people from going crazy, the kernel wraps these magic numbers in macros. Below are the tables we encounter most often when dealing with IPv6 headers and multicast.

Table 8-2: IPv6 Extension Headers and Next Header Values

These are all possible values for the nexthdr field in an IPv6 header. Note that extension headers and upper-layer protocols (TCP/UDP) share this namespace.

Linux SymbolValueDescription
NEXTHDR_HOP0Hop-by-Hop Options header. Must immediately follow the IPv6 header; all routers must examine it.
NEXTHDR_TCP6TCP segment. An old friend.
NEXTHDR_UDP17UDP message. DNS and many game protocols run on this.
NEXTHDR_IPV641IPv6 in IPv6 (tunneling protocol). When you see this, it means you've peeled off one layer of IPv6, only to find another IPv6 layer inside.
NEXTHDR_ROUTING43Routing header (source routing). Used to specify intermediate nodes that the packet must pass through.
NEXTHDR_FRAGMENT44Fragment header (fragmentation). IPv6 fragmentation can only be done by the source; this header records the fragmentation info.
NEXTHDR_GRE47GRE header. Commonly used for VPNs.
NEXTHDR_ESP50Encapsulating Security Payload (part of IPsec). Encrypted data.
NEXTHDR_AUTH51Authentication Header (another part of IPsec). Verifies data integrity.
NEXTHDR_ICMP58ICMP for IPv6. NDP and MLD both live here.
NEXTHDR_NONE59No next header. This means there's nothing after this. Used in certain options or security headers to indicate "end of the line."
NEXTHDR_DEST60Destination Options header. Information that only the final destination needs to process.

Table 8-3: MLDv2 Multicast Address Record Types

These are the type codes used in MLDv2 Report messages, precisely describing a host's "attitude" toward a specific multicast group.

Linux SymbolValueDescription
MLD2_MODE_IS_INCLUDE1Include mode: I only want to receive packets from sources listed in the Sources List.
MLD2_MODE_IS_EXCLUDE2Exclude mode: I want to receive all packets for this group, except those from the Sources List.
MLD2_CHANGE_TO_INCLUDE3Switch to Include mode.
MLD2_CHANGE_TO_EXCLUDE4Switch to Exclude mode.
MLD2_ALLOW_NEW_SOURCES5Allow new sources to join (append to the list in Include mode).
MLD2_BLOCK_OLD_SOURCES6Block old sources (remove from the list in Include mode).

Table 8-4: ICMPv6 Parameter Problem Codes

When the kernel encounters a parsing error, it sends a "Parameter Problem" message. This Code field tells the peer exactly what went wrong.

Linux SymbolValueDescription
ICMPV6_HDR_FIELD0Erroneous header field. There's garbage in a header field.
ICMPV6_UNK_NEXTHDR1Unrecognized Next Header type. The kernel received a nexthdr type it doesn't recognize and doesn't know how to peel off the next layer.
ICMPV6_UNK_OPTION2Unrecognized IPv6 option. Encountered an unknown Option while parsing Hop-by-Hop or Destination Options.

Special Addresses—Those "Untouchable" Constants

The kernel uses a few global struct in6_addr instances to define special addresses that appear on almost every network interface. You can reference them directly without needing to fill in arrays yourself.

Note: These instances are read-only; just use them directly for comparison or assignment.

  • in6addr_any: The all-zeros address ::. Represents "any address" or "unspecified." Commonly used when listening on a socket.
  • in6addr_loopback: The loopback address ::1. Always represents the local machine.
  • in6addr_linklocal_allnodes: The link-local all-nodes address ff02::1. Packets sent to this address are received by all IPv6-enabled hosts on the local link.
  • in6addr_linklocal_allrouters: The link-local all-routers address ff02::2. Only routers listen to this.
  • in6addr_interfacelocal_allnodes: Interface-local all-nodes ff01::1.
  • in6addr_interfacelocal_allrouters: Interface-local all-routers ff01::2.
  • in6addr_sitelocal_allrouters: Site-local all-routers ff05::2. Note that Site-Local addresses (fec0::/10) have been deprecated by RFC 3879, but the kernel retains these constants for compatibility.

Routing Table Management—How User Space Commands Land in the Kernel

On Linux, we generally use two tools to configure routing: the legacy route (net-tools) and the modern iproute2 (the ip command). Although their interfaces differ, they converge at the same low-level kernel implementation.

Adding/Deleting Routes with iproute2

This is the recommended approach today. It communicates with the kernel via Netlink Sockets.

  1. Add a route (ip -6 route add ...):

    • User space sends a Netlink message.
    • On the kernel side, net/ipv6/route.c receives the message and calls inet6_rtm_newroute().
    • inet6_rtm_newroute() parses the parameters and ultimately calls ip6_route_add() to stuff the route entry into the FIB table.
  2. Delete a route (ip -6 route del ...):

    • User space sends a Netlink message.
    • The kernel side calls inet6_rtm_delroute().
    • It ultimately calls ip6_route_del() to remove that route.
  3. View routes (ip -6 route show):

    • The kernel side calls inet6_dump_fib(), traverses the entire FIB table, and spits out each entry to user space via Netlink.

Using the route Command (The Old-School Way)

This is the classic ioctl approach. Although the route command itself is quite old, the kernel retains this code for compatibility.

  1. Add a route (route -A inet6 add ...):

    • The route tool calls ioctl(sockfd, SIOCADDRT, &rt).
    • The kernel's ipv6_route_ioctl() catches this request.
    • Internally, it still calls ip6_route_add() to do the actual work.
  2. Delete a route (route -A inet6 del ...):

    • Calls ioctl(sockfd, SIOCDELRT, &rt).
    • ipv6_route_ioctl() handles it, internally calling ip6_route_del().

Conclusion: No matter which tool you use, the two functions doing the heavy lifting at the very bottom are always the same. This once again illustrates the Linux kernel's layered design philosophy—user interfaces can change, but the core logic stays put.


Chapter Echo

Alright, we've finally finished the entire IPv6 chapter—from the original 128-bit address design, to the granular Neighbor Discovery messages, to multicast source filtering, and today's quick reference.

What this chapter truly leaves you with shouldn't be the names of those dozens of functions, but rather an appreciation for IPv6's "unified" design philosophy. Look at it this way: ARP, RARP, ICMP Redirect, IGMP... these protocols scattered all over the place in IPv4 have been unified under the single banner of ICMPv6 in IPv6. NDP is ICMPv6, MLD is ICMPv6, even error reporting is ICMPv6. This unification makes the protocol stack leaner and more efficient, but it also makes the parsing of every ICMPv6 packet more critical—miss one type, and you might lose a key piece of neighbor information.

We spent a lot of time circling around structures like inet6_dev, mc_list, and fib6_table because network protocols aren't just theoretical state machines—they are real, tangible kernel memory structures. If you don't know which linked list a multicast filter hangs off of, you'll just stare blankly at packets captured by tcpdump when debugging packet loss, with no idea which if statement to set a breakpoint on in the kernel code.

In the next chapter, we'll cross over the protocol stack's processing layer and enter the legendary "great network filter"—Netfilter. That is the most treacherous stretch of the kernel networking stack, and the birthplace of magical features like firewalls, NAT, and Conntrack. You'll see how, just as a packet has finished being parsed for its IPv6 header and is about to be happily handed off to a socket, it's grabbed by the "hand of God" that is Netfilter, which judges whether it lives or dies.

That will be a game of interception and rewriting.


Exercises

Exercise 1: Understanding

Question: During the IPv6 protocol stack initialization, the kernel needs to register specific protocol handling logic (such as TCPv6, UDPv6, or extension header handling) into the system. Suppose you are a kernel developer and need to write a module to handle a new IPv6 extension header (with a Next Header number of N). Which function should you call to complete the registration? What is its signature, and which global data structure in the kernel will store this handling logic?

Answer & Analysis

Answer: You should call the inet6_add_protocol() function. Its signature is int inet6_add_protocol(const struct inet6_protocol *prot, unsigned char protocol). The handling logic will be stored in the global array inet6_protos[].

Analysis: Based on knowledge point inet6_add_protocol(), this is the standard kernel function for registering IPv6 protocol handlers (including transport layer protocols and extension headers). During registration, the protocol number (like IPPROTO_UDP or an extension header identifier) serves as the index, storing the struct inet6_protocol pointer in the inet6_protos array. This way, when ipv6_rcv parses a packet, it can find the corresponding handler callback function through the nexthdr field.

Exercise 2: Application

Question: A host has just generated a link-local IPv6 address. Before passing Duplicate Address Detection (DAD), the address is in a "tentative" state. To verify the address's uniqueness, the Neighbor Discovery Protocol needs to use a special multicast address. If the host's link-local address is fe80::2aa:ff:fe3f:4a21 (ignoring the ff:fe insertion bit), which solicited-node multicast address should it join for DAD verification? Please write out the calculation process and the result.

Answer & Analysis

Answer: The result is ff02::1:ff3f:4a21. Calculation process: take the low 24 bits of the unicast address (3f:4a:21) and prepend the prefix ff02:0:0:0:0:1:ff00::/104.

Analysis: Based on knowledge point Solicited-Node Multicast Address, the solicited-node multicast address is used for Neighbor Discovery and DAD. Its generation rule is to concatenate the low 24 bits of a unicast/anycast address with the fixed prefix ff02:0:0:0:0:1:ff00::/104. The low 24 bits of the address in the question are 3f:4a:21, and concatenating them yields ff02::1:ff3f:4a21. The host sends a Neighbor Solicitation message to this address; if another host owns that address, it will respond with a Neighbor Advertisement.

Exercise 3: Thinking

Question: IPv6's "autoconfiguration" allows hosts to generate IP addresses statelessly. However, the standard EUI-64 interface ID generation method (based on MAC addresses) can lead to user privacy tracking. The Linux kernel provides a mechanism called "Privacy Extensions" to solve this problem. Analyzing this in the context of RFC 4941, why does generating IPv6 interface IDs based on MAC addresses pose a privacy risk? How does the kernel mitigate this risk through the Privacy Extensions mechanism?

Answer & Analysis

Answer: Generating interface IDs based on MAC addresses results in a fixed, unchanging interface ID, and MAC addresses are globally unique hardware identifiers. As long as a user connects to a network, the latter half of their IPv6 address remains constant, making cross-network user behavior tracking easy. The Privacy Extensions (RFC 4941) mechanism mitigates this risk by generating random interface IDs (instead of MAC-based ones) to create temporary addresses, and these temporary addresses periodically expire and are renewed, thereby increasing the difficulty of tracking and protecting user privacy.

Analysis: This question tests a deep understanding of the "Autoconfiguration" and "Privacy Extensions" knowledge points. IPv6 stateless autoconfiguration typically combines the MAC address to generate an interface ID (like EUI-64), which is convenient for address uniqueness but also exposes the device's identity. The core of Privacy Extensions is "decoupling"—by randomizing the interface ID and introducing a lifetime (Preferred Lifetime), the same device uses different IP addresses across different time periods or different networks, thereby preventing persistent surveillance. This is a classic design that maintains network layer connectivity while improving user security.


Key Takeaways

The kernel implementation of IPv6 is built upon the in6_addr data structure and a brand-new address classification system. By using a union to divide the 128-bit address into 8-bit, 16-bit, and 32-bit views, the kernel can efficiently handle memory operations and bitwise calculations at different granularities. At the same time, IPv6 completely abandons broadcast in favor of three modes: unicast, anycast, and multicast. In particular, the introduction of the "Solicited-Node Multicast Address" maps addresses to specific ff02::1:ffxx:xxxx multicast groups, ensuring that the Neighbor Discovery process occurs only within a very small scope. This drastically solves the "Ethernet noise" problem caused by ARP broadcasts in the IPv4 era.

The protocol header design reflects IPv6's engineering philosophy of "streamlined skeleton, flexible extension." The IPv6 header is fixed at 40 bytes, removing the checksum to reduce router burden, and moving all optional features out. Through the nexthdr field, IPv6 introduces a chained processing mechanism for "extension headers" (like Hop-by-Hop, Routing, Fragment), where each header is linked together like a node in a linked list. This design means intermediate routers only need to process the IPv6 main header and a very small number of hop-by-hop options, greatly improving forwarding efficiency and achieving a return from "complex variable-length structures" to "fixed fast processing."

The Autoconfiguration mechanism gives hosts "plug-and-play" capabilities, allowing them to generate globally routable addresses without a DHCP server. This process is divided into four phases: first, generating a link-local address and performing Duplicate Address Detection (DAD); then sending a Router Solicitation (RS); next, receiving a Router Advertisement (RA) containing prefix information; and finally, combining the interface ID (usually MAC-based or randomly generated via Privacy Extensions) to synthesize a global unicast address. Coupled with the Valid Lifetime and Preferred Lifetime mechanisms, network administrators can achieve smooth, seamless renumbering of prefixes across the entire network by adjusting RA parameters.

In the packet receive path ipv6_rcv(), the kernel maintains protocol purity through strict "security screening" rules. The function first drops illegal packets with incorrect version numbers, loopback destination addresses, or multicast source addresses, and forcibly parses the Hop-by-Hop extension header. Afterwards, the packet passes through a Netfilter hook into ip6_rcv_finish(), where a route lookup (fib6_lookup) is performed. Based on the lookup result, the kernel decides the packet's fate: either handing it to the local protocol stack (ip6_input) for onion-style extension header parsing and dispatch to upper layers, or forwarding it (ip6_forward).

The revolution in fragmentation handling is one of the key features that distinguishes IPv6 from IPv4, but it also introduces potential MTU traps. IPv6 prohibits intermediate routers from fragmenting; all fragmentation must be done by the source host. If a smaller MTU link is encountered along the transmission path, the router will simply drop the packet and send back an ICMPv6 "Packet Too Big" message, forcing the source host to perform Path MTU Discovery (PMTUD). Therefore, when configuring firewalls or routes, you must ensure that ICMPv6 traffic is not blocked; otherwise, it will lead to silent connectivity failures for large packets. This is the most common failure point in IPv6 network operations.