← Blog
How it works

The tracker your blocklist cannot see

“My blocker is switched on and the lists are current, and things are still getting through.”

When a blocker is on and ads still get through, the explanation people reach for is a stale list: something new appeared, nobody has added it yet, it loads. That one is real and it fixes itself on the next list update.

It is not the only reason things get through, and this post is about one of the others. It does not fix itself, because there is nothing to add to the list. The request the blocker let through was addressed to the site you were already on.

The evasion is not obfuscation, it is ownership

A tracker normally answers on its own hostname. Filter lists are keyed on hostnames, so it gets blocked, and both sides understand the game.

CNAME cloaking changes who owns the name. The site publishes a subdomain of its own domain, metrics.example.com say, as a DNS alias pointing at the tracker's hostname. Your browser requests /collect from metrics.example.com over TLS. The resolver follows the alias and the bytes come from the tracker, but every part of that request an extension can inspect says example.com.

Nothing is disguised. The URL is honest about the hostname it is fetching. The hostname is simply first-party now, and not blocking the first party is the whole design of a blocklist rather than a bug in it.

Seeing through it takes a DNS permission

To tell a real subdomain from an alias, a blocker has to repeat the lookup the browser's resolver already did and ask what the name resolves to canonically.

Firefox exposes that to extensions as browser.dns.resolve(), which takes a hostname and a list of flags. MDN's page for the function documents the flag that matters in one line:

"canonical_name": The canonical name of the specified host will be queried.

uBlock Origin makes exactly that call:

dnsAPI.resolve(hn, [ 'canonical_name' ])

The call is not the interesting part. Where the file containing it lives is.

One extension, two manifests, one permission apart

uBlock Origin ships a manifest per platform. Set the Firefox and Chromium permission lists side by side and they are nearly identical. Both ask for alarms, privacy, storage, tabs, unlimitedStorage, webNavigation, webRequest, webRequestBlocking and <all_urls>.

Beyond that the Chromium list names contextMenus where the Firefox list names menus, which is the same capability under the name each browser uses for it. Then the Firefox list has one entry that has no counterpart at all: dns.

That line is the entire capability. Without the permission there is no resolver call, so there is no canonical name, so there is nothing to compare the hostname against, and a cloaked subdomain is indistinguishable from a subdomain the site actually runs.

The code follows the permission. The uncloaking logic lives in a Firefox-only file, platform/firefox/vapi-background-ext.js. The Chromium build's file of the same name contains no CNAME handling at all: its only two mentions of DNS are an unrelated X-DNS-Prefetch-Control workaround.

So "I use uBlock Origin" does not answer the question. "I use uBlock Origin on Firefox" does.

When the capability is missing, nothing says so

The first thing the Firefox file does is guard against the API being absent:

const dnsAPI = browser.dns || {
    resolve() {
        return Promise.resolve();
    }
};

Follow what happens if that fallback is the one in use. resolve() produces a promise that fulfils with nothing. The handler caches an entry built from no record, so the entry carries no canonical name. The step that would rewrite the request checks for a canonical name, finds none, and returns without doing anything. The request proceeds exactly as it arrived.

Nothing throws. Nothing is logged. Nothing in the toolbar changes.

In practice that fallback is defensive, since the file only loads in a build where the permission is requested. The shape is still the lesson. A blocker that cannot uncloak and a blocker that uncloaked and found nothing produce byte-identical output: a request that was not blocked. If you are trying to establish what a page actually sent, the blocker's icon tells you it ran. It does not tell you what it was able to see.

It also needs a moment where the decision can wait

Resolving DNS is asynchronous, so the blocker has to hold the request until the answer arrives. uBlock Origin's request handler does that by returning a promise, and the request stays suspended until it settles. Two things are therefore required: a resolver call, and a point in the pipeline where a decision is allowed to wait for one.

uBO Lite, the build for the declarative extension API, describes its own design in its README:

uBOL operates entirely declaratively, meaning no permanent process is required for filtering.

That property is why it costs almost nothing to run. It is also why there is no moment in it that can await anything. The browser matches the shipped rules against the URL as written, and the URL as written says example.com.

The careful part is knowing when not to uncloak

It would be easy to read all of this as "look up the alias and block whatever it points at". The file is mostly the opposite: a set of conditions under which it deliberately does nothing.

By default, an alias inside the same registrable domain is ignored, because that is ordinary internal aliasing and not cloaking. The comparison uses the Public Suffix List, not string suffixes. Also by default, a request addressed to the page's own hostname is not uncloaked, so the document you asked for is left alone. Both are advanced settings rather than fixed rules. There is an ignore list, compiled to a regular expression that matches on label boundaries. When the request goes through a proxy that performs its own DNS, resolution is skipped entirely, on the reasoning that an answer resolved locally is not the answer the proxy got. Answers are cached in a 512-entry ring buffer with a ten minute lifetime.

The detail worth copying is what happens to the URL when a request is uncloaked. By default the rewritten URL keeps the canonical hostname and the path only as far as its first slash. The full path and query are dropped. Re-running your filters against a request under the tracker's real name is useful; handing the tracker's name the entire URL you were fetching is not, and the default refuses to.

The boundary of what was checked

Everything above about uBlock Origin was read from that project's published source: the two platform manifests, the Firefox request handler, the Chromium file that does not implement the same thing, and uBO Lite's README. The canonical_name flag is quoted from the source of MDN's page for the function.

No browser was run and no DNS traffic was captured for this post. The claim about which browsers ship the extension DNS permission rests on the manifests those projects ship and on MDN's compatibility dataset, which records the permission as available in Firefox from version 60, limited to a pre-release channel in Chrome, and absent in Safari.

This post does not measure how common CNAME cloaking is, and does not name any site that uses it. It also says nothing about the timetable on which any browser is changing its extension platform. Those are separate questions, and none of them were checked here.