Files
serverAppConfigs/serverAppConfigs-docs/FrankBook-Pro/qbittorrent.md
T
Frank a5fe045b58 docs(vault): one fact, one owner — repair the doubled pages, single-source the facts
Structural repair:
- 13 pages were the same document twice (each exactly 2x its HEAD size),
  a second frontmatter block whose closing fence had been glued onto a body
  line by a frontmatter rewrite that re-read its own output. Un-doubled by
  cutting at the second frontmatter and keeping the copy whose body is a
  superset; the orphan census reports 0 lost tokens against HEAD.
- log.md: one entry per date, bare `## YYYY-MM-DD` headings, newest first —
  66 dated headings merged into 34, titles demoted to bold leads, every
  content line verified present. The file had been descending at the head
  and ascending at the tail.

Ownership moves:
- New `LLM/frank-medium.md` is the locator for the retired Dankserver
  endpoint: era windows quoted from each era page's own banner, no numbers
  restated, and the two unresolved date conflicts recorded instead of
  silently resolved. `frank-archon.md` links to it instead of restating the
  pre-rename window.
- The four Pipeline-Parallelism stubs (frontmatter only, no body) are gone;
  `Multi-Node Runtimes.md` owns runtime construction, launch, mixed-vendor
  verdicts and cross-split spec decode, and the four links that pointed at
  the stubs were repointed at it or at the drafter page.
- Authentik's three stub pages fold into `Authentik.md`, with the process
  detail and the outpost reference split out so the parent stays under the
  length hint.

Verified corrections (host census + live probe, 2026-10-19):
- The MXFP4 checkpoint two pages called the "on-disk rollback" is gone:
  /home/dank/models/ holds StillDeadcode, rad and tpl.jinja. Stated once, on
  the era page that owns that payload; the swap page now links to it.
- frank-medium-radiance-mxfp4: the 4.24/7 accept rate belongs to the DFlash2
  arm measured on the FP8 target, not to the MXFP4 era; the 3-family soak run
  census is kept once (155 requests) instead of twice (~120 and 155).
- Fleet shapes re-confirmed against the live endpoints before touching any
  page: 3x200000 fp8 KV, 2x200704, 2x262144.

Index rows are regenerated, not hand-carved. Net -1,849 vault lines.
2026-10-19 05:00:43 -07:00

9.2 KiB
Raw Blame History

type, title, description, tags, timestamp
type title description tags timestamp
reference qBittorrent The download client for the *arr stack: pinned image, shared WireGuard netns, WebUI credential recipe.
qbittorrent
arr-stack
frankbook-pro
gluetun
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):

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:

[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 via 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:

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:

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:

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