IPv6 Practical Guide: Addressing, Dual-Stack, and Running IPv6 in Your Homelab
IPv4 exhaustion isn’t a future problem — it’s been the present reality for over a decade. IPv6 adoption crossed 40% of global internet traffic in 2024, and major cloud providers, CDNs, and mobile networks are largely IPv6-first. Yet many engineers still treat IPv6 as someone else’s concern. This guide is about making it yours — understanding the addressing model, configuring dual-stack, and running IPv6 confidently from your homelab to production.
Why IPv6 Is Different (Not Just Bigger Addresses)
IPv6 isn’t IPv4 with longer addresses. The protocol was redesigned from scratch:
- No broadcast: IPv6 uses multicast instead. No more broadcast storms.
- No ARP: Replaced by Neighbor Discovery Protocol (NDP), which runs over ICMPv6.
- Stateless autoconfiguration (SLAAC): Hosts can configure themselves without a DHCP server.
- IPsec in the design: Originally mandatory (now optional), but designed in.
- Simplified header: Fixed 40-byte header (no fragmentation fields, no checksum). Extension headers chain for optional features.
- Mandatory ICMPv6: Never filter all ICMPv6 — it’s required for basic connectivity.
- Jumbo frames: Extension header supports payloads up to 4GB.
Address Structure
An IPv6 address is 128 bits, written as eight groups of four hex digits:
2001:0db8:85a3:0000:0000:8a2e:0370:7334
Abbreviation Rules
- Leading zeros in each group can be omitted:
0db8→db8,0000→0 - One sequence of consecutive all-zero groups can be replaced with
:::
2001:db8:85a3:0:0:8a2e:370:7334
2001:db8:85a3::8a2e:370:7334 # :: replaces the two zero groups
You can only use :: once per address.
Address Anatomy
A typical unicast address has two parts:
| Network Prefix (64 bits) | Interface ID (64 bits) |
| 2001:db8:1234:5678 | ::1a2b:3c4d:5e6f:7a8b |
The network prefix is further divided:
| Global Routing Prefix (48 bits) | Subnet ID (16 bits) | Interface ID (64 bits) |
| 2001:db8:1234 | :5678 | ::1a2b:3c4d:5e6f:7a8b |
Your ISP assigns you a /48 or /56 prefix. You subnet within that. Each subnet is a /64. The interface ID is the last 64 bits — typically derived from the MAC address (EUI-64) or generated randomly for privacy.
Address Types
Global Unicast (GUA)
Routable on the public internet. Currently allocated from 2000::/3:
2001:db8::/32 # Documentation and examples (never route these)
2001::/32 # Teredo (legacy tunnel mechanism)
2002::/16 # 6to4 (legacy, deprecated)
Your real GUAs look like 2001:1234:abcd::/48 — whatever your ISP or RIR assigns.
Link-Local (fe80::/10)
Every IPv6 interface automatically gets a link-local address. Not routable beyond a single link. Used for:
- NDP (neighbor discovery, router discovery)
- DHCPv6 communication
- Routing protocol hello messages (OSPFv3, BGP unnumbered)
|
|
You’ll always see these — they’re mandatory and assigned automatically.
Unique Local Addresses (ULA — fc00::/7)
The IPv6 equivalent of RFC 1918 private space. Use fd00::/8 for your own networks (the fc00::/8 half requires a central authority):
fd12:3456:789a::/48 # Your private addressing — generate randomly
ULA addresses are not routed on the public internet. Unlike RFC 1918, ULA is designed so randomly-generated prefixes from different organizations don’t collide.
Generate a random ULA prefix:
|
|
Multicast (ff00::/8)
Used where IPv4 used broadcast. Key multicast groups:
| Address | Scope | Purpose |
|---|---|---|
ff02::1 |
link-local | All nodes |
ff02::2 |
link-local | All routers |
ff02::fb |
link-local | mDNS |
ff02::1:2 |
link-local | All DHCPv6 servers/relays |
ff02::1:ff00:0/104 |
link-local | Solicited-node multicast (NDP) |
Special Addresses
| Address | Meaning |
|---|---|
::1 |
Loopback (equivalent to 127.0.0.1) |
:: |
Unspecified address |
::ffff:0:0/96 |
IPv4-mapped IPv6 addresses |
64:ff9b::/96 |
NAT64 well-known prefix |
Neighbor Discovery Protocol (NDP)
NDP replaces ARP and adds router discovery. It uses ICMPv6 messages:
| Message Type | ICMPv6 Code | Purpose |
|---|---|---|
| Router Solicitation (RS) | 133 | Host asks: “Are there routers here?” |
| Router Advertisement (RA) | 134 | Router announces prefix, gateway, flags |
| Neighbor Solicitation (NS) | 135 | “Who has this address?” (replaces ARP request) |
| Neighbor Advertisement (NA) | 136 | “I have this address” (replaces ARP reply) |
| Redirect | 137 | Router tells host of a better next-hop |
Solicited-Node Multicast
Instead of broadcasting to find a MAC address, NDP uses solicited-node multicast. For address 2001:db8::1a2b:3c4d:5e6f:7a8b, the solicited-node address is:
ff02::1:ff6f:7a8b # ff02::1:ff + last 24 bits of address
Only the node with that address listens on this multicast group — dramatically reducing the “broadcast” scope even on large subnets.
|
|
Address Configuration Methods
SLAAC (Stateless Address Autoconfiguration)
The default method. When an interface comes up:
-
Host generates a link-local address from its MAC (or randomly)
-
Host performs Duplicate Address Detection (DAD) via NS/NA
-
Host sends a Router Solicitation
-
Router responds with a Router Advertisement containing:
- The
/64prefix for this link - Default gateway (the router’s link-local address)
- Flags for DHCPv6
- Lifetime timers
- The
-
Host combines the prefix with a self-generated interface ID → global unicast address
-
DAD again on the new GUA
The interface ID generation method:
- EUI-64: derived from MAC address (privacy concern — MAC is embedded in address)
- Privacy extensions (RFC 4941): random temporary addresses, rotated periodically (default on most modern OSes)
- Stable privacy addresses (RFC 7217): stable but not guessable from MAC
|
|
DHCPv6
Used when you need centralized address management or need to hand out options SLAAC can’t provide (NTP servers, custom options). Two modes:
Stateful DHCPv6: assigns specific addresses (like DHCPv4) Stateless DHCPv6: SLAAC handles the address, DHCPv6 provides options only
The RA’s M flag (Managed) tells hosts to use stateful DHCPv6. The O flag (Other) tells hosts to get options via DHCPv6 but use SLAAC for the address.
|
|
Static Configuration
For servers, routers, and infrastructure — always use static addresses:
|
|
With systemd-networkd:
|
|
Dual-Stack Configuration
Dual-stack means running IPv4 and IPv6 simultaneously. The goal: both protocols work independently, hosts can communicate over either.
Linux Host (systemd-networkd)
|
|
Nginx Dual-Stack
|
|
Docker Dual-Stack
By default Docker only uses IPv4 internally. Enable IPv6:
|
|
|
|
Docker Compose Dual-Stack
|
|
Kubernetes Dual-Stack
Enable in kubeadm:
|
|
Services get both addresses:
|
|
Prefix Delegation (DHCPv6-PD)
If your ISP supports DHCPv6-PD (most do now), they’ll hand your router a /48 or /56 prefix, and your router subdivides it into /64 subnets for each LAN segment. This gives every device in your home or office a real, globally routable address.
With systemd-networkd as the WAN client
|
|
|
|
With dhcpcd (common on Raspberry Pi / Arch)
# /etc/dhcpcd.conf
interface eth0 # WAN
ipv6rs # Enable router solicitation
ia_na 1 # Request address
ia_pd 2 eth1/0 # Request prefix, delegate to eth1 subnet 0
With radvd for RA on LAN
# /etc/radvd.conf
interface eth1 {
AdvSendAdvert on;
AdvManagedFlag off;
AdvOtherConfigFlag on; # Use DHCPv6 for options
prefix ::/64 { # ::/64 = use delegated prefix
AdvOnLink on;
AdvAutonomous on;
AdvRouterAddr on;
};
RDNSS 2001:4860:4860::8888 2001:4860:4860::8844 {};
};
IPv6 Firewalling
Critical difference from IPv4: NAT is not a security mechanism in IPv6. Every device gets a routable address. You must run a proper stateful firewall.
Also critical: never block all ICMPv6. This breaks connectivity. Specific ICMPv6 types are required.
nftables (recommended)
#!/usr/sbin/nft -f
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow established/related connections
ct state established,related accept
# Allow loopback
iif lo accept
# ICMPv6 — required for IPv6 to function
ip6 nexthdr icmpv6 icmpv6 type {
destination-unreachable,
packet-too-big,
time-exceeded,
parameter-problem,
nd-router-solicit,
nd-router-advert,
nd-neighbor-solicit,
nd-neighbor-advert,
mld-listener-query,
mld-listener-report
} accept
# Allow ping
ip6 nexthdr icmpv6 icmpv6 type echo-request accept
# Allow SSH
tcp dport 22 ct state new accept
# Allow HTTP/HTTPS
tcp dport { 80, 443 } ct state new accept
# Drop everything else
drop
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
# ICMPv6 forwarding
ip6 nexthdr icmpv6 accept
# Allow LAN → WAN
iif eth1 oif eth0 accept
drop
}
chain output {
type filter hook output priority 0; policy accept;
}
}
ip6tables (legacy but still common)
|
|
UFW with IPv6
Ensure IPv6 is enabled in UFW config:
# /etc/default/ufw
IPV6=yes
|
|
Running IPv6 in a Homelab
If your ISP doesn’t provide IPv6 (or if you want isolated testing), several tunneling options exist.
Hurricane Electric Tunnel Broker (Free)
tunnelbroker.net gives you a free /48 and a 6in4 tunnel:
|
|
With systemd-networkd:
|
|
|
|
WireGuard with IPv6
WireGuard handles IPv6 natively. Add an IPv6 address to the tunnel interface alongside IPv4:
|
|
|
|
Testing Connectivity
|
|
DNS for IPv6
AAAA Records
IPv6 addresses go in AAAA (quad-A) records:
example.com. 300 IN AAAA 2001:db8::1
PTR Records (Reverse DNS)
IPv6 reverse DNS is in ip6.arpa. The address is written in reverse nibble format:
For 2001:db8::1:
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
In BIND/NSD zone files:
$ORIGIN 0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 PTR host.example.com.
Split-Horizon DNS for Dual-Stack
When you have both A and AAAA records, clients will prefer IPv6 (RFC 6724 default behavior). If your IPv6 connectivity is broken, applications may time out waiting for IPv6 before falling back to IPv4. This is “happy eyeballs” territory — modern browsers and curl implement RFC 6555 to race both connections.
If you’re troubleshooting and want to force IPv4:
|
|
Common Pitfalls
“My host has an IPv6 address but can’t reach the internet”
Check the default route:
|
|
Forwarding breaks RA acceptance
By default, Linux ignores RAs on interfaces with forwarding enabled:
|
|
“I enabled IPv6 in Docker but containers can’t reach IPv6”
Verify ip6tables is installed and that the Docker daemon restarted:
|
|
Also check the host’s forwarding:
|
|
Link-local address required for %interface notation
When pinging or connecting to link-local addresses, you must specify the interface:
|
|
In URLs: http://[fe80::1%25eth0]/ (the %25 is URL-encoded %)
Applications not binding to IPv6
Check if the application is configured to listen on both addresses:
|
|
For Python servers:
|
|
Quick Reference
Address Types Cheat Sheet
| Prefix | Type | Use |
|---|---|---|
::1/128 |
Loopback | localhost |
fe80::/10 |
Link-local | On-link only, auto-assigned |
fc00::/7 (fd::/8) |
ULA | Private, not routed |
2000::/3 |
GUA | Public internet |
ff00::/8 |
Multicast | Groups, not unicast |
::ffff:0:0/96 |
IPv4-mapped | IPv4 in IPv6 socket |
Useful Commands Cheat Sheet
|
|
sysctl Reference
| Key | Value | Effect |
|---|---|---|
net.ipv6.conf.all.forwarding |
1 |
Enable IPv6 routing |
net.ipv6.conf.<if>.accept_ra |
2 |
Accept RA even when forwarding |
net.ipv6.conf.<if>.use_tempaddr |
2 |
Use privacy extensions |
net.ipv6.conf.<if>.autoconf |
1 |
Enable SLAAC |
net.ipv6.conf.all.disable_ipv6 |
1 |
Disable IPv6 entirely |
Where to Go Next
- Test your setup: test-ipv6.com tells you exactly which IPv6 features work
- IPv6 subnetting calculator: subnetcalc.it
- RIPE NCC IPv6 training: excellent free course covering the theory in depth
- RFC 8200: the current IPv6 specification
- RFC 4861: Neighbor Discovery Protocol
- RFC 4862: Stateless Address Autoconfiguration
- RFC 4941: Privacy Extensions (temporary addresses)
- RFC 7217: Stable, Opaque Interface Identifiers
IPv6 has a reputation for complexity, but most of that complexity lives in the transition period we’re still in. A network that runs native IPv6 end-to-end, with proper firewall rules and prefix delegation, is actually simpler to operate than the NAT gymnastics required for IPv4. The sooner you build the muscle memory, the better positioned you’ll be.
Comments