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, dnsmasq owns :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

WhatResult
Days measured29 of this LAN’s DNS (2026-09-24 → 2026-10-24)
Queries seen658,213, 12,164 distinct domains after noise filtering
Community lists cross-matched26 vetted lists, 667,085 domains
Domains that qualified as telemetry1,723 (each confirmed by ≥2 independent lists and observed here)
Best generic list instead71,276 domains, 77% overlap with mine, 392 of my trackers missing
Share of daily queries the block would have hitmedian 2.86%, p90 6.4%, range 0.63–10.62%
Enforced2026-10-24 19:35 UTC, whole LAN, router DNS, NXDOMAIN
First 9.6 hours489 of 16,077 queries blocked (3.0%), 34 distinct names
Collateral damagenone 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 missesQueries (29d)Devices
us-east-1.hightouch-events.com1,1013
www.googletagmanager.com17710
firebaselogging-pa.googleapis.com1594
cc-api-data.adobe.io1572
ad.doubleclick.net1355
cdp.cloud.unity3d.com1211
vortex.data.microsoft.com1042
sb.scorecardresearch.com956
www.googleadservices.com857

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.

SetDefinitionCountAction
T1Independently classified by ≥2 community lists, observed here1,723enforced now
T2On exactly one list, observed here1,121watchlist, report only
T0On no list at all, yet beacons on a machine-periodic schedule363watchlist, 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:

DomainQueries (29d)DaysDevicesLists
us-east-1.hightouch-events.com1,101933
c.go-mpulse.net8801952
googleads.g.doubleclick.net7332984
app-analytics-services.com6342863
gator.volces.com6212653
bam.nr-data.net4542243
tsa.webex.com3512542
app-measurement.com2952765
stats.pandora.com2451733
ogads-pa.clients6.google.com2382882
analytics.plex.tv2092835
iadsdk.apple.com2082774
ads-img.mozilla.org1872212
www.googletagmanager.com17726102
pubads.g.doubleclick.net1702052
pagead2.googlesyndication.com1652375
firebaselogging-pa.googleapis.com1592142
cc-api-data.adobe.io1572922
trackingengine-us-west-2.adswizz.com1561745

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 blockShare
The sensor103,11930.0%
A Mac laptop93,0628090.9%
The media box (Plex + torrents)87,0855560.6%
A workstation51,6091960.4%
A phone43,86312,45028.4%
A second phone35,3061,9545.5%
The LLM workstation34,08500.0%
A gaming desktop27,0791,8196.7%
The living-room Apple TV26,4351,7646.7%
A third phone25,9741,2875.0%
A tablet12,270900.7%
The camera21,38800.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 resolverQueries (7d)
1.1.1.163,379
1.0.0.161,273
9.9.9.956,329
149.112.112.11251,314
8.8.8.8516

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:

MeasureValue
DNS queries logged16,077
Answered NXDOMAIN by the block489 (3.0%)
Distinct blocked names34
Devices seen12

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 nameHitsWhat it is
analytics.plex.tv320Plex usage analytics
bam.nr-data.net62New Relic browser monitoring
googleads.g.doubleclick.net11ads
fp.msedge.net9browser fingerprinting
ogads-pa.clients6.google.com6Google ad IDs
metrics.icloud.com6Apple metrics
tsa.webex.com6Webex telemetry
arm-ring.msedge.net5Microsoft 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’s servers., metadata., images. and v4. 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.