The Problem
My homelab has multiple VLANs, a Tailscale overlay network, and no ad blocking. DNS was handled entirely by the EdgeRouter X-SFP forwarding to Cloudflare. I wanted:
- Network-wide ad blocking without per-device configuration
- Conditional DNS forwarding so LAN clients can resolve Tailscale hostnames
- Non-Tailscale devices (Rokus, smart TVs) able to reach Tailscale services like Jellyfin
- Client names on the DNS dashboard instead of raw IPs
The Architecture
| |
dns01 is an LXC container on pve01 running AdGuard Home and acting as a Tailscale subnet router advertising all four LAN subnets.
Deploying dns01
The LXC runs Ubuntu 24.04 on Proxmox’s VLAN 60 (management). A few gotchas:
systemd-resolved blocks port 53. Ubuntu 24.04 has it running by default. AdGuard Home can’t bind to port 53 until you disable it:
| |
MagicDNS leaks into LXC containers. If the Proxmox host runs Tailscale with MagicDNS, it leaks 100.100.100.100 into the LXC’s resolv.conf even with pct set --nameserver. The fix is disabling systemd-resolved before installing Tailscale, and running Tailscale with --accept-dns=false.
Minimal LXC templates lack curl and gnupg. Both are needed to add the Tailscale apt repo. Install them as prerequisites in your automation.
Routing Non-Tailscale Devices to Tailscale Services
The streaming devices on the default VLAN don’t run Tailscale, but they need to reach Jellyfin at its Tailscale IP (
Static route on the router pointing the Tailscale CGNAT range at dns01:
| |
IP forwarding on dns01 so it actually forwards packets between eth0 and tailscale0:
| |
iptables MASQUERADE so return traffic routes correctly. Without NAT, Jellyfin sees the source as a LAN IP (10.150.10.x) and has no route back. MASQUERADE rewrites the source to dns01’s Tailscale IP:
| |
I used iptables-persistent to survive reboots and added idempotent Ansible tasks for the whole chain.
DHCP and DNS Integration
EdgeRouter X-SFP runs dnsmasq for both DHCP and DNS forwarding. Pointing DHCP clients at AdGuard was straightforward — set dns01 as primary, router as fallback:
| |
Order matters — the first entry becomes the primary DNS server.
Getting Client Names in AdGuard
AdGuard Home shows client IPs by default. Getting hostnames requires reverse DNS (PTR records), which requires the DNS server to know which IP belongs to which hostname.
The key insight: EdgeOS’s use-dnsmasq enable makes dnsmasq handle DHCP, which means it can serve PTR records from its lease table. But there’s a catch — if you switched from ISC dhcpd to dnsmasq, existing leases are in ISC format. Devices need to renew their DHCP lease before dnsmasq registers them.
In AdGuard Home’s DNS settings, I configured the private reverse DNS server to point at the router (10.150.10.1). I also added a conditional upstream so reverse lookups go to the right place:
| |
After devices renewed their leases over 24 hours, the AdGuard dashboard started showing hostnames instead of IPs.
Results
After one day of operation:
- 37,000+ DNS queries processed
- 10.6% blocked (mostly ad trackers, telemetry, and analytics)
- Top blocked domains: Netflix logs, Amazon telemetry, Roku analytics, Google ads
- All LAN clients showing hostnames on the dashboard
- Streaming devices reaching Jellyfin via Tailscale without running Tailscale themselves
What I’d Do Differently
- Disable systemd-resolved first in the Ansible role, not as a manual step. I hit this twice (jellyfin01 and dns01).
- Test PTR records early. I spent too long debugging reverse DNS before realizing the DHCP engine matters.
- Don’t add random dnsmasq options without testing. I accidentally added
no-resolvto the router which nearly killed all DNS forwarding.