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:
- link-local
- internal stable
- 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:
-
Unique Local Addresses
(
fd00::/8
) are randomized for no reason since the firewall will obscure the source address. - EUI64 autoconfiguration is based on the network interface MAC, so changing hardware results in a new address.
- Stateless autoconfiguration can direct clients to use DHCPv6, but this is not supported on all platforms.
- Temporary addresses prevent SSH from establishing a fingerprint for each IP.
- On some platforms SOII can only be disabled after an OS is installed, blocking automated configuration.
- Address assignments cannot be logged, only discovered from other network traffic.
- Service discovery is not part of the autoconfiguration standards.
All these points imply a two part strategy:
- Use static addressing for workstations, servers, and network appliances.
- Use autoconfiguration for phones, tablets, and other unmanaged clients.
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
-
Adjust
/etc/gai.conf
to Make Linux Prefer IPv4 (glibc only) -
Set
family inet4
in/etc/resolv.conf
on OpenBSD
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