DIY Debian Router - Part 5: IPv6 configuration with systemd-networkd

Oct 8, 2025

Introduction

This is Part 5 of the DIY Debian Router series. See Part 1 for the series introduction and links to all parts.

IPv6 address autoconfiguration (RFC 4862) eliminates the hassle of DHCPv6 (which is still sometimes not supported by certain clients) while providing clients with globally routable addresses. This part covers IPv6 deployment using systemd-networkd's native support for DHCPv6 Prefix Delegation (DHCPv6-PD) and Router Advertisement (RA) generation.

⚠️ Parts of this post aren't fully tested: My ISP does not support native IPv6 (as mentioned below). Due to this, I haven't been able to fully test GUA configuration and full IPv6 functionality. Despite this, I've tried my best to provide a fully working configuration for internal and external IPv6 communication.

We'll cover:

  • Native DHCPv6-PD client for obtaining delegated prefixes from the ISP
  • Integrated Router Advertisement functionality (no separate radvd daemon needed)
  • Automatic prefix assignment to LAN interface
  • Dual addressing: Global Unicast Addresses (GUA) and Unique Local Addresses (ULA)
  • DNS server advertisement via RDNSS (Recursive DNS Server) option
  • Privacy extension support for client devices
  • Stateless operation (no lease tracking required)

⚠️ ISP IPv6 Support Required: This guide assumes your ISP provides native IPv6 via DHCPv6-PD or SLAAC. Some major ISPs, notably Bell Canada (╯°□°)╯︵ ┻━┻, do not support IPv6 for residential networks. If your ISP lacks IPv6 support, you can still deploy ULA addressing internally, put external IPv6 traffic won't work.

IPv6 addressing architecture review

As discussed in Part 1, I intend to support dual IPv6 addressing:

Global Unicast Addresses (GUA)

We'll use DHCPv6 Prefix Delegation (PD) to obtain GUA prefixes from the ISP. The router's WAN interface runs systemd-networkd's built-in DHCPv6 client, which requests a delegated prefix (e.g., /56 or /60) from the ISP's DHCPv6 server. Simultaneously, the WAN interface autoconfigures its own IPv6 address via SLAAC from the ISP's Router Advertisements - these are two separate operations serving different purposes:

  • DHCPv6-PD: Obtains a prefix that the router subdivides and assigns to its LAN interfaces
  • SLAAC (WAN): Configures the WAN interface's own IPv6 address for communication with the ISP

The delegated prefix is automatically assigned to the LAN bridge interface (br0) and advertised to clients. When the ISP updates the delegated prefix, the configuration is automatically refreshed.

Clients on the LAN receive addresses from the delegated prefix, providing globally routable IPv6 connectivity. GUA addresses are Internet-accessible (subject to firewall rules, covered in Part 6).

Unique Local Addresses (ULA)

ULA provides stable internal addressing independent of ISP prefix changes. The fake example addresses I'm using for demonstration are:

Prefix: fd09:dead:beef::/64
Gateway: fd09:dead:beef::1

If real, this prefix would have been correctly randomly generated per RFC 4193 guidelines and should be treated as site-specific. ULA addresses are not routable on the Internet but are reachable within the local network, analogous to internal addresses in IPv4 (but without NAT).

SLAAC vs. DHCPv6

The choice between SLAAC and DHCPv6 for IPv6 address assignment has some implications:

AspectSLAACDHCPv6
Client supportUniversalInconsistent
Configuration complexityLow (RA only)Higher (DHCPv6 server + RA)
Address trackingNone (stateless)Full (lease database)
Privacy extensionsSupportedSupported
Static reservationsNot directly supportedSupported (via DUID/MAC binding)

The primary limitation of SLAAC seems to be the lack of centralized address assignment/tracking. I don't need that, so SLAAC it is.

systemd-networkd IPv6 configuration

systemd-networkd includes native support for both DHCPv6-PD and Router Advertisement generation, eliminating the need for external daemons like wide-dhcpv6-client and radvd.

WAN interface DHCPv6-PD configuration

Update the WAN interface configuration to request prefix delegation from the ISP.

✏️ Replace enp8s0 with your actual WAN interface name.

Update /etc/systemd/network/20-wan.network to contain:

[Match]
Name=enp8s0

[Network]
DHCP=yes
IPv6AcceptRA=yes

[DHCPv4]
UseDNS=no
UseRoutes=yes
RouteMetric=100

[DHCPv6]
# Request prefix delegation from ISP
PrefixDelegationHint=::/64
UseDNS=no

[IPv6AcceptRA]
UseDNS=no
RouteMetric=100
# Accept default route from RA
DHCPv6Client=always

[Link]
RequiredForOnline=yes

