Preventing DNS bypass in RouterOS: NAT redirect to Pi-hole
After setting up Pi-hole across my VLANs, I started noticing something odd in the query logs: certain devices were barely appearing. My iPhone should have been generating hundreds of queries a day. Pi-hole said it had handled maybe a dozen from that IP. The rest were going somewhere else.
That somewhere else was 8.8.8.8. iOS and macOS have aggressive DNS fallback behavior - if the configured DNS server doesn’t respond quickly enough, they try a hardcoded alternative. My Pi-holes were fast, but Apple’s DNS stack didn’t always wait to find out. The DHCP-assigned DNS server was advisory; devices were doing what they wanted.
The router for this setup is an RB4011 running RouterOS. The fix I landed on was NAT redirection: silently intercept any DNS query that wasn’t already headed to Pi-hole and reroute it there transparently.
Redirect versus block
There are two ways to handle DNS bypass in RouterOS. You can block all outbound DNS traffic to non-Pi-hole destinations, or you can intercept it and redirect it to Pi-hole instead.
Blocking is the obvious approach. But it means the client sees a connection failure. Some apps treat an unexpected DNS failure as a sign the network is broken. A blocked query to 8.8.8.8 on an iPhone usually fails silently, but it can surface as weird behavior in apps that handle DNS errors poorly.
NAT redirection is cleaner. The device sends its query to 8.8.8.8. The router intercepts it, forwards it to Pi-hole, and returns the response. The device gets a valid DNS answer. It never knows the query went somewhere different. Ad-blocked domains return 0.0.0.0 just the same - the device just doesn’t know it’s Pi-hole answering.
The NAT rules
The core structure is a dstnat rule matching DNS traffic on port 53 (both UDP and TCP) from the affected VLANs, where the destination is anything not in the DNS_Servers address list:
/ip firewall nat add \
chain=dstnat action=dst-nat \
protocol=udp dst-port=53 \
src-address=10.0.x.0/24 \
dst-address-list=!DNS_Servers \
to-addresses=10.0.x.12 to-ports=53 \
nth=3,1 \
comment="DNS redirect to pihole01 (1-of-3)"
The dst-address-list=!DNS_Servers exclusion matters. Without it, Pi-hole’s own upstream queries - the ones it sends to Unbound - would get redirected back to Pi-hole and loop. The DNS_Servers address list contains all three Pi-hole IPs, so they route freely.
There are three rules per VLAN (one per Pi-hole instance), each with a different nth value - nth=3,1, nth=3,2, nth=3,3 - to distribute load across all three.
Why not PCC
My first attempt used PCC (Per Connection Classifier) for load balancing. PCC is the standard RouterOS approach: hash connection properties into N buckets and send each bucket to a different destination.
It didn’t work. With src-address hashing, all queries from a given device went to the same Pi-hole. Most of my DNS traffic came from a few devices - the Mac mini, the Apple TVs - so everything piled into one instance. I switched to both-addresses-and-ports to improve distribution, but the root issue remained: UDP DNS queries are stateless and RouterOS’s connection tracking is unreliable for them. The connection marking I set up in the mangle chain wasn’t being applied consistently before NAT processed the packets.
nth round-robin sidesteps this entirely. It works directly on packets, no connection state required. Every third DNS packet goes to pihole01, every third+1 to pihole02, every third+2 to pihole03. The distribution evened out immediately after switching.
FastTrack
RouterOS’s FastTrack feature accelerates established TCP/UDP connections by bypassing the normal firewall and NAT processing chains. That’s great for throughput, but it broke DNS load balancing: once a DNS flow was FastTracked, subsequent packets bypassed the NAT chain and nth stopped applying.
The fix is to exclude DNS from FastTrack by adding return rules in the forward chain before FastTrack fires:
/ip firewall filter add \
chain=forward protocol=udp dst-port=53 \
action=return \
comment="Exclude DNS UDP from FastTrack"
/ip firewall filter add \
chain=forward protocol=tcp dst-port=53 \
action=return \
comment="Exclude DNS TCP from FastTrack"
These rules exit the FastTrack evaluation path and let DNS traffic flow through normal NAT processing where nth applies.
DoT and DoH
DNS over TLS (DoT) uses port 853. I blocked it outright in the forward chain. Most clients fall back to plain port 53 when DoT fails, which then hits the NAT redirect. The result: devices attempting DoT get blocked, retry in plaintext, and the retry goes through Pi-hole.
DNS over HTTPS (DoH) is harder. It runs over port 443, identical to regular HTTPS. RouterOS can filter on TLS SNI for known DoH providers:
/ip firewall filter add \
chain=forward tls-host=*.cloudflare-dns.com \
action=drop comment="Block DoH - Cloudflare"
/ip firewall filter add \
chain=forward tls-host=*.dns.google \
action=drop comment="Block DoH - Google"
This covers Cloudflare, Google, OpenDNS, and Quad9. It doesn’t catch a device using an obscure or private DoH resolver, and TLS 1.3 can encrypt the SNI in some configurations. It’s best-effort, not comprehensive.
Verification
After deploying, the test is straightforward:
dig @8.8.8.8 doubleclick.net +short
Run that from a device in a Pi-hole VLAN. If the redirect is working, you’ll get 0.0.0.0 back - Pi-hole’s blocked response - even though the query targeted Google’s DNS. A real IP means the redirect isn’t firing.
To check load distribution, compare query counts across all three Pi-hole web interfaces after a few minutes of normal traffic. With nth they should be roughly equal thirds.
Lessons
- Redirect beats block for transparent DNS enforcement. Clients get a valid response; nobody notices.
- PCC doesn’t work reliably for UDP DNS load balancing. DNS is stateless and connection tracking is unreliable for it. Use
nthdirectly in NAT rules instead. - FastTrack bypasses NAT. Exclude port 53 or your load balancing rules stop applying to established flows.
- Exempt DNS server IPs from their own redirect rules. If Pi-hole’s upstream queries get caught and redirected back to Pi-hole, you get a loop.
- DoH via SNI is partial coverage. It blocks major providers, but there’s no complete solution short of TLS inspection.
Related reading
Trying to load-balance DNS across three Pi-holes (and settling for failover)
One of my three Pi-holes was handling 99.9% of DNS queries while the other two sat idle. The fix I wanted (even load balancing) turned out to be something RouterOS just won't do, and the reason is which firewall chain the queries land in.
Researching MikroTik RouterOS DNS proxy for the homelab
What RouterOS DNS proxy does, how it would touch DHCP, firewall rules, and static DNS, and why I left clients talking to Pi-hole directly.
Fixing the VLAN 30 IoT DNS Isolation Leak to Pi-hole
Pi-hole logs showed IoT devices on the isolated VLAN hitting internal DNS anyway. The cause was RouterOS DHCP plus firewall rule order, not a single mis-ticked box.
Ready to Transform Your Career?
Let's work together to unlock your potential and achieve your professional goals.