The vault was 155 pages / 31,210 lines, most of it measurement ledgers for payloads that no longer run. A throughput figure for a runtime nobody operates is not a record: AI moves faster than the vault, so the number is wrong before anyone reads it, and a wrong number in a runbook gets believed. Per the new purpose test in `AGENTS.md` → *Documentation Policy*, it is destroyed. Gone (`.AGENTTRASH/20261019-destruction/`, KILL-LIST.txt is the manifest): - the `frank-medium` endpoint and its five era pages, `frank-npu`, FastFlowLM, the ikllama/kingjones/dealign/frank-redo eras, the ling/swift15/freetoken/ubatch campaign pages, qwen3.8 spec-matrix and strix-optimizations, the two "Running Qwen3.6 …" pages; - the whole `Pipeline Parallelism/` folder (no live script passes a pipeline flag) and the multi-device/x4-pair/hybrid/offload experiment pages; - every vLLM page — no vLLM endpoint exists on this fleet; - `LLM/deployment.md` (`Architecture/Deployment Flow.md` is the test-referenced owner) and the external surveys (SGLang, Lemonade, llama.cpp forks/diff, self-hosted app census, the TTS survey) that were obsolete by construction; - `log.md`, 2,380 lines. Git holds the chronology; a causal law belongs to the page that owns the thing it governs, never to a diary. Survivors: 101 pages, 17,542 lines, 0 OKF violations, 0 unresolved links. Eras tables keep the window, payload, runtime, reason and rollback pointer; the rate columns are blanked. The live shapes stay verbatim — those numbers are what the deployer and `models.yml` consume.
202 lines
9.1 KiB
Markdown
202 lines
9.1 KiB
Markdown
---
|
||
type: reference
|
||
title: qBittorrent
|
||
description: "The download client for the *arr stack: pinned image, shared WireGuard netns, WebUI credential recipe."
|
||
tags: [qbittorrent, arr-stack, frankbook-pro, gluetun]
|
||
timestamp: 2026-09-03T00:00:00Z
|
||
---
|
||
|
||
# qBittorrent
|
||
|
||
qBittorrent is the torrent download client for the *arr stack. The stack runs on
|
||
**FrankBook-Pro** (the Mac mini) as quadlet units inside its podman machine —
|
||
not on Dankserver. It receives download requests from Sonarr, Radarr, Lidarr,
|
||
and Readarr via category tagging.
|
||
|
||
**Host:** FrankBook-Pro (podman machine) — **Access:** WebUI on port `8080`
|
||
published by the [[Gluetun]] unit because qBittorrent shares that container's
|
||
network namespace (a netns-sharer publishes no ports of its own).
|
||
|
||
## Facts
|
||
|
||
| Item | Value |
|
||
|------|-------|
|
||
| Image | `lscr.io/linuxserver/qbittorrent:4.6.7` — **pinned**, see the pin rationale below |
|
||
| Network | `Network=gluetun.container` (torrent egress exits via the WireGuard/ProtonVPN tunnel) |
|
||
| Ports | 8080 (WebUI) and 6881 TCP+UDP published on the gluetun unit |
|
||
| Env | `PUID=0`, `PGID=0`, `TZ=America/Los_Angeles`, `UMASK_SET=022` |
|
||
| Volumes | `/Volumes/COMPLETE/qbittorrent-data:/config`, plus `/Volumes/COMPLETE` and `/Volumes/INCOMPLETE` mounted at identical paths |
|
||
| Auth | Web UI username/password (`admin` + generated password); LAN bypass not used |
|
||
|
||
## Why the 4.6.7 pin exists (2026-09-01)
|
||
|
||
Readarr's abandoned qBittorrent client rejects qB ≥ 5's **204 empty-body login
|
||
success** as `Authentication failure` — a runtime break, not just an API-create
|
||
issue. The fleet pins `lscr.io/linuxserver/qbittorrent:4.6.7` while Readarr is
|
||
in the stack. **Never unpin to `latest` while Readarr is in the fleet.**
|
||
|
||
Second trap on any version flip: qB 5.x's shutdown conf rewrite leaves a
|
||
`WebUI\Password_PBKDF2=@ByteArray(...)` line that 4.6.7 rejects — every login
|
||
then returns "invalid credentials" (the arrs included). Fix = rewrite the same
|
||
conf line with the same password while qB is stopped.
|
||
|
||
## Persistent WebUI credentials (verified 2026-08-30)
|
||
|
||
qBittorrent 5 mints a **new temporary password at every start** unless a
|
||
password is stored in `qBittorrent.conf`. That rotation silently breaks every
|
||
arr download client after each reboot. The Web-API has no password endpoint
|
||
any more (`/api/v2/app/setPassword` was removed), and the linuxserver image
|
||
has no `WEBUI_AUTH` handling — so persistence must be written to the conf
|
||
directly, in qb's own `salt:hash` format (`src/base/utils/password.cpp`:
|
||
PBKDF2-HMAC-SHA512, 100 000 iterations, 16-byte salt = 4×uint32, then
|
||
`base64(salt) + ":" + base64(hash)` as the raw value):
|
||
|
||
```python
|
||
import base64, hashlib, os
|
||
salt = os.urandom(16)
|
||
h = hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 100000, 64)
|
||
val = (base64.b64encode(salt) + b":" + base64.b64encode(h)).decode()
|
||
# with the container STOPPED, in [Preferences]:
|
||
# WebUI\Username=admin
|
||
# WebUI\Password_PBKDF2=<val>
|
||
```
|
||
|
||
Then `podman start qbittorrent` and confirm login survives a
|
||
`podman restart` — the temp-password rotation stops the moment stored
|
||
credentials verify. The chosen value lives host-local in `/home/dank/.qbpass`
|
||
(mode 600, owner `dank`) — never in git; every arr download client and
|
||
[[Prowlarr]] reference it. (The classic `@ByteArray(salt+hash)` form is WRONG
|
||
for qb5: login 401s on it; verify() base64-decodes the two halves separately.)
|
||
|
||
## Deployment (current)
|
||
|
||
Quadlet unit `qbittorrent.container` from this repo
|
||
(`frankbook-pro/qbittorrent/quadlet/`), installed inside the podman machine at
|
||
`/home/core/.config/containers/systemd/qbittorrent.service` by the
|
||
frankbook-pro handler in `scripts/deploy-remote.sh`:
|
||
|
||
```ini
|
||
[Container]
|
||
ContainerName=qbittorrent
|
||
Image=lscr.io/linuxserver/qbittorrent:4.6.7
|
||
Network=gluetun.container
|
||
Environment=PUID=0
|
||
Environment=PGID=0
|
||
Environment=TZ=America/Los_Angeles
|
||
Environment=UMASK_SET=022
|
||
Volume=/Volumes/COMPLETE/qbittorrent-data:/config
|
||
Volume=/Volumes/COMPLETE:/Volumes/COMPLETE
|
||
Volume=/Volumes/INCOMPLETE:/Volumes/INCOMPLETE
|
||
|
||
[Service]
|
||
TimeoutStartSec=900
|
||
Restart=always
|
||
```
|
||
|
||
## Gotchas (verified live)
|
||
|
||
- Temporary-password rotation on every boot is the default state of an
|
||
untouched qb5 — see the persistence recipe above; without it, all arr
|
||
download clients silently fail queue adds ("Couldn't add release … to
|
||
download queue" in the arr logs) after any restart.
|
||
- API login from `curl` needs a `Referer: http://<host>:<port>/` header
|
||
(CSRF protection) or POSTs get rejected even with a valid SID.
|
||
- Cookie-name quirk by version: qB 5.x names its WebUI session cookie
|
||
`QBT_SID_<port>`; 4.6.7 uses plain `SID`. Any automation must parse the
|
||
cookie generically (e.g. `grep -oE '[A-Za-z_]*SID[A-Za-z0-9_]*=[^;[:space:]]*'`)
|
||
instead of hard-coding a port-suffixed name, or it fails with 403 cascades
|
||
that never reach qB as failed logins.
|
||
- Egress verification for this stack must run **inside the gluetun container**
|
||
(`podman exec gluetun wget -qO- https://ipinfo.io/json`); running the same
|
||
probe one ssh-context out returns the residential IP — a false leak alarm.
|
||
- Ensure umask is set to `022` so downloaded files are readable by arr apps.
|
||
|
||
## E2E verification after any pipeline change
|
||
|
||
Run after ANY change to the torrent pipeline — WireGuard credential rotation,
|
||
qB version change, gluetun reconfig, category re-seeding. Proves the full
|
||
grab path (tunnel egress → qB download → category save path → clean
|
||
removal). All commands from a dev box via `ssh frankbook-pro`; inside the
|
||
Mac shell export `PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"` for
|
||
`podman machine ssh`.
|
||
|
||
**Access note (2026-09-21):** the Mac's login user is **dank**@192.168.0.112
|
||
(NOT core — `core` is the podman-machine VM user, reached only
|
||
`podman machine ssh`). blankpad has key auth for `dank@` (bootstrapped
|
||
2026-09-21) and a `frankbook-pro` alias in `~/.ssh/config`. macOS sshd has a
|
||
low MaxAuthTries: when password-bootstrapping use `-o
|
||
PreferredAuthentications=password -o PubkeyAuthentication=no
|
||
-o IdentitiesOnly=yes` or the key offers exhaust the attempts before the
|
||
password is tried.
|
||
|
||
**1. Egress gate — MUST run inside gluetun:**
|
||
|
||
```bash
|
||
podman machine ssh 'podman exec gluetun wget -qO- https://ipinfo.io/json'
|
||
```
|
||
|
||
Require `org` = AS9009 (M247 — ProtonVPN Panama PA-12 config); judge by org,
|
||
never country alone. NEVER run this one ssh-context out (bare `wget` on the
|
||
Mac host): it silently returns the residential Comcast IP and reads as a
|
||
hard-gate leak that does not exist. A residential reading ⇒ re-run inside
|
||
the container before diagnosing.
|
||
|
||
**2. Login — generic cookie parse.** The QPW fetch is flaky (~1 in N returns
|
||
empty); assert length before use — an empty-password run once produced a
|
||
Forbidden loop that cost hours:
|
||
|
||
```bash
|
||
QPW=$(podman machine ssh 'cat /home/core/Containers/qbittorrent/env' | grep QBIT_WEBUI_PASSWORD | cut -d= -f2)
|
||
[ ${#QPW} -eq 20 ] || exit 1
|
||
SID=$(curl -si --data-urlencode "username=admin" --data-urlencode "password=$QPW" \
|
||
http://127.0.0.1:8080/api/v2/auth/login \
|
||
| grep -oE '[A-Za-z_]*SID[A-Za-z0-9_]*=[^;[:space:]]*' | head -1)
|
||
curl -b "$SID" … # pass the WHOLE match
|
||
```
|
||
|
||
WebUI from the LAN: `http://192.168.0.112:8080` (published on the gluetun
|
||
unit). Username `admin`; password = the machine-local `QBIT_WEBUI_PASSWORD`
|
||
(20 chars, generated once at provisioning by the deploy handler — never in
|
||
the repo). Parse-bug 403s never reach qB as failed logins — no ban accrues;
|
||
server-side truth is `/Volumes/COMPLETE/qbittorrent-data/qBittorrent/config/qBittorrent/logs/qbittorrent.log`
|
||
(`WebAPI login success/failure` + source IP). Repeated REAL failures accrue
|
||
toward qB's 5-failures/3600 s IP ban.
|
||
|
||
**3. Add a legal test torrent.** Blender open-movie torrents (~300 MB, e.g.
|
||
Big Buck Bunny) via `POST /torrents/add` with the raw `.torrent` file
|
||
(`-F "file=@…"`) and `category=Finished Torrents`. Never leave a real-looking
|
||
grab behind.
|
||
|
||
**4. Poll and verify.** Poll `GET /torrents/info` (~20 s): state
|
||
`downloading` → `moving` (INCOMPLETE→COMPLETE move) → `stalledUP`/`uploading`
|
||
at progress=1. `save_path` must be `/Volumes/COMPLETE/Finished Torrents`
|
||
(qB 4.x auto-creates unknown categories at DefaultSavePath; 5.x did NOT
|
||
inherit category paths — one more reason for the 4.6.7 pin). Confirm on
|
||
disk: `ls "/Volumes/COMPLETE/Finished Torrents/"`. Throughput ~17 MB/s is
|
||
normal for the tunnel.
|
||
|
||
**5. Cleanup — delete only your artifact:**
|
||
|
||
```bash
|
||
curl -s -o /dev/null -w "%{http_code}" -b "$SID" \
|
||
--data-urlencode "hashes=<literal-hash>" \
|
||
--data-urlencode "deleteFiles=true" \
|
||
http://127.0.0.1:8080/api/v2/torrents/delete # expect 200
|
||
```
|
||
|
||
Target the known hash literally — the user's real grabs share the directory
|
||
and MUST be untouched. Verify: hash absent from `/torrents/info`, your dir
|
||
gone, all other entries intact.
|
||
|
||
**Version-coupling constraints:** qB is pinned to 4.6.7 (Readarr's EOL
|
||
client rejects qB ≥ 5's `204` login at runtime — see the pin rationale
|
||
above). Do not unpin while Readarr exists. After ANY qB version flip,
|
||
regenerate the `WebUI\Password_PBKDF2` line fresh (same password, 100k
|
||
iterations) — a 5.x-written hash line makes 4.6.7 reject every login.
|
||
Rewrite while qB is stopped.
|
||
|
||
## See Also
|
||
|
||
- [[ARR-Stack]] — full *arr stack documentation
|
||
- [[Gluetun]] — the WireGuard tunnel this container shares
|