Home
Got Linux ?

Blah blah blah... Mostly technical thoughts, rants and gibberish


Triple IPv4 & IPv6 stack with OpenWRT

[2026.10.11]

The sad reality about IPv6 in 2026 is that many still don’t know what this is about when it comes to doing it by one self. Besides, I know of a major ISP, here in Switzerland, that still does not provide it. No IPv6 (even to its business customers). Punkt Schluss. Get Lost!

In such a landscape, talking of dual IPv4 & IPv6 stack is daring one self to face the uninterest and boredom of the many. I can’t imagine what digressing about a triple IPv4 & IPv6 stack might entail…

Even though I’m an early adopter of IPv6 and have been running dual stacks for more than a decade now - thanks to the open-mindedness of Idiap and the support of SWITCH (that was in 2013) - I have been doing so using static or DHCPv6-leased Unique Local Addresses (ULA), along the abomination (!!! :-D) that is IPv6 Network Address and Port Translation (NAPT).

So far, I just could not understand or make my mind on:

That was until a few days ago, while prepping my router for my soon-arriving Swisscom FttH connection, when an AI obliged and called me dumb, explaining real men use triple IPv4 & IPv6 stacks:

Granted, the thing does not solve:

But it did tickle my curiosity on how to achieve the thing on my beloved OpenWRT router.

Here be flames-spitting and soul-eating dragons.

IPv6 Prefixes and Network Addresses

The first thing which sounds unnatural to someone used to the scarcity of IPv4 is the notion of IPv6 Prefixes. Totally unlike IPv4, where you get one public IP address and get told to deal with it (with NAPT), public IPv6 come to one by the 2⁶⁴ lot, if not more! Yes! Quite generally, ISP nowadays assign /56 IPv6 Prefix to your connection, using DHCPv6 Prefix Delegation (PD), leaving you with 2⁷² - 128-56 - IPv6 Network Addresses_ to play with (!!!).

Globally Unique Addresses (GUA)

The very foundation of the IPv6 philosophy is that everyone - everything! - gets a globally unique, public, routable IPv6 address, within the 2000::/3 range. This is why NAPT is considered an abonimation. One just doesn’t need it. Why use it (and consume RAM and CPU cycles)?

Those are what you get from your ISP. 2⁷² of them. The only gotcha is that, like your unique public IPv4, their /56 Prefix might change at any time (unless you pay $$$ for static public IP addresses; I don’t).

Stateless Address Autoconfiguration (SLAAC)

Another cornerstone of IPv6 is the entirely automated and autonomous way hosts get their Network Address: Stateless Address Autoconfiguration (SLAAC). In a few words as possible:

Ok. Great. Every host automagically gets an IP address and may immediately talk to the Wild Wide Web, without any say from any central management system, save for the router and its RAs. Fan-f***ing-tastic!!! Now, how do I know which host is which. How do I send traffic to a server, when it’s IP may be anything?

Before we move on, it is worth noting the 8 bits we have to spare between the /56 Prefix and the /64 Network Addresses. Those are commonly referred to the Subnet ID, giving you 256 subnets - VLANs! - to play with (as in: “INTRANET”, “GUEST”, “DMZ”, “IOT”, “STEPMOTHER”, etc. etc.).

Unique Local Addresses (ULA)

The first answer that comes to mind is using fc00::/7 Unique Local Addresses (ULA), the equivalent of IPv4 private (RFC1918) addresses familiar to granpas, with the exception that everyone gets its own private /48 IPv6 Prefix (among 2⁴¹ - 48-7 - possibilities).

From there, mimicking IPv4 is a no brainer:

Veni. Vidi. Feci.

Triple Stack with OpenWRT

Now that we know all there is to know about GUAs, ULAs, RAs, A-flag, SLAAC, M-flag and DHCPv6, let’s get our hands dirty and our OpenWRT router sweaty.

Before we do, some documentation pointers:

