IPv6 local router with privacy-preserving addresses

In IPv6 everywhere with tinc we saw how to use a tinc VPN to let a host with its own public IPv6 network provide entire subnetworks to remote devices like home computers.

The setup allows each device to have its own public IPv6 addresses to access or provide services to the IPv6 Internet. However, with such a setup the device always uses the same address for outgoing traffic, which may be awful for end-user devices from the privacy perspective, since they are very easy to be uniquely identified worldwide.

Also, the network topology diagram in that post showed that each device may serve its /64 public IPv6 network locally, thus allowing other hosts in the local network to also have full public IPv6. A very simple setup using IPv6 stateless address autoconfiguration (SLAAC) for such hosts, based on the IPv6 router advertisement daemon (radvd) is explained here. However, autoconfigured addresses have the very nasty effect of revealing the host's MAC address, which is even worse privacy-wise since hosts become uniquely identifiable regardless of the network they are in (see this post for a deeper discussion).

This article explains how to easily configure a Debian IPv6 router and its hosts to use temporary IPv6 addresses (privacy extensions for SLAAC) that avoid the problems mentioned above.

Basic routing

Let's review the /etc/tinc/inet6/tinc-up script used by non-gateway nodes as described in the previous post:

#!/bin/sh
ip -6 link set "$INTERFACE" up mtu 1400
ip -6 addr add 2002:0102:0304:0004::1/48 dev "$INTERFACE"
ip -6 route add default dev "$INTERFACE"

The device has the address 2002:0102:0304:0004::1/48 added to tinc's $INTERFACE. It also has a default route to the IPv6 Internet on that interface.

The first thing we need to enable for this device to become a router is IPv6 forwarding in the firewall and in the kernel:

# ip6tables -P FORWARD ACCEPT  # this is usually the default case
# cat > /etc/sysctl.d/local-forwarding.conf << EOF
net.ipv6.conf.all.forwarding=1
EOF
# sysctl -p /etc/sysctl.d/local-forwarding.conf

However the router still has no configuration for the IPv6 local network. Assuming it's on the eth0 interface, an easy way to do it is adding a static configuration like this in /etc/network/interfaces:

auto eth0  # or ``allow-hotplug eth0`` or whatever is already there
iface eth0 inet6 static
    address  2002:0102:0304:0004::1/64

Please note that it's the same address as above, with a /64 prefix instead (the only valid prefix for IPv6 end networks). There's no need to use additional addresses as long as different prefixes are used. Remember to put the interface up (e.g. with service networking restart).

That should be enough for hosts in the local network to use static configurations like this in their /etc/network/interfaces:

iface eth0 inet6 static
    address  2002:0102:0304:0004::1234/64
    gateway  2002:0102:0304:0004::1

Address autoconfiguration

Instead of using a static configuration for hosts in the local network, we may use IPv6's stateless address configuration (SLAAC), which uses the Network Discovery Protocol (NDP) for hosts to choose their own address (based on the interface's MAC address), thus simplifying the whole network configuration.

To do this we install the radvd package in the router and create a very simple configuration file for it to announce its presence as a router in the local network:

# apt-get install radvd
# echo > /etc/radvd.conf << EOF
interface eth0 {
        AdvSendAdvert on;
        prefix ::/64 {};
};
EOF
# service radvd restart

Please note that the prefix option doesn't specify the 2002:0102:0304:0004::/64 network, but just ::/64. This is a special value that tells radvd to automatically announce the router for any networks in the given interface which have a non-link-local address. Since the router's IPv6 address is already configured in eth0 (because of the static entry in the interfaces file) by the time radvd service starts, we can avoid some repetition of addresses in configuration files.

With this in place, local network hosts just need an entry like this in their /etc/network/interfaces to configure their interface's IPv6:

iface eth0 inet6 auto

What if we want that host to provide a network service? It would be nice for it to still have a fixed public address which doesn't depend on the particular hardware. We may add that address with the pre-up option of interfaces, so that it is assigned before autoconfiguration, even if this fails for some reason:

iface eth0 inet6 auto
    pre-up  ip addr add 2002:0102:0304:0004::1234/64 dev $IFACE preferred_lft 0

Why this preferred_lft 0 setting? It marks this static address as deprecated, i.e. it may be used for receiving incoming connections from the outside, but when an outgoing connection is done, other addresses will be preferred, in our case the autoconfigured one.

Temporary addresses

As I mentioned in the introduction, autoconfigured addresses have serious privacy issues. Fortunately, SLAAC and NDP support the use of temporary addresses which are generated randomly and discarded after a while. To use these addresses on autoconfiguration instead of the MAC-based ones, we need to first enable them in the kernel of local network hosts:

# cat > /etc/sysctl.d/local-ipv6-privacy.conf << EOF
net.ipv6.conf.all.use_tempaddr = 2
net.ipv6.conf.default.use_tempaddr = 2
EOF
# sysctl -p /etc/sysctl.d/local-ipv6-privacy.conf

We also need to add the privext 2 setting in the /etc/network/interfaces entry:

iface eth0 inet6 auto
    privext  2

After restarting the host's network, you may see that running ip -6 addr show dev eth0 lists some temporary addresses: old addresses are kept for a while to receive pending traffic and they end up going away.

We can also make the router enjoy temporary addresses for outgoing connections. Its interfaces entry should be similar to this one:

iface eth0 inet6 auto
    pre-up  ip addr add 2002:0102:0304:0004::1/64 dev $IFACE preferred_lft 0
    privext  2

We must remember to also add the preferred_lft 0 setting to the ip -6 addr add... command in the tinc-up script and restart tinc, otherwise we'll end up using that address for outgoing connections!

The configuration above seems to introduce a chicken-and-egg problem. How can we autoconfigure eth0 in the router when radvd is not running there yet? How does radvd know what networks to announce when the interface isn't yet configured? The loop-breaker here is the pre-up command, which sets the network before the interface is configured. Autoconfiguration keeps running in the background, so that when radvd starts, it can see that network, start and thus the autoconfiguration is completed.

This is all we need to do to have a public IPv6 local network for all our hosts. It wasn't that difficult, was it?

A note on my particular setup

I have a laptop with the whole tinc + radvd + privacy extensions enabled. However, since its Ethernet port may be plugged into untrusted networks, I'm not comfortable with running radvd on eth0 and having unknown hosts access the IPv6 Internet through the laptop. I still want to use temporary IPv6 addresses for my browsing, though. What to do, then?

The solution I found was to create a dummy interface and have radvd run on it. I just added dummy to my /etc/modules, changed eth0 by dummy0 in /etc/radvd.conf, and used this interfaces entry:

auto dummy0
iface dummy0 inet6 auto
    pre-up  ip link set multicast on dev $IFACE
    pre-up  ip addr add X:Y:Z:T::1/64 dev $IFACE preferred_lft 0
    privext  2

The first pre-up command is needed since multicasting is not enabled by default on dummy interfaces, and radvd 2 refuses to announce on interfaces without multicast support (see radvd's issue #46).