Pihole and IPv6

I recently bought a cheap server to run at home, and set up pihole. I set up some local records, so my server is available via DNS locally. I also enabled DHCP, so I could give all of my devices fancy and organized local IP addresses.

Unfortunately my ISP, which is part of the Big Telecom oligopoly in Canada, gave me a very locked down router. Inexplicably this router allows you to configure v4 DNS servers or disable DHCP entirely, but it doesn’t allow you to do the equivalent for v6.

The only v6 toggle the router supports is enabling DHCPv6, which is off by default. You cannot configure v6 DNS addresses, and you cannot turn off the RA (Router Advertisement) broadcasts. By default, these RA messages tell the devices on the network to use SLAAC (Stateless Address Autoconfiguration), and the RDNSS (Recursive DNS Server) option is used to distribute the ISP’s v6 DNS addresses. So the router will support my setup for v4, but not for v6.

So what can one do? The correct answer is of course to buy a proper router and put the ISP’s box in bridge mode.

But I don’t feel like doing that right now. And I also don’t want to disable IPv6 entirely, since it’s the year 2026 and I don’t want to live in the stone age. Let’s see what we can do.

First let’s look at the router’s ICMPv6 RA messages. I used radvdump to intercept one:

interface <redacted>
{
	AdvSendAdvert on;
	# Note: {Min,Max}RtrAdvInterval cannot be obtained with radvdump
	AdvManagedFlag off;
	AdvOtherConfigFlag on;
	AdvReachableTime 0;
	AdvRetransTimer 0;
	AdvCurHopLimit 64;
	AdvDefaultLifetime 1800;
	AdvHomeAgentFlag off;
	AdvDefaultPreference medium;
	AdvSourceLLAddress on;
	AdvLinkMTU 1500;
	AdvIntervalOpt on;

	prefix <redacted>
	{
		AdvValidLifetime 8534;
		AdvPreferredLifetime 8234;
		AdvOnLink on;
		AdvAutonomous on;
		AdvRouterAddr off;
	}; # End of prefix definition


	route <redacted>
	{
		AdvRoutePreference medium;
		AdvRouteLifetime 1800;
	}; # End of route definition


	RDNSS <redacted>
	{
		AdvRDNSSLifetime 1800;
	}; # End of RDNSS definition


	DNSSL lan
	{
		AdvDNSSLLifetime 1800;
	}; # End of DNSSL definition

};

First we see AdvManagedFlag off. This means it’s telling devices not to ask a DHCPv6 server for an IP address, and instead rely on SLAAC. Note that some devices (e.g. Android phones) don’t even support DHCPv6, which is why it’s often turned off by default. AdvAutonomous on in the prefix block tells devices to use that prefix (which I’ve redacted) to generate their own IP address via SLAAC. The RDNSS block is the Recursive DNS Server option, which contains the ISP’s DNS server address (which I’ve also redacted). That’s how devices on my network were getting (and generally preferring) v6 DNS servers.

Good for me, AdvDefaultPreference medium is only medium. So if I can get the pihole to send RAs with high preference, devices should prefer those advertisements!

I run pihole version 6, using docker compose. To configure it to broadcast its own RAs, I used the following:

environment:
  FTLCONF_misc_dnsmasq_lines: |-
    address=/${HOST}/${LOCAL_ADDR}
    address=/${HOST}/${LOCAL_ADDR6}
    enable-ra
    ra-param=${INTERFACE},high,10,30
    dhcp-option=option:ntp-server,${NTP_ADDR}
    dhcp-option=option6:ntp-server,[${NTP_ADDR6}]
    dhcp-option=option6:dns-server,[${PIHOLE_ADDR6}]

This sets some local records (the address lines), which I used to access my home server locally. It enables RAs, and configures them to broadcast on a specified interface with high preference, every 10 to 30 seconds.

Then I’ve also configured DHCP to respond with my own NTP server, and DHCPv6 (if a device were to ask, which they shouldn’t) to respond with v6 NTP and pihole addresses. Pihole also supports NTP, but I happen to run different software on a different address.

Now running radvdump to check if it works:

interface <redacted>
{
	AdvSendAdvert on;
	# Note: {Min,Max}RtrAdvInterval cannot be obtained with radvdump
	AdvManagedFlag off;
	AdvOtherConfigFlag on;
	AdvReachableTime 0;
	AdvRetransTimer 0;
	AdvCurHopLimit 64;
	AdvDefaultLifetime 30;
	AdvHomeAgentFlag off;
	AdvDefaultPreference high;
	AdvLinkMTU 1500;
	AdvSourceLLAddress on;

	prefix <redacted>
	{
		AdvValidLifetime infinity; # (0xffffffff)
		AdvPreferredLifetime infinity; # (0xffffffff)
		AdvOnLink on;
		AdvAutonomous on;
		AdvRouterAddr off;
	}; # End of prefix definition


	RDNSS <redacted>
	{
		AdvRDNSSLifetime infinity; # (0xffffffff)
	}; # End of RDNSS definition

}; # End of interface definition

It works! And it seems to work well for the majority of devices on my network. Some, like my old chromecast, clearly hardcode their DNS servers, and there’s little to be done about that without having control over the router.

It isn’t perfect, there will be some leaks even on well-behaving devices. For example, if a device connects to the network and happens to see the router’s RA first, it will use the ISPs DNS servers until it sees the RA from the pihole that should override it. On the other hand, from a reliability perspective it’s nice because if my server is down, devices should theoretically fall back to the ISP’s DNS.

Of course, some devices hardcode DNS addresses (e.g. my old chromecasts) so this whole thing is far from perfect anyway. But at least now I get DNS ad blocking and my local records on most of the devices on my network.