Also worth nothing: OpenWRT default DHCPv6 server is odhcpd-ipv6only, which features many, many Router Advertisement (RA) fine-tuning parameters but does not support DHCPv6 relaying (last time I tried). If you need the latter, you will have to replace it by dnsmasq-full (which documentation is a bit lacking in OpenWRT).

NOTA BENE: The examples below have been run using dnsmasq-full.

Local prefix (ULA)

In the first place, we need to configure the Unique Local Addresses (ULA) Prefix we want OpenWRT to manage; in /etc/config/network:

config globals 'globals'
  option ula_prefix 'fc00:c001:babe:0::/56'

One might ask: why /56 and not /48? My opiniated answer being: align the ULA Prefix with your ISP GUA Prefix. It might become clearer when we’ll speak of OpenWRT ip6hint.

Public prefix (DHCPv6-PD GUA)

As for the upstream IPv6 interface, connected to your ISP, one shall make sure to configure it to:

In /etc/config/network:

config interface 'WAN6'
  option device 'wan'
  option proto 'dhcpv6'
  # DHCPv6 PD (Prefix Delegation)
  option reqprefix '56'
  # Obtain an IPv6 for the router itself
  # - do NOT use DHCPv6 (most ISPs won't answer to such request)
  # - instead, carve out a WAN6-dedicated subnet from the ISP Delegated Prefix
  option reqaddress 'none'
  option ip6assign '64'
  option ip6hint 'ff'
  # (default value)
  #option ip6ifaceid '::1'
  list ip6class 'WAN6'
  # Allow ULA (Unique Local Addresses)
  option sourcefilter '0'

For the sake of the examples below, we’ll assume a 2000:bad:beef:0::/56 GUA Prefix being delegated to us by the ISP.

Dual Stack (IPv4/DHCPv4 + IPv6/DHCPv6 ULA + NAT66)

On the router

In /etc/config/network:

config interface 'LAN'
  option device 'lan'
    option proto 'static'
  # Static IPv4
    option ipaddr '192.168.123.1'
    option netmask '255.255.255.0'
    option delegate '0'
  # Static ULA
  list ip6addr 'fc00:c001:babe:7b::c0ff:fea8:7b01/64'

IMPORTANT: Mark the ULA /64! With a larger mask - e.g. /120 - OpenWRT (dnsmasq?) does not recognize any IPv6 address valid for Router Advertisement (RA) and sends none (thus, on the client host: no RA, no M-flag, no DHCPv6, no address… unless the client is entirely statically configured); dnsmasq-full BUG?

In /etc/config/dhcp:

config dhcp 'LAN'
  option interface 'LAN'
  option dhcpv4 'server'
  option dhcpv6 'server'
  option ra 'server'
  # odhcpd-ipv6only
  option ra_default '2'
  option ra_slaac '0'
  list ra_flags 'managed-config'
  # dnsmasq-full
  option ra_management '2'

In /etc/config/firewall (in addition to other settings):

config zone
  option name 'WAN'
  list network 'WAN6'
  ...
  option masq6 '1'
  list masq_src 'fc00::/7'

After restarting the network (service networking restart):

# Resulting dnsmasq Router Advertisement (RA) configuration
grep -F ra /var/etc/dnsmasq.conf.cfg*
: ra-param=lan,0,7200
: dhcp-range=set:LAN,::,static,constructor:lan,12h
: enable-ra
: quiet-ra

# IPv6 Network Addresses
ip -6 address show dev lan
: (Static ULA)
: inet6 fc00:c001:babe:7b::c0ff:fea8:7b01/64 scope global
:   valid_lft forever preferred_lft forever
: (link-local)
: inet6 fe80::1322:33ff:fe44:5566/64 scope link
:   valid_lft forever preferred_lft forever

# Masquerading Chain
nft -t list chain inet fw4 srcnat_WAN
: table inet fw4 {
:   chain srcnat_WAN {
:     meta nfproto ipv4 masquerade comment "!fw4: Masquerade IPv4 WAN traffic"
:     ip6 saddr fc00::/7 masquerade comment "!fw4: Masquerade IPv6 WAN traffic"
:   }
: }

