Last month a reader emailed me a screenshot of ViralVidVault loaded on his smart TV's built-in browser. The page worked, but he had counted nine third-party ad and tracker domains his TV was still resolving in the background — none of which came from us. We run a deliberately lean stack and ship no display ads, yet his network was lighting up like a Christmas tree because every other tab, app, and embedded SDK on that device was phoning home. He asked the obvious question: "Can I just block this stuff once, for the whole house, instead of installing an extension on every screen?"
The answer is yes, and the cleanest place to do it is the router, at the DNS layer, with NextDNS. This post is the engineering write-up I sent him, expanded. I run the backend for ViralVidVault, a GDPR-compliant European viral-video discovery service, so I care about two things that usually pull in opposite directions: blocking the surveillance-grade ad tech that pollutes video browsing, and not breaking the legitimate video CDNs and thumbnail hosts that make discovery feel instant. DNS-level blocking done badly does both. Done well, it's the highest-leverage privacy change you can make for an entire network.
Why the router, and why DNS specifically
Browser extensions like uBlock Origin are excellent, but they have a coverage problem. They protect exactly one browser on exactly one device. The moment you open a video in a native app, a smart TV, a console, an IoT picture frame, or a guest's phone, you're unprotected. There is no extension surface on a Roku.
DNS filtering works one layer down. Before any of those devices can connect to doubleclick.net or some fingerprinting endpoint, it has to resolve the hostname to an IP. If your router hands DNS to a filtering resolver, the resolution simply fails (returns 0.0.0.0 or NXDOMAIN) and the connection never opens. One configuration point, every device covered, including the ones you can't install software on.
The tradeoffs are real and worth stating up front:
-
DNS blocking is domain-granular, not URL-granular. You can block
ads.example.combut notexample.com/ads. For first-party ads served from the same domain as content, DNS can't help — that's where a browser extension still earns its place. - It can't see inside encrypted traffic. It only sees the hostname being looked up. That's enough for blocking but not for cosmetic filtering (hiding empty ad boxes).
- Misconfigured blocklists break things. Over-aggressive lists kill video thumbnails, CDN shards, and login flows. The whole back half of this article is about avoiding that.
NextDNS specifically is attractive because it's a hosted resolver with a real API, per-profile configuration, query logging you can export, and support for encrypted transport (DoH/DoT/DoQ). You get the control of a self-hosted Pi-hole without running the Pi.
The DNS resolution path you're actually changing
Before touching anything, it helps to see what a lookup looks like and where the block lands. Here's a tiny Go program that resolves a hostname against a specific resolver so you can compare your default resolver against NextDNS and confirm blocking is live.
package main
import (
"context"
"fmt"
"net"
"os"
"time"
)
// resolveVia queries a specific DNS server for a hostname's A records.
func resolveVia(server, host string) ([]string, error) {
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
d := net.Dialer{Timeout: 3 * time.Second}
return d.DialContext(ctx, network, server+":53")
},
}
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
return r.LookupHost(ctx, host)
}
func main() {
host := "doubleclick.net"
if len(os.Args) > 1 {
host = os.Args[1]
}
// Compare a normal resolver against NextDNS (anycast IPs).
for _, srv := range []string{"1.1.1.1", "45.90.28.0"} {
ips, err := resolveVia(srv, host)
if err != nil {
fmt.Printf("%-15s %s -> ERROR/blocked: %v\n", srv, host, err)
continue
}
fmt.Printf("%-15s %s -> %v\n", srv, host, ips)
}
}
Run it against a known ad domain and against i.ytimg.com (YouTube thumbnails). A correctly tuned profile returns 0.0.0.0 or an error for the first and real IPs for the second. If your thumbnails resolve to nothing, your blocklist is too aggressive and you've found the problem before users do.
Setting up the NextDNS profile
Create a profile at my.nextdns.io. You'll get a config ID (six hex characters) and a set of endpoints. The configuration that matters for video discovery breaks into three knobs.
Blocklists. Start conservative. I run exactly three:
- NextDNS Ads & Trackers — the built-in list, well maintained, low false-positive rate.
- OISD Small — a curated list that deliberately avoids breaking common services. The "Small" variant is the right call; the full variant blocks things you'll miss.
- AdGuard DNS filter — good coverage of in-app and mobile ad SDKs that desktop-focused lists miss.
Resist the urge to enable a dozen lists. Blocklist overlap gives you diminishing returns and rising false positives. Two or three good lists beat ten aggressive ones every time.
Native tracking protection. NextDNS has toggles for blocking telemetry from Samsung, LG, Roku, Apple, Windows, and others. For a TV-heavy household this is the single most effective switch — smart TVs are among the most aggressive trackers in the home, and these are the devices you cannot put an extension on.
Denylist / Allowlist. This is your manual override and it's where you keep video discovery working. Pre-seed the allowlist with the hosts your video sources need before anyone complains.
Keeping video CDNs in the allowlist
This is the part most write-ups skip, and it's the part that determines whether your family curses your name. Video discovery depends on a sprawl of thumbnail hosts, manifest servers, and segment CDNs. Block one by accident and a whole category of content silently goes blank.
Here's the allowlist I maintain, and the reasoning:
-
i.ytimg.com,i9.ytimg.com— YouTube thumbnail shards. Blocking these blanks every grid. -
*.googlevideo.com— the actual video segment CDN. Non-negotiable. -
*.cdn.vine.co,video.twimg.com— short-form video hosts referenced by trend feeds. -
*.cdninstagram.com,scontent.cdninstagram.com— Instagram media; some trackers share parent domains, so test carefully. - Your own asset domains. For us that's the Cloudflare-fronted CDN serving ViralVidVault thumbnails.
The failure mode is subtle: a blocklist blocks metrics.somecdn.net, but the same provider also serves media.somecdn.net from a wildcard rule, and now your video is gone. Allowlist entries in NextDNS take precedence over blocklists, so an explicit allow is your safety valve.
You can script the comparison instead of eyeballing the query log. This Python snippet pulls your NextDNS logs via the API and flags any blocked domain that looks like a video CDN — the early-warning system for over-blocking.
import os
import sys
import requests
# NextDNS API: https://api.nextdns.io
API_KEY = os.environ["NEXTDNS_API_KEY"]
PROFILE = os.environ["NEXTDNS_PROFILE"]
# Substrings that indicate a domain we almost certainly want resolving.
VIDEO_HINTS = (
"ytimg", "googlevideo", "cdninstagram", "twimg",
"vimeocdn", "video", "media", "thumb", "img", "cdn",
)
def fetch_blocked(limit=1000):
url = f"https://api.nextdns.io/profiles/{PROFILE}/logs"
headers = {"X-Api-Key": API_KEY}
params = {"limit": limit, "status": "blocked"}
resp = requests.get(url, headers=headers, params=params, timeout=15)
resp.raise_for_status()
return resp.json().get("data", [])
def main():
suspicious = {}
for entry in fetch_blocked():
domain = entry.get("domain", "")
if any(h in domain for h in VIDEO_HINTS):
suspicious[domain] = suspicious.get(domain, 0) + 1
if not suspicious:
print("No video-like domains blocked. Clean.")
return
print("Review these — possible false positives:")
for domain, count in sorted(suspicious.items(), key=lambda x: -x[1]):
print(f" {count:5d} {domain}")
if __name__ == "__main__":
sys.exit(main())
Run it on a cron once a day. Anything it surfaces is a candidate for the allowlist. I run the equivalent of this against our own analytics pipeline: when our European users report blank thumbnails, the first thing I check is whether a popular DNS blocklist started catching one of our CDN shards.
Configuring the router
There are three ways to point your router at NextDNS, in increasing order of robustness.
1. Plain DNS (DHCP). In your router's DHCP/DNS settings, replace the upstream resolvers with the two NextDNS anycast IPs shown in your profile (they're linked to your config ID). Easiest, works on every router, but unencrypted and your ISP can see (and some hijack) port 53.
2. DNS-over-TLS / DNS-over-HTTPS. If your router runs OpenWrt, OPNsense, or has native DoH/DoT (many do now), use the encrypted endpoint. This encrypts lookups and is the configuration I recommend. On OpenWrt this is a few lines of dnsmasq plus https-dns-proxy.
3. Linked IP for plain-DNS profiles. NextDNS ties your profile to your home's public IP so the anycast IPs apply your specific rules. Because home IPs change, NextDNS gives you an updater endpoint. Don't install yet another daemon — your router can hit it on a schedule. Here's a minimal PHP updater you can run from cron on any always-on box (a NAS, a Pi, or in my case a small LiteSpeed/PHP 8.4 box that already runs other household jobs).
<?php
declare(strict_types=1);
// Keeps the NextDNS "linked IP" pointed at your current public address.
// Run from cron, e.g. every 15 minutes: */15 * * * * php /opt/nextdns/update.php
const UPDATE_URL = 'https://link-ip.nextdns.io/abc123/your-secret-token';
const STATE_FILE = '/var/lib/nextdns/last_ip.txt';
function currentPublicIp(): ?string
{
$ctx = stream_context_create(['http' => ['timeout' => 5]]);
$ip = @file_get_contents('https://api.ipify.org', false, $ctx);
$ip = is_string($ip) ? trim($ip) : '';
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : null;
}
function lastKnownIp(): ?string
{
if (!is_file(STATE_FILE)) {
return null;
}
$v = trim((string) file_get_contents(STATE_FILE));
return $v !== '' ? $v : null;
}
$ip = currentPublicIp();
if ($ip === null) {
fwrite(STDERR, "could not determine public IP\n");
exit(1);
}
if ($ip === lastKnownIp()) {
// No change — don't hammer the endpoint.
exit(0);
}
$resp = @file_get_contents(UPDATE_URL, false, stream_context_create([
'http' => ['timeout' => 8, 'ignore_errors' => true],
]));
if ($resp === false) {
fwrite(STDERR, "update request failed\n");
exit(1);
}
@mkdir(dirname(STATE_FILE), 0750, true);
file_put_contents(STATE_FILE, $ip, LOCK_EX);
fwrite(STDOUT, "linked IP updated to {$ip}\n");
The state-file check matters: without it you'd POST to the link endpoint every cron tick whether the IP changed or not. The same instinct that makes me cache aggressively in production — only do work when state actually changed — applies to a 30-line cron script. I store the last IP in a flat file here, the same way ViralVidVault keeps small bits of state in SQLite WAL mode instead of reaching for a heavier store. Right tool, right scale.
Verifying it works end to end
Claiming "it's blocking ads now" without checking is how you end up with a broken network and a false sense of security. Verify in three places.
At the resolver. Re-run the Go probe from earlier. Ad domains should fail; video CDNs should resolve.
On a device. Visit a deliberately ad-heavy page and watch it come back clean. Then — critically — open your actual video discovery flow and confirm thumbnails, autoplay previews, and playback all work. If anything is blank, check the query log immediately.
In the logs. The NextDNS dashboard shows blocked vs. allowed in real time. The number that matters isn't "percentage blocked" — a high block rate can mean you're breaking things. The number that matters is zero blocked video CDN domains combined with a healthy block count on tracker domains.
A quick verification checklist I hand to non-technical users:
- Ad-heavy news site loads with visibly fewer ads — good.
- YouTube/short-form thumbnails all render — good.
- Video playback starts without buffering loops — good.
- Smart TV home screen still works — good (native tracking blocking shouldn't break the UI).
- Login flows on banking/shopping sites work — good (a common over-block casualty).
If all five pass, you're done. If any fail, it's almost always one allowlist entry away from fixed.
What this does and doesn't buy you for privacy
Being precise here matters, especially if you, like me, work under GDPR and have to think about data flows carefully.
DNS filtering at the router stops a large fraction of tracker connections from ever opening. That's genuine: a tracker that can't resolve its endpoint collects nothing. It also collapses a whole class of cross-device tracking, because the TV, phone, and laptop all share one filtered resolver.
What it does not do:
- It doesn't stop first-party analytics served from the same domain as the site you're visiting. Our own analytics at ViralVidVault are first-party and privacy-preserving by design, and DNS blocking wouldn't touch them — which is the correct outcome, because they don't track you across the web.
- It doesn't encrypt the content of your traffic; TLS already does that. It encrypts your lookups if you use DoH/DoT.
- It doesn't make you anonymous. Your ISP still sees the IPs you connect to.
Think of it as removing the surveillance scaffolding around video browsing, not as a cloak. For a household, that's a huge, durable win for one afternoon of setup.
Conclusion
The reader who emailed me set this up over a weekend: a NextDNS profile with three blocklists, native TV tracking protection on, a short video-CDN allowlist, DoT on his OpenWrt router, and the PHP cron keeping his linked IP fresh. His follow-up screenshot showed those nine background tracker domains down to zero, with every thumbnail intact.
The engineering lesson generalizes beyond DNS. Block at the lowest layer that gives you full coverage (the router), keep your blocklists conservative and your allowlist explicit, and never trust a change you haven't verified on a real device. The same discipline I apply to caching and edge config for European video discovery — do the work once, at the right layer, and prove it works — is exactly what makes a home network both ad-free and unbroken. Set it up, run the verification checklist, and let it quietly do its job on every screen in the house.
Top comments (0)