All ~173 managed skills read in full and merged, one polished reference page per skill-cluster: - LLM cross-cutting (17 pages): GGUF Compatibility, Llama Cpp Serving Sizing, Llama Cpp Multi-Device Splits, PCIe Link and Atomics, RDNA4 Container Serving, RDNA4 MoE Expert Offload VRAM Budget, vLLM Boot Forensics, vLLM Porting and Compile Triage, LLM Bench Windows, LLM Performance Diagnosis, Model Swap and Rollback, Tcclaviger Fork Ops, Heterogeneous Split Triage, FreeToken Builds + deltas folded into the retired runbooks (GLM DFlash2 port laws, Lucebox dual-R9700, R9V qualification pipeline, FLM pack registration, ik hybrid envelope, frank-medium split rebalance, llama.cpp GGUF preflight, batch-ubatch split section, bench-window D-state/stale-read notes, second-endpoint bring-up). - Architecture (3): Stale Signal Audit, Quadlet Lifecycle, Deploy State Triage. - Operations (6 + index): Remote Job Runner, Payload and Disk Reclaim, Container Host Reachability, Fleet Ops Playbook, Fleet Audit and Posture, SSH and Remote Access. - Infrastructure: Observability Provisioning + Homepage Status Probes; updates to Authentik, Anubis, LAN-DNS (per-client DoH bypass, Porkbun zone ops), Swap Configuration, OpenWebUI, Hermes (pre-existing page preserved and extended). - FrankBook-Pro: ARR-Stack filled (API split, schema-first probing, auth-posture, auth-config trap); qBittorrent E2E verification. - Desktop/Agent/Research: Noctalia and Niri, OMP Harness, Agent Docs and Edits, Command Center Dotfiles, PICO-8 Asset Standards. - Indexes: LLM Cross-Cutting References section, Operations/ and Agent/ index pages, root-index Skill Distillation section, AGENTS.md pointer. - log.md carries the wave plan. check-vault-links.py 0 unresolved (185 pages); ./tests/run.sh 366/366.
202 lines
9.2 KiB
Markdown
202 lines
9.2 KiB
Markdown
---
|
||
type: reference
|
||
title: qBittorrent
|
||
description: qBittorrent download client for the *arr stack — pinned 4.6.7 on FrankBook-Pro, shares the Gluetun WireGuard netns, PBKDF2 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 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:**
|
||
|
||
```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
|