How to survive despite having Dual-Stack Lite

Malte Poll
6 min readDec 9, 2020

--

Following the exhaustion of the IPv4 address space, many ISPs started rolling out IPv6. However, since many services and networks still only support IPv4, households are best served with dual-stack. While some providers give their customers real dual-stack with at least one routable IPv4 address and at least one /64 IPv6 subnet, others don’t have enough address space left to serve all customers. This leads to the adoption of Dual-Stack Lite. Instead of giving customers routable IPv4 addresses and let them use NAT on their own router, Dual-Stack Lite allows ISPs to assign a single address to multiple customers by performing carrier grade NAT. My ISP (Vodafone / formerly Unitymedia) in Germany uses DS-Lite and it has given me a lot of headaches. This post exists to show you how you can use any modern Linux distribution to get dual-stack internet working.

The theory

While I would strongly encourage everyone to read RF6333 to really understand DS-Lite, I will try to give a rough explanation to get you up to speed.

Basic layout of a DS-Lite deployment

In a real dual-stack environment, the customer router has both an IPv4 and an IPv6 address on the WAN interface and can natively route traffic over it. In the DS-Lite world, the customer router is called B4 and only has IPv6 configured on the WAN interface. IPv4 is tunneled via a 4-in-6 tunnel to a special NAT gateway somewhere in the ISPs backbone called the AFTR. A 4-in-6 tunnel works by wrapping any IPv4 packets inside IPv6 packets and sending them to the AFTR, which then performs the required NATing and sends any response packets back using the same method. On Linux, this is implemented using virtual tunnel interfaces. The tunnel interface should always use the well-known IPv4 subnet of 192.0.0.0/29 where the AFTR always uses the address 192.0.0.1 while the B4 will use 192.0.0.2 in most cases although other addresses should work as well.

The only configuration parameter needed on the B4 is the hostname / IPv6 address of the AFTR needed to establish the 4-in-6 tunnel. While the RFC allows for out-of-band configuration, it also mentions a method to discover the AFTR hostname via DHCPv6 as described in RFC6334. This works by sending an option request asking for the name of the AFTR along with the regular DHCPv6 request. This means that the DHCPv6 client that we use on the B4 also has to support custom option requests or already needs to have support for DS-Lite. When learning about AFTR discovery I wanted to see it in action which is why I recorded a DHCPv6 handshake with the required option request and response as a PCAP that you can use as a reference for your own experiments.

The practice

Now we come to the interesting part where I explain how to configure most modern Linux distributions to correctly discover and automatically configure Dual-Stack Lite. We will split this in two parts: First we take a look at the most popular DHCPv6 clients and how they can be configured to request the AFTR name and how we can actually parse the response. Then we will take a look at how to configure the tunnel interface to successfully route IPv4 traffic originating from the local network OR the router itself (this part took me ages to get right). This guide assumes that you already have a working IPv6 stack and will not go into details about how to setup your firewall correctly so keep this in mind. I will also assume that you use DHCPv6 prefix delegation as this seems to be the case for most ISPs that deploy DS-Lite.

DHCPv6 AFTR Discovery

In my experience there are more network configuration daemons for Linux than init systems. There is Debian’s /etc/network/interfaces, Ubuntu’s netplan, NetworkManager, systemd-networkd, netctl and probably even more. The only good new is that most of these allow you to use either dhclient or dhcpcd as your DHCPv6 client so we can focus on these two as they are both extensible and have a scripting interface.

The isc-dhcp-client (or dhclient) is the default on Debian and is easy to configure using the configuration file in /etc/dhclient.conf or /etc/dhcp/dhclient.conf (Debian).

Append this to your dhclient.conf to discover the AFTR name via DHCPv6

An alternative with similar options and configurations is the modern brother dhcpcd with the configuration file stored under /etc/dhcpcd.conf

Append this to your dhcpcd.conf to discover the AFTR name via DHCPv6

This will ensure that the AFTR name is requested. To actually work with the AFTR name, we need to create a hook that is called when the DHCP lease is aquired. For this, we create a script in the directory containing dhclient exit hooks (/etc/dhcp/dhclient-exit-hooks/ on Debian) or the file containing dhcpcd exit hooks (/etc/dhcpcd.exit-hook) that will extract the AFTR name and then call another script to setup the tunnel interface. Luckily, both use the same environment variables so we can use the same script for dhclient and dhcpcd.

