An IPv6 address has structure! Officially, but let’s see if we care, the first half is the network prefix, itself consisting of a routing prefix and a subnet ID. The second half is the interface identifier. In a nutshell; the first half is for routing, the second half is for addressing within the single routed-to network.
My ISP only gives me a /64, but “behind” the machine with this address range, I have hosts that I want to be publicly IPv6-addressable through their wireguard tunnels. So these are mobility-stable IPs.
And each of them I’d like to give not just one address, but a range from which to pick addresses. So I give the Wireguard interface a /72 that sits within the /64, and each of the Wireguard peers on that network, a /80 that sits in that /72.
Because that’s the IPv6 dream, right? But it’s not by the book! As far as I understand correctly, you’re not supposed to chop up a /64 further, it breaks things such as SLAAC (which I’m not using on the Wireguard interfaces anyway). If things would have gone by the book I would have gotten a /56 (or at least something wider than a /64) from my ISP, but alas, a /64 is all I get with my supercheap bare metal servers. Otherwise I could use something like prefix delegation. But here we are. With some ifs and buts that don’t hurt my requirements, it works. Any host on the other side of the Wireguard tunnel can set an address from its /80, and hurray, it’s pingable from the wider internet and used for connecting to IPv6 hosts on the internet.
But, but. I don’t want to just pick one address and have it forever be related to my laptop or desktop or phone or other thing I’m using these addresses on. And I’m not alone in such thinking, so there’s a thing for that: IPv6 Temporary Addresses. It’s exactly the mechanism I want — having a bunch of random addresses on the endpoint’s interfaces, that will have a limited lifetime and will be rotated out (first no longer used for new connections, then removed a while later).
The snag is that the “official” temporary addresses work by setting the whole interface identifier — the whole last half of the IPv6 address. But I’m subversively using part of that last half for routing. And no matter what I set in /proc/sys/net/ipv6/conf/*/use_tempaddr, the kernel will just not generate these limited-lifetime addresses on a /80 interface.
But we can generate and set them ourselves, from userspace, and get pretty much the same experience! You can just, you know, for example, run ip -6 addr add 1:2:3:4:5::$somerandomparts/80 dev myinterface valid_lft 86400 preferred_lft 1800 — which would give you one randomized address that will be used for new connections for half an hour, and will be cleaned up after a day (which breaks connections). We can have several of these active at any one time, but of course we must periodically add fresh ones lest we’d have no more addresses configured on the interface.
For inspiration, here’s what I’m using, a systemd timer and service.
The timer, simple enough:
# /etc/systemd/system/ipv6-tempaddr@.timer
[Unit]
Description = Maintain temporary IPv6 address on %i
[Timer]
# make this shorter if you require more frequent intervention (small TTLs configured)
OnCalendar = *:0/10
[Install]
WantedBy=timers.target
The service file is bit of a mouthful though:
# /etc/systemd/system/ipv6-tempaddr@.service
[Unit]
Description = Maintain temporary IPv6 address on %i
# Requires 'jq'!
After=systemd-networkd-wait-online.service
Wants=systemd-networkd-wait-online.service
[Service]
Type = oneshot
SyslogIdentifier = ipv6-tempaddr@%i
StandardOutput = null
# connections stay valid for 4 hours
Environment = VALID_LFT=14400
# but an IP will only be used for new connections for a maximum of half an hour
Environment = PREFERRED_LFT=1800
# and a new preferred IP (for new connections) will be added every 20 minutes
# (or rather, when no address with more than 20 minutes of life left in it exists)
Environment = ADD_NEW_WHEN_PREFERRED_LFT_LEFT=1200
# the IP addr prefix
Environment = IP6_PREFIX=12:3456:78:90:ab::
# the number of address components (2-byte groups; hex char quartets, eg "04bf") to generate, should be consistent with netmask and prefix length
Environment = QUARTETS=2
# the CIDR mask length
Environment = CIDR_MASK=80
ExecStart = /bin/bash -c "\
ip -6 --json addr show dev %i \
| jq --exit-status '.[0].addr_info.[] | select(.preferred_life_time > ${ADD_NEW_WHEN_PREFERRED_LFT_LEFT})' \
|| ip -6 addr add ${IP6_PREFIX}$(\
hexdump --length 16 -ve '8/2 \"%%04x:\"' /dev/urandom | cut -d ':' -f 1-${QUARTETS})/${CIDR_MASK} dev %i valid_lft ${VALID_LFT} preferred_lft ${PREFERRED_LFT}"
TimeoutStartSec = 10
RestrictAddressFamilies = AF_NETLINK
ProtectHome = true
ProtectSystem = strict
PrivateTmp = true
ProtectProc = invisible
[Install]
WantedBy=basic.target