From 8dff4550626624ccad61291a25d642582b112ac1 Mon Sep 17 00:00:00 2001 From: Administrator Date: Fri, 19 Jun 2026 19:35:38 +0200 Subject: [PATCH] docs(todo): Tailscale network-bind + fail-closed allowlist plan + review Co-Authored-By: Claude Opus 4.8 (1M context) --- tasks/todo.md | 115 ++++++++++++++------------------------------------ 1 file changed, 32 insertions(+), 83 deletions(-) diff --git a/tasks/todo.md b/tasks/todo.md index c0a3e62..506df08 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -1,87 +1,36 @@ -# Queue-Persistenz Bug: fertige Dateien tauchen nach Neustart wieder auf +# Tailscale / network-bind diagnostics — match the downloader (rd-diagnostics-mcp) -## Symptom -User: 300 Dateien, 100 übrig, Programm schließen + öffnen → manchmal sind bereits -fertig hochgeladene Dateien wieder in der Liste. +Goal: make the MHU read-only diagnostics reachable like the Real-Debrid-Downloader does over +Tailscale — host embedded in the connection code, two bind modes (local / network), and a +fail-closed IP allowlist as the access gate. No Tailscale auto-detection (the downloader has none); +Tailscale is just one of the offered interface IPs reached over the tunnel, gated by the allowlist. -## Root Cause (verifiziert im Code) -- **RC-1 (Persist-Starvation, code-confirmed):** `persistQueueStateSoon()` setzt bei - jedem Progress-Event den Timer per `clearTimeout` zurück; Delay während Upload war - 10000ms. Progress-Events feuern öfter als alle 10s → Timer feuert NIE während eines - aktiven Uploads. Der Disk-Snapshot bleibt auf dem Stand VOR Upload-Start stehen - (alle Jobs `preview`). -- **RC-2 (unzuverlässiger Close-Flush):** beforeunload-Sync-Flush existiert - (app.js:4605) und fängt den sauberen Close ab. Bei hartem Kill / Crash / OS-Kill - läuft er nicht → der stale Snapshot bleibt liegen. -- **RC-3 (Dedup-Asymmetrie):** `_autoDeduplicateFromLog` droppt beim Start nur Jobs - mit Status `done`. Die Ghosts aus dem stale Snapshot stehen aber als `preview` da - → werden NICHT gedroppt → fertige Dateien erscheinen erneut. +## Plan +- [ ] lib/ip-allowlist.js — fail-closed allowlist (normalizeIp/::ffff:, isLoopback, ipv4ToInt, matchIpRule exact+CIDR+wildcard, evaluateClientAllowed: loopback always, empty=loopback-only). + unit tests. +- [ ] remote-server.js — accept config.allowlist; reject non-allowlisted peers at connection (close 4005). + protocol test. +- [ ] config-store.js — diagnostics subtree: bindMode ('local'), publicHost (''), allowlist ([]). +- [ ] main.js — bindMode->host (local=127.0.0.1, network=0.0.0.0, network requires non-empty allowlist); buildDiagnosticCode with host (h); getSuggestedRemoteHosts (os.networkInterfaces); pass allowlist; IPC save-settings/status. +- [ ] gateway/code.js — decode h/p/t/n/fp/s (tolerant of old port/token/label). + test. +- [ ] gateway/index.js — connect_server takes host from the code; host arg optional override. +- [ ] renderer/app.js — bind-mode selector, publicHost input + suggested-host chips, allowlist textarea (network), network-requires-allowlist validation. +- [ ] docs/remote-diagnostics-setup.md — network mode + allowlist + Tailscale (set allowlist to your tailnet, e.g. 100.64.0.0/10). +- [ ] Tests: ip-allowlist unit, remote-server allowlist protocol, gateway decode, integration (network bind + allowlist), adversarial fail-closed (empty allowlist rejects non-loopback). +- [ ] Release v3.3.86 (gitea + github mirror). -## Fix (mechanismus-unabhängig, vom Kern auf) -- [x] **FIX A — Timestamp-gated Dedup (Kern-Fix, durable):** Beim Start jeden restored - Job droppen, dessen file+hoster im Log mit `ts >= floor(savedAt)` steht — egal ob - `preview` oder `done`. Fängt Ghosts auch nach hartem Kill (hängt vom Log ab, nicht - vom Snapshot). `lib/queue-dedup.js` additiver 3. Param `savedAt`; `buildPersistedQueueState` - stempelt `savedAt`; `restoreQueueStateFromConfig` merkt `_restoredSnapshotSavedAt`; - Log-Zeile → `ts` geparst; `_autoDeduplicateFromLog` reicht savedAt durch. -- [x] **FIX B — Throttle mit max-wait:** `lib/throttle-timer.js` (neu). Upload: delay 500 - + maxWait 20000 → Snapshot alle ~20s statt nie. Idle: reine Debounce. Fallback-Shim - honoriert maxWait (kein stilles Starvation-Reintro). -- [x] **FIX C — Close-Write-Härtung:** `save-global-settings-sync` renameSync-Retry bei - EBUSY/EPERM/EACCES + pid-unique tmp + tmp-cleanup. Startup-Sweep `_sweepOrphanConfigTmps` - räumt verwaiste `..tmp` toter PIDs (gegen Orphan-Akkumulation). -- [x] **Seam-Extraktion (Advisor #2):** `lib/upload-log.js` (neu) — `formatUploadLogLine` - + `parseUploadLogLine` aus main.js gezogen; Test fährt den ECHTEN Writer→Reader→Gate- - Vertrag (kein Mirror) → fängt künftige Format-/Epoch-Brüche. +## Review (done) +All steps implemented and verified. lib/ip-allowlist.js (fail-closed, ::ffff:, CIDR incl. +100.64.0.0/10) + 8 unit tests. remote-server.js rejects non-allowlisted peers (close 4005), +opt-in via config.allowlist (existing remote-control unaffected) + 2 protocol tests (fail-closed +wiring + loopback-always-allowed). Code now carries the host (mhu1_{v,h,p,t,n,fp?,s?}); gateway +decode is tolerant of the legacy long keys; connect_server takes the host from the code (host arg +optional override) — proven end-to-end by the integration harness connecting with NO host arg. +Renderer: bind-mode selector + public-host input + suggested-host chips + allowlist textarea + +network-requires-allowlist validation. Docs rewritten for Tailscale (set allowlist to the tailnet, +put the Tailscale IP/MagicDNS in the code address). 393 app tests + 9 gateway tests + e2e + +integration + adversarial all green, lint 0 errors. -## Tests -- [x] `tests/throttle-timer.test.js`: Starvation ohne maxWait → 0 Fires; mit maxWait → - periodische Fires; last-write-wins (distinct fn); flushSync/cancel. -- [x] `tests/queue-dedup.test.js`: ts>=savedAt→DROP; ts loopback only). The +allowlist (real socket peer, ::ffff: normalized, CIDR) + token are the gate; the tunnel +(Tailscale/WireGuard) is the confidentiality layer. Plaintext ws:// — document the trust boundary.