New configuration elements:

  • [DHCPv6] section: Configures DHCPv6 client behavior
  • PrefixDelegationHint=::/64: Request a /64 prefix from the ISP (adjust based on ISP's delegation policy - common values are /56, /60, /64)
  • UseDNS=no: Do not use DNS servers from DHCPv6 (router runs its own DNS resolver)
  • DHCPv6Client=always: Always run DHCPv6 client, even if RA indicates not to (some ISPs require this for PD)

The DHCPv6 client will automatically request prefix delegation (IA_PD) and a non-temporary address (IA_NA) for the WAN interface.

LAN bridge IPv6 configuration

Update the LAN bridge configuration to receive delegated prefixes and advertise them to clients.

✏️ IMPORTANT: Per RFC 4862, the prefix length for SLAAC is required to be /64. If you set it to anything else, your clients may ignore the RAs being sent.

Update /etc/systemd/network/40-br0.network to contain:

[Match]
Name=br0

[Network]
Address=192.168.0.1/24
Address=fd09:dead:beef::1/64
ConfigureWithoutCarrier=yes
IPv6AcceptRA=no
IPv6SendRA=yes
# Request delegated prefix assignment
DHCPPrefixDelegation=yes

[IPv6SendRA]
Managed=no
OtherInformation=no
DNS=fd09:dead:beef::1
Domains=absurdum.ca

[IPv6Prefix]
# ULA prefix - always advertised
Prefix=fd09:dead:beef::/64
PreferredLifetimeSec=3600
ValidLifetimeSec=86400

[DHCPPrefixDelegation]
# GUA prefix - automatically populated from WAN DHCPv6-PD
UplinkInterface=enp8s0
Assign=yes
# Announce this prefix in Router Advertisements
Announce=yes

[Link]
RequiredForOnline=carrier

Configuration breakdown:

  • IPv6SendRA=yes: Enable Router Advertisement transmission on this interface
  • DHCPPrefixDelegation=yes: Request assignment of a subnet from the DHCPv6-PD delegated prefix
  • [IPv6SendRA] section: Configures RA generation
    • Managed=no: Do not set the "managed" flag (no DHCPv6 required for addresses)
    • OtherInformation=no: Do not set the "other" flag (no DHCPv6 required for other config)
    • DNS=fd09:dead:beef::1: Advertise router's ULA address as DNS server via RDNSS option
    • Domains=: Optionally advertise DNS search domain via DNSSL option
  • [IPv6Prefix] section: Defines the static ULA prefix to advertise
    • Prefix=fd09:dead:beef::/64: ULA prefix, static and always advertised
  • [DHCPPrefixDelegation] section: Configures automatic assignment from DHCPv6-PD delegation
    • Assign=yes: Automatically assign an address from the GUA prefix to the bridge interface
    • Announce=yes: Advertise this delegated prefix in Router Advertisements

When systemd-networkd receives a delegated prefix from the ISP (e.g., 2001:db8:1234::/64), it automatically:

  1. Assigns a /64 subnet from the delegation to the bridge (e.g., 2001:db8:1234:0::/64)
  2. Configures an address for the router within that subnet (e.g., 2001:db8:1234::1/64)
  3. Advertises the prefix in Router Advertisements to LAN clients
  4. Updates the prefix advertisement when the delegation changes

This fully automates IPv6 prefix management with no manual intervention required.

Service activation

Restart systemd-networkd to apply configuration:

systemctl restart systemd-networkd

Verify DHCPv6-PD status on WAN interface:

networkctl status enp8s0

Expected output should include:

IPv6 Address: 2001:db8:...::... (dhcp)

Check for delegated prefix:

ip -6 addr show br0

Expected output:

inet6 2001:db8:1234::1/64 scope global dynamic
inet6 fd09:dead:beef::1/64 scope global
inet6 fe80::xxxx:xxxx:xxxx:xxxx/64 scope link

The first address (2001:db8:1234::1/64) is the GUA derived from the ISP-delegated prefix.

View detailed DHCPv6-PD information:

journalctl -u systemd-networkd | grep -i "prefix delegation"

You should see messages indicating successful DHCPv6-PD acquisition.

Testing and verification

Router advertisement (RA)

Capture RAs on the LAN to verify systemd-networkd is transmitting correctly:

tcpdump -i br0 -vvv icmp6 and 'ip6[40] == 134'

This captures ICMPv6 Router Advertisement packets (type 134). Expected output every 200-600 seconds, example:

ICMPv6, router advertisement, length 64
    hop limit 64, Flags [none], pref medium, router lifetime 1800s
      prefix info option (3), length 32 (4): fd09:dead:beef::/64, Flags [onlink, auto]
      prefix info option (3), length 32 (4): 2001:db8:1234::/64, Flags [onlink, auto]
      RDNSS option (25), length 24 (3): lifetime 1200s, addr: fd09:dead:beef::1

You should see both the ULA prefix and the dynamically delegated GUA prefix advertised.

Address assignment

To test address assignment, connect a client device to the LAN. The client should autoconfigure two or more IPv6 addresses:

ip -6 addr show

Expected output:

inet6 2001:db8:1234::abcd:1234:5678:9abc/64 scope global dynamic
inet6 fd09:dead:beef::abcd:1234:5678:9abc/64 scope global dynamic
inet6 fe80::xxxx:xxxx:xxxx:xxxx/64 scope link
  • GUA address: 2001:db8:1234::... (delegated by ISP, globally routable)
  • ULA address: fd09:dead:beef::... (stable internal addressing)
  • Link-local address: fe80::... (required for IPv6 operation)

Privacy extensions

Check for privacy extensions (client-side configuration) - clients supporting (RFC 4941) will generate additional temporary addresses:

inet6 2001:db8:1234::1234:5678:abcd:ef01/64 scope global temporary dynamic
inet6 fd09:dead:beef::1234:5678:abcd:ef01/64 scope global temporary dynamic

Temporary addresses are used for outbound connections to prevent tracking based on stable interface identifiers.

DNS configuration verification

Check that the client received RDNSS configuration:

resolvectl status

You should see the configured IPv6 DNS server address, e.g., fd09:dead:beef::1.

IPv6 connectivity test

From the client, ping the router's ULA address:

ping -6 fd09:dead:beef::1

Ping an external IPv6-enabled host (requires firewall configured in Part 6):

ping -6 google.com

If GUA prefix is configured and firewall allows outbound traffic, you should see replies.

Test DNS resolution over IPv6:

dig @fd09:dead:beef::1 AAAA google.com

Troubleshooting common issues

No IPv6 addresses assigned to clients

1. systemd-networkd not running on the router:

Verify service status:

systemctl status systemd-networkd

If not running, check logs:

journalctl -u systemd-networkd -n 50

2. IPv6 forwarding disabled:

Verify forwarding is enabled (covered in Part 7):

sysctl net.ipv6.conf.all.forwarding

Should return 1. If 0, enable temporarily:

sysctl -w net.ipv6.conf.all.forwarding=1

3. Firewall blocking RAs:

Temporarily disable firewall for testing:

nft flush ruleset

If IPv6 works with firewall disabled, add ICMPv6 rules (see Part 6).

4. Client IPv6 disabled:

On the client, verify IPv6 is enabled. The following should return 0:

sysctl net.ipv6.conf.all.disable_ipv6

If clients only configure link-local addresses, RAs are not being received. Possible causes:

1. IPv6SendRA not enabled on the router:

Verify /etc/systemd/network/40-br0.network has IPv6SendRA=yes.

Restart systemd-networkd:

systemctl restart systemd-networkd

2. Client Not Accepting RAs:

On the client, verify RA acceptance:

Linux:

sysctl net.ipv6.conf.eth0.accept_ra

Should return 1 or 2.

You can also trying running the tcpdump command given earlier above to look for RAs on the client.

IPv6 internet connectivity fails (GUA)

If clients have GUA addresses but cannot reach the Internet:

1. No default route:

Verify the client has a default IPv6 route:

ip -6 route show

Should see:

default via fe80::xxxx:xxxx:xxxx:xxxx dev eth0 proto ra metric 1024

If missing, RAs are not advertising the router as a default gateway. Verify IPv6SendRA=yes in br0 configuration.

2. Firewall blocking IPv6 forwarding:

See Part 6 for firewall rules. IPv6 forwarding must be permitted in nftables.

3. ISP does not provide IPv6:

Verify the WAN interface has an IPv6 GUA:

ip -6 addr show enp8s0

If no GUA is present, the ISP does not provide IPv6 connectivity. Switch ISP...

DHCPv6-PD not receiving delegated prefix

If systemd-networkd is running but no GUA prefix appears on br0:

1. Verify DHCPv6-PD configuration:

Check WAN interface configuration:

cat /etc/systemd/network/20-wan.network

Ensure [DHCPv6] section includes PrefixDelegationHint.

Check logs:

journalctl -u systemd-networkd | grep -i dhcp6

Look for errors such as "DHCPv6 ADVERTISE timeout."

2. ISP Does not support DHCPv6-PD:

Some ISPs use SLAAC on the WAN interface instead of DHCPv6-PD. Verify if the WAN has a GUA via SLAAC:

ip -6 addr show enp8s0

If a GUA is present but no prefix delegation occurs, the ISP may not provide DHCPv6-PD. Solutions:

  • Use the WAN GUA's /64 prefix directly (limited to single subnet)
  • Switch ISP...

3. DHCPv6 server not responding:

Capture DHCPv6 traffic on the WAN to verify ADVERTISE messages are received:

tcpdump -i enp8s0 -vvv port 546 or port 547

Expecte to see SOLICIT messages from the router followed by ADVERTISE/REPLY from the ISP's DHCPv6 server. If no ADVERTISE is received, the ISP's DHCPv6-PD service may not exist or be unavailable.

Next steps

With IPv6 working, clients now have dual-stack connectivity (IPv4 via DHCP, IPv6 via SLAAC). However, the router currently has no firewall, leaving all services exposed. Part 6 covers nftables firewall configuration, implementing stateful packet filtering, NAT, and security policies for both IPv4 and IPv6.

RSS
https://yusefkarim.absurdum.ca/posts/feed.xml