Eric Radman : a Journal

A Strategy for IPv6

I first configured IPv6 Internet access at my home in 2003. More than 20 years later IPv6 is widely available, but remains difficult to implement on a local area network.

From the perspective of a network administrator, an evolution of the protocol will be required to make it as useful as IPv4. Until such time, IPv6 seems to be an experimental means for clients to access public Internet resources.

OpenBSD Router

Public /64 subnets can be assigned to each VLAN, but this is useless for all but the most trivial networks since the subnet prefix (typically a /56) is dynamically assigned by the Internet service provider! The only solution to allocate a private addresses space.

Start by assigning a static, private address from fd00::/8 to each VLAN

# hostname.vlan0
vnetid 80
inet 192.168.0.1/24
inet6 fd00:50::3/64
# hostname.vlan1
vnetid 81
inet 192.168.1.1/24
inet6 fd00:51::3/64

Where 50 and 51 are a reminder of the VLAN ID

$ printf '%x\n' 80
50
$ printf '%x\n' 81
51

Enable IPv6 autoconfiguration for the egress interface

inet6 autoconf

This may provide a link-local default gateway

% netstat -rn -f inet6 | grep default
default     fe80::201:5cff:fe64:9246%cnmac2       UGS      0  6932501     -     8 cnmac2

OpenBSD 7.6 introduced dhcp6leased(8) to provides a means of requesting prefixes using DHCPv6

# dhcp6leased.conf
request prefix delegation on cnmac2 for {
  vlan0/64
  vlan1/64
}

Each VLAN now has three addresses:

  1. link-local
  2. internal stable
  3. public

Use rad(8) to distribute the stable addresses

# rad.conf
interface vlan0 {
  no auto prefix
  prefix fd00:50::/64
  dns {
    nameserver fd00:50::3
  }
}
interface vlan1 {
  no auto prefix
  prefix fd00:51::/64
  dns {
    nameserver fd00:51::3
  }
}

Rather than configuring a port-address translation we can one of OpenBSD's address pool translation methods

match out on egress inet6 from fd00:50::/64 nat-to 2603:7081:5506:d884::/64 source-hash
match out on egress inet6 from fd00:51::/64 nat-to 2603:7081:5506:d885::/64 source-hash

pf.conf(5) allows an interface network to be selected using $interface:network but there does not seem to be a method of choosing the public subnet. These hard-coded translations may break, but can be fixed without renumbering a network.

Stateless Address Autoconfiguration (SLAAC, SOII)

It is easy to obtain addresses using router advertisements, but each client is easily lost even on a local network since each device may assign addresses using a different scheme:

MacOS and OpenBSD use Temporary and Semantically Opaque Interface Identifiers by default. Ubuntu Linux uses stable addresses for wired links, and temporary for wireless interfaces. FreeBSD and Alpine Linux use EUI64.

Stateless autoconfiguration is fatal for network administration:

All these points imply a two part strategy:

Static addressing also mostly solves the difficulty in typing IPv6 addresses since they can be a much shorter chain of hex digits. This is important because corporate VPN clients nearly always redirect DNS.

Precedence

If IPv6 is performing poorly, IPv4 can be prioritized

Name Resolution

The integration of name servers learned from DHCP and router advertisements is not consistent across platforms

OpenBSD

nameserver 192.168.0.3 # resolvd: em0
nameserver fd00:50::3 # resolvd: em0

Mac OS

nameserver 192.168.0.3
nameserver fd00:50::3

FreeBSD

# Generated by resolvconf
nameserver fd00:50::3

Alpine Linux

nameserver 192.168.0.3

Linux-systemd: (resolvectl status)

Global
         Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
  resolv.conf mode: stub

Link 2 (enp0s5)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: fd00:50::3
       DNS Servers: 192.168.0.3 fd00:50::3
        DNS Domain: eradman.com

Docker

A mode called host networking allows an containers to access the network directly (including localhost) but requires root

docker run --net=host -it alpine:latest /bin/sh

Deployments using docker compose can use IPv6 NAT defined in /etc/docker/daemon.json

{
  "ipv6": true,
  "fixed-cidr-v6": "fd00:ffff::/80",
  "ip6tables": true,
  "experimental": true
}

See also Adventures in IPv6 routing in Docker.

Auto-Static Addressing

Since IPv6 access is available during system install, we can use a nameserver lookup to find a designated IP and then pivot. These examples look up the address assigned for ${hostname}.lan

# unbound.conf
local-zone: "lan." static
local-data: "sfhome1.lan   AAAA fd00:52::19"
local-data: "sfhome2.lan   AAAA fd00:52::1a"
local-data: "sfreport1.lan AAAA fd00:52::1b"

