Slices W-A/W-B/W-C und L-A aus UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration. Watchdog (Dead-Man's-Switch, externer Server): - WatchdogHeartbeatService als BackgroundService + DI-Singleton; jeder Sendeversuch gekapselt, ein Ausfall des Watchdogs beeintraechtigt PolyTrader nie. - Eigene Implementierung statt Test-Client des Fremdprojekts: TLS-Pruefung bleibt aktiv, http:// nur fuer localhost (Agent-Token nicht im Klartext ins Netz). - Status aus dem App-Log abgeleitet (Error mit 5-Minuten-Sticky-Fenster, entprellt), Lifecycle-Events started/stopping. - Konfiguration in ServerSettings; Agent-Token [Browsable(false)] mit maskierter Eingabe + Statusanzeige, bei gesetztem Master-Key verschluesselt (enc:v1:). Lizenz (LicenseLabrador, Ed25519): - LicenseGate.RunStartupGate prueft beim Start; bei ungueltiger Lizenz wird die Modulliste leer gebaut, sodass nur die Core-Shell (Terminal/Einstellungen) startet. Bewusst kein Environment.Exit - ein Trading-Bot darf nicht mitten im Lauf hart sterben. TamperSuspected gilt als nicht nutzbar. - LicenseDialog (partial + .Designer.cs) fuer Start- und Verwalten-Modus, mit Hardware-ID zum Kopieren; Smoke-UI konstruiert beide Modi headless. - Master-Key wird jetzt VOR dem Host-Build geladen, da auch der Lizenzschluessel entschluesselt werden muss; derselbe TerminalLogger wird als Singleton weitergereicht, damit die Startmeldungen im Terminal-Fenster erscheinen. Der Lizenz-SDK-Client wird per Cross-Repo-Projektreferenz auf ..\..\LicenseLabrador eingebunden, damit SDK-Fixes ohne Kopie einfliessen. 484 Zeilen Tests fuer den Heartbeat; Suite gruen (438 Tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
430 lines
32 KiB
Markdown
430 lines
32 KiB
Markdown
# Umsetzungsplan: Watchdog & LicenseLabrador in PolyTrader einbinden
|
||
|
||
> Stand: 2026-07-28 · Grundlage: Review von `J:\Softwareprojekte\WatchDog` und
|
||
> `J:\Softwareprojekte\LicenseLabrador`.
|
||
> **Reihenfolge ist bindend:** Teil A (Blocker in den Fremdprojekten) vor Teil B (Einbindung).
|
||
> Wer B ohne A baut, bindet bekannte Lücken fest in den Produktivbetrieb ein.
|
||
|
||
---
|
||
|
||
## 0. Kurzfassung der Bewertung
|
||
|
||
**LicenseLabrador (Server)** ist handwerklich gut: keine Secrets im Repo, Argon2id, CSRF in allen
|
||
Admin-Formularen, durchgängig Prepared Statements, Ed25519 über den Base64-Payload (umgeht das
|
||
Kanonikalisierungsproblem elegant), Nonce-Reflexion, Rate-Limit plus Failed-Key-Sperre, Audit-Log.
|
||
Der Server kann so bleiben. Die Schwächen liegen im **C#-Client**.
|
||
|
||
**Watchdog** ist funktional weit, hat aber Produktiv-Secrets im Git, ein hartkodiertes
|
||
Admin-Passwort und eine Autorisierung, die jeden Agent-Token wie einen Admin behandelt. Vor der
|
||
Anbindung von PolyTrader muss dort aufgeräumt werden.
|
||
|
||
Kritischster Einzelbefund für **unseren** Betrieb: die Hardware-ID von LicenseLabrador ist auf
|
||
einem Rechner mit Mullvad-VPN nicht stabil (siehe L-1). Ohne Fix sperrt PolyTrader sich selbst aus.
|
||
|
||
---
|
||
|
||
## 0a. Nachprüfung vom 29.07.2026
|
||
|
||
Beide Projekte wurden nach dem ersten Review nachgebessert. Stand jetzt:
|
||
|
||
**LicenseLabrador — integrationsbereit.** Die vier Blocker sind behoben (Änderungen liegen noch
|
||
uncommitted im Arbeitsverzeichnis, sollten also committet werden):
|
||
|
||
- **L-1 erledigt.** `HardwareId` nutzt jetzt `MachineGuid` (bzw. `/etc/machine-id`) als
|
||
Primärquelle, `ProcessorCount` ist raus, der MAC-Fallback filtert virtuelle Adapter
|
||
(Mullvad, WireGuard, Docker, Hyper-V, WSL) und sortiert deterministisch. Damit ändert ein
|
||
VPN-Wechsel die Hardware-ID nicht mehr — die Selbst-Aussperrung ist vom Tisch.
|
||
- **L-2 erledigt.** Der gecachte Envelope wird gegen die lokale `hardware_id` geprüft; kopierte
|
||
`state.dat`-Dateien werden verworfen.
|
||
- **L-3 erledigt.** `signatureTampered` liefert jetzt `TamperSuspected` statt still in die
|
||
Offline-Kulanz zu fallen.
|
||
- **L-5 erledigt.** Das HMAC-Secret wird aus der Hardware-ID abgeleitet, und `docs/SECURITY.md`
|
||
führt es korrekt als Laufzeit-Schutz gegen Property-Patching, nicht als kryptografische
|
||
Sicherheit.
|
||
- Das SDK baut fehlerfrei (eine `CS8604`-Warnung in `LicenseClient.cs:47`).
|
||
- **Offen:** L-4 (gecachte Endpoints haben weiter Vorrang vor der Konfiguration, `http://` wird
|
||
weiter akzeptiert) sowie L-6 bis L-13. L-4 entschärfen wir auf unserer Seite, indem wir
|
||
ausschließlich `https://`-Endpoints konfigurieren — da die Antwort signaturgeprüft ist, kann ein
|
||
untergeschobener Endpoint keine gültige Lizenz erfinden, sondern uns höchstens in die Offline-
|
||
Kulanz drängen. Kein Blocker mehr, aber Nachtrag wert.
|
||
|
||
**Watchdog — integrationsbereit mit zwei Auflagen.** Commits `1e453f8` und `404e184` haben die
|
||
Blocker abgearbeitet; nachgeprüft am 29.07.2026:
|
||
|
||
- **Erledigt in `1e453f8`:** W-2 (Backdoor entfernt, nur noch `password_verify`), W-3 (CSRF in
|
||
`admin.php` und `index.php`), W-4 (alle XSS-Vektoren escaped, inklusive `e.message`), **W-5**
|
||
(Token-Scoping greift: `validateToken()` wird benutzt, `$requireAdmin()` schützt `monitor/edit`,
|
||
`monitor/delete`, `icon/upload`), W-6 (Status-API nicht mehr offen), **W-13** (Alarm nur noch
|
||
bei Zustandswechsel, zusätzlich 120 s Dämpfung pro Quelle und Art).
|
||
- **W-14 erledigt in `404e184`.** `Evaluator.php` setzt einen Monitor nur noch auf `up`, wenn
|
||
`last_status === 'ok'` **und** die Metriken unauffällig sind. Zustandsmaschine
|
||
durchgespielt: dauerhaftes `status=error` bleibt auf `error` (ein Alarm, danach Ruhe);
|
||
Rückkehr auf `ok` erzeugt genau eine Entwarnung; Absturz → genau ein Down-Alarm;
|
||
`down` + Rückkehr mit `status=error` → ein `hard_error`. Die Flapping-Schleife ist weg.
|
||
- **W-22 erledigt.** `escapeHtml` liegt jetzt auf Dateiebene (`index.php:630`) vor allen
|
||
Verwendungen. Monitore ohne Metriken — unser Fall — brechen im Detail-Fenster nicht mehr ab.
|
||
- **W-23 erledigt.** `session_start()` steht am Anfang von `api.php`; der Admin-Session-Zweig
|
||
funktioniert.
|
||
- **W-24 erledigt.** `display_errors` und Stack-Traces hängen an `WATCHDOG_DEBUG=1`; API-Fehler
|
||
liefern sauberes JSON ohne interne Details.
|
||
- **W-1 teilweise erledigt — Auflage 1.** Aus dem getrackten `config/config.php` sind die echten
|
||
Werte raus (Platzhalter), `config.example.php` ist ergänzt, `config.local.php` war schon
|
||
gitignoriert und die Merge-Logik ist intakt. **Aber die Secrets stehen weiter in der
|
||
Git-History:** `git show 1e453f8:config/config.php` liefert DB-Passwort, `shared_key` und
|
||
`setup_token` im Klartext. Das lässt sich durch Bearbeiten von HEAD nicht heilen — **alle drei
|
||
müssen rotiert werden** (das FTP-Passwort aus `deploy_config.json` gleich mit, es ist dasselbe).
|
||
- **Auflage 2 — W-16 (UTC vs. `NOW()`).** Unverändert: `cli/tick.php:39,44,106`,
|
||
`ProxmoxPoller.php:56,117` und `admin.php:115,157` mischen UTC-Strings aus PHP mit dem
|
||
`NOW()` der DB-Session. Steht die MySQL-Zeitzone nicht auf UTC, verschieben sich
|
||
Job-Fälligkeit, Lock-Leases und der Log-Cleanup um den Offset. Entweder in der DB-Session
|
||
`SET time_zone = '+00:00'` setzen oder `NOW()` durchgängig durch UTC-Parameter ersetzen.
|
||
Nicht integrationsblockend, aber vor dem Produktivbetrieb zu klären.
|
||
- **Weiterhin offen, ohne Einfluss auf unsere Anbindung:** W-11 (`api.php:157` gibt im eigenen
|
||
`catch` weiter `$t->getMessage()` heraus — der neue Handler in `Autoload.php` macht es richtig,
|
||
wird hier aber umgangen), W-15 (`intervalSeconds` der PS-/Bash-Agents wird weiter ignoriert;
|
||
unser C#-Client sendet `interval` und ist korrekt), **W-17** (`self_ping` ist weiter ein
|
||
`echo`-Stub — es gibt also keinen Alarm, wenn der Watchdog selbst stirbt), W-7 bis W-10, W-12,
|
||
W-18 bis W-21.
|
||
|
||
**Konsequenz für die Reihenfolge:** Schritt 1 und 2 sind erledigt. **Teil B kann vollständig
|
||
beginnen** — B.1 bis B.3 (Heartbeat) und B.4/B.5 (Lizenz) sind nicht mehr blockiert. Parallel
|
||
abzuarbeiten: Secrets rotieren, W-16, W-17.
|
||
|
||
---
|
||
|
||
## Teil A — Blocker in den Fremdprojekten
|
||
|
||
### A.1 Watchdog — Sicherheit
|
||
|
||
| # | Befund | Datei | Maßnahme |
|
||
|---|---|---|---|
|
||
| W-1 | DB-Passwort, `shared_key`, `setup_token` als Defaults im **getrackten** `config/config.php` (auch in der Git-History) | `config/config.php:15,19,20` | Datei nach `config.php.example` umbenennen (Muster von LicenseLabrador), aus dem Index nehmen, **alle drei Secrets rotieren** |
|
||
| W-2 | Hartkodiertes Admin-Passwort, vor der DB-Prüfung ausgewertet | `public/admin.php:31` | Ersatzlos entfernen; nur noch `users`-Tabelle + `password_verify` |
|
||
| W-3 | Kein CSRF-Schutz im Admin — Monitor löschen, Proxmox-Target anlegen, Token widerrufen sind cross-site auslösbar | `public/admin.php` (alle POST-Zweige) | `Csrf`-Klasse aus LicenseLabrador übernehmen (1:1 portierbar) |
|
||
| W-4 | Stored XSS: Heartbeat-`message` und Disk-Namen landen ungefiltert per `innerHTML` im Dashboard | `public/index.php:707,731,744,770` | `textContent` statt `innerHTML`, oder clientseitig escapen |
|
||
| W-5 | Jeder gültige Agent-Token autorisiert **jeden** Endpunkt — auch `/api/monitor/delete`, `/api/monitor/edit`, `/api/link`, `/api/icon/upload` | `public/api.php:66-77` | Zwei Berechtigungsstufen trennen: Agent-Token nur für `heartbeat`/`event`; verwaltende Routen nur mit Admin-Session oder `shared_key`. `TokenManager::validateToken()` (prüft die Source-Bindung) wird derzeit **nirgends** aufgerufen — einbinden |
|
||
| W-6 | `/api/status`, `/api/events`, `/api/lineage` sind bewusst unauthentifiziert und zusätzlich von der Basic-Auth ausgenommen | `public/api.php:60`, `.htaccess:6` | Für uns nicht akzeptabel: „polytrader ist seit 14:03 down" ist eine verwertbare Information. Lesezugriff hinter Session **oder** eigenen Read-Token legen |
|
||
| W-7 | Installer-Download läuft **vor** dem Login-Check; `{{TOKEN}}`/`{{SOURCE}}` werden ungefiltert in ein PowerShell-Skript interpoliert, das als SYSTEM ausgeführt wird | `public/admin.php:58-84` | Hinter `$isLoggedIn` verschieben, Platzhalter strikt validieren (`[A-Za-z0-9_-]`), Host nicht aus `HTTP_HOST` ableiten |
|
||
| W-8 | Agent-Token als **maschinenweite** Umgebungsvariable, Default-URL `http://` | `agents/windows/install.ps1.tmpl:104`, `agents/*/watchdog-agent.*:9,4` | Token in eine ACL-geschützte Datei unter `C:\ProgramData\Watchdog`; HTTPS erzwingen |
|
||
| W-9 | SVG-Upload in denselben Origin, `mkdir(0777)` | `public/api.php:284-296`, `admin.php:240` | SVG entfernen oder als `Content-Disposition: attachment` ausliefern; `0755` |
|
||
| W-10 | Proxmox: `allowSelfSigned = true` als Default, `cert_fingerprint` nie benutzt, Token-Secret im Klartext in der DB | `src/ProxmoxClient.php:18,76`, `schema:87` | Fingerprint-Pinning umsetzen (steht so in HANDOFF §8), Secret verschlüsseln |
|
||
| W-11 | `500`-Antwort gibt `$t->getMessage()` an den Client (DB-Fehlerdetails) | `public/api.php:141-144` | Generische Meldung, Details ins `event_log` |
|
||
| W-12 | FTP-Deploy-Passwort im Klartext, identisch mit DB- und Admin-Passwort | `scripts/deploy_config.json` | Eigenes Passwort, rotieren |
|
||
|
||
### A.2 Watchdog — funktionale Fehler
|
||
|
||
| # | Befund | Wirkung |
|
||
|---|---|---|
|
||
| W-13 | `api.php:120` alarmiert bei **jedem** Heartbeat mit `status=error`/`warning` sofort | Ein dauerhaft fehlerhafter Dienst im 30-s-Takt erzeugt 120 Telegram-Nachrichten pro Stunde. Widerspricht §6 des Umsetzungsplans („nur bei Zustandswechsel") |
|
||
| W-14 | `upsertHeartbeat` erzwingt `state = IF(state="down","up",…)`, der Evaluator setzt bei frischem Heartbeat zusätzlich `error → up` („recovered") | Ein Dienst, der dauerhaft `status=error` meldet, pendelt im Minutentakt zwischen „recovered" und „hard_error" — mit Alarm bei jedem Wechsel |
|
||
| W-15 | Agents senden `intervalSeconds`, die API liest nur `interval`/`expected_interval_sec` | Der gemeldete Takt wird still verworfen, es bleibt bei 60 s. Der C#-Client sendet `interval` und ist zufällig korrekt |
|
||
| W-16 | PHP schreibt UTC-Strings, MySQL vergleicht mit `NOW()` (Session-Zeitzone) | Job-Fälligkeit, Lock-Leases und Log-Cleanup verschieben sich um den UTC-Offset, sofern die DB nicht auf UTC steht. Betrifft `cli/tick.php:39,44,106` und `admin.php:108` |
|
||
| W-17 | `self_ping` ist ein `echo`-Stub, `proxmox_smart` hat keinen Handler, `first_contact_deadline_utc` / `expect_running` / `ack_until_utc` werden nie ausgewertet | Der „Wer wacht über den Wächter"-Mechanismus fehlt komplett; der Nie-Start-Alarm ebenfalls. Beides sind Abnahmekriterien aus dem Plan |
|
||
| W-18 | `warning_digest` schickt HTML durch `notifyImmediate`, das per `htmlspecialchars` escaped | Im Telegram erscheinen `<b>`-Tags als Text |
|
||
| W-19 | Nur der Heartbeat-Zweig liegt im `try/catch`, danach läuft der Code ohne Fehlerbehandlung weiter | Uneinheitliche Fehlerbilder |
|
||
| W-20 | Kein Rate-Limit auf `/api/heartbeat` (Plan §14 fordert es) | Ein durchdrehender Client flutet die DB |
|
||
| W-21 | Keine Tests im gesamten Projekt (Plan §13 fordert Unit-Tests der Zustandsmaschine) | W-13/W-14 wären mit einem Test aufgefallen |
|
||
|
||
**Mindestumfang vor der Einbindung:** W-1, W-2, W-5, W-13, W-14, W-16. Ohne W-13/W-14 ist der
|
||
Watchdog für einen produktiven Trading-Bot unbrauchbar, weil jeder echte Fehler in einer
|
||
Nachrichtenflut untergeht.
|
||
|
||
### A.3 LicenseLabrador — Client
|
||
|
||
| # | Befund | Datei | Maßnahme |
|
||
|---|---|---|---|
|
||
| **L-1** | **Hardware-ID ist instabil.** Die „erste" aktive Netzwerkkarte wird per `FirstOrDefault` ohne stabile Sortierung gewählt, dazu `ProcessorCount` | `HardwareId.cs:47`, `:40` | **Blocker für uns.** Mullvad, Hyper-V und Docker legen virtuelle Adapter an und wieder ab — jede Änderung erzeugt eine neue HWID, damit eine neue Aktivierung, damit `max_activations` (Default 2) erschöpft: PolyTrader sperrt sich selbst aus. Fix: nur `MachineGuid` als Primärquelle; MAC nur als Fallback, wenn die Registry nichts liefert, und dann sortiert und auf physische Adapter gefiltert; `ProcessorCount` entfernen |
|
||
| L-2 | Offline-Cache ist maschinen-portabel: geprüft wird nur `payload.license_key == licenseKey`, **nicht** `hardware_id` und nicht `product` | `LicenseClient.cs:195` | Wer `state.dat` von einer aktivierten Maschine kopiert und das Netz blockiert, läuft bis zu 168 h auf beliebig vielen Rechnern. Der wirksamste Angriff auf das gesamte System — und mit zwei Vergleichen behoben |
|
||
| L-3 | Erkannte Signaturmanipulation (`signatureTampered`) wird verworfen und fällt still in die Offline-Kulanz | `LicenseClient.cs:132-135` | `LicenseState.TamperSuspected` zurückgeben und protokollieren |
|
||
| L-4 | Kein TLS-Zwang; Endpoints aus dem **Cache** haben Vorrang vor der Konfiguration | `LicenseClient.cs:276-292`, `:342` | `http://`-Endpoints ablehnen; konfigurierte Endpoints zuerst versuchen, gecachte nur als Ergänzung |
|
||
| L-5 | HMAC-„Härtung" mit Klartext-Konstante im Assembly, geprüft mit demselben Prozess-Secret | `LicenseClient.cs:23,172` | Schützt gegen nichts, was ein Angreifer nicht in derselben Sitzung mitpatcht. Als Obfuskation behalten, aber in `docs/SECURITY.md` nicht mehr als Schutzmaßnahme führen |
|
||
| L-6 | Offline-`.lic` ohne Hardware-Bindung bzw. ohne Key gilt für **jede** Maschine bzw. **jeden** Key | `LicenseClient.cs:232,242`, `offline_license.php:62` | Im Admin-Dialog `hardware_id` verpflichtend machen (Ausnahme nur mit expliziter Warnung); ein geleaktes Notfall-File ist sonst ein Universal-Crack bis `valid_until` |
|
||
| L-7 | Session-Cookie ohne `secure`-Flag | `server/src/Auth.php:11` | `'secure' => true` + HTTPS erzwingen |
|
||
| L-8 | `key_id` wird signiert und übertragen, vom Client aber ignoriert | `Signature.cs` | Ohne Auswertung ist keine Schlüsselrotation möglich, ohne alle Clients gleichzeitig zu tauschen. Für uns momentan verschmerzbar, aber notieren |
|
||
| L-9 | `LicenseClient` erzeugt einen `HttpClient` pro Instanz und ist nicht `IDisposable` | `LicenseClient.cs:28` | Socket-Leak bei wiederholter Instanziierung; `HttpClient` injizierbar machen |
|
||
| L-10 | `app_version` hartkodiert `"1.0.0"` | `LicenseClient.cs:325` | Aus `LicenseConfig` befüllen — der Server pflegt die Spalte bereits |
|
||
| L-11 | `strtotime()` liefert bei ungültigem Datum `false` → `valid_until: false` im JSON → Deserialisierung wirft → stilles „keine Lizenz" | `offline_license.php:55` | Datum validieren |
|
||
| L-12 | Nur 5 Unit-Tests; `EvaluateOfflineFallback`, Statusabbildung und Grace-Ablauf sind ungetestet | `ClientTests.cs` | Tests für L-1/L-2 mitliefern |
|
||
| L-13 | `create_admin.php`: Minimum 6 Zeichen, Passwort wird im Klartext angezeigt | `server/tools/create_admin.php` | Minimum 12, Eingabe verdecken |
|
||
|
||
**Mindestumfang vor der Einbindung:** L-1, L-2, L-3, L-4.
|
||
|
||
---
|
||
|
||
## Teil B — Einbindung in PolyTrader
|
||
|
||
### B.0 Leitentscheidungen
|
||
|
||
1. **Eigene Client-Implementierung für den Watchdog.** Der mitgelieferte `WatchdogTestClient`
|
||
deaktiviert die Zertifikatsprüfung vollständig (`WatchdogClient.cs:42`). Wir schreiben einen
|
||
schlanken `WatchdogHeartbeatService` in `PolyTrader.Core` — der HTTP-Vertrag ist trivial
|
||
(ein POST mit JSON und einem Header) und wir behalten die Kontrolle über TLS, Timeouts und
|
||
Fehlerverhalten.
|
||
2. **LicenseLabrador.Client als Projektreferenz**, nicht als kopierter Code — dann fließen
|
||
Fixes aus Teil A.3 automatisch ein. Voraussetzung: L-1 bis L-4 sind dort behoben.
|
||
3. **Kein `Environment.Exit` bei ungültiger Lizenz.** `docs/INTEGRATION.md` schlägt das vor; für
|
||
einen Trading-Bot mit offenen Positionen ist ein harter Abbruch gefährlich. Stattdessen:
|
||
Trading-Module gar nicht erst starten (Startfall) bzw. geordnet stoppen (Laufzeitfall).
|
||
4. **Secrets über `SecretProtection`.** Watchdog-Token und Lizenzschlüssel werden mit dem
|
||
bestehenden AES-GCM-Master-Key (`POLYTRADER_MASTER_KEY`) verschlüsselt abgelegt — dieselbe
|
||
Mechanik wie für Wallet-Keys.
|
||
5. **Heartbeat-Metriken bleiben schlank.** Version, aktive Module, Uptime. **Keine** Kontostände,
|
||
Positionen, Wallet-Adressen oder Trader-Namen — der Watchdog-Server liegt beim Hoster und
|
||
seine Status-API war bis W-6 öffentlich lesbar.
|
||
|
||
### B.1 Slice W-A — Heartbeat-Grundgerüst ✅ ERLEDIGT (29.07.2026)
|
||
|
||
Umgesetzt in [WatchdogHeartbeatService.cs](../../src/PolyTrader.Core/Services/WatchdogHeartbeatService.cs)
|
||
mit 21 Tests in [WatchdogHeartbeatTests.cs](../../tests/PolyTrader.Tests/WatchdogHeartbeatTests.cs).
|
||
Abweichungen und Ergänzungen gegenüber der Planung unten:
|
||
|
||
- **TLS-Zwang implementiert:** `TryBuildEndpoint` akzeptiert nur `https://`; `http://` ausschließlich
|
||
für Loopback-Adressen (lokales Testen). Damit kann der Agent-Token nicht versehentlich im
|
||
Klartext übertragen werden — der Grund, warum der mitgelieferte Testclient nicht verwendet wird.
|
||
- **Intervall wird nach unten begrenzt** (10 s), damit ein Fehlkonfigurierter Client den Watchdog
|
||
nicht flutet (W-20 ist serverseitig weiter offen).
|
||
- **Log-Entprellung:** nur der erste Fehlversuch und die Erholung werden protokolliert, nicht
|
||
jeder Tick. Sonst hätte ein längerer Watchdog-Ausfall das Terminal geflutet.
|
||
- **Settings-Quelle injizierbar** (`Func<ServerSettings>`), damit der Dienst ohne Datei testbar ist.
|
||
- **Token darf verschlüsselt sein:** `SecretProtection.Unprotect` reicht Klartext unverändert
|
||
durch und entschlüsselt `enc:v1:…`, wenn ein Master-Key gesetzt ist. Kein neuer Fehlerpfad,
|
||
wenn kein Master-Key existiert.
|
||
- **UI — vollständig über das Settings-Fenster bedienbar:** Enabled, URL, Source, Instance und
|
||
Intervall erscheinen automatisch im bestehenden `PropertyGrid` (Kategorie „Watchdog Monitoring").
|
||
Der **Agent-Token** ist bewusst `[Browsable(false)]` und wird über den Designer-Button
|
||
„Watchdog-Token setzen …" mit **maskierter Eingabe** gepflegt — kein Secret offen im Fenster und
|
||
kein Handanlegen an der `server_settings.xml`. Bei gesetztem Master-Key wird er über
|
||
`SecretProtection.Protect` verschlüsselt abgelegt, sonst mit deutlichem Hinweis im Klartext.
|
||
Die schreibgeschützte Anzeige `WatchdogTokenStatus` zeigt „nicht gesetzt" /
|
||
„gesetzt (verschlüsselt)" / „gesetzt (Klartext)". Zweiter Button: „Test-Heartbeat senden" —
|
||
nutzt die **gespeicherten** Werte, damit ein Erfolg keine Konfiguration bestätigt, die nicht
|
||
auf der Platte liegt.
|
||
- **Änderungen greifen ohne Neustart.** `ExecuteAsync` wertet die Konfiguration in jedem Durchlauf
|
||
neu aus (1-s-Poll, Sendezeitpunkt über eine Fälligkeit, der Takt bleibt also exakt) statt einen
|
||
`PeriodicTimer` mit fest verdrahtetem Intervall zu benutzen. Ein-/Ausschalten, Takt-, URL- und
|
||
Token-Änderungen wirken sofort nach dem Speichern; Zustandswechsel werden je Ursache genau
|
||
einmal geloggt. Zwei Tests fixieren das Live-Ein- und -Ausschalten.
|
||
|
||
Ursprüngliche Planung:
|
||
|
||
**Neu:** `src/PolyTrader.Core/Services/WatchdogHeartbeatService.cs` (`BackgroundService`)
|
||
|
||
- `ExecuteAsync`: `PeriodicTimer` im konfigurierten Takt (Default 60 s). **Jeder** Sendeversuch in
|
||
`try/catch` — ein nicht erreichbarer Watchdog darf PolyTrader niemals beeinträchtigen.
|
||
- Payload: `source`, `instance`, `type: "heartbeat"`, `status`, `message`, `interval`
|
||
(**nicht** `intervalSeconds`, siehe W-15), `group`, `metadata`.
|
||
- Header `X-Watchdog-Key` mit dem entschlüsselten Agent-Token.
|
||
- `HttpClient` aus `SocketsHttpHandler` wie die übrigen Clients in `Program.cs`, mit
|
||
Standard-Zertifikatsprüfung und 10 s Timeout.
|
||
- Registrierung in `Program.cs` neben `MarketSyncService`:
|
||
`services.AddHostedService<WatchdogHeartbeatService>();`
|
||
|
||
**Konfiguration** in `ServerSettings` (`src/PolyTrader.Core/Models/ServerSettings.cs`), analog zu
|
||
den Threema-/Mullvad-Feldern:
|
||
|
||
```
|
||
WatchdogEnabled bool = false
|
||
WatchdogUrl string = "" // https:// erzwungen
|
||
WatchdogSource string = "polytrader"
|
||
WatchdogInstance string = "prod-1"
|
||
WatchdogIntervalSeconds int = 60
|
||
WatchdogToken string = "" // via SecretProtection verschlüsselt
|
||
```
|
||
|
||
Bedienung über die bestehende `SettingsView` (Designer-fähig, partial + `.Designer.cs` — siehe
|
||
UI-Regel), mit einem „Test-Heartbeat senden"-Button.
|
||
|
||
**Abnahme:** Monitor erscheint im Watchdog-Dashboard, `last_seen_utc` läuft mit. PolyTrader startet
|
||
und arbeitet unverändert, wenn `WatchdogUrl` falsch oder der Server aus ist.
|
||
|
||
### B.2 Slice W-B — Status aus dem Log ableiten ✅ ERLEDIGT (31.07.2026)
|
||
|
||
Umgesetzt im `WatchdogHeartbeatService`. Abweichungen gegenüber der Planung:
|
||
|
||
- **Nur `Error` eskaliert**, keine Warnungs-Eskalation: App-Warnungen (VPN-Reconnect, „kein
|
||
Master-Key" …) sind zu häufig, um daraus einen Monitor-Alarm zu rechtfertigen. `Warning`/`Info`
|
||
lassen den Status auf `ok`. Das vermeidet auch eine Rückkopplung, wenn der Dienst selbst warnt.
|
||
- **Entprellung über ein Nachwirkfenster** (`ErrorStickyWindow = 5 min`): Nach einem `Error` meldet
|
||
der Heartbeat so lange `status=error`, bis 5 Minuten ohne neuen Fehler vergangen sind — dann
|
||
wieder `ok`. Kein Flackern error→ok→error, Erholung wird genau einmal gemeldet. (Der 30-Minuten-
|
||
Notbehelf aus der Planung entfällt, weil W-13 im Watchdog behoben ist und ohnehin nur bei
|
||
Zustandswechsel alarmiert wird.)
|
||
- **`Sanitize()`** entfernt Wallet-Adressen/Keys (`0x…`-Hex) und verschlüsselte Secrets
|
||
(`enc:v1:…`) und kürzt auf 200 Zeichen, bevor eine Fehlermeldung an den (extern gehosteten)
|
||
Watchdog geht. Beträge werden bewusst nicht gestrippt (würde die Meldung entwerten; der Kanal ist
|
||
nur für den Betreiber).
|
||
- Angedockt über `TerminalLogger.OnLogMessage` (synchron), Abmeldung in `Dispose`.
|
||
|
||
Ursprüngliche Planung: Der Heartbeat soll `status=error` melden, wenn PolyTrader in Not ist.
|
||
|
||
- `TerminalLogger.OnLogMessage` abonnieren, Fehler der letzten `n` Intervalle zählen.
|
||
- Ableitung: `Error` im Fenster → `status=error` mit der letzten Fehlermeldung als `message`;
|
||
`Warning` → `status=warning`; sonst `ok`.
|
||
- **Entprellen ist Pflicht**, solange W-13 im Watchdog nicht behoben ist: sonst löst jeder
|
||
Heartbeat eine eigene Telegram-Nachricht aus. Bis dahin `status=error` frühestens alle 30 Minuten
|
||
erneut senden.
|
||
- Meldungstexte kürzen (≤ 200 Zeichen) und **keine** Wallet-Adressen, Keys oder Beträge
|
||
durchreichen. Ein Filter über die bekannten Secret-Präfixe gehört dazu.
|
||
|
||
### B.3 Slice W-C — Lifecycle-Events ✅ ERLEDIGT (31.07.2026)
|
||
|
||
Umgesetzt im `WatchdogHeartbeatService` (`SendEventAsync` → `POST /api/event`):
|
||
|
||
- **Start:** `kind: "started"` mit App-Version, genau einmal, sobald der Dienst aktiv wird
|
||
(in der Sendeschleife, nach `AppHost.Start()`), vor dem ersten Heartbeat.
|
||
- **Geplantes Beenden:** `StopAsync` sendet `kind: "stopped_graceful"` (best effort, 3-s-Timeout,
|
||
blockiert das Herunterfahren nie). Läuft über den bestehenden Pfad `AppHost.StopAsync()` nach
|
||
`Application.Run` + den 10-Sekunden-Bestätigungsdialog.
|
||
- Damit unterscheidet der Watchdog „sauber beendet" (Monitor → `stopped`, kein Alarm) von
|
||
„abgestürzt" (Staleness → genau ein Down-Alarm) — der eigentliche Mehrwert.
|
||
- Tests decken „started"→„stopped_graceful" über den Dienst-Lebenszyklus sowie den
|
||
`/api/event`-Vertrag ab.
|
||
|
||
**Abnahme (manuell, live):** PolyTrader über die Menüleiste beenden → Monitor geht auf `stopped`,
|
||
**kein** Alarm. PolyTrader per Task-Manager abschießen → nach `Interval × Faktor + Grace` genau
|
||
**ein** Down-Alarm.
|
||
|
||
---
|
||
|
||
**Stand 31.07.2026:** Watchdog-Seite (B.1/B.2/B.3) und Lizenz-Seite (B.4, B.5 vereinfacht)
|
||
umgesetzt; 438 Tests grün, `--smoke-ui` ok. Offen nur noch die **live**-Abnahme (echter Server)
|
||
und die Fremdprojekt-Auflagen (Watchdog-Secrets rotieren, UTC/NOW(), `self_ping`-Stub).
|
||
|
||
### B.4 Slice L-A — Lizenzprüfung beim Start ✅ ERLEDIGT (31.07.2026)
|
||
|
||
Umgesetzt in [Licensing/LicenseGate.cs](../../Licensing/LicenseGate.cs), Startdialog in
|
||
[Ui/LicenseDialog.cs](../../Ui/LicenseDialog.cs) (+`.Designer.cs`), Verdrahtung in
|
||
[Program.cs](../../Program.cs), Schlüssel-/Statusfeld in
|
||
[ServerSettings.cs](../../src/PolyTrader.Core/Models/ServerSettings.cs). Abweichungen und
|
||
Ergänzungen gegenüber der ursprünglichen Planung unten:
|
||
|
||
- **Ort:** `LicenseGate` liegt im **App-Projekt** (`PolyTraderSharp.Licensing`), nicht in Core.
|
||
Grund: `Program.Main` (App) ist der einzige Aufrufer, und so bleibt die (cross-repo)
|
||
SDK-Referenz auf `PolyTrader.App` beschränkt statt in Core alle Module zu belasten.
|
||
- **SDK-Einbindung:** `LicenseLabrador.Client` als **Cross-Repo-Projektreferenz** von
|
||
`PolyTrader.App` auf `..\..\LicenseLabrador\...` (Fixes am SDK fließen ohne Kopie ein). Baut
|
||
sauber, kein BouncyCastle-Konflikt mit Nethereum.
|
||
- **Slug `pt`** (nicht `polytrader`), Endpoint **`https://license.mhdf.de`**, eingebetteter
|
||
Public-Key. **End-to-End verifiziert** (ohne Aktivierung zu verbrauchen): `validate` mit
|
||
bewusst ungültigem Schlüssel liefert signiertes `not_found`/„Invalid license key" (⇒ Slug `pt`
|
||
bestätigt, Produkt existiert), Nonce wird reflektiert, und die Server-**Signatur verifiziert
|
||
gegen den eingebetteten Public-Key** (openssl Ed25519 „Verified Successfully"). Der Client wird
|
||
echte Antworten also akzeptieren.
|
||
- **Startablauf:** Master-Key wird jetzt **vor** dem Host-Build geladen (`ConfigureSecretProtection(bootLog)`),
|
||
dann der Gate. Bei `IsUsable == false` → modaler Dialog (Schlüssel eingeben, **Hardware-ID
|
||
anzeigen + kopieren**, „eingeschränkt starten"); bleibt es ungültig, wird die Modulliste
|
||
**leer** gebaut → nur die Core-Shell (Terminal/Settings) startet. **Kein `Environment.Exit`.**
|
||
`TamperSuspected` wird als nicht nutzbar behandelt und im Dialog benannt.
|
||
- **Konfiguration wie beim Watchdog-Token:** `ServerSettings.LicenseKey` (`[Browsable(false)]`,
|
||
bei Master-Key verschlüsselt) + Statusanzeige. Der Schlüssel `9KHEZ-…` ist bereits in
|
||
`server_settings.xml` hinterlegt → beim ersten echten Start validiert der Gate automatisch und
|
||
aktiviert auf **dieser** Maschine (kein Dialog nötig, falls gültig).
|
||
- **Ein Dialog für beide Wege (31.07.2026):** Der Settings-Toolstrip-Button „Lizenz prüfen /
|
||
setzen …" öffnet **denselben** `LicenseDialog` wie der Start, nur im Verwalten-Modus
|
||
(`startupContext:false`, Button „Schließen" statt „eingeschränkt starten"). Damit validiert man
|
||
auch aus den laufenden Einstellungen gegen den Server (Hardware-ID sichtbar/kopierbar); der
|
||
frühere einfache Text-Prompt entfällt. Ein gültig validierter Schlüssel wird verschlüsselt
|
||
gespeichert und gilt ab dem nächsten Start.
|
||
- **Verifikation:** Solution baut fehlerfrei, 430 Tests grün, `--smoke-ui` konstruiert zusätzlich
|
||
`license.dialog`. Eigene Unit-Tests für den Gate wurden bewusst **nicht** angelegt: er ist
|
||
dünner Leim über dem SDK (das seine eigenen Krypto-Tests mitbringt), und ein Test bräuchte eine
|
||
Referenz vom Testprojekt auf die WinExe. Stattdessen deckt der Smoke-Test Konstruktion/Compile ab.
|
||
- **Offen (bewusst):** L-4 bleibt serverseitige Hygiene — sicherstellen, dass die `endpoints`-
|
||
Einstellung im Lizenz-Admin **https oder leer** ist (gecachte Endpoints hätten sonst im SDK
|
||
Vorrang). Und die eigentliche **erfolgreiche** Aktivierung mit dem echten Schlüssel passiert
|
||
erst beim realen Start aus PolyTrader (mit dessen Hardware-ID) — von hier aus bewusst nicht
|
||
getestet, um keinen Aktivierungs-Slot auf der falschen Maschine zu verbrennen.
|
||
|
||
Ursprüngliche Planung:
|
||
|
||
**Neu:** `src/PolyTrader.Core/Security/LicenseGate.cs`
|
||
|
||
- Wird in `Program.Main` **nach** `ConfigureSecretProtection` und **vor** `AppHost.Start()`
|
||
aufgerufen — der Lizenzschlüssel liegt verschlüsselt und braucht den Master-Key.
|
||
- `LicenseConfig`: `ProductSlug = "polytrader"`, eingebetteter `PublicKeyBase64`,
|
||
Endpoints ausschließlich `https://`.
|
||
- Ergebnis:
|
||
- `IsUsable == true` → normaler Start. Bei `ValidOffline` zusätzlich eine sichtbare
|
||
Restlaufzeit im Launcher.
|
||
- `IsUsable == false` → Lizenzdialog (Schlüssel eingeben, Hardware-ID anzeigen und kopierbar
|
||
machen), Trading-Module werden **nicht** registriert. Der Rest der Shell darf starten, damit
|
||
Terminal und Settings erreichbar bleiben.
|
||
- `TamperSuspected` (nach L-3) wird deutlich angezeigt und wie „nicht nutzbar" behandelt.
|
||
|
||
**Wichtig:** Der Schlüssel gehört in die verschlüsselte Ablage, nicht in `appsettings.json`.
|
||
|
||
### B.5 Slice L-B — ~~Revalidierung im Betrieb~~ VERWORFEN (Entscheidung 31.07.2026)
|
||
|
||
**Es gibt bewusst KEINE Laufzeit-Revalidierung.** Richards Vorgabe: im laufenden Betrieb darf
|
||
lizenzseitig **gar nichts** passieren — die Software darf niemals mitten in einer Sitzung aufhören
|
||
zu arbeiten (offene Positionen, laufende API-Aufrufe). Die einzige Durchsetzung ist der
|
||
**Startup-Gate (B.4)**: beim nächsten Start einer ungültigen/widerrufenen/kopierten Lizenz startet
|
||
die Software nicht mehr voll, sondern zeigt die Aufforderung, eine (neue) gültige Lizenz anzugeben.
|
||
Kein `BackgroundService`, kein 12-h-Timer, kein Netzwerkaufruf während der Sitzung.
|
||
|
||
**Warum das reicht (Bedrohungsmodell = Schutz vor einfachem Kopieren auf ein fremdes System):**
|
||
Die Software läuft überwiegend auf eigenen Systemen. Geschützt werden soll nur gegen „jemand
|
||
kopiert die Software auf ein eigenes System und macht dort weiter". Das leistet der Startup-Gate
|
||
durch drei Mechanismen des SDK, **alle beim Start**:
|
||
|
||
1. **Hardware-gebundene Aktivierung** — ein fremdes System hat eine andere `MachineGuid` ⇒ andere
|
||
Hardware-ID ⇒ braucht einen eigenen Aktivierungs-Slot.
|
||
2. **`max_activations`** — sind alle Slots belegt, liefert der Server `activation_limit` ⇒ die
|
||
Kopie startet nicht (nur die Aufforderung erscheint).
|
||
3. **Hardware-gebundener Offline-Cache (L-2-Fix)** — ein mitkopierter `state.dat` gehört zu einer
|
||
fremden Hardware-ID und wird offline verworfen ⇒ die Kopie kann sich auch ohne Netz nicht
|
||
„durchmogeln".
|
||
|
||
**Zwei Stellschrauben im Lizenz-Admin (Produkt `pt`):**
|
||
- **`max_activations`** großzügig auf die Zahl der eigenen Maschinen + Reserve setzen (ein
|
||
Windows-Neuinstall erzeugt eine neue `MachineGuid` = neuer Slot; alte Aktivierungen im Admin
|
||
freigeben). Sonst sperrt man sich bei einem Reinstall selbst aus.
|
||
- **`default_cache_ttl_hours`** (Offline-Kulanz, Default 168 h/7 Tage) = wie lange eine **eigene**,
|
||
bereits aktivierte Maschine ohne Serverkontakt neu starten darf. Für rein eigene Systeme darf
|
||
das gern länger sein. Die Kopie profitiert davon nicht (Punkt 3).
|
||
|
||
**Verhalten bei fehlender/ungültiger Lizenz beim Start (aktuell umgesetzt):** modaler Dialog
|
||
(Schlüssel eingeben / Hardware-ID kopieren / „eingeschränkt starten"). Ohne gültige Lizenz startet
|
||
nur die Core-Shell (Terminal + Einstellungen), **keine Module** — die Software „arbeitet" also
|
||
nicht, bleibt aber bedienbar genug, um eine Lizenz einzutragen. Wer stattdessen ein hartes
|
||
Beenden statt des eingeschränkten Modus will, ist eine Ein-Zeilen-Änderung im Dialog (offen).
|
||
|
||
### B.6 Slice L-C — Predictalytics
|
||
|
||
Beide Dienste sind mandantenfähig ausgelegt: ein zweites Produkt `predictalytics` im
|
||
LicenseLabrador-Admin, ein zweiter Monitor im Watchdog. Der Heartbeat-Service ist bewusst
|
||
projektunabhängig geschrieben und lässt sich unverändert übernehmen. Fällt an, sobald
|
||
Predictalytics produktiv geht.
|
||
|
||
---
|
||
|
||
## Reihenfolge und Aufwand
|
||
|
||
| Schritt | Inhalt | Abhängigkeit |
|
||
|---|---|---|
|
||
| 1 | Watchdog A.1 (W-1, W-2, W-5) + A.2 (W-13, W-14, W-16) | — |
|
||
| 2 | LicenseLabrador A.3 (L-1 bis L-4) + Tests | — |
|
||
| 3 | B.1 Heartbeat-Grundgerüst + Settings-UI | 1 |
|
||
| 4 | B.2 Status aus dem Log | 3 |
|
||
| 5 | B.3 Lifecycle-Events | 3 |
|
||
| 6 | B.4 Lizenzprüfung beim Start | 2 |
|
||
| 7 | B.5 Revalidierung | 6 |
|
||
| 8 | Restliche Befunde aus A.1/A.2/A.3 | — |
|
||
| 9 | B.6 Predictalytics | 3, 6 |
|
||
|
||
Schritte 1 und 2 sind unabhängig voneinander und können parallel laufen.
|
||
|
||
---
|
||
|
||
## Offene Entscheidungen für Richard
|
||
|
||
1. **Wo läuft der Watchdog?** Aktuell ein Shared-Webserver bei derselben Hosting-Firma. Für den
|
||
Zweck („Außensicht auf einen Server-Crash") reicht das, solange es nicht dieselbe Maschine ist.
|
||
Der `self_ping` an healthchecks.io (W-17) fehlt aber noch komplett — bis dahin gibt es keinen
|
||
Alarm, wenn der Watchdog selbst stirbt.
|
||
2. **Lizenzierung für uns selbst oder für Kunden?** Wenn PolyTrader vorerst nur auf unseren
|
||
eigenen Maschinen läuft, ist die Offline-Kulanz von 168 h großzügig und L-2 entsprechend
|
||
entspannter. Sobald Dritte die Software bekommen, sind L-1, L-2 und L-6 harte Blocker.
|
||
3. **`max_activations`** sollte für unsere eigene Lizenz großzügig gesetzt werden (5+), solange
|
||
L-1 nicht behoben ist — sonst reicht ein VPN-Wechsel für eine Aussperrung.
|