How to Select Default IPv6 Source Address for Outbound Traffic in OpenVZ 7
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) that does not have a MAC address.
venet devices are not fully IPv6 compliant, 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:
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:
# 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:
$ 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:
$ 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) is a very complicated topic.
An application can explicitly specify a source address.
For example, I can invokeping -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:
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 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:
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:
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.
$ 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.