On the client host

# Observe RA (ICMPv6) and DHCPv6 traffic
tcpdump -i eth0 -vpn 'ip6 and (icmp6 or udp port 547)'
: ICMP6, router advertisement
:   hop limit 64, Flags [managed, other stateful], pref medium, router lifetime 7200s, reachable time 0ms, retrans timer 0ms
:     prefix info option (3), length 32 (4): fc00:c001:babe:7b::/64, Flags [onlink], valid time 43200s, pref. time 43200s
:     source link-address option (1), length 8 (1): 11:22:33:44:55:66
: dhcp6 solicit ((IA_NA ...) (option-request DNS-server DNS-search-list SNTP-servers NTP-server opt_82) ...)
: dhcp6 reply ((IA_NA ... (IA_ADDR fc00:c001:babe:7b:0:c0ff:fea8:7b65 pltime:86400 vltime:86400)) (DNS-server ...) ...)

# IPv6 Network Addresses
ip -6 address show eth0
: (DHCPv6-ed ULA)
: inet6 fc00:c001:babe:7b:0:c0ff:fea8:7b65/128 scope global dynamic noprefixroute
:   valid_lft 85861sec preferred_lft 85861sec
: (link-local)
: inet6 fe80::6455:fffe:4433:2211/64 scope link noprefixroute
:   valid_lft forever preferred_lft forever

# Routing
ip -6 route get 2620:fe::fe
: 2620:fe::fe from :: via fe80::1322:33ff:fe44:5566 dev eth0 proto ra src fc00:c001:babe:7b:0:c0ff:fea8:7b65 metric 100 pref medium

Take-away

Dual Stack (IPv4/DHCPv4 + IPv6/SLAAC ULA + NAT66)

On the router

In /etc/config/network:

config interface 'LAN'
  ...
  # Use the fc00:c001:babe:7b::/56 ula_prefix
  list ip6class 'local'
  option ip6assign '64'
  option ip6hint '7b'
  # (default value)
  #option ip6ifaceid '::1'

Mark the ip6class, ip6assign and ip6hint configuration parameters, telling OpenWRT to carve out a /64 Prefix, using the 7b Subnet ID, from the local ULA fc00:c001:babe:0::/56 prefix we configured in the first place.

In /etc/config/dhcp:

config dhcp 'LAN'
  ...
  # odhcpd-ipv6only
  option ra_default '1'
  option ra_slaac '1'
  list ra_flags 'other-config'
  # dnsmasq-full
  option ra_management '0'

In /etc/config/firewall (in addition to other settings):

config zone
  option name 'WAN'
  list network 'WAN6'
  ...
  option masq6 '1'
  list masq_src 'fc00::/7'

After restarting the network (service networking restart):

# Resulting dnsmasq Router Advertisement (RA) configuration
grep -F ra /var/etc/dnsmasq.conf.cfg*
: ra-param=br-lan.2404,0,7200
: dhcp-range=set:LAN,::,constructor:lan,ra-stateless,ra-names
: enable-ra
: quiet-ra

# IPv6 Network Addresses
ip -6 address show dev lan
: (Prefix-delegated ULA)
: inet6 fc00:c001:babe:7b::1/64 scope global noprefixroute
:   valid_lft forever preferred_lft forever
: ...

On the client host

# Observe RA (ICMPv6) and DHCPv6 traffic
tcpdump -i eth0 -vpn 'ip6 and (icmp6 or udp port 547)'
: ICMP6, router advertisement, length 56
:   hop limit 64, Flags [other stateful], pref medium, router lifetime 7200s, reachable time 0ms, retrans timer 0ms
:     prefix info option (3), length 32 (4): fc00:c001:babe:7b::/64, Flags [onlink, auto], valid time infinity, pref. time infinity
:     source link-address option (1), length 8 (1): 11:22:33:44:55:66
: dhcp6 inf-req ((option-request DNS-server DNS-search-list SNTP-servers lifetime NTP-server opt_83) ...)
: dhcp6 reply ((DNS-server fc00:c001:babe:7b::1) ...)