Normally the system hostname comes from IPv4 DHCP, but MAC addresses could be encoded in DNS as well

# unbound.conf
local-data: "00-0c-29-31-87-4d.lan CNAME sfhome1.lan"
local-data: "00-16-3e-10-0a-8d.lan CNAME sfhome2.lan"
local-data: "00-16-3e-36-1b-ff.lan CNAME sfreport1.lan"

Dereference using

mac=$(ifconfig enp0s5 | awk '/ ether / { gsub(":", "-"); print $2 }')
hostname=$(getent ahostsv6 $mac.lan | awk '/.lan$/ { print $NF }')

RHEL/RockyAlma

Network configuraiton in Anacondia can be set using nmcli commands outside of the chroot environment

%post --nochroot --logfile=/mnt/sysimage/root/kickstart-post-nochroot.log
set -x
# find egress interface
inet6_if=$(route --inet6 -n | awk '/::\/0/ { print $NF; exit }')
# infer inet6 address from DNS
inet6_addr=$(getent ahostsv6 $(hostname -s).lan | awk '/^fd00:/ { printf "%s/64", $1; exit }')
# use nameserver and gateway on same subnet
inet6_gw=$(echo $inet6_addr | awk -F: '{ printf "%s:%s::7", $1, $2 }')
inet6_ns=$(echo $inet6_addr | awk -F: '{ printf "%s:%s::3", $1, $2 }')
# update network configuration
nmcli con mod $inet6_if ipv6.address "$inet6_addr"
nmcli con mod $inet6_if ipv6.gateway "$inet6_gw"
nmcli con mod $inet6_if ipv6.dns "$inet6_ns"
nmcli con mod $inet6_if ipv6.method manual
cp /etc/NetworkManager/system-connections/$inet6_if.* /mnt/sysimage/etc/NetworkManager/system-connections/
%end

Ubuntu/Cloud-init

Ubuntu uses Netplan for network configuration. Unfortunately the hostname is not set, but we can discover it from DHCP lease data.

# user-data
early-commands:
  - |
    apt-get -y install net-tools
late-commands:
  - |
    # set hostname based on DHCP lease
    myname=$(awk -F= '/HOSTNAME/ { print $2 }' /var/run/systemd/netif/leases/?)
    echo $myname > /target/etc/hostname
    hostname $myname
  - |
    # find egress interface
    inet6_if=$(route --inet6 -n | awk '/::\/0/ { print $NF; exit }')
    # infer inet6 address from DNS
    inet6_addr=$(getent ahostsv6 $(hostname -s).lan | awk '/^fd00:/ { printf "%s/64", $1; exit }')
    # use nameserver and gateway on same subnet
    inet6_gw=$(echo $inet6_addr | awk -F: '{ printf "%s:%s::7", $1, $2 }')
    inet6_ns=$(echo $inet6_addr | awk -F: '{ printf "%s:%s::3", $1, $2 }')
    # update network configuration
    netplan set "network.ethernets.$inet6_if.addresses=['$inet6_addr']"
    netplan set "network.ethernets.$inet6_if.nameservers.addresses=['$inet6_ns']"
    netplan set "network.ethernets.$inet6_if.routes=[{'to': '::0/0', 'via': '$inet6_gw'}]"
    netplan set "network.ethernets.$inet6_if.accept-ra=false"
    netplan apply
    cp /etc/netplan/00-installer-config.yaml /target/etc/netplan/

Link-Local Gateways

Even though inet6 addresses are very long, it is common practice to use a non-routable address as the default gateway. This works because Neighbor Discovery Protocol also probes for routers, indicated by the R flag

$ ndp -an
Neighbor                                Linklayer Address   Netif Expire    S Flags
2001:19f0:5c01:15b5:5400:3ff:fed7:9dc1  56:00:03:d7:9d:c1    vio0 permanent R l
fe80::5400:3ff:fed7:9dc1%vio0           56:00:03:d7:9d:c1    vio0 permanent R l
fe80::fc00:3ff:fed7:9dc1%vio0           fe:00:03:d7:9d:c1    vio0 29s       R R

Since all link-local addresses (fe80::/10) are on the same subnet the route has to include the interface name. I have no idea what happens if there is more than one router on the segment.

Address Space

The irony of IPv6 is that it may be easier to exhaust because the global address space (2000::/3) is divided into a strict hierarchy

  3  Global Prefix
 13  Top-Level Aggregation Identifier
  8  Reserved
 24  Next-Level Aggregation Identifier

45 bits is still larger than 32, but IPv4 addresses can be distributed with very little wasted address space.

Further reading: Is the IPv6 Address Space Too Small? by Ian Marshall