# DNS prefetch leak with a proxy, and the rule that stops it

> Chrome's DNS prefetcher resolves names through your own resolver even with a proxy set. Mapping every name to NOTFOUND makes the leak fail closed.

Source: https://mosaicbrowser.com/blog/dns-prefetch-and-proxies

## DNS prefetching, the lookup that skips your proxy

Chrome's DNS prefetcher resolves names through your own resolver even with a proxy set. Mapping every name to NOTFOUND makes the leak fail closed.

![NOTFOUND: Every local lookup fails, so none of them leak](https://mosaicbrowser.com/covers/dns-prefetch-and-proxies.webp)

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](https://issues.chromium.org/issues/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](https://www.chromium.org/developers/design-documents/network-stack/socks-proxy/) 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.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Provider minting](https://mosaicbrowser.com/features/proxy-providers)

## 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](https://mosaicbrowser.com/blog/proxy-checker-says-working) 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](https://mosaicbrowser.com/blog/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](https://mosaicbrowser.com/blog/chrome-preconnects-before-you-measure). 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](https://mosaicbrowser.com/guides/how-to-check-a-proxy-before-you-use-it).

## Questions

### Does Chrome send DNS through an HTTP or SOCKS proxy?

For page loads through an HTTP proxy or SOCKS5, Chrome hands the hostname to the proxy and the exit resolves it. Lookups Chrome makes on its own, such as DNS prefetching, go to the system resolver instead.

### How do I stop DNS prefetch leaks in Chrome with a proxy?

Launch Chrome with --host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE <proxy host>". Every local lookup then fails, except for the proxy's own address, so nothing can be resolved behind the proxy's back.

### Is turning off prediction in Chrome's settings enough?

It removes the best-known source of local lookups. Chromium's own SOCKS proxy documentation calls that approach fragile, because you have to know every part of the browser that resolves names directly.

### Does a DNS leak test show prefetch leaks?

Only if the test page triggers a lookup that leaks. To see every lookup, watch port 53 on your own machine while you browse, for example with sudo tcpdump -n port 53 on a Mac.

## Related

[Chrome opens connections before your test starts](https://mosaicbrowser.com/blog/chrome-preconnects-before-you-measure): We counted 1, 0 and 0 CONNECTs in three launches and thought Chrome was bypassing the proxy for HTTPS. Our counter was reset after Chrome had started.

[What happens to QUIC when Chrome has a proxy](https://mosaicbrowser.com/blog/chrome-quic-through-a-proxy): With an HTTP, HTTPS or SOCKS5 proxy set, Chrome reaches sites over TCP tunnels, so a page that gets HTTP/3 direct gets HTTP/2 through the proxy.

[What your proxy provider can and cannot see](https://mosaicbrowser.com/blog/what-a-proxy-provider-can-see): Over HTTPS a proxy provider sees each hostname, port, time and byte count, never the pages or passwords. Over plain HTTP it sees everything you send.

[How to check a proxy before you use it](https://mosaicbrowser.com/guides/how-to-check-a-proxy-before-you-use-it): Check that a proxy answers and tunnels, where its exit really is, whether the address is residential or blocklisted, and that the browser stays behind it.

[A green tick that means the socket opened is worthless](https://mosaicbrowser.com/blog/proxy-checker-says-working): A proxy checker can pass while sites fail, because most test plain HTTP and the browser needs a CONNECT tunnel. Exit and stability are separate answers too.

[A CONNECT to a raw IP means your machine resolved the name](https://mosaicbrowser.com/blog/a-numeric-connect-means-dns-leaked): To check if DNS goes through your proxy, read what the proxy is asked for. CONNECT example.com:443 means it resolved the name; a raw IP means you did.

[A proxy that answers HTTP and refuses CONNECT](https://mosaicbrowser.com/blog/proxy-http-works-https-fails): A proxy that works for HTTP but not HTTPS is refusing CONNECT, the tunnel every HTTPS page needs. A check that fetches a plain-HTTP page cannot see it.

[socks5 and socks5h: where Chrome resolves the name](https://mosaicbrowser.com/blog/socks5-and-socks5h-in-chrome): In curl, socks5h sends the hostname to the proxy and socks5 resolves it locally. Chrome's socks5 already sends the name; the leak is elsewhere.

## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
