2026-08-08 The Indexer Prowlarr Never Asked FlareSolverr About

What I set out to do

Explain a Sonarr health warning. IndexerLongTermStatusCheck was reporting two indexers unavailable for more than 6 hours: 1337x and The Pirate Bay, both via Prowlarr.

What I actually did

Peeled it back a layer at a time, and was wrong at nearly every layer before being right. The final answer is one line, and I reached it last.

1337x has banned our IPv4 address. Our browser reaches it over IPv6. Docker’s bridge network is IPv4-only. That is the entire difference.

The Pirate Bay was transient: it failed a batch test but passed an isolated retest. TokyoTosho is separately throwing constant 522s, which is the site being down. That left 1337x as the only real failure.

1337x returns Cloudflare Error 1006, “Access denied”, which explicitly names the banned address: has banned your IP address (100.8.27.177). That is our IPv4. The site never banned the IPv6.

The measurement that settled it, identical client, only the address family varying:

clientIPv6IPv4
plain curlCHALLENGEBLOCK-1006
curl_cffi chrome131CHALLENGEBLOCK-1006
curl_cffi firefox133CHALLENGEBLOCK-1006

macOS prefers IPv6 via Happy Eyeballs, so Chrome and Firefox get the ordinary “Just a moment” challenge and solve it. The stack’s network has EnableIPv6=false on subnet 172.20.0.0/16, and containers have no IPv6 egress at all, so FlareSolverr, Prowlarr, and everything else arrive on the banned IPv4 and get 1006.

The knock-on effect explains the silent failure. A 1006 page is not a recognized Cloudflare challenge, so Prowlarr’s CloudFlareDetectionService never fires and never calls FlareSolverr for 1337x at all: zero incoming requests during an indexer test. The solver layer is bypassed entirely, which is why swapping FlareSolverr for Byparr or Trawl changes nothing. Prowlarr #2672 reports the same silence, closed by a maintainer with “1337x has been banning people using prowlarr.”

What was striking

I built a confident, elaborate, and completely wrong theory out of a confound.

Comparing “plain curl gets challenged, browser-fingerprint clients get blocked,” I concluded Cloudflare was hard-blocking TLS impersonators while merely challenging honest non-browser clients. It was a tidy story with a plausible mechanism. It was an artifact: host curl was going out over IPv6 and every containerized client over IPv4. I was measuring the address family and calling it a fingerprint.

What should have caught it sooner was already sitting in front of me. The user said their browser loaded the site fine, and I had “proved” that browser TLS fingerprints get hard-blocked. Their working browser directly contradicted my rule and I wrote the contradiction into a summary without noticing. The tell for a bad theory was in the data I already had, not in the data I kept collecting.

The controls I did run were good ones and all passed: I ruled out User-Agent, FlareSolverr’s chromedriver patches, arm64, LANG, stale impersonation profiles, HTTP headers, and time-varying escalation from my own test traffic. None of that mattered, because I never varied the one thing that did. Careful controls on the wrong axis buy nothing.

Second wall worth remembering separately, documented at guyg2232/byparr-proxy: even when a solver succeeds, Prowlarr discards the returned HTML and re-fetches with its own .NET HttpClient, which gets re-challenged. That is a real and independent problem, and it may still bite once IPv6 gets us past the ban.

Sits with 2026-05-16 Media Stack Download Pipeline Recovery and Homelab Services Architecture. The 1337x indexer is created in setup-prowlarr.sh, not Terraform, which is also where Persist Prowlarr Indexer Priorities in setup-prowlarr.sh operates. Candidate fix is enable_ipv6 on the compose network rather than dropping the indexer. No stack changes were made; every test ran in throwaway containers.

The fix, and what it cost

Enabling IPv6 on the compose network works, verified end to end. FlareSolverr went from 1006 to Challenge solved! with a real 26 KB page of torrent rows, and Prowlarr’s 1337x indexer test returned HTTP 200 for the first time. Five of six indexers pass; TPB is flaky in batch runs and passes in isolation, which it also did before any of this.

Two traps on the way.

