docs(todo): Tailscale network-bind + fail-closed allowlist plan + review

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-06-19 19:35:38 +02:00
parent 0c6c502aab
commit 8dff455062

View File

@ -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 Goal: make the MHU read-only diagnostics reachable like the Real-Debrid-Downloader does over
User: 300 Dateien, 100 übrig, Programm schließen + öffnen → manchmal sind bereits Tailscale — host embedded in the connection code, two bind modes (local / network), and a
fertig hochgeladene Dateien wieder in der Liste. 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) ## Plan
- **RC-1 (Persist-Starvation, code-confirmed):** `persistQueueStateSoon()` setzt bei - [ ] lib/ip-allowlist.js — fail-closed allowlist (normalizeIp/::ffff:, isLoopback, ipv4ToInt, matchIpRule exact+CIDR+wildcard, evaluateClientAllowed: loopback always, empty=loopback-only). + unit tests.
jedem Progress-Event den Timer per `clearTimeout` zurück; Delay während Upload war - [ ] remote-server.js — accept config.allowlist; reject non-allowlisted peers at connection (close 4005). + protocol test.
10000ms. Progress-Events feuern öfter als alle 10s → Timer feuert NIE während eines - [ ] config-store.js — diagnostics subtree: bindMode ('local'), publicHost (''), allowlist ([]).
aktiven Uploads. Der Disk-Snapshot bleibt auf dem Stand VOR Upload-Start stehen - [ ] 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.
(alle Jobs `preview`). - [ ] gateway/code.js — decode h/p/t/n/fp/s (tolerant of old port/token/label). + test.
- **RC-2 (unzuverlässiger Close-Flush):** beforeunload-Sync-Flush existiert - [ ] gateway/index.js — connect_server takes host from the code; host arg optional override.
(app.js:4605) und fängt den sauberen Close ab. Bei hartem Kill / Crash / OS-Kill - [ ] renderer/app.js — bind-mode selector, publicHost input + suggested-host chips, allowlist textarea (network), network-requires-allowlist validation.
läuft er nicht → der stale Snapshot bleibt liegen. - [ ] docs/remote-diagnostics-setup.md — network mode + allowlist + Tailscale (set allowlist to your tailnet, e.g. 100.64.0.0/10).
- **RC-3 (Dedup-Asymmetrie):** `_autoDeduplicateFromLog` droppt beim Start nur Jobs - [ ] Tests: ip-allowlist unit, remote-server allowlist protocol, gateway decode, integration (network bind + allowlist), adversarial fail-closed (empty allowlist rejects non-loopback).
mit Status `done`. Die Ghosts aus dem stale Snapshot stehen aber als `preview` da - [ ] Release v3.3.86 (gitea + github mirror).
→ werden NICHT gedroppt → fertige Dateien erscheinen erneut.
## Fix (mechanismus-unabhängig, vom Kern auf) ## Review (done)
- [x] **FIX A — Timestamp-gated Dedup (Kern-Fix, durable):** Beim Start jeden restored All steps implemented and verified. lib/ip-allowlist.js (fail-closed, ::ffff:, CIDR incl.
Job droppen, dessen file+hoster im Log mit `ts >= floor(savedAt)` steht — egal ob 100.64.0.0/10) + 8 unit tests. remote-server.js rejects non-allowlisted peers (close 4005),
`preview` oder `done`. Fängt Ghosts auch nach hartem Kill (hängt vom Log ab, nicht opt-in via config.allowlist (existing remote-control unaffected) + 2 protocol tests (fail-closed
vom Snapshot). `lib/queue-dedup.js` additiver 3. Param `savedAt`; `buildPersistedQueueState` wiring + loopback-always-allowed). Code now carries the host (mhu1_{v,h,p,t,n,fp?,s?}); gateway
stempelt `savedAt`; `restoreQueueStateFromConfig` merkt `_restoredSnapshotSavedAt`; decode is tolerant of the legacy long keys; connect_server takes the host from the code (host arg
Log-Zeile → `ts` geparst; `_autoDeduplicateFromLog` reicht savedAt durch. optional override) — proven end-to-end by the integration harness connecting with NO host arg.
- [x] **FIX B — Throttle mit max-wait:** `lib/throttle-timer.js` (neu). Upload: delay 500 Renderer: bind-mode selector + public-host input + suggested-host chips + allowlist textarea +
+ maxWait 20000 → Snapshot alle ~20s statt nie. Idle: reine Debounce. Fallback-Shim network-requires-allowlist validation. Docs rewritten for Tailscale (set allowlist to the tailnet,
honoriert maxWait (kein stilles Starvation-Reintro). put the Tailscale IP/MagicDNS in the code address). 393 app tests + 9 gateway tests + e2e +
- [x] **FIX C — Close-Write-Härtung:** `save-global-settings-sync` renameSync-Retry bei integration + adversarial all green, lint 0 errors.
EBUSY/EPERM/EACCES + pid-unique tmp + tmp-cleanup. Startup-Sweep `_sweepOrphanConfigTmps`
räumt verwaiste `<config>.<pid>.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.
## Tests ## Security model shift
- [x] `tests/throttle-timer.test.js`: Starvation ohne maxWait → 0 Fires; mit maxWait → v3.3.85 hard-locked loopback. This change replaces that with the downloader's model: network bind
periodische Fires; last-write-wins (distinct fn); flushSync/cancel. (0.0.0.0) is allowed ONLY with a non-empty fail-closed IP allowlist (empty => loopback only). The
- [x] `tests/queue-dedup.test.js`: ts>=savedAt→DROP; ts<savedAtKEEP; same-secondDROP; allowlist (real socket peer, ::ffff: normalized, CIDR) + token are the gate; the tunnel
max-ts; Multi-Hoster Teilabschluss (reale Bug-Form); ohne savedAt/ohne ts→Legacy. (Tailscale/WireGuard) is the confidentiality layer. Plaintext ws:// — document the trust boundary.
- [x] `tests/upload-log.test.js`: realer Writer→Reader-Roundtrip + Seam-Drop/Keep.
- [x] 334/334 grün, ESLint clean, Smoke-Boot identisch zu Baseline (kein Regress).
## Review
- **Adversariale Multi-Agent-Review (4 Dimensionen, 15 Findings):** 14 refuted (meist
"ist korrekt"-Bestätigungen, Kommentar-Drift, Test-Härtungs-Vorschläge). 1 confirmed
(LOW): pid-unique tmp konnte bei Hard-Kill zwischen write und rename verwaisen → mit
Startup-Sweep behoben. Stale-Kommentare (queue-dedup Header + _autoDeduplicateFromLog)
auf die Zwei-Regel-Logik korrigiert.
- **Was bewiesen ist:** Komponenten-Logik (Unit-Tests inkl. realer Format-Seam),
Code-getraceter Wiring-Pfad, adversariale Gegenprüfung. Der Fix ist
MECHANISMUS-UNABHÄNGIG: greift egal ob der stale Snapshot von Starvation, einem
Mid-Upload-Close-Race ODER einem Hard-Kill kommt.
- **Ehrliche Einschränkung:** KEIN Live-Repro mit echtem byse-Key (Key unter anderem
Windows-Profil verschlüsselt, nicht entschlüsselbar). Symptom tritt nur auf bei
Close WÄHREND aktivem Upload oder Hard-Kill — ein sauberer Idle-Close war schon
vorher korrekt.
## Runde 2 — "noch intensiver" (v3.3.81)
- **Echte ausführbare Tests** für bisher nur logisch abgedeckte Pfade: `tests/orphan-tmp.test.js`
(Sweep-Entscheidung, extrahiert nach `lib/orphan-tmp.js`), config-store `pendingQueue`+`savedAt`
Roundtrip, `tests/queue-persistence-scenario.test.js` (exakte 300/100-Bug-Form + multi-hoster),
`tests/queue-dedup-property.test.js` (3000+500 Fuzz-Iterationen gegen die formale Invariante).
- **Breite adversariale Bug-Jagd** übers GANZE Subsystem: lief tooling-bedingt teils kaputt
(bug-analyzer ohne File-Tools, Socket-Fehler) → 3 unverifizierte Hypothesen SELBST am Code
geprüft:
- #2 (gedroppte done-Jobs reappear via buildQueuePreview) → REFUTED: `_completedUploadKeys`
(full path) == buildQueuePreview-Key; nach Gate räumt `syncSelectedFilesFromQueue` selectedFiles.
- #3 (done-File bleibt in selectedFiles → re-preview) → **REAL (Advisor-Catch) & gefixt.** Meine
erste Abweisung war zu schnell: `syncSelectedFilesFromQueue` läuft NICHT beim Mid-Upload-Close.
Bei `removeFromQueueOnDone=ON` werden fertige Jobs aus queueJobs entfernt, bleiben aber in
selectedFiles; `updateUploadView`→`buildQueuePreview` (Startup, Zeile 990) re-materialisiert sie
als Preview-Ghost NACH dem Gate → sticky. Fix: `completedSelectionKeys` (queue-dedup.js) seedet
beim Start `_completedUploadKeys` (full-path, log-basiert/hard-kill-durabel, gleiche Ambiguity-
Guard) → buildQueuePreview überspringt fertige (file|hoster)-Paare. Nur relevant bei
removeFromQueueOnDone=ON (Default OFF).
- #1 (basename-Kollision droppt PENDING Datei = Lost Work) → **REAL & gefixt.** FIX A's ts-Regel
keyt auf basename, Restore-Collapse auf full path → zwei gleichnamige Dateien aus verschiedenen
Ordnern an denselben Hoster: die geloggte droppte fälschlich auch die andere PENDING. **Ambiguity-
Guard** in queue-dedup.js: ts-Regel wird unterdrückt, wenn ein basename|hoster-Key auf mehrere
DISTINKTE Pfade zeigt (done-Regel unberührt). Fail-safe: schlimmstenfalls überlebt ein
sichtbarer Ghost, NIE stiller Datenverlust.
- **Un-gehuntete Bereiche selbst abgeklopft:** DST/Clock-Skew → Fehler nur in SICHERER Richtung
(Ghost bleibt, kein Lost Work), inhärente Grenze von Sekunden-Lokalzeit-Logs. Log-Discovery
readdir-Filter `startsWith(base)&&endsWith(ext)` fängt single/daily/session — keine verpassten
Log-Files. 353/353 grün, ESLint clean, 3× Suite ohne Flake.