Simple NDP Proxy to Route Your IPv6 VPN Addresses
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.