Part of the reason I self-host my own apps and services is that it’s a great way to learn new things. IPv6 has been a curiosity for quite a while, but I’ve never really dug into the details to understand it well.

Some folks might say IPv6 is a waste of time as you can get by with just using IPv4 + NAT. I agree and think this is a totally valid point - but I’m still interested in learning more about it.

New Concepts and Terminology

There are some key differences and things to understand when adding IPv6 to a home network.

One misconception I had was there is a single global address space in IPv6 where every device has one globally unique IPv6 address. In reality, there are multiple addresses for every IPv6 interface. Every device will have a link-local address in the range fe80::/10. This address is only used for communicating with other devices on the same subnet. It is the first address on the interface and is used to bootstrap the rest of the network configuration.

IPv6 networks can also support unique local addresses similar to how IPv4 supports private ranges like 192.168.0.0/16, 10.0.0.0/8, etc. These addresses are not globally unique or routable, but they can be used for communication on a private network. Also, while they are not guaranteed to be unique, the probability of a collision is extremely small (unlike the IPv4 equivalents) due to the fact they have a randomly generated prefix called the ULA Prefix.

SLAAC

There is a good overview on Wikipedia. While IPv4 typically assigns addresses through DHCP, SLAAC also allows for auto-configuration of an IPv6 address inside the network’s IPv6 space. The router provides the network’s prefix and the host generates the suffix. This is one of the options for assigning client IPv6 addresses (and the primary mechanism for Android devices).

Improving on IPv4 Layout

In hindsight, there are some improvements I’d like to make on the current IPv4 implementation on my network.

  • Better support for various subnets on my network. This includes setting up distinct subnets for:
    • network devices - routers, switches, WiFi APs, etc
    • IoT devices
    • trusted clients - both with static IPs and DHCP/SLAAC assigned IPs
    • guest devices
    • etc
  • Improved segmentation for “sites” such as my primary home location, a remote site, VPN segments, etc.

Getting Started

Given what I know so far, my general approach to getting started is similar to the current IPv4 configuration but adding improvements mentioned above.

  1. Generate a ULA prefix for my internal network using a tool like this.
  2. Segment the network to better allocate IP space for sites and subnets.
  3. Assign static IPv6 addresses to my DHCP/DNS servers.
  4. Ensure DHCP and DNS is working on IPv6.

Subnet Layout Approach

Let’s start with a random ULA prefix - say fd42:1111:1111::/48. Given that we want to allow for multiple sites and then subnets in each site, we can use the format fd42:1111:1111:SSXX::/64 where SS is the site and XX is the subnet. This will allow for 256 sites, 256 subnets in each site, and 64 bits of space in each subnet.

NOTE: The /64 space for a IPv6 subnet is a key design aspect and requried for SLAAC support.

We can then layout subnets along these lines:

  • fd42:1111:1111:SS00::/64 - network hardware devices such as firewalls, switches, WiFi APs, etc.
  • fd42:1111:1111:SS01::/64 - DNS/DHCP servers
  • fd42:1111:1111:SS02::/64 - servers
  • fd42:1111:1111:SS03::/64 - trusted clients
  • fd42:1111:1111:SS04::/64 - guest clients
  • fd42:1111:1111:SS05::/64 - IoT devices
  • etc

Setup Static IPs on DHCP Servers

My current DHCP/DNS is served via dnsmasq on two redundant Raspberry Pis running Raspberry Pi OS 13. I configure static IPs for these via an Ansible playbook. These devices run ifupdown as their network configuration service so the interface configuration is defined in /etc/network/interfaces.d/eth0.

An example for a particular host after applying the change looks like:

# /etc/network/interfaces.d/eth0
auto eth0

# Current IPv4 address
iface eth0 inet static
    address 192.168.0.14/23
    gateway 192.168.0.1
    dns-domain example.com
    dns-nameservers 9.9.9.9 1.1.1.1

# New IPv6 address
iface eth0 inet6 static
    address fd42:1111:1111:0101::14:1/56
    gateway fd42:1111:1111:0100::1
    dns-search example.com
    dns-nameservers 2620:fe::fe 2606:4700:4700::1111

After restarting the network service, let’s check what IPs the host has.

