If you tried setting up an IPv6-capable VPN on a VPS provider that gave you an IP range to play with, perhaps a /64 or larger, you would want to assign some of the IPv6 addresses you have to your clients. In this post, we suppose that you have the range 2001:db8::/64.

This should be a simple process: enable the sysctl option net.ipv6.conf.all.forwarding to 1 (or whatever the equivalent is on your system), use DHCPv6 or SLAAC to assign the addresses to the clients, and then your client should have working IPv6.

The Problem

Unfortunately, this is not so simple. Most VPS providers are not actually routing the entire subnet 2001:db8::/64 to you. Rather, they just connect a number of VPSes onto the same virtual Ethernet network and rely on the Neighbour Discovery Protocol (NDP) to find the router.

So for example, your VPS has a public interface eth0 with public IPv6 address 2001:db8::1, and a private VPN interface tap_vpn. Your VPN client is assigned the IPv6 address 2001:db8::2. When the VPS provider receives a packet for 2001:db8::2, their router sends out a neighbour solicitation request over NDP, basically asking “who has IP 2001:db8::2?”

Your server receives the packet on eth0. Since eth0 does not have the address 2001:db8::2, it does not answer the solicitation request. No other VPS on the network has it either, so no one answers it, and the router sends back “host not found”.

The problem is that your VPS is the router for 2001:db8::2, and so, it should answer the neighbour solicitation request in order to get the IP packets for 2001:db8::2.

The Solution

Linux comes with a feature called NDP proxying. You can declare a list of IP addresses to answer neighbour solicitation requests for, and the system will answer them for you, allowing you to receive packets for them.

First, you have to enable this feature by setting the sysctl option net.ipv6.conf.all.proxy_ndp to 1. You can do this by adding the following line to /etc/sysctl.conf (or whatever the equivalent on your system is):

net.ipv6.conf.all.proxy_ndp = 1

Once you do this, run sysctl -p as root to activate it immediately.

Then, for every IP address you wish the VPS to route, you have to run:

ip -6 neigh add proxy <ip> dev <interface>

For example, if you want to answer for 2001:db8::2 on eth0, run:

ip -6 neigh add proxy 2001:db8::2 dev eth0

Afterwards, your server will tell your external router, connected to eth0, that it should receive packets for 2001:db8::2. Your server now routes the traffic properly!

Scalability

Unfortunately, ip -6 neigh add proxy only allows you to add one IPv6 address at a time. There is no way to add an entire range of IP addresses.

You can pre-add a bunch of IP addresses and use DHCPv6 to only assign those to clients, but that’s rather ugly, and depending on how many addresses you added, may cause significant slowdowns.

The common wisdom is to run ndppd, a program that answers neighbour solicitation requests. It can be thought of as a replacement for the kernel’s NDP proxying feature. However, it has been relatively unmaintained, and multiple users reported that it does not work anymore. It did not work for me either.

Enter dnsmasq

Since I am using dnsmasq already to announce the SLAAC prefix as well as providing a DNS server for the VPN, it seems natural to check to see if there are any additional features of dnsmasq that I could use.

Turns out, it supports hook scripts that run when a new client connects or disconnects, and these hooks receive the IP(s) assigned to clients via DHCP.

So naturally, the solution is to switch dnsmasq to assign IPs via DHCPv6 and use a hook script to automatically add and delete NDP proxy rules.

The following hook script does the job well:

#!/bin/bash
action="$1"
ip="$3"

case "$ip" in
    2001:db8::*) ;;
    *) exit ;;
esac

case "$action" in
    add|old)
        ip -6 neigh add proxy "$ip" dev eth0
        ;;
    del)
        ip -6 neigh del proxy "$ip" dev eth0
        ;;
esac

You should replace the IP prefix and eth0 with your own values.

To use this hook, add the following line to your /etc/dnsmasq.conf (or wherever dnsmasq.conf is on your setup):

dhcp-script=<path to the shell script above>

Downsides

Unfortunately, there is a downside to this. Since your IPv6 address is now assigned by DHCPv6, the privacy extensions no longer work. Your system cannot generate new IP addresses to use periodically.

On the other hand, since most services now identify you by your /64 prefix, knowing that the remaining bits change with privacy extensions, this does not make too much of a difference.

Still, ndppd works for this use case, so you can give it a try. You might have better luck than I did with it.

In any case, I hope you found this post useful. Have a nice day.