Files
felhom.eu/documentation/audits/qbittorrent-2026-10-12
admin c4d1b0ff98
gates / gates (push) Successful in 6m27s
Website: the two download clients on the app page; the campaign's evidence; R-988 filed
The catalog published qBittorrent and Transmission today (app-catalog 9081a47). Here:

- two cards in the media category on both language sets, each with the project's own logo,
  each saying what a closed peer port costs (slower swarms, no incoming peers)
- the published count 58 -> 60 on every page that states it, FAQ JSON-LD with its visible twin
- documentation/audits/qbittorrent-2026-10-12/: both bench runs, both box walks and verdicts,
  verify-fixes.json, teardown-newapps.json, and the README brought to its final state
- the 2026-09-30 walk harness: a `type: path` deploy field is filled with the drive ROOT —
  the controller refuses a path inside a drive (409), so the walker could not install any app
  with a drive field at all
- R-988: the Usenet pair (SABnzbd, NZBGet), operator request, not started
- STATUS.md and REPORT-newapps-2026-10-12.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
2026-10-12 21:03:11 +02:00
..

Download clients for the catalog — qBittorrent and Transmission (2026-10-12)

Why. Radarr, Sonarr and Seerr have been in the catalog for months and none of them downloads anything — they decide what to fetch and hand it to a download client. There was no download client. Radarr's own template said so in its prerequisites („Letöltő kliens szükséges (pl. qBittorrent — külön telepítendő)") while the website promised „Automatikus film letöltő… a filmek automatikusan a könyvtáradba kerülnek". Operator ruling, 2026-10-12: add them.

Where this run got to. Both are PUBLISHED — app-catalog-felhom.eu 9081a47, the two templates and their two complete onboarding records in one commit, with the README and FIRST-ADMIN rows, the fixtures for both venues and the website cards. Each was measured twice before that: on the bench (LXC 9401 on demo-hp) and on scratch guest 9202 through the product.

What is in here

bench/ qBittorrent on LXC 9401: image, telemetry, entrypoint, first admin, storage, ownership, health, memory, a real download, the config as the app rewrote it, and MV-qbittorrent/ — the harness verdict for the ladder step
transmission-bench/ the same twelve questions asked of Transmission, plus MV-transmission/
box/ guest 9202 through the product: the two box walks, the per-app box verdicts under qbittorrent/ and transmission/, the app logs, verify-fixes.json (the proof the defects below are gone) and teardown-newapps.json
tools/ boxwalk_newapps.py (deploy + stranger + sign-in + lock-out through traefik) and verify_fixes.py (the re-measure). Both reuse the 2026-09-30 harness for the session, CSRF and setup gate

Six defects, each found by measuring and fixed before publication

  1. qBittorrent was unreachable on its own public name. WebUI\ServerDomains with a comma list makes the app answer 401 „Invalid Host header" to everything, its own name included; semicolons work through the API (the four-way test: comma 401/401, single 200/401, semicolon 200/200, * 200/200). Nothing upstream documents the separator.
  2. …and the config file's own dialect drops the extra entries anyway. A semicolon list seeded into qBittorrent.conf comes back from the app's first read-write cycle as the public name alone, after which the controller's health probe — which dials by container name — is refused („Invalid Host header … 'qbittorrent:8080'"). The list moved into after_install, where the API honours it, and the marker is printed only after both the list and the UPnP flag are read back.
  3. Both clients asked the household's router to open a port. qBittorrent logs UPnP/NAT-PMP support: ON and tries a mapping; Transmission ships "port-forwarding-enabled": true. The box opens no port at home (../architecture/01-topology-and-trust.md §7), so both are off now. qBittorrent's seeded Session\UseUPnP=false is written and then ignored at first start — the app reported "upnp": true with that very line in its config — so it goes through after_install, which the app does honour: ON at 17:21:51, OFF at 17:21:56 (box/verify-fixes.json).
  4. Transmission could never become healthy. It answers 401 on every path once authentication is on, so curl -f always failed (exit 22). The probe accepts 200 or 401 now, with a negative control that still fails.
  5. Transmission's host whitelist refused its own healthcheck (400 to Host: 127.0.0.1). The local names are admitted beside the public one.
  6. Two fixtures were reading the wrong thing. The box fixture looked for a cookie named SID; qBittorrent 5.x names it QBT_SID_<port>. The Transmission fixture read the session id with an awk that matched the header NAME on a second line and so got 400 on every RPC; it uses an anchored sed now, with the payload on stdin.

And one finding that was not a defect. The first box walk looked like a household lock-out. Stranger and household ran from the same machine, so a shared ban could not be told from a per-visitor one. Re-run from two addresses: the stranger is banned (401 401 401 401 401 403, and 403 on the right password) while the household signs in at the same instant (204). qBittorrent reads the real visitor address through the proxy headers; its log names 192.168.0.180, not traefik. Transmission does not ban at all.

Does a download work with no inbound port? Measured, yes

The operator's question. With port mapping off and neither peer port published, each client was given one real torrent (Big Buck Bunny, Blender Foundation, CC BY) through its own interface:

finished where the files landed
qBittorrent yes, inside a minute — progress: 1, downloaded: 276610512 …/media/torrents/Big Buck Bunny/ (bench/09-download.txt)
Transmission yes — percentDone: 1.0, status: 6 (seeding) …/media/transmission/complete/Big Buck Bunny/ (transmission-bench/05-download.txt)

Both files are the completed-state snapshots, which is why their peer counts read 0: the swarm work was over. The tunnel is not involved either way — Cloudflare carries the app's web page inbound, while peer traffic goes out through the household's own router by NAT. What a closed port costs is incoming peers (slower on sparse swarms, less upload), and the app text now says so.

State left behind — three layers, verified

  • Machine (guest 9202): both apps removed through the product (POST …/stop then …/remove with the drive data), deployed: false for both, the two config volumes gone, no container left, and the two download folders deleted from the scratch drive by name. The guest's standing apps (filebrowser, paperless) are untouched and running. box/teardown-newapps.json.
  • Catalog pointer: the controller is back on the live catalog from this run's own saved copy (controller.yaml.pre-newapps1012), the cache removed with it (R-615 — repo_url alone is inert), and the restoration is proven from a different channel than the controller's own verdict (R-286): the sha256 of qbittorrent, transmission and grocy's .felhom.yml on the box equals the sha256 of the same files at the published origin/main, with a decoy pair that must differ and does. The config names the drill repo zero times.
  • Host (demo-hp): bench LXC 9401 has no containers and no volumes and is stopped, as it was found. The drill catalog admin/app-catalog-drill is reset to the live main (9081a47), so it carries the published templates and no work in progress.
  • Hub: nothing to undo — 9202 reports to no hub by its own configuration (the config header says hub reporting OFF, no tunnel, no agent local API).