# IPv6 Network Addresses
ip -6 address show eth0
: (SLAAC-ed ULA)
: inet6 fc00:c001:babe:7b:1163:ab49:9d7c:ffa0/64 scope global noprefixroute
:   valid_lft forever preferred_lft forever
: ...

# Routing
ip -6 route get 2620:fe::fe
: 2620:fe::fe from :: via fe80::9683:c4ff:fea5:4a65 dev eth0 proto ra src fc00:c001:babe:7b:1163:ab49:9d7c:ffa0 metric 100 pref medium

Take-away

Triple Stack (IPv4/DHCPv4 + IPv6/DHCPv6 ULA + IPv6/SLAAC GUA)

On the router

In /etc/config/network:

config interface 'LAN'
  ...
  # Use the DHCPv6 PD-ed GUA prefix
  list ip6class 'WAN6'
  option ip6assign '64'
  option ip6hint '7b'
  # (default value)
  #option ip6ifaceid '::1'
  # Static ULA
  list ip6addr 'fc00:c001:babe:7b::c0ff:fea8:7b01/120'

Mark the ULA /120! This is what prevents SLAAC to kick-in within the ULA range! If you don’t want the router itself from having its own ULA address, just remove it.

Also mark the ip6class now being WAN6, telling OpenWRT to carve out a /64 Prefix (7b Subnet ID) from 2000:bad:beed:0::/56 GUA prefix attributed to us by the ISP.

In /etc/config/dhcp:

config dhcp 'LAN'
  ...
  # odhcpd-ipv6only
  option ra_default '0'
  option ra_slaac '1'
  list ra_flags 'managed-config'
  # dnsmasq-full
  option ra_management '1'

In /etc/config/firewall (in addition to other settings):

config zone
  option name 'WAN'
  list network 'WAN6'
  ...

Mark the absence of masq settings. We may also want to remove the sourcefilter '0' setting in the WAN interface configuration.

After restarting the network (service networking restart):

# Resulting dnsmasq Router Advertisement (RA) configuration
grep -F ra /var/etc/dnsmasq.conf.cfg*
: ra-param=lan,0,7200
: dhcp-range=set:LAN,::,static,constructor:lan,slaac,ra-names,12h
: enable-ra
: quiet-ra

# IPv6 Network Addresses
ip -6 address show dev lan
: (Static ULA)
: inet6 fc00:c001:babe:7b:0:c0ff:fea8:7b01/120 scope global
:   valid_lft forever preferred_lft forever
: (Prefix-delegated GUA)
: inet6 2000:bad:beef:7b::1/64 scope global dynamic noprefixroute
:   valid_lft 21293sec preferred_lft 6893sec
: ...

On the client host

# Observe RA (ICMPv6) and DHCPv6 traffic
tcpdump -i eth0 -vpn 'ip6 and (icmp6 or udp port 547)'
: ICMP6, router advertisement, length 80
:   hop limit 64, Flags [managed, other stateful], pref medium, router lifetime 7200s, reachable time 0ms, retrans timer 0ms
:     prefix info option (3), length 32 (4): 2000:bad:beef:7b::/64, Flags [onlink, auto], valid time 21137s, pref. time 6737s
:     source link-address option (1), length 8 (1): 11:22:33:44:55:66
:     rdnss option (25), length 24 (3):  lifetime 6737s, addr: 2000:bad:beef:7b::1
: dhcp6 solicit ((IA_NA ...) (option-request DNS-server DNS-search-list SNTP-servers NTP-server opt_82) ...)
: dhcp6 reply ((IA_NA ... (IA_ADDR fc00:c001:babe:7b:0:c0ff:fea8:7b65 pltime:86400 vltime:86400)) (DNS-server ...) ...)

