Setting up IPv6 at home
- 12 minutes read - 2427 wordsWhy? My new ISP has support for IPv6. It’s the “new” (old af) kid on the block and I thought I should get a better understanding of it.
This is not meant to be a guide. Mostly just me working through the steps to get this to work. The resources that I used are linked at the bottom and include several guides that I followed and help me understand how this all worked together.
Tools and hardware used
- Intel 2.5Gb i226 NIC
- OpenBSD (virtualized)
- rad – router advertisement daemon
- dhcpd – IPv4 DHCP server
- dhcp6leased – prefix delegation negotiation
- pf – packet filter (firewall + router)
- slaacd – SLAAC daemon for autoconfiguring IPv6 address
Getting started
Before getting started with this, I had essentially no idea how IPv6 worked. I spent some time discussing with some friends online about how it worked and started to get a loose outline of how this should all play together.
At this point, I started doing lots of research on how to get this setup.
The final solution implemented here is IPv6 mostly for specific VLANs. Details about the decisions for this will come later.
This is probably a good point to add that I’m not really sure this is something that really benefits anyone outside of people that are designing networks or like making their lives overly complicated (I land in this camp).
That said, there is increasingly parts of the internet that are IPv6 only but I have not really encountered them (that I can tell). There are also large chunks of the internet that are still IPv4 only (more on this at the end). And when faced with a page you can’t reach due to lack of support for either version, you get a very unhelpful “Page cannot be reached” error. This is your DNS server returning an NXDOMAIN error, which means it didn’t find a record matching the domain.
(Note: IPv4 can still be reached when using IPv6 only courtesy of NAT64, which will be discussed later).
Public IPv6 address
My first step with actually getting this started was just setting up IPv6 on my WAN (Wide-Area Network – the public internet) interface on the router. This was as simple as adding these two lines to my /etc/hostname.pppoe0 file as specified by pppoe:
inet6 autoconf
!/sbin/route add -inet6 default -ifp pppoe0 fe80::%pppoe0
This tells OpenBSD to request an IPv6 address. The default uses the slaacd tool to generate a SLAAC (StateLess Address AutoConfiguration) address. DHCPv6 can be configured but I opted not to. I’m not sure my ISP even supports this.
The second one adds the default route over the link-local address. This means that all IP addresses the router does not know will be sent to the ISP via the link-local address.
Once I brought the interface back up, I now had an IPv6 address.
Several public IPv6 addresses
Now with a public IPv6 address, I need to configure the router to distribute IPv6 addresses to end devices. This will be done in 3 steps:
- VLAN address assignment
- Prefix delegation
- Router advertisement
VLAN address assignement
With a public IPv6 address, I needed to get IPv6 on my VLAN interfaces. The first step was to get a Unique Local Address (ULA) in the /etc/hostname.if file:
inet6 alias fd00:0000:0000:0000::1/64
Ideally, this should be created using a tool that randomizes the ULA prefix according to RFC 4193 to help avoid some case where you may later want to merge networks and avoid renumbering (extremely unlikely in a household but this is best practice). A ULA is non-routable and functions similarly to a local address in v4. This is important for maintaining IPv6 networking if the GUA gets changed either by the ISP or if you change ISP. This can alternatively be solved by falling back to v4.
Prefix Delegation
The other address we need is a Globally Unique Address (GUA). This is the public IP address for the interface which will be generated using prefix delegation. The WAN interface needs to negotiate a prefix for each interface requiring a GUA since the interfaces aren’t able to communicate with the ISP to get one themselves. This isn’t needed with IPv4 since the interfaces don’t get a public IP.
The tool to do this in OpenBSD is dhcp6leased (which replaces the non-native dhcpcd used before). This part caused me some confusion because the documentation for dhcp6leased specifies to make only 1 delegation request and to create the subnets yourself. What I later realized was that the tool actually manages all of that for you! This makes life very easy. Create a file config file (/etc/dhcp6leased.conf) with the following:
request prefix delegation on <WAN_IF> for { <LAN_IF> }
In my particular example, I have 2 VLANs I would like to have prefixes for, and my WAN interface is a PPPoE interface. This would look like this:
request prefix delegation on pppoe0 for { vlan50 vlan10 }
This also assumes the smallest allowable prefix of /64. To specify a prefix smaller than /64, you can append it after the name of the interface (e.x. vlan50/60).
And with that, the interfaces vlan10 and vlan50 are now able to get both a public GUA and a ULA IPv6 address.
Note: With my new ISP, I receive a /56 block. This should be the standard for most household configurations but note that some ISPs may only provide a single /64 prefix. If you get a /64 prefix, you will be stuck with only 1 range/subnet. The ISP may also only provide a /64 prefix until you specifically request a larger block (this is done with dhcp6leased in this case).
Router Advertisement
Unlike in IPv4 where you get an address via a DHCP server, v6 supports autoconfiguration of IP addresses using SLAAC.
For a system to know what network prefixes are available with which to configure SLAAC addresses, there will need to be a router advertiser. In OpenBSD, this is rad (router advertisement daemon).rad will both respond to router solicitation IMCP messages and periodically send router advertisement ICMP messages sharing the available prefixes to end devices.
The config file should look something like this:
default router yes
interface vlan10
interface vlan50
dns {
nameserver <dns_ip>
search home.arpa
}
nat64 prefix 64:ff9b::/96
My DNS server is static and not running on the router, so the one global entry is enough.
The nat64 prefix value will be discussed later.
Firewall changes
My firewall was initially configured to explictly drop all incoming and outgoing v6. That obviously would not work anymore.
The key rule modifications that had to be made were to allow incoming IMCP messages for IPv6 (these are essential for v6 for neighbour discovery, amongst other use cases) and to allow traffic in from port 547 to port 546 for prefix delegation.
Here is the snippet including additions made for v6 (these rules were added in addition to my pre-existing rules and don’t represent my whole pf config):
table <martians6> {
::/128 ::1/128 ::ffff:0:0/96
64:ff9b::/96 64:ff9b:1::/48
100::/64 2001:0::/32 2001:2::/48
2001:10::/28 2001:20::/28 2001:db8::/32
2002::/16 fc00::/7 fec0::/10
}
block in quick on egress inet6 from <martians6> to any
block return out quick on egress inet6 from any to <martians6>
pass in quick on egress inet6 proto udp from any port 547 to (egress) port 546
match in quick on egress inet6 proto ipv6-icmp from any to { (egress), ff02::/16 } icmp6-type {
echoreq echorep
unreach toobig timex paramprob
neighbrsol neighbradv routersol routeradv
} tag ICMP_IN
pass in quick inet6 from any to 64:ff9b::/96 af-to inet from (egress:0) keep state
pass in on $l_lan inet6 tag publicv6
pass in on $switch inet6 tag publicv6
pass out on $ext_if inet6 tagged publicv6
pass out inet6 from ($ext_if)
NAT64 and DNS 64
For v6 IPs to reach v4 only content, you can use NAT64 and DNS64. There are 3 parts to this:
- In
/etc/pf.conf:
pass in quick inet6 from any to 64:ff9b::/96 af-to inet from (egress:0) keep state
- In
/etc/rad.conf:
nat64 prefix 64:ff9b::/96
- In
unbound.conf(not on the router):
module-config: "dns64 validator interator"
The first part is the actual NAT translation. It assigns the outgoing data to the first (0) IPv4 address assigned to the egress interface and translates accordingly.
The second part announces the NAT64 prefix to share that devices should use to designate the destination as a NAT64 address and embed the v4 address into the last 32 bits.
The third part tells unbound to provide DNS64 records as query responses.
With this, the system is able to communicate to v4 only services via v6.
Problems and considerations
Local routing
The problem of how to do local routing was a big consideration for this project. With v4, the solution with static IPs is pretty simple. With SLAAC, it’s not as simple. That left 2 options:
- Static ULA on each device
- DHCPv6
I was under the impression that SLAAC addresses would be stable, and with that, I opted to grab the address the device autoconfigured and plug that into a static local DNS entry. This ended up not working as the address would cycle every so often. I don’t have an exact time frame on that, but surely frequent enough to cause problems. This left me scratching my head for a bit until I realized that I was basically generating a temporary address (since all end devices have their own v6 address, a common practice is to to have a temporary address that cycles every so often to help make tracking the device a bit trickier).
Using DHCPv6 would behave much like its v4 counterpart and allow for me to configure unique static IPs at the server level rather than specifying them on a per-host basis, but I already did static IPs per-host anyway and my network isn’t very large so doing things a bit more manually seemed acceptable to me. So I set up custom ULA addresses on the hosts I that required it and that largely solved my problem.
That is, until I discovered the option to explicitly configure temporary addresses and prefer them for outgoing messages using NetworkManager:
nmcli connection modify eth0 ipv6.ip6-privacy 2
This has since created what seems to be a stable SLAAC address that stays hidden behind temporary addresses. I’m still using the custom static ULA addresses though because those can be manually configured easily and survive OS reinstalls (which I had the pleasure of doing this weekend) once reconfigured. I also think the privacy mode is preferred anyway so I’ll just have both versions of static IP addresses. One benefit of IPv6 is the official suport for several IP addresses per interface.
IPv6 only
I was very curious to try having my desktop setup to use IPv6 only. This was mostly just to see if it would work as promised using NAT64 and DNS64. This ended up being a complete flop, but it had nothing to do with NAT64 or DNS64 (those both worked perfectly). The issue I’ve found is that my systems don’t seem to send the router solicitation messages over the physical wire, which means I don’t configure an address until I receive a router announcement. This is very problematic seeing as the interface will disable itself if it can’t get an IP address within a short window.
The weird thing is that the message makes it all the way to the local tcpdump logs but when viewing the traffic received, there is no packet. I’ve managed to reproduce it on both desktop and laptop. This has left me with my current system that still uses v4, basically just as a mechanism to keep the interface live long enough to get v6 addresses.
IPv6 limitation is my network
Another huge limiting factor to the usability of v6 in the network at home is the support for it. My Ruckus R350 AP running the unleashed firmware has no support for v6 that I can tell. I also have some IoT devices connected that may or may not support IPv6 but since the wifi doesn’t support it in the first place, and I’m also otherwise stuck with some form of a dual stack anyway, I’ll continue to stick with this as is.
Firefox
Firefox is a hot mess with a dual stack. If I enable IPv6, it will return a public IPv6 before a private IPv4. Same with the inverse, regardless of the preference for IPv6 selected. I’ve also just had massive issues with DNS and firefox in general. My solution to this was to duplicate each static DNS entry for each locally hosted service with the tailscale IPv6 address. This means that it always returns the same endpoint regardless of the protocol selected. Very annoying though. Part of this is my fault for having a wildcard DNS cert for my domain but I opted for that for ease of use and a little obscurity online.
Sites only supporting IPv4
In the time I took to write this, I discovered a site that only supported access via IPv4 despite having an IPv6 address available. This left an app hanging with no feedback other than a process timing out. Once I set my server to prefer IPv4, this problem completely disappeared. So it seems that, for the time being at least, I will be stuck preferring v4 on my server. Just one example of surely many of the pit falls surrounding the minimal support for IPv6.
To prefer v4 over v6, you can edit the /etc/gai.conf file and comment out the line for IPv4 preference:
precedence ::ffff:0:0/96 100
Closing thoughts
I’m glad I got around to doing this. I went into this very scared of setting up IPv6 because it seemed very cryptic compared to my knowledge of IPv4. After setting it all up, it really is the same thing but without NAT (more or less).
That said, this has also taught me a bit about why this isn’t ubiquitous. It feels very clunky to set up compared to v4. Part of the clunkiness is a lack of familiarity but it takes a few different parts to get this working well.
It’s also very hard to justify committing to a full v6 stack when I have devices in my network that definitely don’t support it. I’m also certain I will be adding other devices that don’t support it.
It’s also worth noting that if an off-the-shelf router supports IPv6, it likely will do all of this for you, greatly reducing the amount of knowledge required and the headache involved.
Resources
- https://github.com/Misfit-138/OpenBSD-FiOS-and-IPv6-Demystified
- https://blog.infected.systems/posts/2024-12-07-building-an-ipv6-focused-openbsd-home-router/
- https://man.openbsd.org/
- https://major.io/p/enable-ipv6-privacy-networkmanager/
- https://www.rfc-editor.org/info/rfc4193/#section-3.2