diff --git a/documentation/audits/qbittorrent-2026-10-12/README.md b/documentation/audits/qbittorrent-2026-10-12/README.md new file mode 100644 index 00000000..e49f7b0f --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/README.md @@ -0,0 +1,75 @@ +# 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 templates are written and **measured twice** — on the bench (LXC 9401, +demo-hp) and on the scratch guest 9202 **through the product**. Neither is published: the onboarding +records are not complete, and `catalog_gates.py` refuses a new template without one. + +## 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 | +| `transmission-bench/` | the same twelve questions asked of Transmission | +| `box/` | guest 9202 through the product: the two box walks, the app logs, and `verify-fixes.json` — the proof the defects below are gone | +| `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 | + +## Five defects, each found by measuring and fixed + +1. **qBittorrent was unreachable on its own public name.** `WebUI\ServerDomains` takes **semicolons**; + with a comma list the app answers 401 „Invalid Host header" to everything, its own name included. + Nothing upstream documents the separator — measured through the app's own API + (`box/verify-fixes.json`, and the four-way test: comma 401/401, single 200/401, semicolon 200/200, + `*` 200/200). +2. **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 (`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**. +3. **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. +4. **Transmission's host whitelist refused its own healthcheck** (400 to `Host: 127.0.0.1`). +5. **The first box walk looked like a lock-out and was not one.** 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 + +Operator's question. With UPnP off and the port confirmed closed (`"port-is-open": false`): + +| | after 30 s | peers | finished | +|---|---|---|---| +| qBittorrent | 98 % at **4.0 MB/s** | 34 seeds | yes, inside a minute | +| Transmission | 96 % | 48 connected, 43 sending | yes | + +The tunnel is not involved: 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 and says the household can switch +it on themselves. + +## What is still open before these can be published + +- **2.5 / 2.8** — a backup and a restore through the product, data read back (the dawarich route: + the update's per-app backup → remove keeping backups → restore). +- **6.1–6.3** — an upgrade fixture, and a first ladder step proven on bench + 9202 + (`upgrade-test.py --write-ladder`). +- **8.3–8.5** — logo, screenshots, the cards on both language pages, the app count 58 → 60, and the + three *arr descriptions that promise automatic downloading. +- **The two onboarding records**, which the gate requires complete before either template may land. + +## State left behind + +Guest 9202: both apps **removed** through the product (volumes gone, containers gone, the test +downloads deleted from the scratch drive), the controller **restored to the live catalog** from this +run's own saved copy, its cache rebuilt (60 templates, neither new app present). Bench LXC 9401: +deployments down with their volumes, **stopped** as it was found. The live catalog's `main` is +untouched (`3a6e930`). The two templates live in the private drill repo as work in progress. diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/01-image.txt b/documentation/audits/qbittorrent-2026-10-12/bench/01-image.txt new file mode 100644 index 00000000..7b1d0bd9 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/01-image.txt @@ -0,0 +1,7 @@ +# image + architectures (docker manifest inspect), 2026-10-12T15:42:37+00:00 +repo digest: qbittorrentofficial/qbittorrent-nox@sha256:92bfd78d731e254ba64f62b77b0696add6447a33642ee5feda106a26b285aa7f + "architecture": "386", "os": "linux" + "architecture": "amd64", "os": "linux" + "architecture": "arm", "os": "linux", + "architecture": "arm64", "os": "linux" + "architecture": "riscv64", "os": "linux" diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/02-telemetry-and-net.txt b/documentation/audits/qbittorrent-2026-10-12/bench/02-telemetry-and-net.txt new file mode 100644 index 00000000..e9697554 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/02-telemetry-and-net.txt @@ -0,0 +1,27 @@ +# preferences that reach the internet by themselves (read through the app's own API) + dht.transmissionbt.com:6881 +"alternative_webui_enabled":false +"alternative_webui_path":"" +"announce_ip":"" +"announce_port":0 +"announce_to_all_tiers":true +"announce_to_all_trackers":false +"anonymous_mode":false +"dht":true +"dht_bootstrap_nodes":"dht.libtorrent.org:25401 +"ip_filter_enabled":false +"ip_filter_path":"" +"ip_filter_trackers":false +"lsd":true +"max_concurrent_http_announces":50 +"pex":true +"reannounce_when_address_changed":false +"rss_auto_downloading_enabled":false +"rss_download_repack_proper_episodes":true +"rss_fetch_delay":2 +"rss_max_articles_per_feed":50 +"rss_processing_enabled":false +"rss_refresh_interval":30 +"rss_smart_episode_filters":"s(\\d+)e(\\d+)\n(\\d+)x(\\d+)\n(\\d{4}[.\\-]\\d{1 +"web_ui_https_cert_path":"" +"web_ui_https_key_path":"" diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/03-process-and-entrypoint.txt b/documentation/audits/qbittorrent-2026-10-12/bench/03-process-and-entrypoint.txt new file mode 100644 index 00000000..4ede123b --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/03-process-and-entrypoint.txt @@ -0,0 +1,113 @@ +# the running process (production binary, dropped to qbtUser) +PID USER COMMAND + 1 root /sbin/tini -g -- /entrypoint.sh + 7 qbtUser qbittorrent-nox --confirm-legal-notice --profile=/config --torrenting-port=6881 --webui-port=8080 + 93 root ps -o pid,user,args + +# the image entrypoint, read inside the image +#!/bin/sh + +downloadsPath="/downloads" +profilePath="/config" +qbtConfigFile="$profilePath/qBittorrent/config/qBittorrent.conf" + +isRoot="0" +if [ "$(id -u)" = "0" ]; then + isRoot="1" +fi + +if [ "$isRoot" = "1" ]; then + if [ -n "$PUID" ] && [ "$PUID" != "$(id -u qbtUser)" ]; then + sed -i "s|^qbtUser:x:[0-9]*:|qbtUser:x:$PUID:|g" /etc/passwd + fi + + if [ -n "$PGID" ] && [ "$PGID" != "$(id -g qbtUser)" ]; then + sed -i "s|^\(qbtUser:x:[0-9]*\):[0-9]*:|\1:$PGID:|g" /etc/passwd + sed -i "s|^qbtUser:x:[0-9]*:|qbtUser:x:$PGID:|g" /etc/group + fi + + if [ -n "$PAGID" ]; then + _origIFS="$IFS" + IFS=',' + for AGID in $PAGID; do + AGID=$(echo "$AGID" | tr -d '[:space:]"') + addgroup -g "$AGID" "qbtGroup-$AGID" + addgroup qbtUser "qbtGroup-$AGID" + done + IFS="$_origIFS" + fi +fi + +if [ ! -f "$qbtConfigFile" ]; then + mkdir -p "$(dirname $qbtConfigFile)" + cat << EOF > "$qbtConfigFile" +[BitTorrent] +Session\DefaultSavePath=$downloadsPath +Session\Port=6881 +Session\TempPath=$downloadsPath/temp +Session\TempPathEnabled=true +[Meta] +MigrationVersion=9999 +[Preferences] +WebUI\Port=8080 +EOF +fi + +argLegalNotice="" +_legalNotice=$(echo "$QBT_LEGAL_NOTICE" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') +if [ "$_legalNotice" = "confirm" ]; then + argLegalNotice="--confirm-legal-notice" +else + # for backward compatibility + # TODO: remove in next major version release + _eula=$(echo "$QBT_EULA" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') + if [ "$_eula" = "accept" ]; then + echo "QBT_EULA=accept is deprecated and will be removed soon. The replacement is QBT_LEGAL_NOTICE=confirm" + argLegalNotice="--confirm-legal-notice" + fi +fi + +argTorrentingPort="" +if [ -n "$QBT_TORRENTING_PORT" ]; then + argTorrentingPort="--torrenting-port=$QBT_TORRENTING_PORT" +fi + +argWebUIPort="" +if [ -n "$QBT_WEBUI_PORT" ]; then + argWebUIPort="--webui-port=$QBT_WEBUI_PORT" +fi + +if [ "$isRoot" = "1" ]; then + # those are owned by root by default + # don't change existing files owner in `$downloadsPath` + if [ -d "$downloadsPath" ]; then + chown qbtUser:qbtUser "$downloadsPath" + fi + if [ -d "$profilePath" ]; then + chown qbtUser:qbtUser -R "$profilePath" + fi +fi + +# set umask just before starting qbt +if [ -n "$UMASK" ]; then + umask "$UMASK" +fi + +if [ "$isRoot" = "1" ]; then + exec \ + doas -u qbtUser \ + qbittorrent-nox \ + "$argLegalNotice" \ + --profile="$profilePath" \ + "$argTorrentingPort" \ + "$argWebUIPort" \ + "$@" +else + exec \ + qbittorrent-nox \ + "$argLegalNotice" \ + --profile="$profilePath" \ + "$argTorrentingPort" \ + "$argWebUIPort" \ + "$@" +fi diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/04-first-admin.txt b/documentation/audits/qbittorrent-2026-10-12/bench/04-first-admin.txt new file mode 100644 index 00000000..24e44dad --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/04-first-admin.txt @@ -0,0 +1,18 @@ +# first admin: class 1, seeded at first boot. Measured 2026-10-12T15:42:46+00:00 +## 1. no temporary password anywhere in the log (the whole log, after a restart): +0 +## 2. the generated password signs in (HTTP code): +204 +## 3. a wrong password is refused (body): +Unauthorized +## 4. an unauthenticated API call (HTTP code): +403 +## 5. lock-out after wrong tries (qBittorrent bans the address; code per try): +try 1: 401 +try 2: 401 +try 3: 401 +try 4: 401 +try 5: 403 +try 6: 403 +## 6. and the household, right after those tries, from the same address: +403 diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/05-storage.txt b/documentation/audits/qbittorrent-2026-10-12/bench/05-storage.txt new file mode 100644 index 00000000..f3e4e75a --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/05-storage.txt @@ -0,0 +1,33 @@ +# every path the app writes, and where it is mounted +Filesystem Size Used Available Use% Mounted on +/dev/loop2 58.8G 6.7G 49.0G 12% /downloads +/dev/loop2 58.8G 6.7G 49.0G 12% /downloads + +## /config (named volume, NVMe) — settings + resume data: +/config/qBittorrent/data/GeoDB/dbip-country-lite.mmdb +/config/qBittorrent/data/logs/qbittorrent.log +/config/qBittorrent/data/BT_backup/dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c.fastresume +/config/qBittorrent/data/BT_backup/dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c.torrent +/config/qBittorrent/data/BT_backup/queue +/config/qBittorrent/config/qBittorrent-data.conf +/config/qBittorrent/config/lockfile +/config/qBittorrent/config/watched_folders.json +/config/qBittorrent/config/rss/feeds.json +/config/qBittorrent/config/categories.json +/config/qBittorrent/config/qBittorrent.conf +8.1M /config + +## /downloads (the household's drive, bind mount): +total 16 +drwxr-xr-x 4 qbtUser qbtUser 4096 Oct 12 17:39 . +drwxr-xr-x 1 root root 4096 Oct 12 17:37 .. +drwxr-xr-x 2 qbtUser qbtUser 4096 Oct 12 17:39 .temp +drwxr-xr-x 2 qbtUser qbtUser 4096 Oct 12 17:39 Big Buck Bunny + +## anything written OUTSIDE a mount (the container's own writable layer): +A /tmp/c3 +A /tmp/ck +A /tmp/c +A /tmp/c2 +A /config +A /downloads diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/06-ownership.txt b/documentation/audits/qbittorrent-2026-10-12/bench/06-ownership.txt new file mode 100644 index 00000000..4fe25b07 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/06-ownership.txt @@ -0,0 +1,6 @@ +# file owner on both sides of the bind mount (PUID/PGID = 1000) +total 4 +drwxr-xr-x 2 1000 1000 4096 Oct 12 17:39 Big Buck Bunny +## host side: +total 4 +drwxr-xr-x 2 1000 1000 4096 Oct 12 15:39 Big Buck Bunny diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/07-health.txt b/documentation/audits/qbittorrent-2026-10-12/bench/07-health.txt new file mode 100644 index 00000000..2e98b76b --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/07-health.txt @@ -0,0 +1,9 @@ +# healthcheck +state: healthy +test: [CMD curl -fsS -o /dev/null http://127.0.0.1:8080/] interval=30s start_period=30s +## the tool the healthcheck uses, inside the image: +/usr/bin/curl +## negative control — the probe against a port nothing serves: +curl: (7) Failed to connect to 127.0.0.1:9999 after 0 ms: Could not connect to server +000 +curl exit: 7 diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/08-memory.txt b/documentation/audits/qbittorrent-2026-10-12/bench/08-memory.txt new file mode 100644 index 00000000..7ae7ff72 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/08-memory.txt @@ -0,0 +1,13 @@ +# memory, measured on this run +now: 17.04MiB / 1GiB 1.66% +cgroup peak (incl. page cache): 21 MiB +anon: 16 MiB +file: 0 MiB +oom_kill: +oom 0 +oom_kill 0 +oom_group_kill 0 +swap on the bench: + total used free shared buff/cache available +Mem: 10240 105 7956 0 2178 10134 +Swap: 0 0 0 diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/09-download.txt b/documentation/audits/qbittorrent-2026-10-12/bench/09-download.txt new file mode 100644 index 00000000..4c6a57e9 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/09-download.txt @@ -0,0 +1,28 @@ +# a real torrent, added through the app's own API and finished +"content_path":"/downloads/Big Buck Bunny" +"downloaded":276610512 +"name":"Big Buck Bunny" +"progress":1 +"save_path":"/downloads" +"state":"stalledUP" +"total_size":276445467 +## the file on the household's drive: +/opt/qbt-bench/userdata/media/torrents: +total 16 +drwxr-xr-x 4 1000 1000 4096 Oct 12 15:39 . +drwxr-xr-x 3 root root 4096 Oct 12 15:37 .. +drwxr-xr-x 2 1000 1000 4096 Oct 12 15:39 .temp +drwxr-xr-x 2 1000 1000 4096 Oct 12 15:39 Big Buck Bunny + +/opt/qbt-bench/userdata/media/torrents/.temp: +total 8 +drwxr-xr-x 2 1000 1000 4096 Oct 12 15:39 . +drwxr-xr-x 4 1000 1000 4096 Oct 12 15:39 .. + +/opt/qbt-bench/userdata/media/torrents/Big Buck Bunny: +total 269984 +drwxr-xr-x 2 1000 1000 4096 Oct 12 15:39 . +drwxr-xr-x 4 1000 1000 4096 Oct 12 15:39 .. +-rw-r--r-- 1 1000 1000 140 Oct 12 15:38 Big Buck Bunny.en.srt +-rw-r--r-- 1 1000 1000 276134947 Oct 12 15:39 Big Buck Bunny.mp4 +-rw-r--r-- 1 1000 1000 310380 Oct 12 15:38 poster.jpg diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/10-config-as-written.txt b/documentation/audits/qbittorrent-2026-10-12/bench/10-config-as-written.txt new file mode 100644 index 00000000..dd2ba618 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/10-config-as-written.txt @@ -0,0 +1,25 @@ +[BitTorrent] +Session\DefaultSavePath=/downloads +Session\Port=6881 +Session\QueueingSystemEnabled=true +Session\SSL\Port=34909 +Session\TempPath=/downloads/.temp +Session\TempPathEnabled=true + +[LegalNotice] +Accepted=true + +[Meta] +MigrationVersion=8 + +[Network] +Cookies=@Invalid() + +[Preferences] +WebUI\Address=* +WebUI\Password_PBKDF2= +WebUI\Port=8080 +WebUI\ReverseProxySupportEnabled=true +WebUI\ServerDomains=torrent.bench.local +WebUI\TrustedReverseProxiesList=172.16.0.0/12 +WebUI\Username=admin diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/11-container-log.txt b/documentation/audits/qbittorrent-2026-10-12/bench/11-container-log.txt new file mode 100644 index 00000000..8744f57e --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/11-container-log.txt @@ -0,0 +1,17 @@ +[felhom] first boot - seeding qBittorrent config and the generated WebUI password +WebUI will be started shortly after internal preparations. Please wait... + +******** Information ******** +To control qBittorrent, access the WebUI at: http://localhost:8080 +Catching signal: SIGTERM +Exiting cleanly +WebUI will be started shortly after internal preparations. Please wait... + +******** Information ******** +To control qBittorrent, access the WebUI at: http://localhost:8080 +Catching signal: SIGTERM +Exiting cleanly +WebUI will be started shortly after internal preparations. Please wait... + +******** Information ******** +To control qBittorrent, access the WebUI at: http://localhost:8080 diff --git a/documentation/audits/qbittorrent-2026-10-12/bench/12-compose-resolved.txt b/documentation/audits/qbittorrent-2026-10-12/bench/12-compose-resolved.txt new file mode 100644 index 00000000..96e093cb --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/bench/12-compose-resolved.txt @@ -0,0 +1,70 @@ +name: qbt-bench +services: + qbittorrent: + container_name: qbittorrent + deploy: + resources: + limits: + memory: "1073741824" + entrypoint: + - /bin/sh + - -c + - | + CONF=/config/qBittorrent/config/qBittorrent.conf + if [ ! -f "$$CONF" ]; then + echo "[felhom] first boot - seeding qBittorrent config and the generated WebUI password" + mkdir -p /config/qBittorrent/config + HASH=$$(QBT_PW="$$QBT_ADMIN_PASSWORD" python3 -c "import base64,hashlib,os,sys; s=os.urandom(16); k=hashlib.pbkdf2_hmac('sha512', os.environ['QBT_PW'].encode('utf-8'), s, 100000, 64); sys.stdout.write('@ByteArray(%s:%s)' % (base64.b64encode(s).decode(), base64.b64encode(k).decode()))") + printf '%s\n' '[BitTorrent]' 'Session\DefaultSavePath=/downloads' 'Session\TempPath=/downloads/.temp' 'Session\TempPathEnabled=true' 'Session\Port=6881' '[Meta]' 'MigrationVersion=9999' '[Preferences]' 'WebUI\Username=admin' "WebUI\\Password_PBKDF2=\"$$HASH\"" 'WebUI\Port=8080' 'WebUI\Address=*' "WebUI\\ServerDomains=$$QBT_PUBLIC_HOST" 'WebUI\ReverseProxySupportEnabled=true' 'WebUI\TrustedReverseProxiesList=172.16.0.0/12' > "$$CONF" + fi + exec /sbin/tini -g -- /entrypoint.sh + environment: + PGID: "1000" + PUID: "1000" + QBT_ADMIN_PASSWORD: + QBT_LEGAL_NOTICE: confirm + QBT_PUBLIC_HOST: torrent.bench.local + QBT_TORRENTING_PORT: "6881" + QBT_WEBUI_PORT: "8080" + TZ: Europe/Budapest + UMASK: "022" + healthcheck: + test: + - CMD + - curl + - -fsS + - -o + - /dev/null + - http://127.0.0.1:8080/ + timeout: 5s + interval: 30s + retries: 3 + start_period: 30s + image: qbittorrentofficial/qbittorrent-nox:5.2.4-1 + labels: + traefik.enable: "true" + traefik.http.routers.qbittorrent.entrypoints: websecure + traefik.http.routers.qbittorrent.rule: Host(`torrent.bench.local`) + traefik.http.routers.qbittorrent.tls: "true" + traefik.http.routers.qbittorrent.tls.certresolver: letsencrypt + traefik.http.services.qbittorrent.loadbalancer.server.port: "8080" + networks: + traefik-public: null + restart: unless-stopped + volumes: + - type: bind + source: /opt/qbt-bench/userdata/media/torrents + target: /downloads + bind: + create_host_path: true + - type: volume + source: qbittorrent_config + target: /config + volume: {} +networks: + traefik-public: + name: traefik-public + external: true +volumes: + qbittorrent_config: + name: qbt-bench_qbittorrent_config diff --git a/documentation/audits/qbittorrent-2026-10-12/box/boxwalk-log.txt b/documentation/audits/qbittorrent-2026-10-12/box/boxwalk-log.txt new file mode 100644 index 00000000..d206ccd7 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/box/boxwalk-log.txt @@ -0,0 +1,35 @@ +19:09:14 logged in to the 9202 dashboard +19:09:14 sync -> 200 {'ok': True, 'data': {'ok': True, 'updated': ['transmission'], 'message': 'Sablonok frissítve — frissítve: transmission' +19:09:14 rescan -> 200 {'ok': True, 'message': 'Rescan completed: 67 stacks found'} +19:09:14 === qbittorrent === +19:09:14 [qbittorrent] template_visible: True +19:09:17 [1] made the drive paths this app requires: ['/mnt/felhom-drives/scratch_hdd/userdata/qbittorrent'] +19:09:17 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH', 'QBT_ADMIN_PASSWORD'] +19:09:17 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +19:09:37 [1] deployed, controller state=running, pinned={'qbittorrent': 'qbittorrentofficial/qbittorrent-nox:5.2.4-1'} +19:09:37 [qbittorrent] deploy: True +19:09:37 [qbittorrent] generated_password_length: 30 +19:09:38 [qbittorrent] app_answers_on_its_public_name: True +19:09:38 [qbittorrent] stranger_root: 200 +19:09:38 [qbittorrent] stranger_api: 403 +19:09:38 [qbittorrent] household_signin_through_traefik: 204 +19:09:38 [qbittorrent] wrong_password_through_traefik: 401 Unauthorized +19:09:38 [qbittorrent] six_wrong_tries: ['401', '401', '401', '401', '403', '403'] +19:09:38 [qbittorrent] household_right_after_the_wrong_tries: 403 Your IP address has been banned after to +19:09:42 [qbittorrent] app_log_captured: True +19:09:45 === transmission === +19:09:45 [transmission] template_visible: True +19:09:48 [1] made the drive paths this app requires: ['/mnt/felhom-drives/scratch_hdd/userdata/transmission'] +19:09:48 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH', 'TR_ADMIN_PASSWORD'] +19:09:48 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +19:10:08 [1] deployed, controller state=running, pinned={'transmission': 'lscr.io/linuxserver/transmission:version-4.1.3-r0'} +19:10:08 [transmission] deploy: True +19:10:08 [transmission] generated_password_length: 30 +19:10:08 [transmission] app_answers_on_its_public_name: True +19:10:08 [transmission] stranger_root: 401 +19:10:08 [transmission] stranger_api: 401 +19:10:08 [transmission] household_signin_through_traefik: 409 +19:10:08 [transmission] wrong_password_through_traefik: 401 +19:10:09 [transmission] six_wrong_tries: ['401', '401', '401', '401', '401', '401'] +19:10:09 [transmission] household_right_after_the_wrong_tries: 409 +19:10:12 [transmission] app_log_captured: True \ No newline at end of file diff --git a/documentation/audits/qbittorrent-2026-10-12/box/boxwalk-results.json b/documentation/audits/qbittorrent-2026-10-12/box/boxwalk-results.json new file mode 100644 index 00000000..e80bc27d --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/box/boxwalk-results.json @@ -0,0 +1,42 @@ +{ + "qbittorrent": { + "template_visible": true, + "deploy": true, + "generated_password_length": 30, + "app_answers_on_its_public_name": true, + "stranger_root": "200", + "stranger_api": "403", + "household_signin_through_traefik": "204 ", + "wrong_password_through_traefik": "401 Unauthorized", + "six_wrong_tries": [ + "401", + "401", + "401", + "401", + "403", + "403" + ], + "household_right_after_the_wrong_tries": "403 Your IP address has been banned after to", + "app_log_captured": true + }, + "transmission": { + "template_visible": true, + "deploy": true, + "generated_password_length": 30, + "app_answers_on_its_public_name": true, + "stranger_root": "401", + "stranger_api": "401", + "household_signin_through_traefik": "409", + "wrong_password_through_traefik": "401", + "six_wrong_tries": [ + "401", + "401", + "401", + "401", + "401", + "401" + ], + "household_right_after_the_wrong_tries": "409", + "app_log_captured": true + } +} \ No newline at end of file diff --git a/documentation/audits/qbittorrent-2026-10-12/box/qbittorrent-app-log.txt b/documentation/audits/qbittorrent-2026-10-12/box/qbittorrent-app-log.txt new file mode 100644 index 00000000..4821d265 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/box/qbittorrent-app-log.txt @@ -0,0 +1,5 @@ +[felhom] first boot - seeding qBittorrent config and the generated WebUI password +WebUI will be started shortly after internal preparations. Please wait... + +******** Information ******** +To control qBittorrent, access the WebUI at: http://localhost:8080 diff --git a/documentation/audits/qbittorrent-2026-10-12/box/qbittorrent-applog-after-fix.txt b/documentation/audits/qbittorrent-2026-10-12/box/qbittorrent-applog-after-fix.txt new file mode 100644 index 00000000..55b4c92c --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/box/qbittorrent-applog-after-fix.txt @@ -0,0 +1,26 @@ +(N) 2026-10-12T17:21:51 - qBittorrent v5.2.4 started. Process ID: 9 +(N) 2026-10-12T17:21:51 - Using config directory: /config/qBittorrent/config +(N) 2026-10-12T17:21:51 - Trying to listen on the following list of IP addresses: "0.0.0.0:6881,[::]:6881" +(I) 2026-10-12T17:21:51 - Peer ID: "-qB5240-" +(I) 2026-10-12T17:21:51 - HTTP User-Agent: "qBittorrent/5.2.4" +(I) 2026-10-12T17:21:51 - Distributed Hash Table (DHT) support: ON +(I) 2026-10-12T17:21:51 - Local Peer Discovery support: ON +(I) 2026-10-12T17:21:51 - Peer Exchange (PeX) support: ON +(I) 2026-10-12T17:21:51 - Anonymous mode: OFF +(I) 2026-10-12T17:21:51 - Encryption support: ON +(I) 2026-10-12T17:21:51 - Successfully listening on IP. IP: "127.0.0.1". Port: "TCP/6881" +(I) 2026-10-12T17:21:51 - Successfully listening on IP. IP: "127.0.0.1". Port: "UDP/6881" +(I) 2026-10-12T17:21:51 - Successfully listening on IP. IP: "172.18.0.6". Port: "TCP/6881" +(I) 2026-10-12T17:21:51 - Successfully listening on IP. IP: "172.18.0.6". Port: "UDP/6881" +(I) 2026-10-12T17:21:51 - Successfully listening on IP. IP: "::1". Port: "TCP/6881" +(I) 2026-10-12T17:21:51 - Successfully listening on IP. IP: "::1". Port: "UDP/6881" +(I) 2026-10-12T17:21:51 - UPnP/NAT-PMP support: ON +(W) 2026-10-12T17:21:51 - Couldn't load IP geolocation database. Reason: No such file or directory +(N) 2026-10-12T17:21:51 - Using built-in WebUI. +(W) 2026-10-12T17:21:51 - Couldn't load WebUI translation for selected locale (C). +(N) 2026-10-12T17:21:51 - WebUI: Now listening on IP: *, port: 8080 +(I) 2026-10-12T17:21:51 - Detected external IP. IP: "37.191.56.193" +(I) 2026-10-12T17:21:51 - IP geolocation database loaded. Type: DBIP-Country-Lite. Build time: Thu Oct 1 01:36:12 2026. +(I) 2026-10-12T17:21:51 - Successfully updated IP geolocation database. +(N) 2026-10-12T17:21:56 - WebAPI login success. IP: ::ffff:127.0.0.1 +(I) 2026-10-12T17:21:56 - UPnP/NAT-PMP support: OFF diff --git a/documentation/audits/qbittorrent-2026-10-12/box/qbittorrent-paths.txt b/documentation/audits/qbittorrent-2026-10-12/box/qbittorrent-paths.txt new file mode 100644 index 00000000..22d30f26 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/box/qbittorrent-paths.txt @@ -0,0 +1,2 @@ +ls: cannot access '/mnt/felhom-drives/scratch_hdd/userdata/media/qbittorrent': No such file or directory +qbittorrent_qbittorrent_config diff --git a/documentation/audits/qbittorrent-2026-10-12/box/transmission-app-log.txt b/documentation/audits/qbittorrent-2026-10-12/box/transmission-app-log.txt new file mode 100644 index 00000000..29f2e625 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/box/transmission-app-log.txt @@ -0,0 +1,20 @@ +User UID: 1000 +User GID: 1000 +─────────────────────────────────────── +Linuxserver.io version: 4.1.3-r0-ls363 +Build-date: 2026-09-29T16:34:59+00:00 +─────────────────────────────────────── + +stat: cannot statx '/downloads/complete': No such file or directory +find: ‘/downloads/complete’: No such file or directory +**** Permissions could not be set. This is probably because your volume mounts are remote or read-only. **** +**** The app may not work properly and we will not provide support for it. **** +stat: cannot statx '/downloads/incomplete': No such file or directory +find: ‘/downloads/incomplete’: No such file or directory +**** Permissions could not be set. This is probably because your volume mounts are remote or read-only. **** +**** The app may not work properly and we will not provide support for it. **** +[custom-init] No custom files found, skipping... +Connection to localhost (::1) 9091 port [tcp/*] succeeded! +[2026-10-12T19:09:55.432+0200] ERR watchdir-inotify.cc:103 Couldn't watch '/watch': No such file or directory (2) (watchdir-inotify.cc:103) +[2026-10-12T19:09:55.432+0200] ERR utils.cc:145 Couldn't read '/config/queue.json': No such file or directory (2) (utils.cc:145) +[ls.io-init] done. diff --git a/documentation/audits/qbittorrent-2026-10-12/box/transmission-paths.txt b/documentation/audits/qbittorrent-2026-10-12/box/transmission-paths.txt new file mode 100644 index 00000000..3c4f11da --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/box/transmission-paths.txt @@ -0,0 +1,4 @@ +total 8 +drwxrwsr-x 2 1000 1000 4096 Oct 12 17:09 . +drwxrwsr-x 14 root 1000 4096 Oct 12 17:09 .. +transmission_transmission_config diff --git a/documentation/audits/qbittorrent-2026-10-12/box/transmission-settings-after-fix.txt b/documentation/audits/qbittorrent-2026-10-12/box/transmission-settings-after-fix.txt new file mode 100644 index 00000000..ebe45c2a --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/box/transmission-settings-after-fix.txt @@ -0,0 +1,3 @@ + "dht-enabled": true, + "peer-port": 51413, + "port-forwarding-enabled": false, diff --git a/documentation/audits/qbittorrent-2026-10-12/box/verify-fixes.json b/documentation/audits/qbittorrent-2026-10-12/box/verify-fixes.json new file mode 100644 index 00000000..bf7d5096 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/box/verify-fixes.json @@ -0,0 +1,38 @@ +{ + "qbittorrent": { + "redeployed": true, + "app_answers": true, + "upnp_lines_in_the_app_log": [ + "(I) 2026-10-12T17:21:51 - UPnP/NAT-PMP support: ON", + "(I) 2026-10-12T17:21:56 - UPnP/NAT-PMP support: OFF" + ], + "invalid_host_header_lines": 0, + "household_before_any_wrong_try": "204", + "stranger_six_wrong_tries_from_192_168_0_180": [ + "401", + "401", + "401", + "401", + "401", + "403" + ], + "stranger_right_after": "403", + "HOUSEHOLD_right_after_from_its_own_address": "204" + }, + "transmission": { + "redeployed": true, + "app_answers": true, + "settings_after_fix": "\"dht-enabled\": true, \"peer-port\": 51413, \"port-forwarding-enabled\": false,", + "household_before_any_wrong_try": "409", + "stranger_six_wrong_tries_from_192_168_0_180": [ + "401", + "401", + "401", + "401", + "401", + "401" + ], + "stranger_right_after": "409", + "HOUSEHOLD_right_after_from_its_own_address": "409" + } +} \ No newline at end of file diff --git a/documentation/audits/qbittorrent-2026-10-12/tools/boxwalk_newapps.py b/documentation/audits/qbittorrent-2026-10-12/tools/boxwalk_newapps.py new file mode 100644 index 00000000..678fd330 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/tools/boxwalk_newapps.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""boxwalk_newapps.py — qbittorrent and transmission installed on guest 9202 THROUGH THE PRODUCT. + +Closes the checklist items that only a real install can close: 3.5 (the install window — what a +stranger reaches), 3.6 (lock-out: can a stranger lock the household out), 3.9 (sign in the way each +client does, through traefik over https), 3.10 (the visitor's address), 2.6 (remove with data / keep +data), 9.2 (a fresh install as household and as stranger). + +EVIDENCE, NOT PRODUCT. It presses the buttons a person presses — POST /api/stacks//deploy, +GET /api/stacks/, POST /api/stacks//remove — and reaches each app on its own public name. +Reuses the proven harness of 2026-09-30 (`pg-last-six-2026-09-30/tools/walk.py`) for the session, the +CSRF, the setup gate and the deploy-field filling. + +Runs ON DOOPLEX. 9202 must already be pointed at the drill catalog (09 §6.5). +""" +import json +import os +import sys +import time + +TOOLS = "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools" +sys.path.insert(0, TOOLS) +os.environ.setdefault("SC", os.environ.get("SC", "/tmp/newapps-walk")) +os.makedirs(os.environ["SC"], exist_ok=True) +import walk as w # noqa: E402 + +EV = os.environ.get("EV", "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/qbittorrent-2026-10-12/box") +os.makedirs(EV, exist_ok=True) + +APPS = [ + # (template name, subdomain, the app's own sign-in probe) + ("qbittorrent", "torrent", { + "login_path": "/api/v2/auth/login", + "user_field": "username", "pass_field": "password", + "pw_env": "QBT_ADMIN_PASSWORD", + "unauth_probe": "/api/v2/app/preferences", # must refuse a stranger + }), + ("transmission", "transmission", { + "login_path": "/transmission/rpc", + "basic": True, + "pw_env": "TR_ADMIN_PASSWORD", + "unauth_probe": "/transmission/rpc", + }), +] + +OUT = {} + +# the scratch guest's drive, as the controller names it +DRIVE_ROOT = os.environ.get("DRIVE_ROOT", "/mnt/felhom-drives/scratch_hdd") + + +def rec(app, key, value): + OUT.setdefault(app, {})[key] = value + w.say(f" [{app}] {key}: {str(value)[:160]}") + + +def stranger(sub, path): + """A stranger: the app's public name, no gate cookie, no credentials.""" + r = w.sh(["curl", "-sSk", "--max-time", "20", "-H", f"Host: {sub}.{w.DOMAIN}", + "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}{path}"]) + return (r.stdout or "").strip() + + +def main(): + w.login() + w.say("logged in to the 9202 dashboard") + + # the box must have the drill catalog's templates + code, d = w.ctl("POST", "/api/sync", {}) + w.say(f"sync -> {code} {str(d)[:120]}") + code, d = w.ctl("POST", "/api/stacks/rescan", {}) + w.say(f"rescan -> {code} {str(d)[:120]}") + + for name, sub, spec in APPS: + w.say(f"=== {name} ===") + st = w.stack(name) + if not st: + rec(name, "template_visible", False) + continue + rec(name, "template_visible", True) + + # ---- 9.2 / 3.5: deploy as the household + # HDD_PATH must be a DRIVE, not a folder on it: the controller refuses the folder with + # „Az alkalmazas adatainak helye egy meghajto lehet, nem egy mappa a meghajton belul" + # (409, measured 2026-10-12 19:02). walk.py's deploy_values builds /userdata/ + # for a `type: path` field, which is right for an app that wants a folder and wrong here. + ok = w.deploy(name, sub, extra_values={"HDD_PATH": DRIVE_ROOT}) + rec(name, "deploy", ok) + if not ok: + continue + pw = (w.GENERATED.get(name) or {}).get(spec["pw_env"], "") + rec(name, "generated_password_length", len(pw)) + + answered = w.wait_app(sub, "/", tries=40, delay=5) + rec(name, "app_answers_on_its_public_name", answered) + + # ---- 3.5: what a STRANGER reaches in the install window + rec(name, "stranger_root", stranger(sub, "/")) + rec(name, "stranger_api", stranger(sub, spec["unauth_probe"])) + + # ---- 3.9: the household signs in through traefik over https + if spec.get("basic"): + r = w.sh(["curl", "-sSk", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", + "-u", f"admin:{pw}", "-o", "/dev/null", "-w", "%{http_code}", + f"{w.BASE}{spec['login_path']}"]) + rec(name, "household_signin_through_traefik", (r.stdout or "").strip()) + r = w.sh(["curl", "-sSk", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", + "-u", "admin:definitely-not-it", "-o", "/dev/null", "-w", "%{http_code}", + f"{w.BASE}{spec['login_path']}"]) + rec(name, "wrong_password_through_traefik", (r.stdout or "").strip()) + else: + rc, code, body = w.app_curl( + sub, spec["login_path"], "-H", f"Referer: https://{sub}.{w.DOMAIN}", + "--data-urlencode", f"{spec['user_field']}=admin", + "--data-urlencode", f"{spec['pass_field']}={pw}", method="POST") + rec(name, "household_signin_through_traefik", f"{code} {body[:40]}") + rc, code, body = w.app_curl( + sub, spec["login_path"], "-H", f"Referer: https://{sub}.{w.DOMAIN}", + "--data-urlencode", f"{spec['user_field']}=admin", + "--data-urlencode", f"{spec['pass_field']}=definitely-not-it", method="POST") + rec(name, "wrong_password_through_traefik", f"{code} {body[:40]}") + + # ---- 3.6: can a stranger lock the household out? (the qBittorrent ban, through traefik) + codes = [] + for i in range(6): + if spec.get("basic"): + r = w.sh(["curl", "-sSk", "--max-time", "20", "-H", f"Host: {sub}.{w.DOMAIN}", + "-u", f"admin:wrong{i}", "-o", "/dev/null", "-w", "%{http_code}", + f"{w.BASE}{spec['login_path']}"]) + codes.append((r.stdout or "").strip()) + else: + _, code, _ = w.app_curl(sub, spec["login_path"], + "-H", f"Referer: https://{sub}.{w.DOMAIN}", + "--data-urlencode", "username=admin", + "--data-urlencode", f"password=wrong{i}", method="POST") + codes.append(code) + rec(name, "six_wrong_tries", codes) + # and the household, right after, from the same address traefik presents + if spec.get("basic"): + r = w.sh(["curl", "-sSk", "--max-time", "20", "-H", f"Host: {sub}.{w.DOMAIN}", + "-u", f"admin:{pw}", "-o", "/dev/null", "-w", "%{http_code}", + f"{w.BASE}{spec['login_path']}"]) + rec(name, "household_right_after_the_wrong_tries", (r.stdout or "").strip()) + else: + _, code, body = w.app_curl(sub, spec["login_path"], + "-H", f"Referer: https://{sub}.{w.DOMAIN}", + "--data-urlencode", "username=admin", + "--data-urlencode", f"password={pw}", method="POST") + rec(name, "household_right_after_the_wrong_tries", f"{code} {body[:40]}") + + # ---- 3.10: what address the app thinks the visitor has + logs = w.guest(f"docker logs --tail 40 {name} 2>&1 | tail -20") + open(f"{EV}/{name}-app-log.txt", "w").write(logs) + rec(name, "app_log_captured", True) + + # where the data landed, as the box sees it + paths = w.guest(f"ls -la {w.DRIVE}/media/{name} 2>&1 | head -6; " + f"docker volume ls --format '{{{{.Name}}}}' | grep -i {name} | head -4") + open(f"{EV}/{name}-paths.txt", "w").write(paths) + + json.dump(OUT, open(f"{EV}/boxwalk-results.json", "w"), indent=2, ensure_ascii=False) + open(f"{EV}/boxwalk-log.txt", "w").write("\n".join(w.LOG)) + print(json.dumps(OUT, indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/documentation/audits/qbittorrent-2026-10-12/tools/verify_fixes.py b/documentation/audits/qbittorrent-2026-10-12/tools/verify_fixes.py new file mode 100644 index 00000000..28b599b6 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/tools/verify_fixes.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""verify_fixes.py — redeploy both apps on 9202 after the UPnP and host-list fixes, and prove: + + 1. neither app asks the household's router for a port any more (3.x / 01 §7) + 2. the controller's probe is no longer refused for a bad Host header + 3. the lock-out is per VISITOR, not per app: a stranger's wrong tries from ONE address do not shut + the household out from ANOTHER (checklist 3.6 + 3.10) + +The third one is why this file exists: the first box walk ran the stranger and the household from the +same machine, so a shared ban could not be told apart from a per-visitor one. Here the stranger is +DooPlex (192.168.0.180) and the household is the guest itself — two addresses, one app. + +Runs ON DOOPLEX, after the drill catalog carries the fixed templates. +""" +import json +import os +import sys +import time + +TOOLS = "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools" +sys.path.insert(0, TOOLS) +os.environ.setdefault("SC", "/tmp/newapps-walk") +import walk as w # noqa: E402 + +EV = "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/qbittorrent-2026-10-12/box" +DRIVE_ROOT = "/mnt/felhom-drives/scratch_hdd" +OUT = {} + + +def rec(app, k, v): + OUT.setdefault(app, {})[k] = v + w.say(f" [{app}] {k}: {str(v)[:150]}") + + +def redeploy(name, sub): + st = w.stack(name) + if st.get("deployed"): + w.ctl("POST", f"/api/stacks/{name}/stop", {}) + time.sleep(10) + code, d = w.ctl("POST", f"/api/stacks/{name}/remove", + {"remove_hdd_data": True, "remove_backups": True}) + w.say(f" removed {name} -> {code} {str(d)[:140]}") + time.sleep(8) + return w.deploy(name, sub, extra_values={"HDD_PATH": DRIVE_ROOT}) + + +def main(): + w.login() + w.ctl("POST", "/api/sync", {}) + w.ctl("POST", "/api/stacks/rescan", {}) + + for name, sub in (("qbittorrent", "torrent"), ("transmission", "transmission")): + w.say(f"=== {name} ===") + rec(name, "redeployed", redeploy(name, sub)) + pw = (w.GENERATED.get(name) or {}).get( + "QBT_ADMIN_PASSWORD" if name == "qbittorrent" else "TR_ADMIN_PASSWORD", "") + rec(name, "app_answers", w.wait_app(sub, "/", tries=40, delay=5)) + + # ---- 1. no port asked of the router + if name == "qbittorrent": + log = w.guest("docker exec qbittorrent sh -c " + "'cat /config/qBittorrent/data/logs/qbittorrent.log' 2>/dev/null | head -40") + upnp = [l for l in log.splitlines() if "UPnP" in l or "NAT-PMP" in l] + rec(name, "upnp_lines_in_the_app_log", upnp[:3]) + badhost = [l for l in log.splitlines() if "Invalid Host header" in l] + rec(name, "invalid_host_header_lines", len(badhost)) + open(f"{EV}/{name}-applog-after-fix.txt", "w").write(log) + else: + s = w.guest("docker exec transmission sh -c " + "'grep -E \"port-forwarding-enabled|peer-port\\\"|dht-enabled\" /config/settings.json'") + rec(name, "settings_after_fix", s.strip().replace("\n", " ")) + open(f"{EV}/{name}-settings-after-fix.txt", "w").write(s) + + # ---- 2 + 3. the lock-out, from TWO different addresses + login_path = ("/api/v2/auth/login" if name == "qbittorrent" else "/transmission/rpc") + + def as_stranger(password): + """from DooPlex""" + if name == "qbittorrent": + r = w.sh(["curl", "-sSk", "--max-time", "20", "-H", f"Host: {sub}.{w.DOMAIN}", + "-H", f"Referer: https://{sub}.{w.DOMAIN}", + "--data-urlencode", "username=admin", + "--data-urlencode", f"password={password}", + "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}{login_path}"]) + else: + r = w.sh(["curl", "-sSk", "--max-time", "20", "-H", f"Host: {sub}.{w.DOMAIN}", + "-u", f"admin:{password}", "-o", "/dev/null", "-w", "%{http_code}", + f"{w.BASE}{login_path}"]) + return (r.stdout or "").strip() + + def as_household(password): + """from INSIDE the guest — a different source address, through the same traefik""" + if name == "qbittorrent": + cmd = (f"curl -sSk --max-time 20 -H 'Host: {sub}.{w.DOMAIN}' " + f"-H 'Referer: https://{sub}.{w.DOMAIN}' " + f"--data-urlencode username=admin --data-urlencode password={password} " + f"-o /dev/null -w '%{{http_code}}' https://127.0.0.1{login_path}") + else: + cmd = (f"curl -sSk --max-time 20 -H 'Host: {sub}.{w.DOMAIN}' " + f"-u admin:{password} -o /dev/null -w '%{{http_code}}' " + f"https://127.0.0.1{login_path}") + return w.guest(cmd).strip().splitlines()[-1] if w.guest(cmd).strip() else "" + + rec(name, "household_before_any_wrong_try", as_household(pw)) + codes = [as_stranger(f"wrong{i}") for i in range(6)] + rec(name, "stranger_six_wrong_tries_from_192_168_0_180", codes) + rec(name, "stranger_right_after", as_stranger(pw)) + rec(name, "HOUSEHOLD_right_after_from_its_own_address", as_household(pw)) + + json.dump(OUT, open(f"{EV}/verify-fixes.json", "w"), indent=2, ensure_ascii=False) + print(json.dumps(OUT, indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/01-image.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/01-image.txt new file mode 100644 index 00000000..eeed6493 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/01-image.txt @@ -0,0 +1,5 @@ +# image + architectures +repo digest: lscr.io/linuxserver/transmission@sha256:4813745e2c721997d8849f5718786fd4406b3c656fbef479f87a161e51383cbd + "architecture": "amd64", + "architecture": "arm64", + "architecture": "unknown", diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/02-first-admin.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/02-first-admin.txt new file mode 100644 index 00000000..a5decab8 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/02-first-admin.txt @@ -0,0 +1,26 @@ +# first admin: class 1, USER/PASS applied by the image's init on EVERY start. 2026-10-12T16:23:06+00:00 +## 1. authentication is required (unauthenticated RPC, want 401): +401 +## 2. the generated password is accepted (want 409 = the CSRF challenge, i.e. past auth): +409 +## 3. a wrong password is refused (want 401): +401 +## 4. what the settings say (redacted): + "download-dir": "/downloads/complete", + "incomplete-dir": "/downloads/incomplete", + "incomplete-dir-enabled": true, + "peer-port": 51413, + "rpc-authentication-required": true, + "rpc-host-whitelist": "transmission.bench.local,localhost,127.0.0.1", + "rpc-host-whitelist-enabled": true, + "rpc-password": + "rpc-username": "admin", + "rpc-whitelist": "127.0.0.1", + "rpc-whitelist-enabled": false, +## 5. five wrong tries, then the household from the same address: +try 1: 401 +try 2: 401 +try 3: 401 +try 4: 401 +try 5: 401 +household right after: 409 diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/03-init-script.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/03-init-script.txt new file mode 100644 index 00000000..c969aae7 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/03-init-script.txt @@ -0,0 +1,74 @@ +# the init script the image runs (read inside the image) +#!/usr/bin/with-contenv bash +# shellcheck shell=bash + +# copy config +if [[ ! -f /config/settings.json ]]; then + cp /defaults/settings.json /config/settings.json +fi + +if [[ -n "${USER}" ]] && [[ -n "${PASS}" ]]; then + echo -E "$(jq -r '.["rpc-authentication-required"] = true' /config/settings.json)" >/config/settings.json + echo -E "$(jq -r --arg user "${USER}" '.["rpc-username"] = $user' /config/settings.json)" >/config/settings.json + echo -E "$(jq -r --arg pass "${PASS}" '.["rpc-password"] = $pass' /config/settings.json)" >/config/settings.json +else + echo -E "$(jq -r '.["rpc-authentication-required"] = false' /config/settings.json)" >/config/settings.json +fi + +if [[ -n "${WHITELIST}" ]]; then + echo -E "$(jq -r '.["rpc-whitelist-enabled"] = true' /config/settings.json)" >/config/settings.json + echo -E "$(jq -r --arg whitelist "${WHITELIST}" '.["rpc-whitelist"] = $whitelist' /config/settings.json)" >/config/settings.json +else + echo -E "$(jq -r '.["rpc-whitelist-enabled"] = false' /config/settings.json)" >/config/settings.json +fi + +if [[ -n "${HOST_WHITELIST}" ]]; then + echo -E "$(jq -r '.["rpc-host-whitelist-enabled"] = true' /config/settings.json)" >/config/settings.json + echo -E "$(jq -r --arg host_whitelist "${HOST_WHITELIST}" '.["rpc-host-whitelist"] = $host_whitelist' /config/settings.json)" >/config/settings.json +else + echo -E "$(jq -r '.["rpc-host-whitelist-enabled"] = false' /config/settings.json)" >/config/settings.json +fi + +if [[ -n "${PEERPORT}" ]]; then + echo -E "$(jq -r --argjson peerport "${PEERPORT}" '.["peer-port"] = $peerport' /config/settings.json)" >/config/settings.json + echo -E "$(jq -r '.["peer-port-random-on-start"] = false' /config/settings.json)" >/config/settings.json +fi + +if ! test -f /proc/net/if_inet6; then + echo -E "$(jq -r '.["rpc-bind-address"] = "0.0.0.0"' /config/settings.json)" > /config/settings.json +fi + +if [[ -n "${UMASK}" ]]; then + echo -E "$(jq -r --arg umask "${UMASK}" '.["umask"] = $umask' /config/settings.json)" >/config/settings.json +fi + +if [[ -z ${LSIO_NON_ROOT_USER} ]] && [[ -z ${LSIO_READ_ONLY_FS} ]]; then + # Handle old theme locations + mkdir -p {/transmissionic,/combustion-release,/flood-for-transmission,/kettu,/transmission-web-control} + echo /transmissionic /combustion-release /flood-for-transmission /kettu /transmission-web-control | xargs -n1 ln -s /defaults/index.html +fi + +if [[ -z ${LSIO_NON_ROOT_USER} ]]; then + lsiown -R abc:abc \ + /config + + if grep -qe ' /downloads ' /proc/mounts; then + if [[ "$(stat -c '%U' /downloads)" != "abc" ]]; then + lsiown abc:abc /downloads + fi + + if [[ "$(stat -c '%U' /downloads/complete)" != "abc" ]]; then + lsiown abc:abc /downloads/complete + fi + + if [[ "$(stat -c '%U' /downloads/incomplete)" != "abc" ]]; then + lsiown abc:abc /downloads/incomplete + fi + fi + + if grep -qe ' /watch ' /proc/mounts; then + if [[ "$(stat -c '%U' /watch)" != "abc" ]]; then + lsiown abc:abc /watch + fi + fi +fi diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/04-process.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/04-process.txt new file mode 100644 index 00000000..aaa34653 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/04-process.txt @@ -0,0 +1,13 @@ +# process list + PID USER COMMAND + 1 root /package/admin/s6/command/s6-svscan -d4 -- /run/service + 17 root s6-supervise s6-linux-init-shutdownd + 19 root /package/admin/s6-linux-init/command/s6-linux-init-shutdownd -d3 -c /run/s6/basedir -g 3000 -C -B + 36 root s6-supervise svc-cron + 37 root s6-supervise svc-transmission + 38 root s6-supervise s6rc-oneshot-runner + 39 root s6-supervise s6rc-fdholder + 47 root /package/admin/s6/command/s6-ipcserverd -1 -- /package/admin/s6/command/s6-ipcserver-access -v0 -E -l0 -i data/rules -- /package/admin/s6/command/s6-sudod -t 30000 -- /package/admin/s6-rc/command/s6-rc-oneshot-run -l ../.. -- + 201 root bash ./run svc-transmission + 202 root busybox crond -f -S -l 5 + 316 root ps -o pid,user,args diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/05-download.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/05-download.txt new file mode 100644 index 00000000..fdeb02e1 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/05-download.txt @@ -0,0 +1,23 @@ +# the download, through the app's own RPC +{"arguments":{"torrents":[{"downloadDir":"/downloads/complete","name":"Big Buck Bunny","peersConnected":0,"percentDone":1.0,"status":6,"totalSize":276445467}]},"result":"success"} +## on the household's drive: +/opt/tr-bench/userdata/media/transmission: +total 16 +drwxr-xr-x 4 1000 1000 4096 Oct 12 16:29 . +drwxr-xr-x 3 root root 4096 Oct 12 16:22 .. +drwxr-xr-x 3 1000 1000 4096 Oct 12 16:29 complete +drwxr-xr-x 2 1000 1000 4096 Oct 12 16:29 incomplete + +/opt/tr-bench/userdata/media/transmission/complete: +total 12 +drwxr-xr-x 3 1000 1000 4096 Oct 12 16:29 . +drwxr-xr-x 4 1000 1000 4096 Oct 12 16:29 .. +drwxr-xr-x 2 1000 1000 4096 Oct 12 16:29 Big Buck Bunny + +/opt/tr-bench/userdata/media/transmission/complete/Big Buck Bunny: +total 269980 +drwxr-xr-x 2 1000 1000 4096 Oct 12 16:29 . +drwxr-xr-x 3 1000 1000 4096 Oct 12 16:29 .. +-rw-r--r-- 1 1000 1000 140 Oct 12 16:27 Big Buck Bunny.en.srt +-rw-r--r-- 1 1000 1000 276134947 Oct 12 16:29 Big Buck Bunny.mp4 +-rw-r--r-- 1 1000 1000 310380 Oct 12 16:27 poster.jpg diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/06-storage.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/06-storage.txt new file mode 100644 index 00000000..6cc487c9 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/06-storage.txt @@ -0,0 +1,33 @@ +# where everything is written +total 32 +drwxr-xr-x 5 abc users 4096 Oct 12 18:22 . +drwxr-xr-x 1 root root 4096 Oct 12 18:22 .. +-rw-r--r-- 1 abc users 2 Oct 12 18:22 bandwidth-groups.json +drwxr-xr-x 2 abc users 4096 Oct 12 18:19 blocklists +drwxr-xr-x 2 abc users 4096 Oct 12 18:19 resume +-rw-r--r-- 1 abc users 3038 Oct 12 18:22 settings.json +-rw-r--r-- 1 abc users 127 Oct 12 18:22 stats.json +drwxr-xr-x 2 abc users 4096 Oct 12 18:19 torrents + +28K /config + +total 8 +drwxr-xr-x 2 abc users 4096 Oct 12 18:22 . +drwxr-xr-x 1 root root 4096 Oct 12 18:22 .. +## outside a mount (container writable layer): +A /combustion-release +A /combustion-release/index.html +C /defaults +A /transmissionic +A /transmissionic/index.html +A /tmp/tr_session_id_wCZJBbrDvP9Hfp2Rn8ATzV5cMuU6b3IAR7dtuJB9HOPE4AnS +A /downloads +A /kettu +A /kettu/index.html +C /app +A /flood-for-transmission +A /flood-for-transmission/index.html +A /transmission-web-control +A /transmission-web-control/index.html +A /run/service +grep: write error: Broken pipe diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/07-ownership.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/07-ownership.txt new file mode 100644 index 00000000..8dd95d46 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/07-ownership.txt @@ -0,0 +1,3 @@ +# ownership on both sides of the bind mount +total 0 +total 0 diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/08-health.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/08-health.txt new file mode 100644 index 00000000..d9b8b6bb --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/08-health.txt @@ -0,0 +1,5 @@ +# health with the probe that accepts a 401 (2026-10-12T16:32:44+00:00) +state: healthy +test: [CMD-SHELL curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:9091/transmission/web/ | grep -qE '^(200|401)$'] start_period=30s +## negative control, the same probe against a dead port: +exit: 1 (non-zero = the probe can fail) diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/09-memory.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/09-memory.txt new file mode 100644 index 00000000..0a64dc9c --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/09-memory.txt @@ -0,0 +1,8 @@ +# memory with the download running +now: 5.945MiB / 512MiB 1.16% +cgroup peak: 18 MiB +anon: 3 MiB +file: 2 MiB +oom 0 +oom_kill 0 +oom_group_kill 0 diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/10-restart.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/10-restart.txt new file mode 100644 index 00000000..b7f4ef12 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/10-restart.txt @@ -0,0 +1,9 @@ +# after a restart +login after restart: 409 (409 = past auth) + +400 Bad Request + +

