How to Select Default IPv6 Source Address for Outbound Traffic in OpenVZ 7
Tags: linux, debian, ipv6
Published: 2020-12-13 23:44:17

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

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 OpenVZ 7 virtualization technology, in which I installed Debian 10 operating system.
Commonly, OpenVZ 7 uses [virtual network device (*venet*)](https://wiki.openvz.org/Virtual_network_device) that does not have a MAC address.
*venet* devices are [not fully IPv6 compliant](https://wiki.openvz.org/IPv6), but still works if you statically assign IPv6 addresses.
Moreover, every IP address used in a container must be configured from the host node, because *venet* would drop ip-packets from the container with a source address, and in the container with the destination address, which is not corresponding to an ip-address of the container.
Therefore, I must use the VPS control panel, in this case SolusVM, to assign IPv6 addresses to my server:

![IPv6 Subnet management in SolusVM](https://yoursunny.com/t/2020/preferred-lft-vz7/SolusVM-IPv6.png)

In the *Add IP* section, the IPv6 subnet prefix `2001:db8:f1c1:8454:0964:` is already shown.
Notice that I am putting a colon (`:`) in front of the suffix `beef`, so that they concatenate to the full address `2001:db8:f1c1:8454:0964::beef`.
Forgetting this colon would cause "Invalid Entry" error.

After making this change in the SolusVM control panel, the `/etc/network/interface` file on my server is updated automatically:

```properties
# This configuration file is auto-generated.
# WARNING: Do not edit this file, otherwise your changes will be lost.
# Please edit template /etc/network/interfaces.template instead.
auto lo
iface lo inet loopback
# Auto generated venet0 interfaces
auto venet0
iface venet0 inet static
        address 127.0.0.1
        netmask 255.255.255.255
        broadcast 0.0.0.0
        up route add default dev venet0
iface venet0 inet6 static
        address ::2
        netmask 128
        up ip -6 r a default dev venet0
        up ip addr add 2001:db8:f1c1:8454:0964::2/80 dev venet0
        up ip addr add 2001:db8:f1c1:8454:0964::beef/80 dev venet0
auto venet0:0
iface venet0:0 inet static
        address 10.10.23.159
        netmask 255.255.255.255
```

I'm also seeing two IPv6 addresses:

```shell
$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: venet0: <BROADCAST,POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
    link/void
    inet 127.0.0.1/32 scope host venet0
       valid_lft forever preferred_lft forever
    inet 192.0.2.30/32 brd 192.0.2.30 scope global venet0:0
       valid_lft forever preferred_lft forever
    inet6 2001:db8:f1c1:8454:0964::beef/80 scope global
       valid_lft forever preferred_lft forever
    inet6 2001:db8:f1c1:8454:0964::2/80 scope global
       valid_lft forever preferred_lft forever
    inet6 ::2/128 scope global
       valid_lft forever preferred_lft forever
```

I intend to host my secret beef recipes on its unique IPv6 address `2001:db8:f1c1:8454:0964::beef`, and use the other address `2001:db8:f1c1:8454:0964::2` 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:9f16:8fc7::9

$ sudo tcpdump -n icmp6
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens3, link-type EN10MB (Ethernet), capture size 262144 bytes
10:25:18.264905 IP6 2001:db8:f1c1:8454:0964::beef > 2001:db8:9f16:8fc7::9: ICMP6, echo request, seq 1, length 64
10:25:18.265014 IP6 2001:db8:9f16:8fc7::9 > 2001:db8:f1c1:8454:0964::beef: ICMP6, echo reply, seq 1, length 64
10:25:19.264939 IP6 2001:db8:f1c1:8454:0964::beef > 2001:db8:9f16:8fc7::9: ICMP6, echo request, seq 2, length 64
10:25:19.265013 IP6 2001:db8:9f16:8fc7::9 > 2001:db8:f1c1:8454:0964::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:f1c1:8454:0964::2 2001:db8:9f16:8fc7::9` 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.

As shown in the `ip addr` output above, currently both addresses are "preferred" on my server.
This means, both addresses are equally possible of being used as the default source address.
If I can make `2001:db8:f1c1:8454:0964::2` "preferred" and all other addresses "deprecated", I would achieve my goal of making `2001:db8:f1c1:8454:0964::2` 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.

The command to change `preferred_lft` of an existing IPv6 address is:

```bash
sudo ip addr change 2001:db8:f1c1:8454:0964::beef/80 dev venet0 preferred_lft 0
```

This change takes effect immediately, and outgoing packets start using `2001:db8:f1c1:8454:0964::2` as source address, as I wanted.
However, after a reboot, both IPv6 addresses would become "preferred" again.

As we have seen, the `/etc/network/interfaces` file is adding IPv6 addresses in a [**post-up** command](https://manpages.debian.org/stretch/ifupdown/interfaces.5.en.html) that runs after `ifupdown` package brings the interface up.
Can we change this command and set `preferred_lft` to zero?

So I modified the `/etc/network/interfaces` file, changing that line to:

```properties
up ip addr add 2001:db8:f1c1:8454:0964::beef/80 dev venet0 preferred_lft 0
```

However, modifying `/etc/network/interfaces` in an OpenVZ 7 container would not work.
Although I can see the modification right away, after a reboot, the file is automatically restored to the default state, reverting any changes.

After poking around for a while, I figured out the solution: create a systemd service to change the `preferred_lft` attribute.
The following commands will do the magic:

```bash
sudo apt install -y jq
sudo mkdir -p /usr/local/bin

sudo tee /usr/local/bin/network-preferredlft.sh > /dev/null <<'EOT'
#!/bin/bash
set -e
set -o pipefail

ip -j addr show dev $IFACE \
  | jq -r '
    .[] | select(.addr_info) | .addr_info[] |
    select(.family=="inet6" and .scope=="global") |
    select(.local | (endswith(":1") or endswith(":2")) | not) |
    "ip addr change "+.local+"/"+(.prefixlen|tostring)+" dev "+env.IFACE+" preferred_lft 0"' \
  | sh
EOT

sudo chmod +x /usr/local/bin/network-preferredlft.sh

sudo tee /etc/systemd/system/network-preferredlft.service > /dev/null <<'EOT'
[Unit]
Description=Change preferred_lft
Documentation=https://yoursunny.com/t/2020/preferred-lft-vz7/
After=network-online.target
Wants=network-online.target

[Service]
Environment="IFACE=venet0"
Type=oneshot
ExecStart=/usr/local/bin/network-preferredlft.sh

[Install]
WantedBy=multi-user.target
EOT

sudo systemctl daemon-reload
sudo systemctl enable network-preferredlft
```

The script `/usr/local/bin/network-preferredlft.sh` retrieves a list of IP addresses assigned to the network interface specified by the environment variable `$IFACE`.
For each global-scope IPv6 address that does not end with `:1` or `:2`, the `preferred_lft` attribute is changed to zero.

After executing the above command and rebooting, I can see that the IPv6 address `2001:db8:f1c1:8454:0964::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:f1c1:8454:0964::beef` without worrying about others discovering this "deprecated" IPv6 address through my outbound network traffic.

```shell
$ ip addr show dev venet0
2: venet0: <BROADCAST,POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
    link/void
    inet 127.0.0.1/32 scope host venet0
       valid_lft forever preferred_lft forever
    inet 192.0.2.30/32 brd 192.0.2.30 scope global venet0:0
       valid_lft forever preferred_lft forever
    inet6 2001:db8:f1c1:8454:0964::beef/80 scope global deprecated
       valid_lft forever preferred_lft 0sec
    inet6 2001:db8:f1c1:8454:0964::2/80 scope global
       valid_lft forever preferred_lft forever
    inet6 ::2/128 scope global
       valid_lft forever preferred_lft forever

$ ping 2001:db8:9f16:8fc7::9

$ sudo tcpdump -n icmp6
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens3, link-type EN10MB (Ethernet), capture size 262144 bytes
11:03:40.185496 IP6 2001:db8:f1c1:8454:0964::2 > 2001:db8:9f16:8fc7::9: ICMP6, echo request, seq 1, length 64
11:03:40.185598 IP6 2001:db8:9f16:8fc7::9 > 2001:db8:f1c1:8454:0964::2: ICMP6, echo reply, seq 1, length 64
11:03:41.187229 IP6 2001:db8:f1c1:8454:0964::2 > 2001:db8:9f16:8fc7::9: ICMP6, echo request, seq 2, length 64
11:03:41.187273 IP6 2001:db8:9f16:8fc7::9 > 2001:db8:f1c1:8454:0964::2: 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 a systemd service that invokes `ip addr` command after ifupdown brings up the network interface.
The described technique works in OpenVZ 7 and Debian 10, and has been tested in a VPS provided by Gullo's Hosting.
If you are using KVM and Ubuntu 20.04, check out [How to Select Default IPv6 Source Address for Outbound Traffic with Netplan](https://yoursunny.com/t/2020/preferred-lft-netplan/).