Extra Stupid Firewall Tricks
It rhymes with IPv6
What I didn't mention in the post about port forwarding DNS requests was that the AdGuard Home instance I'm running is running on the same OPNsense firewall as my primary, non-filtered DNS resolver.
Setting up a virtual IP with an IPv4 address is pretty simple. The main resolver is on the firewall's primary LAN address (ends in .1, unsurprisingly); the AdGuard virtual interface is on .101 on the same subnet. Setting up IPv6, however, stumped me. I've found that even though our IPv4 WAN address tends to survive reboots, we get a new IPv6 prefix with every reboot. IPv6 does some things with prefixing and link-local addresses that I still don't fully understand. For instance, only four characters in the IPv6 address ever change, but they're in the middle of the address. I needed to understand enough to make this work, but I didn't need to understand enough to run a whole IPv6 network.
What I actually needed was an IPv6 address that would stay consistent enough for me to bind a service to it without having that service crash the next time the firewall restarted, but that wouldn't in the process cause other things on the firewall to go wrong. If I tried to create a new IPv6 address in the same subnet as the main link-local network, things went wrong. I started experimenting. After many false starts, here's what has ended up working for me. (ORIGINAL: There may be a better, more official, way to accomplish this, but this setup has worked for me for a while now.) UPDATE: Turns out the “correct” way to do this is with an IPv6 address fragment, so I've updated a couple steps below.
Set up an IPv4 virtual address on the same LAN subnet. Pick one that won't be assigned by your DHCP server, but that's easy enough to remember. It's important to set the “Deny service binding” checkbox to keep OPNsense from binding other services to the new virtual IP.
Set up an IPv6 virtual address. (DEPRECATED: I copied the existing IPv6 link-local address and changed the beginning of its prefix from
fe80tofd80but left the rest exactly the same, with a/64subnet. This makes it easy enough to read just the first and last bits of the binding when looking at interfaces on the firewall. On closer reading now I think I can't actually usefd80(see: I don't fully understand, above), but it was the first thing that actually worked so I'm just going to let it be wrong until something else breaks.)) Turns out you can do this with an IPv6 address fragment. I'm using::5353/128for this purpose. Again, click that “Deny service binding” checkbox.Turn on router advertisement. (DEPRECATED: for the
fd80::prefix with a/64subnet — turns out that with an IPv6 fragment you can just use the defaults, but you do need the service turned on so other hosts will know how to reach the IPv6 virtual address fragment created above)Install the AdGuard Home package for OPNsense (I use the full repo there and not just the AdGuard one, because I also have the Unifi controller running. That's left as an exercise for the reader).
Enable the “Adguardhome” service but deselect “Primary DNS” so it doesn't try to bind to the standard ports on the primary LAN interface. It'll still probably try to bind its management web interface to
:80, so have fun figuring out how to get the defaults working temporarily.To the command line! Edit
/usr/local/AdGuardHome/AdGuardHome.yamland configure itshttpsection to bind to the IPv4 address you created in step 1. Also configure thebind_hostssection of thednssection and bind it to both virtual addresses created above. You may or may not be able to do this without just using vi, but I used vi because I have the muscle memory for it.Once you've verified that the service is up and running alongside the built-in Unbound DNS service (one or both may require restarts, and see below for a way to test that they're both working), you can assign the resolver to individual hosts using your DHCP server of choice, unless the DHCP server of your choice is Kea, which currently lacks this functionality. If you're using Kea, or you only want to maintain one set of configurations, skip directly to the port forwarding described in my other post.
To verify that both services are running from another computer on the network, you can use the host command on the command line, e.g.: host google.com 192.168.1.1 would ask the primary resolver at .1 (presumably your router; adjust accordingly), while host google.com 192.168.1.101 would ask the AdGuard Home resolver. Substitute the IPv6 address of your router's primary interface (up to you to figure out) and ::5353 for the virtual interface to check that it's also working over IPv6, if that's a thing that matters to you.
After typing all this out I'm sure there has to be a less convoluted way to do what I wanted, but all the services I want to run coexist on the same hardware and everything works after restarts, so I consider this job done for now.