Files
felhom.eu/documentation/audits/qbittorrent-2026-10-12/README.md
T
adminandClaude Opus 5 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

89 lines
6.8 KiB
Markdown

# 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).