IPv6 setup for a router on Linux

Saturday, 6 April 2024

This is a three-part article about a project to set up a home network with redundant network connections. Here are the parts:

  1. PPP over Ethernet: MTU, MSS and Packet Loss
  2. IPv6 setup for a router on Linux
  3. VRRP for router redundancy with keepalived

Part 2

I am fortunate to be with one of the few UK ISPs that actually support IPv6, which is still sadly rare. I didn't want to lose this functionality when using my own router, since it is so useful to have globally routable IP addresses for all of the machines on the home network. But IPv6 has a learning curve and setting up an IPv6 router was new to me.

On my router, I was able to use IPv4 services straight away. PPP does a lot of the setup itself, e.g. creating a default route and obtaining an IPv4 address. One IPv4 address is shared by everything on the network by network address translation (NAT). To share the connection with NAT, you need only set net.ipv4.ip_forward=1 in /etc/sysctl.conf and then create a NAT rule for nftables, such as:

    table ip nat {
        chain prerouting {
            type nat hook prerouting priority dstnat; policy accept;
        }

        chain postrouting {
            type nat hook postrouting priority srcnat; policy accept;
            ip saddr 192.168.1.0/24 masquerade
        }
    }

The only other steps for IPv4 might involve running a DHCP and DNS server on your LAN, and for this I use dnsmasq, which makes it really easy to refer to computers on your LAN using hostnames and also assign static addresses.

When IPv6 is introduced, things become more complicated:

  • NAT is a workaround for the limitations of IPv4: computers shouldn't really have to share IP addresses, and with IPv6, they don't. But NAT also simplifies setup. With NAT, machines on the LAN have completely private IPv4 addresses which are independent of any globally-routable address. These IPv4 addresses are assigned before the connection is available and they persist even if the connection drops or is replaced by an alternative. When IPv6 is introduced, this simplification disappears, because all machines on the LAN must obtain their globally-routable addresses from the router once the connection is available. This requires additional setup.
  • The PPP daemon doesn't provide routable IPv6 addresses as part of its automatic configuration. Instead it produces only non-routable "link-local" for both ends of the connection. In order to use these to reach the Internet, the router must use the PPP connection to determine a globally routable address separately. Again, this requires additional setup.
  • Internal network services that were previously hidden by NAT may become externally accessible via IPv6. There is no "port forwarding" or "DMZ" - these are NAT concepts. All ports are potentially reachable unless blocked. Hence a firewall configuration becomes essential. Once again, this requires additional setup.

Each of these complications can be resolved as follows.

Sharing globally routable addresses with the LAN

Router advertisement (RA) messages are broadcast on the LAN so that IPv6-capable computers can determine globally-routable addresses for themselves. They must choose addresses within the subnet used by the router. RA messages are broadcast by a daemon running on the router and the usual choice on Linux is radvd.

The RA daemon is somewhat analogous to a DHCP daemon in that it assigns addresses. It can also broadcast a DNS server address. However, unlike DHCP, the RA daemon does not do anything until a globally-routable address is known to the router.

My RA daemon configuration in /etc/radvd.conf was based on an example and looks like this:

    interface eth0
    {
        AdvSendAdvert on;
        MinRtrAdvInterval 3;
        MaxRtrAdvInterval 10;
        AdvDefaultPreference low;
        AdvHomeAgentFlag off;
        prefix ::/64
        {
            AdvOnLink on;
            AdvAutonomous on;
            AdvRouterAddr off;
            AdvPreferredLifetime 120;
            AdvValidLifetime 300;
        };
        # Next line has the IPv6 address of a DNS server:
        RDNSS 2001:4860:4860::8888
        {
            AdvRDNSSLifetime 30;
        };
    };

RA either works or not, so it is harder to make mistakes here, though I did mess up somewhat by attempting to leave the ISP's router running as a WiFi hotspot. Despite my attempts to disable all services on that old router, it still continued to broadcast RA messages. I think the IPv6 clients on the LAN ignored these spurious messages, but I still took the whole thing offline just in case.

