How to Select Default IPv6 Source Address for Outbound Traffic with Netplan
Tags: linux, ubuntu, netplan, ipv6
Published: 2020-12-07 02:20:54

> This post is originally published on yoursunny.com blog https://yoursunny.com/t/2020/preferred-lft-netplan/

I bought a few Virtual Private Servers (VPS) on Black Friday, and have been busy setting them up.
Nowadays, most VPS comes with an IPv6 *subnet* that contains millions of possible addresses.
Initially, only one IPv6 address is assigned to the server, but the user can assign additional addresses as desired.
Given that I plan to run multiple services within a server, I added a few more IPv6 addresses so that each service can have a unique IPv6 address.

One of my servers is using KVM virtualization technology, in which I installed Ubuntu 20.04 operating system manually from an ISO image.
Unlike a template-based installation, an ISO-installed Ubuntu 20.04 system manages its networks using [Netplan](https://netplan.io/), a backend-agnostic network configuration utility that generates network configuration from YAML files.
Most VPS control panels, including SolusVM and Virtualizer, are unable to generate the YAML files needed by Netplan.
IPv4 works out of box via DHCP, but IPv6 has to be configured manually.
To assign two IPv6 addresses to my server, I need to write the following in `/etc/netplan/01-netcfg.yaml`:

```yaml
network:
  version: 2
  ethernets:
    ens3:
      dhcp4: true
      addresses:
        - 2001:db8:30fa:5877::1/64
        - 2001:db8:30fa:5877::beef/64
      routes:
        - to: ::/0
          via: 2001:db8:30fa::1
          on-link: true
      nameservers:
        addresses:
        - 2001:4860:4860::8888
        - 2606:4700:4700::1111
```

I intend to host my secret beef recipes on its unique IPv6 address `2001:db8:30fa:5877::beef`, and use the other address `2001:db8:30fa:5877::1` for outbound traffic such as pings and traceroutes.
However, I noticed that the wrong address is being selected for outgoing packets:

```shell
$ ping 2001:db8:57eb:8479::2

$ sudo tcpdump -n icmp6
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on venet0, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
00:44:48.704099 IP6 2001:db8:30fa:5877::beef > 2001:db8:57eb:8479::2: ICMP6, echo request, seq 1, length 64
00:44:48.704188 IP6 2001:db8:57eb:8479::2 > 2001:db8:30fa:5877::beef: ICMP6, echo reply, seq 1, length 64
00:44:49.704011 IP6 2001:db8:30fa:5877::beef > 2001:db8:57eb:8479::2: ICMP6, echo request, seq 2, length 64
00:44:49.704099 IP6 2001:db8:57eb:8479::2 > 2001:db8:30fa:5877::beef: ICMP6, echo reply, seq 2, length 64
```

I started searching for a solution, and learned that:

* [Default Address Selection for Internet Protocol version 6 (IPv6)](https://tools.ietf.org/html/rfc3484) is a very complicated topic.
* An application can explicitly specify a source address.
  For example, I can invoke `ping -I 2001:db8:30fa:5877::1 2001:db8:57eb:8479::2` to use the desired source address.
* Each local IPv6 address can be either "preferred" or "deprecated".
  If the application does not specify a source address, the system would prefer to use a "preferred" address instead of a "deprecated" address.

Currently, both addresses are "preferred" on my server:

```shell
$ ip addr show dev ens3
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 92:b6:ab:eb:04:00 brd ff:ff:ff:ff:ff:ff
    inet 192.0.2.30/24 brd 192.0.2.255 scope global dynamic ens3
       valid_lft 21599977sec preferred_lft 21599977sec
    inet6 2001:db8:30fa:5877::1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2001:db8:30fa:5877::beef/64 scope global
       valid_lft forever preferred_lft forever
```

This means, both addresses are equally possible of being used as the default source address.
If I can make `2001:db8:30fa:5877::1` "preferred" and all other addresses "deprecated", I would achieve my goal of making `2001:db8:30fa:5877::1` the default source address for outbound traffic.

How can I set an IPv6 address as "deprecated"?
After some digging, I found that it is controlled by the `preferred_lft` (preferred lifetime) attribute.
This attribute indicates the remaining time an IP address is to remain "preferred".
Unless it is set to "forever", `preferred_lft` counts down every second, and the IP address becomes "deprecated" when it reaches zero.
If the IP address was added with `preferred_lft` set to zero, it would be "deprecated" since the beginning.

Netplan gained support for `preferred_lft` setting [very recently](https://bugs.launchpad.net/netplan/+bug/1803203) since version `0.100-0ubuntu4`.
The syntax to specify `preferred_lft` of an IPv6 address is:

```yaml
addresses:
  - 2001:db8:30fa:5877::1/64
  - 2001:db8:30fa:5877::beef/64:
      lifetime: 0
```

Notice that there is a colon (`:`) after the IPv6 address `2001:db8:30fa:5877::beef/64`, because it is syntactically a map key instead of a string value.
The equivalent structure in JSON looks like:

```json
{
  "addresses": [
    "2001:db8:30fa:5877::1/64",
    {
      "2001:db8:30fa:5877::beef/64": {
        "lifetime": 0
      }
    }
  ]
}
```

After applying this change, the IPv6 address `2001:db8:30fa:5877::beef` is correctly marked as "deprecated" and no longer selected as the default source address.
Now I can securely host my secret beef recipes on `2001:db8:30fa:5877::beef` without worrying about others discovering this "deprecated" IPv6 address through my outbound network traffic.

```shell
$ sudo netplan apply

$ ip addr show dev ens3
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 92:b6:ab:eb:04:00 brd ff:ff:ff:ff:ff:ff
    inet 192.0.2.30/24 brd 192.0.2.255 scope global dynamic ens3
       valid_lft 21598263sec preferred_lft 21598263sec
    inet6 2001:db8:30fa:5877::1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 2001:db8:30fa:5877::beef/64 scope global deprecated
       valid_lft forever preferred_lft 0sec

$ ping 2001:db8:57eb:8479::2

$ sudo tcpdump -n icmp6
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on venet0, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
00:44:48.704099 IP6 2001:db8:30fa:5877::1 > 2001:db8:57eb:8479::2: ICMP6, echo request, seq 1, length 64
00:44:48.704188 IP6 2001:db8:57eb:8479::2 > 2001:db8:30fa:5877::1: ICMP6, echo reply, seq 1, length 64
00:44:49.704011 IP6 2001:db8:30fa:5877::1 > 2001:db8:57eb:8479::2: ICMP6, echo request, seq 2, length 64
00:44:49.704099 IP6 2001:db8:57eb:8479::2 > 2001:db8:30fa:5877::1: ICMP6, echo reply, seq 2, length 64
```

This article explained how to change default IPv6 source address selection by marking an IPv6 address "deprecated" via Netplan.
The described technique works in KVM and Ubuntu 20.04, and has been tested in a VPS provided by Spartan Host.
If you are using a OpenVZ 7 container, check out [How to Select Default IPv6 Source Address for Outbound Traffic in OpenVZ 7](https://yoursunny.com/t/2020/preferred-lft-vz7/).