Configuring DHCPv6 Prefix Delegation (DHCPv6-PD) on Linux: Production Guide

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

Problem Statement: The Pain of Static IPv6 Subnetting

Six months ago, our team redesigned the edge routing for 12 regional branch offices. Each site runs two Ubuntu-based gateways handling three customer VLANs, an IoT network, and an internal lab segment. To keep initial deployment simple, we hardcoded static /64 prefixes on every downstream interface. We assumed our upstream ISP rarely changed allocated blocks.

We were wrong. During an unannounced midnight maintenance window, the ISP rotated our allocated /48 blocks. Instantly, all 12 branch locations lost IPv6 connectivity.

Over 60 individual VLAN interfaces went dark, triggering nearly four hours of emergency maintenance as engineers updated configs box-by-box. Manual tracking broke under real-world churn. We needed our Linux routers to request upstream blocks, carve them into /64 subnets, and re-advertise new ranges automatically. That brought us to DHCPv6 Prefix Delegation (DHCPv6-PD).

Core Concepts of DHCPv6 Prefix Delegation

Defined in RFC 3633 and updated in RFC 8415, DHCPv6-PD handles addressing differently from traditional stateful DHCPv4 or DHCPv6 IA_NA. Rather than granting a single IP address to one interface, the upstream server delegates an entire routing prefix—usually a /56 (256 subnets) or /48 (65,536 subnets)—to your border router.

Key Roles and Identifiers

  • Delegating Router (DR): The upstream ISP router or core switch that manages the parent IPv6 address pool.
  • Requesting Router (RR): Your Linux edge system. It pulls an allocated block over its WAN link and assigns carved /64 subnets to local interfaces.
  • Identity Association for Prefix Delegation (IA_PD): The DHCPv6 container holding the delegated prefix, valid lifetimes (typically 30 days), and preferred lifetimes (typically 7 days).
  • SLAAC and Router Advertisements (RA): Endpoints never speak DHCPv6-PD directly. Once your Linux box binds a carved /64 to a LAN port, it announces that prefix using ICMPv6 RAs so client devices auto-assign their own addresses.

The Four-Way Handshake

Negotiation uses UDP port 546 on the client and port 547 on the server:

  1. SOLICIT: The Linux router sends a multicast message to ff02::1:2 requesting an IA_PD.
  2. ADVERTISE: The ISP router offers candidate blocks (for example, 2001:db8:1a00::/56).
  3. REQUEST: The Linux router formally requests the offered pool.
  4. REPLY: The ISP router commits the lease and confirms preferred and valid expiration timers.

Hands-on Practice: Building a Linux DHCPv6-PD Gateway

Consider this reference topology. A dedicated gateway runs Ubuntu 24.04 LTS across three physical NICs:

  • eth0: WAN uplink to ISP.
  • eth1: Corporate LAN (Subnet ID 0x01).
  • eth2: Guest and IoT network (Subnet ID 0x02).

Step 1: Kernel Preparation and IPv6 Forwarding

Linux ignores Router Advertisements once packet forwarding is enabled. Because a router must forward transit packets yet still learn its default upstream route via WAN RAs, set accept_ra = 2 on the uplink.

# /etc/sysctl.d/90-ipv6-routing.conf
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1

# Accept RAs on WAN even when forwarding packets
net.ipv6.conf.eth0.accept_ra = 2

# Disable temporary privacy addresses on the router interfaces
net.ipv6.conf.all.use_tempaddr = 0

Load these settings into the running kernel:

sudo sysctl --system

Step 2: Configuring systemd-networkd for DHCPv6-PD

Modern Linux distributions handle prefix slicing natively through systemd-networkd. You do not need standalone daemons like dhcp6c or radvd.

Configure the uplink on eth0 to request an IA_PD:

# /etc/systemd/network/10-wan.network
[Match]
Name=eth0

[Network]
DHCP=ipv6
IPv6AcceptRA=yes
LinkLocalAddressing=ipv6

[DHCPv6]
PrefixDelegation=yes
WithoutRA=solicit

Assign the first sliced /64 (Subnet 1) to the office LAN on eth1:

