Setting up IPv6 on a Linux Router
It’s fairly straightforward to build your own router, and there are a number of tutorials for setting up IPv4 forwarding and NAT rules on Linux1. However, IPv6 is a bit more complicated. There are many BSD and Linux based operating systems like pfSense and OpenWRT, which have web management tools to make setting up IPv6 straight forward. However, if you like to run your own custom Linux distribution on your home router and control everything from the command line, this tutorial will take you through configuring dhcpcd, dnsmasq, unbound, iptables and ip6tables for full IPv6 support on your local network.
Interface Setup
The router I’m currently using has several independent network adapters. I went ahead an set the kernel options biosdevname=0 net.ifnames=0
2 so I could rename the adapters as if1
through if6
through the following udev rules. In my configuration, if1
is my WAN port and if2
/if3
are bridged together for the LAN (one going to a local switch and the other to a Wi-Fi 6 access point). These are just my personal choices, and you may choose a different naming convention or use the unique names set by the Linux kernel.
# /etc/udev/rules.d/70-netif-names.rules SUBSYSTEM=="net",ACTION=="add",DRIVERS=="?*",ATTR{address}=="xx:xx:xx:xx:xx:10",ATTR{dev_id}=="0x0",ATTR{type}=="1",KERNEL=="eth*",NAME="if1" SUBSYSTEM=="net",ACTION=="add",DRIVERS=="?*",ATTR{address}=="xx:xx:xx:xx:xx:11",ATTR{dev_id}=="0x0",ATTR{type}=="1",KERNEL=="eth*",NAME="if2" SUBSYSTEM=="net",ACTION=="add",DRIVERS=="?*",ATTR{address}=="xx:xx:xx:xx:xx:12",ATTR{dev_id}=="0x0",ATTR{type}=="1",KERNEL=="eth*",NAME="if3" SUBSYSTEM=="net",ACTION=="add",DRIVERS=="?*",ATTR{address}=="xx:xx:xx:xx:xx:13",ATTR{dev_id}=="0x0",ATTR{type}=="1",KERNEL=="eth*",NAME="if4" SUBSYSTEM=="net",ACTION=="add",DRIVERS=="?*",ATTR{address}=="xx:xx:xx:xx:xx:14",ATTR{dev_id}=="0x0",ATTR{type}=="1",KERNEL=="eth*",NAME="if5" SUBSYSTEM=="net",ACTION=="add",DRIVERS=="?*",ATTR{address}=="xx:xx:xx:xx:xx:15",ATTR{dev_id}=="0x0",ATTR{type}=="1",KERNEL=="eth*",NAME="if6"
Void Linux recommends using /etc/rc.local
to set up networking, if you’re not using a tool like NetworkManager. Network setup varies by distribution, sometimes residing in /etc/network
or /etc/netplan
. Set up your interfaces according to your distribution’s documentation. In the following example, I create a bridge called brlan
, attach if2
and if3
to it, and give the bridge a private IPv4 address and /24
subnet.
# /etc/rc.local ip link set dev if2 up ip link set dev if3 up brctl addbr brlan brctl addif brlan if2 brctl addif brlan if3 ip addr add 10.10.10.1/24 dev brlan ip link set dev brlan up
DHCP / WAN
One of the best guides I’ve found on IPv6 router support in Linux is by Tor Hveem3. Unfortunately he uses the wide-dhcpv6-client
, which isn’t available in Void Linux, nor does it seem to be maintained. However, the dhcpcd
client, found in many distributions, has fairly well documented support for IPv64. You may need to adjust ia_na
and ia_pd
, as the correct values for getting a delegated prefix seems to vary depending on your ISP5.
The following is my /etc/dhcpcd.conf
. It’s configured to not allow its DNS servers to be set by my ISP, because I’ll be running my own DNS server which we’ll see later.
controlgroup wheel duid persistent vendorclassid nooption domain_name_servers, domain_name, domain_search option classless_static_routes option interface_mtu option host_name option rapid_commit require dhcp_server_identifier slaac private noipv6rs # disable routing solicitation denyinterfaces brlan # Don't touch the LAN at all interface if1 ipv6rs # enable routing solicitation for WAN adapter ia_na 1 # request an IPv6 address ia_pd 1 brlan/0 # request a PD and assign it to the LAN
Local Network
dhcpcd will give us IP addresses from our ISP, but we then need to assign addresses to our network. Some tutorials may instruct people to use dhcpd for issuing IPv4 addresses and radvd for IPv6 router advertisements. Others may use dhcpd for both IPv4 and IPv6. The trouble with many of these tutorials is that they tend to have hard coded, static IPv6 addresses. I found the best solution was to use dnsmasq which provides a DHCP server, IPv6 router advertisements and even local DNS.
The following is my /etc/dnsmasq.conf
. Change if1
to be your WAN interface, brlan
to be your LAN interface or bridge, and setup local
to whatever you want your internal DNS name to be. dnsmasq will intercept DNS requests to this special domain, allowing you to reach devices by their advertised hostname. The bottom of this file also contains examples for statically assigning IPs and hostnames to certain machines based on their hardware MAC addresses6. Finally, we use no-resolv
and server
to forward DNS requests to an unbound server running on port 5252.
# Wan Interface except-interface=if1 bogus-priv enable-ra dhcp-range=tag:brlan,::1,constructor:brlan, ra-names, slaac, 12h local=/penguin.internal/ expand-hosts domain=penguin.internal dhcp-range=lan,10.10.10.50,10.10.10.100,12h dhcp-authoritative dhcp-host=aa:bb:cc:dd:ee:01,10.10.10.10,fileserver dhcp-host=aa:bb:cc:dd:ee:05,10.10.10.20,gaming dhcp-host=aa:bb:cc:dd:ee:10,10.10.10.30,laptop no-resolv server=127.0.0.1#5252
I use unbound for DNS. By default, my ISP’s DNS servers do terrible things like redirecting all requests to their portal page and insisting you install their mobile app before using the Internet. They likely redirect non-existent domains to their search page as well. So I ignore them and run my own. Since dnsmasq also runs a local DNS server, we’ll run unbound on port 5252 and let dnsmasq forward requests there. The following is my /etc/unbound.conf
, but you can use PowerDNS Recursor, BIND , a DNS-over-HTTPS proxy, or any DNS server you want.
server: use-syslog: yes port:5252
If you decide to use your ISPs DNS servers, you’ll need to remove domain_name_servers
from dhcpcd.conf
, as well as no-resolv
and server=127.0.0.1#5252
from the dnsmasq.conf
.
Firewall Rules
We’ll need some firewall rules for IPv4 NAT and security, as well as for allowing/blocking IPv6 traffic. Here is an example /etc/iptables/iptables.rules
for IPv4 traffic. It blocks all incoming traffic, allows established outbound connections, and forwards appropriately to the internal network with NAT masquerading for outbound connections.
*filter :INPUT DROP [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] -A INPUT -i brlan -j ACCEPT -A INPUT -i lo -j ACCEPT -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -A FORWARD -s 10.10.10.0/24 -i brlan -j ACCEPT -A FORWARD -d 10.10.10.0/24 -i if1 -j ACCEPT COMMIT *nat :PREROUTING ACCEPT [0:0] :INPUT ACCEPT [0:0] :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -A POSTROUTING -o if1 -j MASQUERADE COMMIT
The rules for IPv6 are fairly straightforward as well. Remember, unlike IPv4, all your machines are getting public IPv6 addresses. So firewalls at this level are essential at keeping your private network secure. For IPv6 router advertisements to work correctly, you must allow udp/5467, or else your WAN interface will not be able to get an IPv6 address assignment.
*filter :INPUT DROP [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] -A INPUT -j ACCEPT -m state --state RELATED,ESTABLISHED -A INPUT -m conntrack --ctstate NEW -m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT -A FORWARD -j ACCEPT -i brlan COMMIT
If you’re trying to diagnose connectivity issues, you can also add the following lines to allow ICMP (ping) to IPv6 addresses within your internal network.
-A INPUT -j ACCEPT -p icmpv6 -A FORWARD -p ipv6-icmp -j ACCEPT -A OUTPUT -p ipv6-icmp -j ACCEPT
Finally, you’ll need to make sure the kernel has forwarding and router advertisements enabled for the appropriate interfaces. The following is an example /etc/sysctl.conf
:
net.ipv6.conf.all.forwarding=1 net.ipv6.conf.if1.accept_ra=2 net.ipv4.ip_forward=1
Fitting it Together
This tutorial assumes you have experience with Linux services, and know how to start or reload all the services we’ve configured, as well as being able to check the logs to ensure everything starts up. If everything is setup correctly, you should be able to run ip -br -c a
and see a public IPv4 address on your WAN interface, a private address on your LAN interface, and public IPv6 addresses on both interfaces.
Your other devices should now be getting globally scoped IPv6 addresses as well.
Closing Thoughts
I’ve been running custom Linux routers for nearly twenty years, however they’ve always been IPv4 only. I’ve only worked at one company that implemented IPv6 on their corporate network, and this is only the second residential ISP I’ve had with IPv6 support. The previous was AT&T, which locked network authentication to their proprietary routers (there were workarounds, but I moved out of their service area before I could seriously look into it).
It’s not a trivial task to implement dual-stack IPv4/IPv6. I’ve taken the time to configure IPv6 on all my self-hosted infrastructure. IPv6 adoption is growing, mostly in cellphone networks, but also in residential service providers. A lot of existing router software, both commercial and opensource, does make configuring IPv6 easier (if not trivial). However, setting up a router from the command line in Linux, without the fancy web interfaces, is a great way to learn about how networking and routing works at a fairly low level. It can also help us understand the complexities around IPv6, and why global uptake of the protocol has taken as long as it has.
-
Masquerading Made Simple HOWTO. 21 July 2004. Tapsell. TLDP. ↩
-
Disable consistent network device naming in RHEL7. 11 June 2014. Baird. Red Hat Customer Portal. Retrieved 8 October 2021. ↩
-
Using dnsmasq on a Linux router for DHCPv6. 20 December 2014. Hveem. ↩
-
dhcpcd.conf(5). Retrieved 8 October 2021. Arch Manual pages. ↩
-
Setting up IPv6 using a DHCP client. 21 October 2016. K3a. ↩
-
DHCP static IP addresses with dnsmasq. 3 September 2008. Bokma. ↩
-
What are the essential iptables rules for IPv6 to work properly?. 1 July 2018. Pierre. UNIX Stack Exchange. ↩