This is a drop-in script for dhclient or dhcpcd to extract the AFTR name from DHCPv6 responses

After creating the hook, make it executable using chmod +x and restart your DHCP client.

This configuration will work for most distributions. The /etc/network/interfaces config can use dhclient or dhcpcd, netctl on Arch Linux can use both of these as well and manual configurations using either of these clients will — of course — work. However there are two configuration systems we did not cover yet. NetworkManager was not working for me since they either use their own, limited DHCPv6 client or use an external client with a dynamically created configuration file which makes it harder to use. Since NetworkManager is mostly used by client devices with changing, dynamic networks I decided to ignore it.
The second configuration system is systemd-networkd. It is the default on multiple distributions and implements its own internal DHCPv6 client that almost works. While it is able to request custom DHCPv6 options, it is unable to parse the response correctly. As I wanted this guide to be as complete as possible, I decided to write a script that will sniff DHCPv6 responses on the WAN interface to extract the AFTR address that way. Please understand that this is a dirty hack and is not recommended. In my tests it works fine so I include this as an option.

Changes to a systemd-networkd network file to request the DHCPv6 AFTR name
Python script to sniff the AFTR name. Has to run as a service in the background.

Creating the 4-in-6 tunnel interface

Now that we know the AFTR name, all that is left to do is getting the IPv6 address of the AFTR, creating a 4-in-6 tunnel between the B4 and AFTR with the address 192.0.0.2 and setting a default route for IPv4. Creation of the tunnel is done using the commandip -6 tunnel add [INTERFACE] mode ipip6 local [B4 WAN IPv6 ADDR] remote [AFTR IPv6 ADDR] . When setting the interface address and default route, there is a special trick I only discovered through a lot of trial and error:

Packets forwarded from clients will work fine when adding the address 192.0.0.2/29 to the tunnel interface. However, any IPv4 traffic originating from the router will then use 192.0.0.2 as the source address of outgoing packets and will not be routed correctly. To fix this, we will add another address to the tunnel interface and use it for packets originating from the router to have them routed correctly. The following script does it all for you automatically. It is meant to be used with the DHCPv6 AFTR discovery scripts from the previous section.

Script to create a DS-Lite 4-in-6 tunnel interface. Put somewhere in the path and make executable.

Please note that it is not recommended to do NAT masquerading on the B4 as it is not needed (the private IPv4 address for packets originating from your LAN is not altered inside the 4-in-6 tunnel). When everything is configured, your network configuration should look like this:

# ip a show dslite0
6: dslite0@enp0s2: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1280 qdisc noqueue state UNKNOWN group default qlen 1000
link/tunnel6 2a02:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx peer 2a02:xxxx::xxxx:xxxx
inet 192.0.0.2 peer 192.0.0.1/32 brd 255.255.255.255 scope global dslite0
valid_lft forever preferred_lft forever
inet 192.168.77.1/32 scope global dslite0
valid_lft forever preferred_lft forever
inet6 fe80::84bb:c3ff:fe33:4412/64 scope link
valid_lft forever preferred_lft forever
# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.0.0.1 0.0.0.0 UG 0 0 0 dslite0
192.0.0.1 0.0.0.0 255.255.255.255 UH 0 0 0 dslite0

A short note on OpenWRT

OpenWRT seems to be the only Linux distribution with solid DS-Lite autoconfiguration out of the box. You can pretty much plug in your router and it will detect the AFTR name and create the tunnel interface. The one thing I struggled with is IPv4 traffic originating from the DS-Lite tunnel interface as OpenWRT only assigns the 192.0.0.2/29 address. However, using the same trick as before we can assign an extra address to the tunnel interface as well and use it as the source address. It should be possible to create a static (not ad-hock created) interface that already has the extra address but I did not yet get this to work. Alternatively, you can create a hotplug script in /etc/hotplug.d/iface to add the private ip address and change the default route src.

This script will update the default route to use a private IPv4 address as src to route traffic correctly

--

--

Responses (2)