# /etc/systemd/network/20-lan.network
[Match]
Name=eth1

[Network]
LinkLocalAddressing=ipv6
IPv6SendRA=yes
DHCPPrefixDelegation=yes

[DHCPPrefixDelegation]
SubnetId=1

[IPv6SendRA]
Managed=no
OtherInformation=no
RouterLifetimeSec=1800

Carve the second /64 (Subnet 2) for guest devices on eth2:

# /etc/systemd/network/30-guest.network
[Match]
Name=eth2

[Network]
LinkLocalAddressing=ipv6
IPv6SendRA=yes
DHCPPrefixDelegation=yes

[DHCPPrefixDelegation]
SubnetId=2

[IPv6SendRA]
Managed=no
OtherInformation=no
RouterLifetimeSec=1800

Apply the configuration:

sudo systemctl restart systemd-networkd

Step 3: Alternative Setup with WIDE-DHCPv6 (dhcp6c) and radvd

If you run Debian 11, Alpine, or custom OpenWrt images without systemd, use the classic wide-dhcpv6-client and radvd stack.

Install both utilities:

sudo apt-get update && sudo apt-get install -y wide-dhcpv6-client radvd

Configure the delegation client in /etc/wide-dhcpv6/dhcp6c.conf:

interface eth0 {
    send ia-pd 0;
    request domain-name-servers;
};

id-assoc pd 0 {
    prefix-interface eth1 {
        sla-id 1;
        sla-len 8; # Adds 8 bits: 56 + 8 = /64
        ifid 1;    # Router gets ::1 on this interface
    };

    prefix-interface eth2 {
        sla-id 2;
        sla-len 8;
        ifid 1;
    };
};

Start the client daemon:

sudo systemctl restart wide-dhcpv6-client
sudo systemctl restart radvd

Step 4: Verification and Traffic Inspection

Check whether eth1 and eth2 derived routable global unicast addresses (GUA):

ip -6 addr show dev eth1
ip -6 addr show dev eth2

You should see addresses matching your upstream block, such as 2001:db8:1a00:1::1/64 on eth1. Next, inspect the kernel routing table for the default gateway learned from WAN advertisements:

ip -6 route show default

Capture the raw lease exchange on the WAN interface to verify timer negotiation:

sudo tcpdump -vvn -i eth0 port 546 or port 547

Inspect the output for the IA_PD block. You will find the assigned prefix, preferred lifetime (e.g., preferred-lifetime 604800), and valid lifetime (e.g., valid-lifetime 2592000).

Step 5: Firewall Rules for DHCPv6-PD

A strict drop policy often blocks DHCPv6 before the handshake finishes. Update nftables to allow link-local negotiation on your WAN interface:

# /etc/nftables.conf
table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;

        # Permit established flows and loopback
        ct state established,related accept
        iifname "lo" accept

        # Allow DHCPv6 replies from link-local upstream routers
        iifname "eth0" ip6 saddr fe80::/10 udp sport 547 udp dport 546 accept

        # Allow vital ICMPv6 neighbor and router discovery
        ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } accept
    }
}

Field Notes from Six Months in Production

Automating DHCPv6-PD eliminated manual address management across our branch offices. During an upstream renumbering test last month, the ISP switched our prefix allocation from 2001:db8:1a00::/56 to 2001:db8:2b00::/56. Gateways picked up the change, bound new addresses to downstream interfaces, and broadcasted updated RAs in under 30 seconds without dropping a single TCP connection.

Three operational rules we follow:

  • Lock down your DUID: Preserve /etc/systemd/networkd.conf or /var/lib/dhcpv6/dhcp6c_duid across machine rebuilds. If your DUID changes, the ISP treats the gateway as a new device and hands you a completely different prefix.
  • Always request a /56: Some upstream providers default to a single /64 if unprompted. A /56 provides 256 subnets, giving you plenty of space for Docker networks, hypervisors, and guest VLANs.
  • Inspect multicast snooping upstream: If your WAN port connects to an intermediate managed switch, make sure MLD snooping does not drop ff02::1:2 multicast frames before they reach the provider handoff.
Share: