How to Assign IPv6 Addresses to LXD Containers on a VPS

Good news for the future of the Internet: IPv6 connectivity is an increasingly common feature offered by VPS providers these days. Unfortunately, many of them also cheap out. Instead of providing a true IPv6 prefix like 2607:f8b0:4004:811::1/64, they’ll provision you some arbitrary range, like the set of 10 addresses between 2607:f8b0:4004:811::1/128 and 2607:f8b0:4004:811::10/128.

I can’t fathom why the providers do this. There are literally trillions upon trillions of IPv6 addresses; if you can afford to hand out IPv4 addresses at no extra charge, you could surely afford to give everyone a /48 prefix, too. Alas, such is the cloud business.

I use my VPSes to run containers using Ubuntu’s LXD stack, and my current provider,, gives me 10 IPv6 addresses to work with. The other day, I was trying to figure out how to assign each address to a container in this scenario—only to find that no such guide existed.

  • This guy accomplished this with NAT66. (Ew.) Unfortunately, this is your only option if your host only provides a single /128 address.
  • This guy avoided NAT, but he did use an old program (npd6) that’s no longer relevant for modern kernels. He was also writing for LXC, not LXD.

So here, I’m going to present my solution, which avoids NAT66, doesn’t rely on manual firewall rules, and is tailored for the Ubuntu sysadmin stack (LXD, systemd, netplan).

Set up LXD networking

When setting up LXD, say “yes” to IPv6 addressing, but “no” to IPv6 NAT. Then add the addresses you want to assign to your containers as extra routes on the LXD bridge, like so:

$ lxc network set lxdbr0 ipv6.routes '2607:f8b0:4004:811::2/128, 2607:f8b0:4004:811::3/128'

The host’s network config should look something like this:

$ lxc network show lxdbr0
  ipv4.nat: "true"
  ipv6.address: fc02::1/120
  ipv6.dhcp.stateful: "true"
  ipv6.routes: 2607:f8b0:4004:811::2/128, 2607:f8b0:4004:811::3/128
$ ip -6 route
2607:f8b0:4004:811::2 dev lxdbr0 proto static metric 1024 pref medium
2607:f8b0:4004:811::3 dev lxdbr0 proto static metric 1024 pref medium

Set up NDP proxies

The kernel needs to know to advertise the containers’ addresses using the IPv6 neighbor discovery protocol (NDP). You do this using the ip neighbour add proxy (yes, British spelling) command for each address:

$ ip -6 neighbour add proxy 2607:f8b0:4004:811::2 dev net0
$ ip -6 neighbour add proxy 2607:f8b0:4004:811::3 dev net0
$ ip -6 neighbour list proxy
2607:f8b0:4004:811::2 dev net0  proxy
2607:f8b0:4004:811::3 dev net0  proxy

This list needs to be recreated each boot, so I have a systemd service to run these commands:

# /etc/systemd/system/proxy-ndp.service

Description=Announce all IPv6 addresses allocated to this server

ExecStart=/sbin/ip -6 neighbour add proxy 2607:f8b0:4004:811::2 dev net0
ExecStart=/sbin/ip -6 neighbour add proxy 2607:f8b0:4004:811::3 dev net0


Enable IPv6 packet forwarding and proxy relaying

The kernel disables these features by default, so you also need to modify these sysctl properties:

# /etc/sysctl.d/91-forward-ipv6.conf 


Set up container networking

Finally, you can assign each container the IPv6 address you desire. This configuration is done from within the container, just as if it were a virtual machine or a real computer. Each container already receives a private IPv6 address from LXD (like fc02::6e); you just need to assign its public address as an additional static address. Here’s how that’s done by hand using ip,

# ip -6 address add 2607:f8b0:4004:811::2 dev eth0

and here’s how it’s done in Ubuntu’s netplan:

# /etc/netplan/50-cloud-init.yaml

# This file is generated from information provided by
# the datasource.  Changes to it will not persist across an instance.
# To disable cloud-init's network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
    version: 2
            dhcp4: true
            dhcp6: true
            - 2607:f8b0:4004:811::2/128

It’s not necessary to specify a gateway (as in IPv4) thanks to the magic of IPv6 router advertisements.


And that’s it! Your containers should have IPv6 networking with Internet-reachable addresses now. Under the default libc configuration, traffic will be preferentially routed over IPv6.

# ip -6 route
2607:f8b0:4004:811::2 dev eth0 proto kernel metric 256 pref medium
fc02::/120 dev eth0 proto ra metric 100 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
default via fe80::c0c4:4aff:fedf:89a6 dev eth0 proto ra metric 100 mtu 1500 pref medium
# ping
PING (2a00:1450:400e:80b::200e)) 56 data bytes
64 bytes from (2a00:1450:400e:80b::200e): icmp_seq=1 ttl=57 time=5.16 ms
64 bytes from (2a00:1450:400e:80b::200e): icmp_seq=2 ttl=57 time=5.33 ms
64 bytes from (2a00:1450:400e:80b::200e): icmp_seq=3 ttl=57 time=5.49 ms
64 bytes from (2a00:1450:400e:80b::200e): icmp_seq=4 ttl=57 time=5.30 ms
--- ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3003ms
rtt min/avg/max/mdev = 5.169/5.326/5.499/0.128 ms

I didn’t touch upon the IPv4 stack in this short tutorial, but you will most likely want to stick with LXD’s default configuration: DHCP and NAT on a /24.

$ lxc list
|       NAME       |  STATE  |       IPV4        |            IPV6              |    TYPE    | SNAPSHOTS |
| container1       | RUNNING | (eth0) | fc02::6e (eth0)              | PERSISTENT | 0         |
|                  |         |                   | 2607:f8b0:4004:811::2 (eth0) |            |           |
| container2       | RUNNING | (eth0)  | fc02::5 (eth0)               | PERSISTENT | 0         |
|                  |         |                   | 2607:f8b0:4004:811::3 (eth0) |            |           |

Enjoy having end-to-end connectivity on your containers, the way the Internet was meant to be experienced.

Leave a Reply