The prefix must not be a ULA. With fd00:1337::/64, Chromium still got “Access denied”: RFC 6724 ranks ULA (fc00::/7, precedence 3) below IPv4-mapped (35), so containers fall back to v4 and land back on the banned address. curl picked IPv6 anyway, which made the first test look like a success and hid the problem. On a global-scope prefix (matching ::/0, precedence 40) Chromium got “Just a moment”. Same image, same network, opposite verdicts, one line of config apart.

Dual-stack silently broke qBittorrent. Containers then prefer IPv6, so Sonarr reached qbittorrent at 2001:db8:1337::3, and the WebUI allowlist was v4-only. All download clients went unavailable. The compose file’s own comment had warned that the subnet is a pinned invariant qBittorrent depends on, and I enabled a second family without extending the allowlist to match.

Then I made it worse by patching the generated qBittorrent.conf in the deploy dir instead of the Nix source. qBittorrent rewrote the file on restart and the edit vanished, which is precisely the failure mode AGENTS.md warns about, and the user caught it before I did. The real change is a subnet6 module option threaded through flake.nix secrets.nix placeholderCtx generate.nix, with a switch-time guard mirroring the v4 one (the v6 subnet is the second - subnet: line, so the existing head -n1 guard keeps working untouched). nix flake check passes and the generated conf now carries both families.

Deploy dir is reverted to the known-good IPv4-only state so downloads keep working; the change lands atomically on the next home-manager switch, which also needs subnet6 set in the consumer config since it has no default, by the same deliberate fail-closed convention as subnet.

The lesson worth keeping: a shortcut on a generated file is not faster, it is just a change that silently reverts.

Landed as media-stack#50, merge commit 7e42ef4, all four checks green. The automated review found no correctness bugs but flagged two real portability assumptions (host needs outbound IPv6; Docker Engine 27+ for automatic ip6tables NAT), now documented in a follow-up commit, plus one deferred nit: both compose-subnet guards match positionally (head -n1, sed -n 2p), so a future second networks: block would silently match the wrong line. Worth scoping to the media-stack-net: block eventually.

One trap on the consumer side: .config’s flake.lock still pinned media-stack to the pre-merge commit, so setting subnet6 there would have failed the switch with “option does not exist” until nix flake update media-stack bumped it. Adding a required option is a three-part change, not two: module, consumer config, and the lock.

The second wall, confirmed on a different indexer

1337x ended up fully fixed: Prowlarr tests it green, and Prowlarr has cached a real cf_clearance cookie for it. Making qBittorrent.conf module-managed along the way turned up a latent bug worth more than the tidying: the live instance carried a whole tuning profile (queueing on with 20 active slots, 32 async I/O threads, 8 hashing threads, 2 GiB working set, 500 file pool) that existed only in qBittorrent’s own copy of the file. The generator never knew it. Because the seed is if-absent, any re-seed would have silently reverted downloads to the stock 3/5/3 queue, and a fresh host would never have had it. Now declared in generate.nix (#51).

The Pirate Bay then demonstrated the other wall, the one I had flagged as theoretical. Its search fails with CloudFlareProtectionException on apibay.org. Clearing the backoff worked mechanically (Sonarr went fully clean, no health issues) and then re-escalated within one search cycle, which is how you tell a stale counter from a live failure.

The mechanism is the mirror image of 1337x’s, and the asymmetry is the interesting part. FlareSolverr fetches apibay perfectly - HTTP 200, valid JSON, “Challenge not detected!“. But precisely because its browser is never challenged, it mints no cf_clearance. Prowlarr throws away the body it was handed, re-requests with its own .NET client, and that client is challenged, with no cookie to carry. 1337x escapes it only because its challenge does mint a cookie, which Prowlarr caches. So the solver succeeding is not sufficient; the solver has to be challenged for Prowlarr’s replay model to work at all.

Also worth remembering: apibay is challenged over both address families, so this is genuinely unrelated to the IPv6 work, and it was in the very first testall of the session, before any changes. Resisting the urge to “fix” it by clearing the counter again is the right call - that treats the symptom and re-hides the cause.