# IPv6 Network Addresses
ip -6 address show eth0
: (DHCPv6-ed ULA)
: inet6 fc00:c001:babe:7b:0:c0ff:fea8:7b65/128 scope global dynamic noprefixroute
:   valid_lft 86368sec preferred_lft 86368sec
: (SLAAC-ed GUA)
: inet6 2000:bad:beef:7b:ab4a:65c2:a0ad:c6f0/64 scope global dynamic noprefixroute
:   valid_lft 21105sec preferred_lft 6705sec

# Routing
ip -6 route get 2620:fe::fe
: 2620:fe::fe from :: via fe80::1322:33ff:fe44:5566 dev eth0 proto ra src 2000:bad:beef:7b:ab4a:65c2:a0ad:c6f0 metric 100 pref medium

Take-away

Quadruple Stack (IPv4/DHCPv4 + IPv6/DHCPv6 ULA + IPv6/SLAAC ULA + IPv6/SLAAC GUA)

(what for? just for fun!)

On the router

In /etc/config/network:

config interface 'LAN'
  ...
  # Use the DHCPv6 PD-ed GUA prefix
  list ip6class 'WAN6'
  option ip6assign '64'
  option ip6hint '7b'
  option ip6ifaceid '::1'
  # Static ULA
  list ip6addr 'fc00:c001:babe:7b::c0ff:fea8:7b01/64'

Mark the ULA /64! This is what makes SLAAC kick-in within the ULA range (even though we use ip6class 'WAN6')!

After restarting the network (service networking restart):

# IPv6 Network Addresses
ip -6 address show dev lan
: (Static ULA)
: inet6 fc00:c001:babe:7b:0:c0ff:fea8:7b01/64 scope global
:   valid_lft forever preferred_lft forever
: (Prefix-delegated GUA)
: inet6 2000:bad:beef:7b::1/64 scope global dynamic noprefixroute
:   valid_lft 20854sec preferred_lft 6454sec

On the client host

# Observe RA (ICMPv6) and DHCPv6 traffic
tcpdump -i eth0 -vpn 'ip6 and (icmp6 or udp port 547)'
: ICMP6, router advertisement, length 112
:   hop limit 64, Flags [managed, other stateful], pref medium, router lifetime 7200s, reachable time 0ms, retrans timer 0ms
:     prefix info option (3), length 32 (4): 2000:bad:beef:7b::/64, Flags [onlink, auto], valid time 20783s, pref. time 6383s
:     prefix info option (3), length 32 (4): fc00:c001:babe:7b::/64, Flags [onlink, auto], valid time 43200s, pref. time 43200s
:     source link-address option (1), length 8 (1): 11:22:33:44:55:66
:     rdnss option (25), length 24 (3):  lifetime 6383s, addr: 2000:bad:beef:7b::1
: dhcp6 solicit ((IA_NA ...) (option-request DNS-server DNS-search-list SNTP-servers NTP-server opt_82) ...)
: dhcp6 reply ((IA_NA ... (IA_ADDR fc00:c001:babe:7b:0:c0ff:fea8:7b65 pltime:86400 vltime:86400)) (DNS-server ...) ...)

# IPv6 Network Addresses
ip -6 address show eth0
: (DHCPv6-ed ULA)
: inet6 fc00:c001:babe:7b:0:c0ff:fea8:7b65/128 scope global dynamic noprefixroute
:   valid_lft 86342sec preferred_lft 86342sec
: (SLAAC-ed ULA)
: inet6 fc00:c001:babe:7b:1163:ab49:9d7c:ffa0/64 scope global dynamic noprefixroute
:   valid_lft 43142sec preferred_lft 43142sec
: (SLAAC-ed GUA)
: inet6 2000:bad:beef:7b:7cca:9c82:bccb:6b6d/64 scope global dynamic noprefixroute
:   valid_lft 20725sec preferred_lft 6325sec
: ...