Bad Request

+ + +health: starting diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/11-container-log.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/11-container-log.txt new file mode 100644 index 00000000..75641214 --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/11-container-log.txt @@ -0,0 +1,90 @@ +[migrations] started +[migrations] no migrations found +─────────────────────────────────────── + + ██╗ ███████╗██╗ ██████╗ + ██║ ██╔════╝██║██╔═══██╗ + ██║ ███████╗██║██║ ██║ + ██║ ╚════██║██║██║ ██║ + ███████╗███████║██║╚██████╔╝ + ╚══════╝╚══════╝╚═╝ ╚═════╝ + + Brought to you by linuxserver.io +─────────────────────────────────────── + +To support LSIO projects visit: +https://www.linuxserver.io/donate/ + +─────────────────────────────────────── +GID/UID +─────────────────────────────────────── + +User UID: 1000 +User GID: 1000 +─────────────────────────────────────── +Linuxserver.io version: 4.1.3-r0-ls363 +Build-date: 2026-09-29T16:34:59+00:00 +─────────────────────────────────────── + +stat: cannot statx '/downloads/complete': No such file or directory +find: ‘/downloads/complete’: No such file or directory +**** Permissions could not be set. This is probably because your volume mounts are remote or read-only. **** +**** The app may not work properly and we will not provide support for it. **** +stat: cannot statx '/downloads/incomplete': No such file or directory +find: ‘/downloads/incomplete’: No such file or directory +**** Permissions could not be set. This is probably because your volume mounts are remote or read-only. **** +**** The app may not work properly and we will not provide support for it. **** +[custom-init] No custom files found, skipping... +Connection to localhost (::1) 9091 port [tcp/*] succeeded! +[2026-10-12T18:22:26.881+0200] ERR watchdir-inotify.cc:103 Couldn't watch '/watch': No such file or directory (2) (watchdir-inotify.cc:103) +[2026-10-12T18:22:26.881+0200] ERR utils.cc:145 Couldn't read '/config/queue.json': No such file or directory (2) (utils.cc:145) +[ls.io-init] done. +127.0.0.1:9091/transmission/rpc/ acknowledged notification +Closing transmission session... done. +[migrations] started +[migrations] no migrations found +usermod: no changes +─────────────────────────────────────── + + ██╗ ███████╗██╗ ██████╗ + ██║ ██╔════╝██║██╔═══██╗ + ██║ ███████╗██║██║ ██║ + ██║ ╚════██║██║██║ ██║ + ███████╗███████║██║╚██████╔╝ + ╚══════╝╚══════╝╚═╝ ╚═════╝ + + Brought to you by linuxserver.io +─────────────────────────────────────── + +To support LSIO projects visit: +https://www.linuxserver.io/donate/ + +─────────────────────────────────────── +GID/UID +─────────────────────────────────────── + +User UID: 1000 +User GID: 1000 +─────────────────────────────────────── +Linuxserver.io version: 4.1.3-r0-ls363 +Build-date: 2026-09-29T16:34:59+00:00 +─────────────────────────────────────── + +ln: failed to create symbolic link '/transmissionic/index.html': File exists +ln: failed to create symbolic link '/combustion-release/index.html': File exists +ln: failed to create symbolic link '/flood-for-transmission/index.html': File exists +ln: failed to create symbolic link '/kettu/index.html': File exists +ln: failed to create symbolic link '/transmission-web-control/index.html': File exists +stat: cannot statx '/downloads/complete': No such file or directory +find: ‘/downloads/complete’: No such file or directory +**** Permissions could not be set. This is probably because your volume mounts are remote or read-only. **** +**** The app may not work properly and we will not provide support for it. **** +stat: cannot statx '/downloads/incomplete': No such file or directory +find: ‘/downloads/incomplete’: No such file or directory +**** Permissions could not be set. This is probably because your volume mounts are remote or read-only. **** +**** The app may not work properly and we will not provide support for it. **** +[custom-init] No custom files found, skipping... +Connection to localhost (::1) 9091 port [tcp/*] succeeded! +[2026-10-12T18:24:00.115+0200] ERR watchdir-inotify.cc:103 Couldn't watch '/watch': No such file or directory (2) (watchdir-inotify.cc:103) +[2026-10-12T18:24:00.115+0200] ERR utils.cc:145 Couldn't read '/config/queue.json': No such file or directory (2) (utils.cc:145) +[ls.io-init] done. diff --git a/documentation/audits/qbittorrent-2026-10-12/transmission-bench/12-compose-resolved.txt b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/12-compose-resolved.txt new file mode 100644 index 00000000..18a7b21b --- /dev/null +++ b/documentation/audits/qbittorrent-2026-10-12/transmission-bench/12-compose-resolved.txt @@ -0,0 +1,57 @@ +name: tr-bench +services: + transmission: + container_name: transmission + deploy: + resources: + limits: + memory: "536870912" + environment: + HOST_WHITELIST: transmission.bench.local,localhost,127.0.0.1 + PASS: + PEERPORT: "51413" + PGID: "1000" + PUID: "1000" + TZ: Europe/Budapest + UMASK: "022" + USER: admin + healthcheck: + test: + - CMD + - curl + - -fsS + - -o + - /dev/null + - http://127.0.0.1:9091/transmission/web/ + timeout: 5s + interval: 30s + retries: 3 + start_period: 30s + image: lscr.io/linuxserver/transmission:version-4.1.3-r0 + labels: + traefik.enable: "true" + traefik.http.routers.transmission.entrypoints: websecure + traefik.http.routers.transmission.rule: Host(`transmission.bench.local`) + traefik.http.routers.transmission.tls: "true" + traefik.http.routers.transmission.tls.certresolver: letsencrypt + traefik.http.services.transmission.loadbalancer.server.port: "9091" + networks: + traefik-public: null + restart: unless-stopped + volumes: + - type: bind + source: /opt/tr-bench/userdata/media/transmission + target: /downloads + bind: + create_host_path: true + - type: volume + source: transmission_config + target: /config + volume: {} +networks: + traefik-public: + name: traefik-public + external: true +volumes: + transmission_config: + name: tr-bench_transmission_config