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:
parent
0c6c502aab
commit
8dff455062
115
tasks/todo.md
115
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 `<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.
|
||||
## 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<savedAt→KEEP; same-second→DROP;
|
||||
max-ts; Multi-Hoster Teilabschluss (reale Bug-Form); ohne savedAt/ohne ts→Legacy.
|
||||
- [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.
|
||||
## Security model shift
|
||||
v3.3.85 hard-locked loopback. This change replaces that with the downloader's model: network bind
|
||||
(0.0.0.0) is allowed ONLY with a non-empty fail-closed IP allowlist (empty => 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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user