Every bot rule eventually needs an exception for search engines — nobody wants to block Googlebot. The
obvious way to write that exception is to match the user-agent string, and it is the wrong way, because
the client chooses its own user-agent. One curl -A flag is the whole attack:
curl -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' https://example.com/
A UA-string allowlist is a door with the password written on it. And the incentive runs the wrong way: the
more trusted a crawler is, the more worthwhile its name is to borrow, so the UAs an operator
most wants to trust are exactly the ones under the most pressure.
The useful part is that this pressure produced a fix. The vendors whose crawlers are worth impersonating
— Google, Microsoft, OpenAI, Apple and others — publish the IP ranges their crawlers
actually come from, as machine-readable JSON they keep current. That turns identity into something
the client cannot choose.
How unmask decides
unmask ships those published ranges as presets and matches the address, not the name. Two
independent settings are involved, and the useful mental model is that each one controls its own
rescue path:
Setting
Where
What it does
Official IP range presets
settings → Bypass IPs
One preset per published list. Enabled ships every one of them, so a fresh install already recognises the genuine crawlers by address.
Where an IP range can verify a bot, ignore its UA and match the address
settings → UA filter
The standing policy. For every pattern backed by a published range, the UA string stops being a pass at all — only an address inside the range gets through. On by default.
With both in their shipped state, Googlebot/2.1 arriving from a rented VPS is
not a search engine; it is an ordinary unknown client and it meets the ordinary challenge
flow. The real Googlebot, arriving from Google's published range, is rescued as before — and it does not
have to solve anything, so your rankings are unaffected.
A crawler whose vendor publishes no range is passed on its user-agent string alone, and that
string is free to claim. So the name buys deliberately little: it clears the challenge a browser would get, and
nothing else. A trap it trips, a community listing, a country, network or custom rule, a protected path, a deny
and every rate limit apply to it as to anyone; the name counts only for a GET or HEAD; and a per-address budget
(settings → UA filter, 30 requests a minute by default) caps how much one address gets
through on the name before it meets a CAPTCHA. Each crawler's Detail dialog shows what the last day let through
on its name and from how many addresses, so a name that is being abused can be set to normal checks or stopped.
A link-preview fetcher is a named crawler too, and it calls from abroad, so a country rule written against the
world's scrapers would stop it with them — a day of posts without their cards. The fetchers of X, Facebook
and LINE are therefore checked a second way: the name together with the network the request comes from
(AS13414, AS32934, AS38631 — a vendor's own system, which nobody else can send from). A fetch that passes
both is exempt from the country and ASN rules only; a custom rule, a protected path, a trap and the rate limits
still apply. This needs an ASN mmdb (settings → Network). Without one, or for a fetcher
calling from a cloud (Slack, Discord), the country tab shows as you edit a rule which fetchers the last day's
traffic says it would stop, and the notification list says so when it starts happening.
This is not rDNS. The classic advice for verifying Googlebot is a reverse-DNS lookup followed
by a forward confirmation. It works, but it puts a blocking DNS round-trip in the request path. Matching a
prefix list that is already in memory costs nothing per request, which is why unmask ships the lists instead.
Which crawlers can be verified this way
These vendors publish a machine-readable range list, so their crawlers are identified by address:
Vendor
Covers
Google
Googlebot and every Google-branded fetcher — Image / News / Video, AdsBot, Mediapartners, Inspection Tool, Google-Extended, Read-Aloud, Site-Verification. Three separate published lists; unmask matches against their union, because Google moves products between the files.
Microsoft
bingbot, BingPreview
OpenAI
GPTBot, OAI-SearchBot, ChatGPT-User — one list per product
Anthropic(v0.1.30+)
ClaudeBot, Claude-User, Claude-SearchBot — one shared list covers the whole fleet. Anthropic used to publish no ranges at all; now that it does, the retired Claude-Web / anthropic-ai names are covered on purpose as well.
Perplexity
PerplexityBot, Perplexity-User
DuckDuckGo
DuckDuckBot, DuckAssistBot
Apple
Applebot, Applebot-Extended
Amazon
Amazonbot
Retired crawler names are covered on purpose. Google Web Preview and similar products no
longer generate genuine traffic, so leaving them on UA-only rescue would only ever pass a spoof —
“Googlebot is verified, but Google Favicon walks through” is not a defence. One vendor-branded
pattern left open is the whole hole.
Crawlers with no published range — smaller search engines, feed readers, link
unfurlers, in-house monitors — can only be recognised by their UA string, so that remains their rescue
path. There is no way around this: if a vendor does not publish where it comes from, nobody can tell its
crawler from an impersonation. Judge them by what they are worth to you, and set the ones that are not to
Normal checks or Stop in their group's Detail under settings → UA filter.
The one thing worth checking
The two settings have to agree, and there is a deliberate asymmetry between them that is worth understanding
before you change either.
Turning a range preset off re-opens the UA path for that vendor. This is the safe direction,
not an oversight: if disabling a preset also dropped the UA rescue, the genuine crawler would have no way
left to pass and you would be blocking Googlebot. So unmask never lands in
“UA refused and range off”. The consequence is that
a preset switched off quietly restores exactly the spoofable behaviour the policy exists to
remove.
Setting a crawler to Pass on the UA filter tab does not carve out an exception. While the
switch is on, a pattern whose vendor range preset is enabled is judged by address even when it is set to
Pass; it passes on its UA string as well only if you turn the switch off. So the side to check is the presets: on
the UA filter tab each range-backed row carries a ๐ก badge, green when its published range is enabled on
the Bypass IPs tab right now. A badge that is not green is a vendor back on its UA path.
Both axes work the same with the nginx module on your host and in the gateway container, so the answer does
not depend on how you deploy.
Checking your own install
Send yourself a spoofed crawler. From any machine outside the vendor's ranges — your laptop, a VPS,
anything — ask for a page that unmask protects:
A 403 carrying the challenge page is the correct answer: the name was ignored and the address decided. A 200 means the UA string still rescues that pattern — check the two settings
above.
You cannot test the other half from outside, and that is the point: producing a request from inside Google's
published range is precisely what an impersonator cannot do. What you can confirm is that
genuine crawlers still arrive — the admin's AI / crawler traffic
card (on the Dashboard and Stats pages) breaks crawler traffic down by category, a category opens into the
individual crawlers with ๐ก on the ones verified by address, and Search Console's live URL inspection
fetches your page as the real Googlebot, from the real range, on demand.
After changing either setting on the nginx module, remember that the decision lives in
generated nginx config. Saving the tab renders it again at once, but the running nginx keeps the previous
render until it is reloaded (nginx -t, then reload; the gateway container reloads itself).
unmask doctor warns while nginx is running an older render, and when the rendered files no
longer match config.yml (after a hand edit, run unmask render-nginx).