---
title: "Forcing every DNS query through AdGuard (and the LG TV)"
description: "My LG TV was quietly reaching ad and tracking hosts about 470 times a week past my blocklist, so I blocked them per-client and then made every plain DNS query on the LAN go through AdGuard."
url: "https://michael.zazon.us/notes/forcing-every-dns-query-through-adguard-and-the-lg-tv"
published: "2026-09-10T12:00:00-04:00"
kind: "build-log"
subject: "homelab"
tags: ["dns"]
series: "Home networking"
part: 7
---
While browsing the query log of AdGuard (which I set up in [AdGuard Home and the query-log shipper](https://michael.zazon.us/notes/adguard-home-and-the-query-log-shipper)), the LG TV in our living room quickly caught my eye. It made about 470 queries per week to LG's ad, content-recognition, and tracking hosts, but my main blocklist [HaGeZi Pro](https://github.com/hagezi/dns-blocklists) only matched exactly 1 subdomain. I assumed that the blocklist had me covered for the TV, but the device that is most interested in watching me was barely touched.

[Image: Hourly DNS queries from the LG TV in early September, blocked versus allowed, with blocked spiking from 09/09](https://michael.zazon.us/notes/forcing-every-dns-query-through-adguard-and-the-lg-tv)

First, I wanted to fix this on the TV only, so I created 5 AdGuard rules (which only apply to that single client) and marked them as important, so no blocklist or allowlist can ever re-open those hosts. This doesn't affect anything else on the LAN. I deliberately didn't block the LG firmware/update hosts because I do want the TV to receive updates. The `$client` and `$important` modifiers are documented in AdGuard's [blocklist syntax docs](https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists).

```
'||<lg-acr-host>^$client=<tv>,important'
'||<lg-ads-host>^$client=<tv>,important'
'||<lg-fingerprint-host>^$client=<tv>,important'
'||<lg-beacon-host>^$client=<tv>,important'
'||<lg-iot-host>^$client=<tv>,important'
'||<lg-service-platform-host>^$client=<tv>,important'
```

The sixth rule I added a day later, when I noticed that the TV Home Dashboard was phoning home an inventory of devices to an LG service-platform host. I blocked that too, accepting the risk that it might break LG Channels or the ThinQ home-screen tiles. Again, this is scoped to the TV only, like the other five.

The real weak spot here is that client-scoped rules only work if the client queries AdGuard. A smart TV can hardcode its own resolver, and we know they do when blocking makes DNS look broken. I tested this by making one query from my home server directly to Google's public resolver, getting the real answer. Nothing on my network was stopping a device from bypassing AdGuard, and I didn't see any evidence of the TV doing it over IPv4. But what if it did? So my next fix was preventive.

Before I close that hole, I wanted to check who is bypassing AdGuard on purpose. I knew I had set up "direct DNS on some machines" before. That's only for my Sonos speakers, which use Google DNS via DHCP, and that's intentional. HaGeZi blocks some endpoints that the Sonos speakers need to be controlled. No one else bypasses it. Had I skipped this step, the intercept would have silently broken my speakers.

> [!tip] Find deliberate bypassers first
> My Sonos speakers use Google DNS via DHCP because HaGeZi blocks some endpoints they need. Without the exemption, the intercept would have silently broken the speakers.

This catches every stray query. My router runs OpenWrt (see [OpenWrt to the rescue](https://michael.zazon.us/notes/openwrt-to-the-rescue)), and it now redirects any plain DNS query from the LAN to its own AdGuard, regardless of what resolver the device named. The reply gets rewritten on the way back, so the device thinks Google answered. The query shows up in AdGuard under the device's own address. DNS-over-TLS queries from the LAN to the internet get rejected instead of dropped, so they fail immediately instead of waiting for a timeout. My Sonos speakers are in the exemption set, so they go direct like before. Queries already addressed to the AdGuard replica on my home server go through untouched, so DHCP DNS failover still works. I built this from the redirect and ipset options in [OpenWrt's firewall docs](https://openwrt.org/docs/guide-user/firewall/firewall_configuration). It's deployed via Ansible like everything else on my router (see [Ansible manages every config on my router and access points](https://michael.zazon.us/notes/ansible-manages-every-config-on-my-router-and-access-points)).

```yaml
# fw4 redirect semantics: `dest_ip` is the address to DNAT TO; the ORIGINAL
# destination is matched by `src_dip` (single value, `!` allowed).
- name: Configure LAN DNS intercept (redirect stray DNS to AGH, refuse DoT)
  community.openwrt.command:
    cmd: "{{ item }}"
  loop:
    - uci set firewall.dns_intercept=redirect
    - uci set firewall.dns_intercept.src='lan'
    - uci set firewall.dns_intercept.src_dport='<dns-port>'
    - uci set firewall.dns_intercept.proto='tcp udp'
    - uci set firewall.dns_intercept.src_dip='!{{ openwrt_dns_intercept_passthrough }}'
    - uci set firewall.dns_intercept.dest_ip='{{ openwrt_dns_intercept_target }}'
    - uci set firewall.dns_intercept.dest_port='<dns-port>'
    - uci set firewall.dns_intercept.target='DNAT'
    - uci set firewall.dns_intercept.ipset='!agh_bypass'
    - uci set firewall.dot_block=rule
    - uci set firewall.dot_block.src='lan'
    - uci set firewall.dot_block.dest='wan'
    - uci set firewall.dot_block.dest_port='<dot-port>'
    - uci set firewall.dot_block.target='REJECT'
    - uci set firewall.dot_block.ipset='!agh_bypass'
```

[Diagram: A DNS query redirected to AdGuard, with Sonos exempt, DoT refused and DoH passing](https://michael.zazon.us/notes/forcing-every-dns-query-through-adguard-and-the-lg-tv)

As proof, here's a test you can run on any network. I asked Google's resolver for a blocklisted host and got 0.0.0.0 (which Google would never return). Then I asked Google for a LAN-only name and got the local answer (which only AdGuard could provide). Finally, a DNS-over-TLS query failed fast (connection refused).

```sh
dig +short @<google-dns> <blocklisted-host>   # 0.0.0.0 -> intercepted and filtered
dig +short +tls @<cloudflare-dns> example.com # fails fast: DoT refused
fw4 print 2>&1 >/dev/null                     # warnings a green Ansible run won't show
```

This rule took me three deploys. Ansible ran clean and green, but the firewall chain was missing. It turns out that OpenWrt's fw4 silently skips a redirect section with a list of source addresses (where I had put the Sonos exemptions), so I had to put those into an ipset instead. And in a further surprise, on a redirect the field name `dest_ip` is where the query is sent, while the original destination is matched by `src_dip`.. almost the opposite of what you'd expect. The only command that tells you this is `fw4 print`, which shows the warnings Ansible hides. Two silent skips in one night mean a green fw4 run proves nothing until you inspect the chain and send a real query.

> [!warning] fw4 skips sections silently
> fw4 silently ignores a redirect section with a list of source addresses, so put them in an ipset instead. When redirecting, `dest_ip` is where the query is sent, the original destination is matched by `src_dip`. `fw4 print 2>&1 >/dev/null` shows the warnings Ansible hides.

My first version was IPv4-only, but the TV quickly exposed that gap: it turns out the TV also has an IPv6 address and was querying LG hosts over v6. Those landed on the router's AdGuard and were logged. I tested with a query to Google's v6 resolver and got real answers back, so I had the same hole on another protocol. The next day I added the IPv6 twin: a redirect plus a DNS-over-TLS block. No pass-through, no exemptions.. the router now advertises only its own address over v6.

What I didn't do: DNS-over-HTTPS still gets through (it uses the normal HTTPS port, so blocking that would break the web; stopping it is a blocklist job, not a firewall job). The IoT network isn't intercepted (it's built around talking directly upstream, not running its own AdGuard listener). It shipped with a runbook and a kill switch (one uci line per section, plus `fw4 reload`).

DNS is limited against the TV, but the TV's own privacy settings are next: content recognition, viewing and voice information agreements, set on the TV itself. I may also move the TV to the IoT network, once I decide whether that network gets its own intercept.