What this is: 29 days of my own DNS logs, cross-matched against 26 community lists, which named 1,723 telemetry domains this LAN actually hits. I enforced that set on the router instead of a 71,000-domain list. First ten hours: 3.0% of queries answered
NXDOMAIN, nothing broken.Environment: GL.iNet GL-MT6000 on OpenWrt,
dnsmasqowns:53. Security Onion sensor with Zeek on a mirror of the WAN link. Per-device DNS log pulled off the router. 26 lists for ground truth. Substitute your own.Placeholders: every address is RFC 5737 (
203.0.113.0/24,198.51.100.0/24,192.0.2.0/24). Devices appear by role. No real MACs.Verified: the 29-day figures come out of this LAN’s own logs, the enforcement figures out of the router and its first 9.6 hours after the switch. Anything estimated says so.
New here? Section 1 is the fan-out — why one page turns into dozens of lookups, and what rides along in them.
Not shipped: the rule file and the sync script. Section 7 has enough to write your own, which is the point. Yours should come from your traffic.
Plex was sitting idle. Nobody watching anything. It was still resolving
analytics.plex.tv a few times an hour.
Once I could see that one, the rest showed up. A phone app checking in with an attribution service. A gaming desktop reporting to a JavaScript error tracker. A TV box asking Google for an ad ID. All of it outbound. None of it visible unless something is answering the DNS side.
The router won’t tell me any of that. It forwards, it forgets. But the two pieces
were already running: Zeek on a mirror of the WAN link, and a poller that pulls
the dnsmasq query log off the router every two minutes and writes one JSON line
per query. Timestamp, client, qname, qtype. So the measurement existed a month
before I had a reason to want it, which is the only reason this post has numbers
in it instead of opinions.
0. The short version
| What | Result |
|---|---|
| Days measured | 29 of this LAN’s DNS (2026-09-24 → 2026-10-24) |
| Queries seen | 658,213, 12,164 distinct domains after noise filtering |
| Community lists cross-matched | 26 vetted lists, 667,085 domains |
| Domains that qualified as telemetry | 1,723 (each confirmed by ≥2 independent lists and observed here) |
| Best generic list instead | 71,276 domains, 77% overlap with mine, 392 of my trackers missing |
| Share of daily queries the block would have hit | median 2.86%, p90 6.4%, range 0.63–10.62% |
| Enforced | 2026-10-24 19:35 UTC, whole LAN, router DNS, NXDOMAIN |
| First 9.6 hours | 489 of 16,077 queries blocked (3.0%), 34 distinct names |
| Collateral damage | none found. Playback, remote access, iCloud, updates, cameras, servers all fine |
2.86% predicted off a month of logs, 3.0% real on the first night. Two numbers landing that close is the only confirmation I get that the classifier does what I think it does, because I never see the payload.
1. One page is not one domain
Never looked at this? The volume is the surprise. A page load isn’t a request. It’s dozens, spread across companies you have never heard of, and every app on every phone does the same thing with no page involved at all.
Take one article on a news site. HTML from one origin. That origin pulls a tag container, the container pulls an ad exchange, the exchange pulls a header bidding library, the library fans out to four more DSPs to figure out what your attention is worth. Plus a CMP, a session recorder, an A/B SDK, an APM beacon, a font CDN, an image CDN, and a newsletter vendor that is a data broker in a hat:
news.example the page you asked for
├─ cdn.example-assets.net fonts, images
├─ tags.example-tagmanager.com tag container
│ ├─ ads.example-exchange.net the auction
│ └─ bidder.example-dsp.net more bidders
├─ replay.example-session.io session recorder
├─ errors.example-apm.com error tracker
└─ consent.example-cmp.net consent widget
Nine names. One page. None of them the site you typed.
Then the apps. An SDK fires on cold start, on resume, on a timer, on an in-app purchase. Every one of those is a qname followed by an upload. That is where the phone at 28% came from (section 5): not browsing, just being on.
Then the appliances, which nobody thinks about. TVs, cameras, printers, speakers. Nightly check-ins, cloud relay keepalives, vendor telemetry against hostnames the manual does not mention.
Three reasons it ends up this spread out. Failure isolation: the ad server goes down and the page still has to render, so it lives on its own host, on its own domain. State: a third party can’t read the first party’s cookies, so identity rides in the query string, in a request header, or in a first-party CNAME that resolves to them. Ownership: whoever collects the data almost never owns the site you visited, and the qname is the straightest answer to who is actually being asked.
The payload is worse than the name. Screen and page, model, OS build, an install
ID, referrer, and events: taps, dwell time, scroll depth. Some of it is
fingerprinting properly done — canvas hash, WebGL renderer string, audio context,
font list. On mobile it’s IDFA or GAID, or an SKAdNetwork conversion value if the
client is being polite about it. fp.msedge.net is in my block set. fp is not
for fast page.
Where a network view stops: device and qname, nothing else. No TLS decryption, so I know a phone asked an attribution endpoint for something, not what it told them. Every figure here is a request count, never a payload. Which is enough — the same name off the same device several times an hour is its own argument.
And the chokepoint is the router, because you cannot install a browser extension on a television, and there is no per-app switch covering the SDK inside three other apps. One rule set, one place to audit it, one way to turn it off.
What this isn’t: a claim that volume equals surveillance. Most of a modern page is plumbing — CDNs, fonts, APIs, update checks — and blocking all of it breaks things for nothing. The line I drew is section 4: two independent groups have to call it tracking, and my own sensor has to have watched a device ask for it.
2. The data was already there
Nothing new got built for this. Three things were already running.
A sensor on a WAN tap. A managed switch mirrors the internet link, Security Onion runs Zeek over the copy. That is how I check later whether a block is even reachable (section 6).
A per-device DNS log. A small poller pulls the router’s dnsmasq query log every
couple of minutes, maps each client IP to a name off the DHCP leases, and appends
one JSON line per query: timestamp, client, domain, qtype. Twenty-nine days of
that is 658,213 lines.
A daily report that already deduped known noise and printed what each device asked for.
The noise filter matters more than it sounds. Raw distinct domains over 29 days
is well past 12,164, and the difference is *.local, mDNS and LLMNR chatter,
PTR sweeps, and the sensor resolving its own ruleset URLs. A list built on the
unfiltered set would have been full of things that never leave the LAN.
The measurement you need exists the day you decide to build the block. I didn’t start this with a privacy plan. A report already existed and I wanted to see what was in it.
3. Why I threw the generic list away
The obvious move is the curated list everyone recommends: HaGeZi Multi LIGHT, 71,276 domains, maintained continuously, low breakage. I simulated it against my own traffic before installing anything. It covered 1,331 of my 1,723 — 77%.
Fine, until you count the 392 it misses, which are exactly the trackers this network uses:
| Domain the light tier misses | Queries (29d) | Devices |
|---|---|---|
us-east-1.hightouch-events.com | 1,101 | 3 |
www.googletagmanager.com | 177 | 10 |
firebaselogging-pa.googleapis.com | 159 | 4 |
cc-api-data.adobe.io | 157 | 2 |
ad.doubleclick.net | 135 | 5 |
cdp.cloud.unity3d.com | 121 | 1 |
vortex.data.microsoft.com | 104 | 2 |
sb.scorecardresearch.com | 95 | 6 |
www.googleadservices.com | 85 | 7 |
Top row is the tell. Marketing automation, 1,101 requests in a month, three devices, and the safe curated tier doesn’t carry it — on a list built for the whole internet that name isn’t heavy traffic. On mine it is the busiest tracker I have.
So the trade runs backwards. 71,276 domains I have never contacted, bought with missing a tenth of the ones I have. I would rather ship 1,723 rules where every rule is a name my sensor watched a device ask for, twice, on more than one day, and where a bad rule traces to a device and a timestamp instead of to a stranger’s curation call.
Both, if you want the long tail: my measured set as the base, a generic tier on top. What I wouldn’t run is the generic tier alone, because then “what did I block” answers with a link to someone else’s repo.
4. Three sets, one rule
Sorting the observed traffic into three buckets is what makes enforcement defensible. A domain becomes a rule only if two independent groups of people called it tracking and this LAN was seen talking to it.
| Set | Definition | Count | Action |
|---|---|---|---|
| T1 | Independently classified by ≥2 community lists, observed here | 1,723 | enforced now |
| T2 | On exactly one list, observed here | 1,121 | watchlist, report only |
| T0 | On no list at all, yet beacons on a machine-periodic schedule | 363 | watchlist, report only |
T2 is the interesting middle. One list is a claim, not a consensus, and the price
of being wrong is a broken app. Several of the biggest T2 names are plainly
functional. settings-win.data.microsoft.com at 5,519 queries is Windows
settings sync, not a tracker. stun.l.google.com at 745 is WebRTC NAT traversal,
which video calls need. They stay out until a human says otherwise, which is
what a single-list tier is for.
T0 is the humbling set. Three hundred and sixty-three domains no community list knows about, hitting this LAN on a stable nightly or machine-periodic interval, which is the signature of beaconing. Most of it turned out to be my own infrastructure: a chat service I host, a game client’s session discovery, my LLM endpoints, and one camera vendor’s cloud relay touching four devices nightly. Nothing malicious. But a heartbeat detector ranking a camera relay next to malware C2 is information I did not have before.
T1 by volume, on this LAN:
| Domain | Queries (29d) | Days | Devices | Lists |
|---|---|---|---|---|
us-east-1.hightouch-events.com | 1,101 | 9 | 3 | 3 |
c.go-mpulse.net | 880 | 19 | 5 | 2 |
googleads.g.doubleclick.net | 733 | 29 | 8 | 4 |
app-analytics-services.com | 634 | 28 | 6 | 3 |
gator.volces.com | 621 | 26 | 5 | 3 |
bam.nr-data.net | 454 | 22 | 4 | 3 |
tsa.webex.com | 351 | 25 | 4 | 2 |
app-measurement.com | 295 | 27 | 6 | 5 |
stats.pandora.com | 245 | 17 | 3 | 3 |
ogads-pa.clients6.google.com | 238 | 28 | 8 | 2 |
analytics.plex.tv | 209 | 28 | 3 | 5 |
iadsdk.apple.com | 208 | 27 | 7 | 4 |
ads-img.mozilla.org | 187 | 22 | 1 | 2 |
www.googletagmanager.com | 177 | 26 | 10 | 2 |
pubads.g.doubleclick.net | 170 | 20 | 5 | 2 |
pagead2.googlesyndication.com | 165 | 23 | 7 | 5 |
firebaselogging-pa.googleapis.com | 159 | 21 | 4 | 2 |
cc-api-data.adobe.io | 157 | 29 | 2 | 2 |
trackingengine-us-west-2.adswizz.com | 156 | 17 | 4 | 5 |
Ten devices asking for Google Tag Manager, eight for both DoubleClick and Firebase logging. That’s not a browsing problem, that’s a library problem. The tag container is embedded in pages and storefronts, so it fires wherever the device goes.
5. The number that set the scope
Why LAN-wide instead of piloting one device: the per-device share. Whole-LAN would have taken a median 2.86% of daily queries, distributed like this.
| Device (by role) | Queries (29d) | Would block | Share |
|---|---|---|---|
| The sensor | 103,119 | 3 | 0.0% |
| A Mac laptop | 93,062 | 809 | 0.9% |
| The media box (Plex + torrents) | 87,085 | 556 | 0.6% |
| A workstation | 51,609 | 196 | 0.4% |
| A phone | 43,863 | 12,450 | 28.4% |
| A second phone | 35,306 | 1,954 | 5.5% |
| The LLM workstation | 34,085 | 0 | 0.0% |
| A gaming desktop | 27,079 | 1,819 | 6.7% |
| The living-room Apple TV | 26,435 | 1,764 | 6.7% |
| A third phone | 25,974 | 1,287 | 5.0% |
| A tablet | 12,270 | 90 | 0.7% |
| The camera | 21,388 | 0 | 0.0% |
Servers and cameras: zero. Workstations: near zero. One phone: 28% of everything
it resolves. That’s the ad SDKs — attribution libraries firing on launch, on
resume, on a timer, each one a lookup. None of that device’s actual use changes
if the answer is NXDOMAIN. The apps just stop being able to report.
So there was nothing to pilot. The device I would have piloted is the one with no telemetry at all, and the risk sat in a single phone whose worst case is an app that reports nothing. If the block had broken something, the fix is one command (section 7).
That table is capacity planning, not a forecast. It’s a 29-day average, device activity moves, and one phone going quiet shifts its own number by a lot. The useful part is knowing a 3% cut is not evenly spread.
6. Can a device walk around it
A DNS block is worth exactly what your devices’ resolver config says it is.
Before enforcing I pulled seven days of Zeek dns and ssl off the WAN tap:
| Upstream resolver | Queries (7d) |
|---|---|
1.1.1.1 | 63,379 |
1.0.0.1 | 61,273 |
9.9.9.9 | 56,329 |
149.112.112.112 | 51,314 |
8.8.8.8 | 516 |
All four of the big ones are the router’s own upstream set at router scale.
Client to public resolver directly is 516 queries in a week — the router plus the
odd hardcoded fallback, not a client stepping around dnsmasq. Encrypted DNS
appears only as the router’s own upstreams (one.one.one.one, dns.quad9.net).
No client on this LAN is doing DoH to a resolver I don’t control, and nothing is
booting a resolver over DoT on 853.
That check matters because the classic way this fails at home is a streaming
stick with 8.8.8.8 burned into the firmware, which walks past a router block
and leaves you convinced the block doesn’t work. Nothing here does that yet. The
incentive to start appears the moment blocking does, so it’s a standing check,
not a one-time audit.
The other limit is structural, and worth saying without decoration: this is a fence at the name layer. An app shipping its own resolver, an endpoint addressed by IP literal, a tracker delivered through a first-party CNAME, anything inside a TLS session I don’t decrypt, and anything negotiated by ECH passes through untouched. It cuts volume and routine reporting. It is not privacy, and I don’t run it as if it were.
7. Enforcement: one file and a kill switch
The sensor can’t block. Its capture is a mirror port, so it sees and never drops.
Enforcement is the router, which already owns :53 for the whole LAN.
The mechanism is a dnsmasq drop-in, one line per domain, no address means
NXDOMAIN:
# /etc/telemetry-block.conf (generated, do not hand-edit)
address=/ads.example-tracker.com/
address=/analytics.example-sdk.net/
Included from dnsmasq.conf, and listed in the router’s upgrade-persist file so
a firmware flash doesn’t silently unblock the house.
Three things make that safe to leave running on the box that serves DNS to every device in the house.
The file is generated, not typed. A daily job rebuilds the set from the DNS log
plus cached lists, renders it deterministically (parent-deduped, sorted, so
identical input gives byte-identical output), and compares checksums. No change,
no restart, so the ordinary day never touches dnsmasq.
dnsmasq --test guards the swap. Candidate config fails to parse, push is
abandoned, previous file stays. That’s the difference between a bad rule and a
house with no DNS.
The set is additive and human-demotable. Newly qualifying domains get added each run; anything I decide was a mistake goes into a demote file that wins over the automatic classification. A false positive costs one line and a day, not an argument with a maintainer.
The file the router serves today carries 2,217 rules, up from the 1,723 the 29-day measurement qualified, because the daily run keeps adding what newly qualifies.
The kill switch is the part I actually care about, because a block you can’t undo in a hurry is a liability:
# on the router, or wrapped by a helper script elsewhere
telemetry-block-toggle off # ~7 seconds: comment the include, restart dnsmasq
telemetry-block-toggle status # current state, expected state, rule count
off comments out the include, restarts dnsmasq, and runs the same --test
guard with an automatic revert. No reboot, no reflash, no SSH archaeology at 11pm
with a child asking why the TV stopped working. I tested the revert before
enforcing, and re-tested that a second sync run is a no-op.
Acceptance check: blocked names answer NXDOMAIN, dnsmasq --test passes,
status reports the expected rule count, a second sync prints unchanged and
restarts nothing, and the functional domains in section 9 still resolve.
8. The first ten hours
Enforced 2026-10-24 19:35 UTC, whole LAN, no pilot. Logs from then until morning:
| Measure | Value |
|---|---|
| DNS queries logged | 16,077 |
Answered NXDOMAIN by the block | 489 (3.0%) |
| Distinct blocked names | 34 |
| Devices seen | 12 |
3.0% against a predicted 2.86%. The classifier did on the first night what the month said it would.
Busiest blocked names were dull, which is the point:
| Blocked name | Hits | What it is |
|---|---|---|
analytics.plex.tv | 320 | Plex usage analytics |
bam.nr-data.net | 62 | New Relic browser monitoring |
googleads.g.doubleclick.net | 11 | ads |
fp.msedge.net | 9 | browser fingerprinting |
ogads-pa.clients6.google.com | 6 | Google ad IDs |
metrics.icloud.com | 6 | Apple metrics |
tsa.webex.com | 6 | Webex telemetry |
arm-ring.msedge.net | 5 | Microsoft telemetry |
Half that raw count is the router’s own resolver and loopback rather than client devices. Clients alone: 215 of 15,553, so 1.4%. Same shape either way — one media app’s analytics, an error tracker, ad IDs.
Nothing broke. What I checked after enforcement, and again the next morning:
- Plex playback, remote access, watch-state sync, artwork. All fine, because the set carries the analytics leaves and not the functional hosts.
- iCloud, App Store and OS updates on the Apple devices. Fine. Only the ad SDK and the metrics endpoint are blocked.
- Both servers, the cameras, the LLM box. Zero blocked queries, matching what was measured before enforcement.
- One gaming desktop at 15% of its own queries, highest on the LAN. That’s storefront and overlay telemetry, nothing it needs.
Nine and a half hours is a sample, not a verdict. What it establishes is that the set is narrow enough to survive a normal evening of streaming, gaming and phones, which is the failure mode that kills these projects.
9. What I won’t block, and what’s next
- Opt out first, where a switch exists. Plex playback-data sharing and ad personalization, Apple’s Settings → Privacy → Analytics & Improvements. That’s a fix at the source. A block is a fence around a machine that still wants to report.
- Functional hosts stay out even when they look like telemetry.
xp.apple.com, Apple’s software update hosts, and Plex’sservers.,metadata.,images.andv4.are explicitly excluded. The set is built to be boringly functional. - T2 and T0 wait. 1,121 single-list domains and 363 unlisted beaconers get reported daily, not blocked. Some of T0 is my own infrastructure, some is a camera vendor’s cloud. Neither goes in a rule until I know which.
- Re-run the bypass check. Nothing dodges DNS today because nothing has a reason to try yet. Enforcement is the reason, so the Zeek check is now a standing question.
The list worth having is the one your own network wrote. Mine is 1,723 domains, each with a device, a timestamp and two independent classifications behind it. It costs 3% of daily DNS queries, and when something breaks I can name the rule that did it. The 71,000-domain list takes ten minutes to install and is impossible to explain.
