Chrome's --host-resolver-rules switch takes a comma-separated list of MAP and EXCLUDE rules. Chromium reads the MAP rules top to bottom and stops at the first pattern that matches the hostname, so their order is the whole behaviour. EXCLUDE has no position. Chromium consults every one of them each time a MAP matches. We found out about the first half from our own extension, whose API call died behind a MAP * ~NOTFOUND we had put ahead of its name.
The two rule types
The grammar is short. In Chromium's net/base/host_mapping_rules.cc:
MAP <hostname pattern> <replacement host>[:<port>]
EXCLUDE <hostname pattern>
Patterns take * as a wildcard. A replacement of ~NOTFOUND means the name does not exist, so the request fails with ERR_NAME_NOT_RESOLVED. Rules are joined with commas, spaces around them are allowed, and the keywords are case-insensitive.
MAP * ~NOTFOUND is the rule people come for. Chromium's SOCKS proxy documentation gives it as the structural fix for DNS leaks, with an EXCLUDE for the proxy's own host, and our post on DNS prefetch leaks explains why: a name lookup has no way through an HTTP or SOCKS proxy, so the only safe local lookup is one that fails. Page loads are unaffected, because Chrome already hands the hostname to the proxy, SOCKS5 included (socks5 and socks5h in Chrome has the difference from curl).
How Chromium evaluates MAP and EXCLUDE
RewriteHost is about twenty lines, and reading it settles every ordering question:
- Walk the
MAPrules in the order given. - Skip a rule unless its pattern matches the hostname, or
host:port. - On the first one that matches, check every
EXCLUDE. If any matches the hostname, stop and leave the name alone. - Otherwise rewrite to that rule's replacement and stop. No later
MAPis looked at.
Three things follow. A specific MAP has to come before a wildcard, or it never runs. An EXCLUDE can go anywhere, and the common MAP * x, EXCLUDE y works for that reason. And an EXCLUDE pattern is compared with the hostname only, never with a port.
The fingerprint is changed inside the engine, not painted on topIn Mosaic: Its own Chromium. Tesseract Chrome: values changed in C++, not JavaScript. 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.
How it works: Its own Chromium
The call that came back "Failed to fetch"
Every Mosaic profile carries a companion extension. Its popup shows the profile's notes, logins and two-factor codes, and it gets them by calling Mosaic's local API from inside the profile's browser. That call goes out under the same network switches as any page, and those switches exist to keep pages away from the machine.
The proxy caught it first. We launch with --proxy-bypass-list=<-loopback>, which sends even 127.0.0.1 through the proxy so that a page cannot reach the operator's computer. A residential gateway will not carry a request to loopback, and this one answered with a status of its own making:
464 Connection to the specified target is not permitted due to security policies.
464 is not a standard status and nothing in our code emits it, so searching our repository for it found nothing. The fix was a random *.localhost name per launch for the companion, named in the bypass list. RFC 6761 makes .localhost loopback, and Chromium treats it as potentially trustworthy, which an arbitrary http:// host is not.
The popup still failed. This time it said TypeError: Failed to fetch, with no log line anywhere, and it looked as if the first fix had not worked. It had. The second wall was the resolver. With "Resolve DNS through the proxy only" on, the engine adds MAP * ~NOTFOUND so nothing resolves behind the relay's back. The wildcard matched the companion's name first, and that was the end of it.
What we launch with now
Today the companion's host gets its own MAP to 127.0.0.1, written before the wildcard:
--host-resolver-rules=MAP <companion>.localhost 127.0.0.1 , MAP * ~NOTFOUND , EXCLUDE 127.0.0.1
It is built in src/main/engine/args.ts, and the comment beside it says why it is a MAP and not an assumption. RFC 6761 says the name is loopback and Chromium implements that, but a rule that states it costs one switch and cannot be wrong. An EXCLUDE of the companion host would also have got the name past the wildcard, and would then have relied on the resolver doing the right thing with .localhost. We would rather the rule say so.
Three settings decide whether a request leaves the browser, and the name appears in all of them. The bypass list carries it when a profile has a proxy. The PAC script carries it as its first rule when a profile has none. The resolver rules carry it ahead of the wildcard. npm run check:companion-reach runs in our build and fails if the three spell different names, or if a launch without the companion exempts anything at all. Before trusting it we put the proxy half of the fault back, then the resolver half, and saw it fail on each.
After both fixes the popup's GET /profiles/<id> came back 200 on a profile with a residential proxy and DNS through the proxy, and 200 on a profile with no proxy at all. Then we tried the same doors from an ordinary page on https://example.com. With no proxy, 127.0.0.1, the LAN and the companion's host all stayed silent. With the proxy, 127.0.0.1 got the gateway's refusal back in 270 ms and nothing more.
One more place the switch shows up
Chromium treats --host-resolver-rules as an unsupported flag and says so in a yellow band: "You are using an unsupported command-line flag", followed by the switch itself. Ours printed the companion's mapping, random hostname and all, across the top of every window. We had always passed --disable-infobars. In Chromium's DisableInfoBars() that switch only counts alongside --headless, unless the build is branded Chrome for Testing, so in a headed profile it was read and ignored. Patch 0140 in our series removes the headless condition. Bars with a button still show, because Chromium only ever suppresses the buttonless kind.
For your own launches, the order is specific MAP rules, then MAP * ~NOTFOUND, then any EXCLUDE. Test the name that must survive from a page inside the browser. curl in a terminal resolves through the operating system and never sees the browser's switches, so it will report success for a rule the browser applies differently. And start watching before the browser starts: Chrome opens connections during launch, which is the subject of Chrome preconnects before you measure.




