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:
| client | IPv6 | IPv4 |
|---|---|---|
plain curl | CHALLENGE | BLOCK-1006 |
| curl_cffi chrome131 | CHALLENGE | BLOCK-1006 |
| curl_cffi firefox133 | CHALLENGE | BLOCK-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.
Related
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.