# Routing
ip -6 route get 2620:fe::fe
: 2620:fe::fe from :: via fe80::1322:33ff:fe44:5566 dev eth0 proto ra src 2000:bad:beef:7b:7cca:9c82:bccb:6b6d metric 100 pref medium

Take-away

Quintuple Stack (IPv4/DHCPv4 + IPv6/DHCPv6 ULA + 2*IPv6/SLAAC ULA + IPv6/SLAAC GUA)

(this is getting out of hands…)

On the router

In /etc/config/network:

config interface 'LAN'
  ...
  # Use the fc00:c001:babe:7b::/56 ula_prefix and the DHCPv6 PD-ed GUA prefix
  list ip6class 'local'
  list ip6class 'WAN6'
  option ip6assign '64'
  option ip6hint '7b'
  option ip6ifaceid '::1'
  # Static ULA
  list ip6addr 'fc00:c001:babe:7b::c0ff:fea8:7b01/64'

Mark both the local and WAN6 ip6class, telling OpenWRT to use both ULA and GUA Prefixes for SLAAC.

After restarting the network (service networking restart):

# IPv6 Network Addresses
ip -6 address show dev lan
: (Static ULA)
: inet6 fc00:c001:babe:7b:0:c0ff:fea8:7b01/64 scope global
:   valid_lft forever preferred_lft forever
: (Prefix-delegated ULA)
: inet6 fc00:c001:babe:7b::1/64 scope global noprefixroute
:   valid_lft forever preferred_lft forever
: (Prefix-delegated GUA)
: inet6 2000:bad:beef:7b::1/64 scope global dynamic noprefixroute
:   valid_lft 20634sec preferred_lft 6234sec

On the client host

# Observe RA (ICMPv6) and DHCPv6 traffic
tcpdump -i eth0 -vpn 'ip6 and (icmp6 or udp port 547)'
: ICMP6, router advertisement, length 112
:   hop limit 64, Flags [managed, other stateful], pref medium, router lifetime 7200s, reachable time 0ms, retrans timer 0ms
:     prefix info option (3), length 32 (4): fc00:c001:babe:7b::/64, Flags [onlink, auto], valid time 43200s, pref. time 43200s
:     prefix info option (3), length 32 (4): 2000:bad:beef:7b::/64, Flags [onlink, auto], valid time 20569s, pref. time 6169s
:     source link-address option (1), length 8 (1): 11:22:33:44:55:66
:     rdnss option (25), length 24 (3):  lifetime 6169s, addr: 2000:bad:beef:7b::1
: dhcp6 solicit ((IA_NA ...) (option-request DNS-server DNS-search-list SNTP-servers NTP-server opt_82) ...)
: dhcp6 reply ((IA_NA ... (IA_ADDR fc00:c001:babe:7b:0:c0ff:fea8:7b65 pltime:86400 vltime:86400)) (DNS-server ...) ...)

# IPv6 Network Addresses
ip -6 address show eth0
: (DHCPv6-ed ULA)
: inet6 fc00:c001:babe:7b:0:c0ff:fea8:7b65/128 scope global dynamic noprefixroute
:   valid_lft 86376sec preferred_lft 86376sec
: (SLAAC-ed ULA)
: inet6 fc00:c001:babe:7b:1163:ab49:9d7c:ffa0/64 scope global dynamic noprefixroute
:   valid_lft 43184sec preferred_lft 43184sec
: (SLAAC-ed GUA)
: inet6 2000:bad:beef:7b:7cca:9c82:bccb:6b6d/64 scope global dynamic noprefixroute
:   valid_lft 20545sec preferred_lft 6145sec

# Routing
ip -6 route get 2620:fe::fe
: 2620:fe::fe from :: via fe80::1322:33ff:fe44:5566 dev eth0 proto ra src 2000:bad:beef:7b:7cca:9c82:bccb:6b6d metric 100 pref medium

Take-away

Sextuple Stack

(joking)

QUESTIONS ?