Built with Nix CI License: MIT
$ nix run github:fzakaria/anubis-fetch -- https://lore.kernel.org/linux-mm/some-thread/T/A fast (Go) CLI to fetch URLs from behind Anubis proof-of-work walls and Cloudflare fingerprint checks β solving the challenge in-process, and only reaching for a real browser when it has to.
A growing number of sites β lore.kernel.org, GNOME, kernel.org, and many more β sit behind Anubis, a bot-wall that makes your browser solve a SHA-256 proof-of-work before it serves any content. It's great at stopping scrapers. It's also great at stopping you when you just want to curl a mailing-list thread:
$ curl -s https://lore.kernel.org/linux-mm/some-thread/T/ | grep -o '<title>.*</title>' <title>Making sure you're not a bot!</title> # π€’
anubis-fetch gets you the actual page:
$ anubis-fetch https://lore.kernel.org/linux-mm/some-thread/T/ | grep -o '<title>.*</title>' <title>[PATCH 0/2] ...</title> # π₯³
Bot-walls live at two different layers, and anubis-fetch handles both, cheapest step first:
- Reuse a saved cookie. Anubis hands out a signed auth cookie (
techaro.lol-anubis-auth) after you pass once; a browser isn't re-challenged on the next visit, and neither are we. Cookies are persisted per host (see Cookie persistence). - Solve the proof-of-work in-process. The request goes out over
reqimpersonating a real Chrome β same TLS/JA3 + HTTP/2 fingerprint β which also clears Cloudflare's passive fingerprinting. If the response is an Anubis challenge, we brute-force the nonce and submit it. No browser, ~0.6s. - Fall back to a real browser. For anything the fast path can't do, we drive headless Chromium via
chromedp, which runs whatever JavaScript the site serves.
Note
The fallback fires for: Anubis' preact / metarefresh challenge methods, an unknown/future method, a difficulty too high to brute-force, a rejected solution, or a Cloudflare active JS challenge (Managed Challenge / Turnstile / "I'm Under Attack"). Impersonation clears passive Cloudflare; only a browser clears the active JS tiers. So anubis-fetch degrades to "slower", never "broken".
Because it's Γγ°γ€ slower and drags a ~200 MB Chromium into every fetch. The in-process solver is the common case; the browser is the safety net.
| Path | Wall time | Needs Chromium |
|---|---|---|
| Solver (Anubis proof-of-work) | ~0.6s | no |
| Browser fallback | ~2.0s | yes |
Anubis embeds a challenge as JSON in the page:
{"rules":{"algorithm":"fast","difficulty":4},
"challenge":{"id":"...","method":"fast","randomData":"6214bd88...","difficulty":4}}Solving it means finding a nonce such that hex(sha256(randomData β nonce)) begins with difficulty zero characters (the nonce is its base-10 string). We then hand the answer back:
GET /.within.website/x/cmd/anubis/api/pass-challenge?id=...&response=<hash>&nonce=<n>&redir=<url>&elapsedTime=<ms>
...which sets the auth cookie and redirects to the real page. Difficulty 4 (the default on lore/kernel.org/GNOME) is ~65k hashes β sub-millisecond in Go.
Run it directly:
$ nix run github:fzakaria/anubis-fetch -- <url>Install into your profile:
$ nix profile install github:fzakaria/anubis-fetchOr add it to your own flake:
{ inputs.anubis-fetch.url = "github:fzakaria/anubis-fetch"; # then, e.g. in home.packages / environment.systemPackages: # inputs.anubis-fetch.packages.${system}.default }
$ anubis-fetch [flags] URL| Flag | Meaning |
|---|---|
--text |
render readable plain text instead of HTML |
--timeout MS |
per-step timeout in milliseconds (default 30000) |
--ua STRING |
override the User-Agent |
--browser |
skip the solver; go straight to the headless browser |
--no-browser |
never use the browser; exit 3 if the solve can't apply |
--no-cache |
don't read or write the persistent cookie jar |
# HTML to stdout $ anubis-fetch https://lore.kernel.org/linux-mm/some-thread/T/ # readable plain text $ anubis-fetch --text https://lore.kernel.org/linux-mm/some-thread/T/ # lean/fast only β useful in scripts; exits 3 if it would need a browser $ anubis-fetch --no-browser https://example.com/ && echo "got it"
After a successful fetch, the auth cookie is written to
$XDG_CACHE_HOME/anubis-fetch/cookies/<host>.json (falling back to
~/.cache/...). The next run for that host is let straight through β no
proof-of-work, no browser β exactly like a browser revisit. Cookies obtained
via the browser fallback are saved too, so a subsequent run can take the fast
HTTP path. Use --no-cache to disable, or just delete the file to force a
re-solve.
Everything is wired through the flake:
$ nix develop # dev shell: go, gopls, chromium, treefmt $ go test ./... # unit tests (hermetic β no network) $ nix build # build the wrapped binary $ nix flake check # build + tests + formatting $ nix fmt # format Go + Nix via treefmt (gofmt + alejandra)
The proof-of-work implementation is pinned against Anubis' own published test
vector (sha256("hunter" + "0")) so a drift in the hash construction fails a
unit test rather than silently returning garbage.
- TecharoHQ/anubis β the bot-wall this politely negotiates with.
- imroc/req β the HTTP client whose Chrome impersonation clears passive Cloudflare fingerprinting.
- chromedp/chromedp β drives the headless-browser fallback.