A DNS prefetch leak happens because Chrome's proxy setting covers page loads, and the DNS prefetcher is not a page load. The prefetcher looks up hostnames before you click on them, and there is no way to send a bare DNS lookup through an HTTP or SOCKS proxy. So those lookups go to the system resolver: your ISP's, from your real address, while the pages themselves leave through the proxy. The fix that holds is to make every local lookup fail.

Why a prefetch can't go through the proxy

An HTTP proxy takes requests, not name lookups. To load a page through one, Chrome sends the hostname to the proxy, either inside a GET http://... or in a CONNECT host:443, and the proxy resolves it at the exit. SOCKS5 works the same way when the client sends a name rather than an address. Neither protocol has a message that means "just resolve this name for me and tell me the answer".

A DNS prefetch is exactly that message. Its job is to have the address ready before the click, and with a proxy in the way there's nowhere to ask except the local resolver. The Chromium issue on it, 40783300, says this in one line from a Chromium developer: "there's no way to prefetch DNS through a SOCKS proxy (or indeed an HTTP/HTTPS proxy)". Chromium's own SOCKS proxy documentation adds that --proxy-server "applies to URL loads only", and names the DNS prefetcher as the most notable component that resolves directly.

What the leak gives away

The pages still load through the proxy, and the leak carries no page content. What it carries is the hostnames: links on the page you are reading, names a page hints at with <link rel="dns-prefetch">, and names the browser predicts you are about to visit. Each one goes to your own resolver with your real address behind it.

That is enough to matter. Your ISP's resolver sees a list of sites next to your home IP. And a site can watch for it: a DNS leak test puts a unique hostname on its page, then checks which resolver asked for it. If the answer is your ISP's resolver in your country while the page loaded from an exit somewhere else, the two halves of one visit disagree.

Paste one gateway line. Every profile mints its own exitIn Mosaic: Provider minting. One gateway line, a separate exit for every profile. Free holds 3 profiles. Pro is $49 a month for any number.

How it works: Provider minting

Turning prefetching off is the fragile fix

Chrome's settings can turn off page preloading and prediction, and an extension can send X-DNS-Prefetch-Control headers. Both reduce the lookups. Chromium's SOCKS documentation calls that approach fragile, because you have to know every part of the browser that issues raw DNS requests, and a new one can arrive with any update.

The documentation gives the structural fix in the same paragraph:

--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE myproxy"

MAP * ~NOTFOUND tells Chrome's own resolver that no name exists. EXCLUDE keeps one name resolvable, the proxy's own host, so the browser can still reach it. Page loads are unaffected, because the hostname goes to the proxy and never touches the local resolver. Anything else that tries to resolve locally gets nothing back and fails. It fails closed, instead of quietly leaking, and that holds for code paths nobody has listed yet.

How Mosaic applies the rule

In Mosaic the switch is called "Resolve DNS through the proxy only". It sits under Leak protection on a profile's Privacy tab and is on by default. With it on and a proxy attached, the engine launches with:

--host-resolver-rules=MAP * ~NOTFOUND , EXCLUDE 127.0.0.1

The excluded address is 127.0.0.1 rather than the seller's gateway, because with this switch on, the browser's proxy is Mosaic's own relay on this computer (or a traffic inspector in front of it, if you have switched one on). The relay carries the seller's credentials and resolves names upstream. The browser never needs to resolve a public name itself.

One name had to survive the rule, and it taught us that order matters. Each profile's companion extension talks to Mosaic on a private loopback hostname. Rules apply in order and the first match wins, so the wildcard swallowed the extension's own call. It showed up as TypeError: Failed to fetch, with nothing in any log, and looked exactly like a proxy refusal we had just fixed. That hostname is now mapped to 127.0.0.1 first, ahead of the wildcard.

It also helps to know where the rule makes no difference. For ordinary page loads through --proxy-server, a stock Chrome hands the hostname to the proxy too. The rule is for the other paths, the ones that would otherwise go to the local resolver. It is a guarantee about what this computer's resolver sees, and it says nothing about the resolver the exit uses.

The resolver at the far end

Once a name reaches the proxy, the exit node resolves it with whatever resolver it is configured with. We checked one US residential session with curl and no browser at all. The exit was Verizon in the United States, and the resolvers that answered were Google's, in Germany and the Netherlands. The same lookup from our build Mac with no proxy reported its own Austrian ISP, which showed the browser's DNS really was going through the proxy.

No switch on our side reaches the exit's resolver, so Mosaic's proxy check reports it as a fact about the exit. If the resolver's country disagrees with the exit's, a different pool is the only fix. What else the seller in the middle can see is in what a proxy provider can see.

How to check for a DNS prefetch leak yourself

  1. Close other apps that browse, then run sudo tcpdump -n port 53 in a terminal on a Mac.
  2. Open the browser through the proxy and load a page with plenty of links.
  3. Any query in the capture for a name on that page came from your machine, not the exit.
  4. Relaunch with --host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE <proxy host>" and repeat. The page should load and the capture should stay quiet.

Start the capture before the browser opens. A restored tab can connect during startup, which is how we once measured a bypass that wasn't there. The full set of checks to run on a proxy before trusting an account to it is in how to check a proxy before you use it.