Getting a globally routable address

A globally routable address can be obtained using wide-dhcpv6-client. Most DHCP functions are replaced by RA, so most clients do not need to use DHCPv6, but it may be needed on a router in order to obtain an address range from the ISP's own router. DHCPv6 is only superficially similar to DHCP for IPv4. One difference is that an entire block of addresses are assigned at once!

I used an example configuration for wide-dhcpv6 which I placed in /etc/wide-dhcpv6/dhcp6c.conf. It specifies the two relevant interfaces (eth0 for the LAN, ppp0 for the Internet connection). The address is requested via ppp0 and then assigned to eth0.

    interface ppp0 {
        send ia-pd 0;
    };
    id-assoc pd {
        prefix-interface eth0 {
                sla-id 1;
        };
    };

Additionally (and somewhat confusingly) two options are required in the PPP daemon options file (e.g. /etc/ppp/peers/provider). These are:

    +ipv6
    defaultroute6

It makes sense to me that IPv6 would need to be enabled for PPP (with +ipv6), but the need for defaultroute6 is less clear. What this actually does is create a routing table entry (visible with "ip -6 route"):

    default dev ppp0 metric 1024 pref medium

This is needed in addition to the address configuration so that the kernel knows where to send IPv6 packets. If it's missing, the packets won't be routable, even though the addresses are correct.

My mistakes here were rooted in IPv4 thinking. I expected the PPP daemon to be able to do the address configuration itself, as for IPv4, or that addresses would be automatically configured by RA. This didn't work, despite some misleading advice suggesting that you could set "net.ipv6.conf.ppp0.accept_ra=1" or a strange setting like "net.ipv6.conf.all.forwarding=2" in sysctl.conf (there was once some some really odd behaviour here). As far as I can tell, stateless address autoconfiguration is not the right way to set up a router, and DHCPv6 is probably what you want unless the ISP tells you otherwise.

Firewall setup

A firewall for IPv6 needs to allow IPv6 control messages to reach the router, from both the PPP interface and the Ethernet interface. But it should also block most incoming traffic into your LAN, and you should specifically enable any services that you actually want to make accessible from outside.

Linux won't forward either IPv4 or IPv6 packets by default. They both have to be enabled separately. IPv4 forwarding is enabled by setting net.ipv4.ip_forward=1 in /etc/sysctl.conf while IPv6 is enabled by net.ipv6.conf.all.forwarding=1.

Here is part of firewall configuration I added to /etc/nftables.conf:

    table ip6 filter {
        chain input {
            # accept all IPv6 traffic for the router
            type filter hook input priority 0;
            policy accept;
        }
        chain forward {
            type filter hook forward priority filter; policy drop;

            # established/related connections
            ct state established,related accept

            # invalid connections
            ct state invalid drop

            # loopback interface
            iifname lo accept

            # icmp allowed into the LAN
            icmpv6 type { echo-reply, echo-request } accept

            # open tcp ports: sshd (22)
            tcp dport {ssh} accept
        }

        chain output {
            type filter hook output priority filter; policy accept;
        }
    }

I tested my firewall configuration from outside of the LAN by using nmap to port-scan various machines.

My mistake here was to block IPv6 traffic into the router. It's very important that the ICMPv6 messages used by DHCPv6 are able to get through. If IPv6 is configured incorrectly, the result may not be an immediate connection failure. The actual problems may only be noticed after a few minutes, as address assignments expire. My advice is to reboot after making configuration changes (to ensure the new configuration persists) and then wait at least 5 minutes before testing the connection again.

In conclusion, if you need IPv6 on your router, you probably need radvd and wide-dhcpc6 as well as everything needed for IPv4, and you need to configure the firewall to protect the machines on your LAN. Enable +ipv6 and defaultroute6 for the PPP daemon too.