DNS Leaks

I’ve used PiHole running on a Raspberry Pi 3 Model B as a private DNS server for a couple of years now. It works quite well once configured and definitely reduces web surfing latency when there are multiple devices on the LAN visiting the same websites.

A recent LabZilla post, Your Smart TV is probably ignoring your PiHole, reminded me that a device on the local network is under no obligation to use the DNS server that is advertised by the network router. The post was specifically about “Smart” TVs bypassing the PiHole DNS by using a hardcoded DNS server such as 1.1.1.1 or 8.8.8.8, and described a solution to intercept and redirect this traffic to the PiHole using a pfSense router.

My TV is air-gapped—it is not connected via Ethernet and has had its Wi-Fi module lobotomized, but I still have other potentially malicious devices on the local network that may be circumventing the PiHole DNS (a Chromecast and a Windows laptop). My router is also running OpenWRT, not pfSense, so I needed to do a little tinkering to apply the technique described in the LabZilla post.

Intercept and Redirect DNS Queries

DNS requests are typically made on port 53, so the main idea here is two-fold:

  1. Create a port forward rule that intercepts all traffic destined for the Internet on port 53 and redirect it to the PiHole
  2. Create a NAT rule that rewrites the source of the DNS response (the PiHole) to match the intended source (the hardcoded DNS)

The second item is important. If a software client makes a DNS request to 1.1.1.1 but gets a response from some other IP address, it will complain. We want the PiHole to masquerade as the DNS server that the client was trying to reach.

Port Forward Rule

On the OpenWRT router under Network > Firewall > Port Forwards, I added the following rule:

  • Protocol: TCP, UDP
  • Source zone: lan
  • External port: 53
  • Destination zone: lan
  • Internal IP address: 192.168.1.101 (this is address of my PiHole, yours may be different)
  • Internal port: 53

The tricky bit here was that I needed to exempt the PiHole from this rule, otherwise its own traffic would be redirected back to itself. Using the ! symbol will negate a pattern match.

Under the Advanced Settings of the new port forward rule I added:

  • Source IP address: !192.168.1.101

Which indicates that this rule should apply to all devices on lan except the PiHole.

Port Forward Summary Port forward overview

General Settings Port forward general settings

Advanced Settings Port forward advanced settings

We can stop here and test the new port forward rule by creating a fake DNS record in the PiHole under Local DNS > DNS Records:

  • Domain: piholetest.example.com
  • IP: 10.0.1.1

This is a website that does not exist and a DNS query will only return 10.0.1.1 if it is fulfilled by the PiHole.

A fake local DNS record

Now let’s try to query CloudFlare’s 1.1.1.1 DNS server from a device on the local network using dig:

dig piholetest.example.com @1.1.1.1
;; reply from unexpected source: 192.168.1.101#53, expected 1.1.1.1#53

We can see that the redirect is working, but dig complains that the response is coming from an unexpected source. Let’s fix that next.

NAT Rule

Back on the OpenWRT router under Network > Firewall > NAT Rules, I added the following rule:

  • Protocol: TCP, UDP
  • Outbound zone: lan
  • Source address: any
  • Source port: any
  • Destination address: 192.168.1.101
  • Destination port: 53
  • Action: MASQUERADE - Automatically rewrite to outbound interface IP
  • Rewrite port: do not rewrite

This rules makes it so any traffic originating on lan that ultimately goes to the PiHole on port 53 will have the source IP rewritten to match the original, intended IP address (the hardcoded DNS).

NAT Rule Summary NAT rule overview

General Settings NAT rule general settings

Testing

The dig command from before now correctly returns 10.0.1.1, and it appears as though the response is coming from 1.1.1.1 enough though it is actually coming from 192.168.1.101. Success!

dig piholetest.example.com @1.1.1.1

; <<>> DiG 9.16.1-Ubuntu <<>> piholetest.example.com @1.1.1.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13835
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;piholetest.example.com.		IN	A

;; ANSWER SECTION:
piholetest.example.com.	2	IN	A	10.0.1.1

;; Query time: 12 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Tue Dec 15 22:25:18 EST 2020
;; MSG SIZE  rcvd: 67

Final Thoughts

With these OpenWRT configurations, all DNS queries on port 53—even hardcoded ones—are intercepted and redirected to the PiHole and the offending device is none the wiser about what server is actually fulfilling the request. This means that the PiHole’s block lists can be applied to all devices—even the sneaky ones—which thwarts ads/tracking, decreasing data usage. Something that is all too important in an age of ever restrictive ISP data caps.

However, a malicious device on the local network could still get its DNS queries out through the firewall if the hardcoded DNS server is running on a port other than port 53. As far as I know, there’s not much that can be done about this that doesn’t involve sniffing every packet that attempts to leave the network out to the Internet.