➜  ip addr show
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 noprefixroute 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether dc:a6:32:5b:da:4c brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.14/23 brd 192.168.1.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fd42:9ab1:6c0d:101::14:1/56 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::dea6:32ff:fe5b:da4c/64 scope link proto kernel_ll 
       valid_lft forever preferred_lft forever

This confirms the host now has a link local (fe80::*) and a unique network (fd42:1111:1111:101::14:1) address.

NOTE: Tools like dig and ip will abbreviate IPv6 addresses by removing leading zeros. The address fd42:1111:1111:0101::14:1 and fd42:1111:1111:101::14:1 are equivalent. You can also use IPv6 notation to reduce redundant zeros with :: - the full IPv6 address is really fd42:1111:1111:0101:0000:0000:0014:0001.

Testing connectivity with ping

We can also confirm IPv6 traffic is working on the network by pinging between our two IPv6 devices (14:1 and 15:1).

➜  ping -6 -c 1 fd42:1111:1111:101::14:1          # Ping host itself
PING fd42:1111:1111:101::14:1 (fd42:1111:1111:101::14:1) 56 data bytes
64 bytes from fd42:1111:1111:101::14:1: icmp_seq=1 ttl=64 time=0.069 ms

--- fd42:1111:1111:101::14:1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.069/0.069/0.069/0.000 ms

➜  ping -6 -c 1 fd42:1111:1111:101::15:1          # Ping other DHCP server with static IP
PING fd42:1111:1111:101::15:1 (fd42:1111:1111:101::15:1) 56 data bytes
64 bytes from fd42:1111:1111:101::15:1: icmp_seq=1 ttl=64 time=0.230 ms

--- fd42:1111:1111:101::15:1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.230/0.230/0.230/0.000 ms

DNS Configuration

Now that our servers have IPv6 addresses, we need to ensure that DNS is working with IPv6 as well. Let’s check it by testing resolution of the servers themselves. We will use dig to query the A (IPv4) and AAAA (IPv6) IPs of the host fuzzpi4.

➜  dig @fd42:1111:1111:101::14:1 fuzzpi4 A fuzzpi4 AAAA +short
192.168.0.14

As you can see we are only getting the IPv4 address. That is because we haven’t told dnsmasq what IPv6 address fuzzpi4 is running at. The local network hosts are configured in dnsmasq via an addn-hosts file and we can confirm what addresses are defined for fuzzpi4.

➜  cat /etc/dnsmasq.conf | grep addn-hosts
addn-hosts=/etc/dnsmasq-local-net.list
➜  cat /etc/dnsmasq-local-net.list | grep fuzzpi4
192.168.0.14 fuzzpi4

Let’s add the IPv6 addresses to the addn-hosts file, reload dnsmasq, and confirm we can then resolve both addresses.

➜  cat /etc/dnsmasq-local-net.list | grep fuzzpi4             
192.168.0.14 fuzzpi4
fd42:1111:1111:0101::14:1 fuzzpi4   # Now we have IPv6 addresses as well!
➜  dig @fd42:1111:1111:101::14:1 fuzzpi4 A fuzzpi4 AAAA +short
192.168.0.14
fd42:1111:1111:101::14:1            # Nice - IPv6 too!

We can then confirm we can ping the host by name and not just IP.

➜  ping -c 1 -6 fuzzpi4                 
PING fuzzpi4 (fd42:1111:1111:101::14:1) 56 data bytes
64 bytes from fuzzpi4 (fd42:1111:1111:101::14:1): icmp_seq=1 ttl=64 time=0.169 ms

--- fuzzpi4 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.169/0.169/0.169/0.000 ms

What’s Next

This post covers the initial, foundational steps for introducing IPv6 to a home network. By generating a Unique Local Address (ULA) prefix, designing a logical subnetting scheme, and configuring static addresses for core services, we are off to a good start. We’ve verified that our DNS servers can resolve IPv6 addresses and that basic communication between hosts is working.

However, this is just the start. The current setup only covers static IP addresses on a local, non-routable network. The next steps will involve exploring dynamic address assignment with DHCPv6 and SLAAC for other clients, and investigating how this impacts network routing into our self-hosted containers.