Improve VPN status reporting and recovery

This commit is contained in:
2026-10-13 06:53:52 +03:00
parent fdb15ae462
commit 7a716b4c9c
188 changed files with 6953 additions and 1911 deletions
+19
View File
@@ -0,0 +1,19 @@
# Goal: Recover stale VPN DNS and ship the approved icons
Use Krypton Execution on `docs/goals/startup-dns-and-icons/PLAN.md`. Preserve user work and auth behavior. Root owns privileged DNS logic and integration; the icon agent has an explicitly disjoint write scope. Prove recovery using synthetic journal/process/DNS fixtures, never real network changes. Report unrun native/reboot acceptance and snapshotless legacy limits.
## Result — 2026-10-08
Implemented in source. Normal installation updates the root helper and ships the native icon renderer. The app gates connection on a capability-checked startup recovery, displays failures/external sessions, and retries through Refresh. CLI exit replaces its finished process before recovery, retaining the client's exit code. DNS snapshots are data-only, versioned, private, durable and serialized with native lockf; no arbitrary private-address reset remains.
PRE review passed after adding unresolved-journal retention, shared locking and verified owner ancestry. Post-review fixes cover Bash pipeline/progress siblings and custom DNS on newly enabled network services. Final integration review: no outstanding blockers.
Automated evidence:
- `python3 tests/dns_recovery.py`: all 16 cases passed; subsequent focused competing-engine regression under the same CLI also passed. Original helper replay fails the new reboot recovery contract as expected.
- `python3 tests/dns_cli.py`: noninteractive command, legacy helper rejection, fixed JSON, path quoting and preserved client exit codes passed.
- `python3 tests/menu.py` and `python3 tests/icons.py`: startup lifecycle, fake CLI, real session timing, both icon themes and offscreen pixel checks passed.
- `tests/smoke.sh`, `python3 tests/configuration.py`, `python3 tests/auth_settings.py`, `python3 tests/logging.py`, and the installed openconnect-lite Python running `tests/yandex_import.py`: passed.
- Shell syntax, `git diff --check` and `swift build -c release --package-path app`: passed.
Real installation, app/GUI launch, live VPN/DNS changes and actual reboot acceptance were not run. Snapshotless pre-upgrade DNS must first be restored to the user's normal settings; no software can infer an unrecorded custom value. Other network locations, deleted services or invalid journals fail visibly and preserve the record.
+40
View File
@@ -0,0 +1,40 @@
# Startup DNS Recovery and Approved Icons Implementation Plan
**Intent:** Install the approved shield icon family with gradual interior fill, and restore normal DNS at app startup after an interrupted VPN session.
**Current Behavior:** App init only refreshes status. vpnc-script persistently changes network-service DNS; abrupt shutdown skips its cleanup and /var/run backups disappear at boot. The existing helper resets any 10.x DNS on three service names and is skipped on ordinary reinstall. Runtime icons still differ from the approved set.
**Expected Outcome:** App startup runs a noninteractive guarded recovery before allowing Connect. A root-owned vpnc wrapper records original DNS and exact VPN DNS before changes, persists the journal across reboot, and restores matching leftovers only when no VPN/SSO is active. Approved vector geometry and real session progress are used by runtime icons.
**Target-Perspective Output:** Isolated reboot/crash fixtures recover recorded DHCP or custom DNS, preserve unrelated/changed DNS, skip active tunnels/SSO, survive reused PIDs and surface failed recovery. Icon bitmap/geometry checks and Swift build prove the native implementation without launching the GUI.
**Truth Owner:** Root-owned DNS wrapper/journal owns before/expected DNS evidence and mutations. CLI owns invocation/events/capability reporting. Swift only requests recovery and reports results. SessionTiming still owns derived icon progress.
**Contract Boundary:** Add `--repair-dns` to CLI; root helper protocol 2 has `--recover`, `--vpnc` and capability inspection. Existing JSON fields remain; DNS capability is additive. App calls recovery only after the CLI advertises support. OpenConnect receives the root wrapper through --script; the wrapper delegates normal routes/DNS setup to installed vpnc-script.
**Cutover:** One root helper serves hook, exit and startup recovery. Remove broad automatic 10.x reset and the CLI's unowned fallback. Installer always updates the root helper. Root journal lives under /var/db, not /var/run. Default runtime icon becomes shield; previously saved ring/clock/time selections remain.
**Displaced Path:** Ad hoc unconditional DNS resets and independent Swift icon shapes. No new daemon, VPN client, credential path, root Python from user-controlled locations or general network-settings API.
**Value Density:** Reuse native networksetup, vpnc-script and existing sudoers wrapper boundary. New files are limited to native icon drawing and focused regression checks; no dependencies.
**Acceptance Evidence:** Real helper source executed with substituted system tools/temp journal, synthetic OpenConnect hook environment and recorded command log; CLI recovery/fake client argument checks; startup wiring/build; headless icon rendering checks and existing suites.
**Evidence Lane:** Automated fixtures only. No actual reboot, live DNS mutation, real VPN, Keychain, app/GUI launch or screenshots.
**Kill Criteria:** Recovery cannot clear arbitrary non-default DNS; current values must match recorded VPN values. Live or indeterminate process checks cannot authorize cleanup. No connection begins during startup recovery. No stale-PID-only guard. New helper and Swift source must ship on normal update.
**Architecture Slice:** `libexec/lemana-vpn-dns-cleanup` + `bin/vpn-lemanapro.sh` + `VPNManager.swift` + installer/template/tests; independent `VPNIcons.swift` + `LemanaVPNApp.swift` + `SessionTiming.swift`. All prior dirty work is preserved.
**Plan Review Gate:** PRE review passed; implementation and post-review checks completed (see GOAL.md).
## DNS contract
- The helper uses fixed system commands and root-owned persistent files. Snapshot stores location, service names, original DNS (empty means DHCP), exact INTERNAL_IP4_DNS, boot identity and owning OpenConnect PID/start identity. It never sources state as code.
- All hook and recovery transactions share native `/usr/bin/lockf -k` in the protected root directory. The helper runs its embedded shell body under the lock, with no public unlocked entry point. The lock releases on process death and survives reboot as an unlocked file. Validate versioned journal data and reject symlink/untrusted state paths.
- Never overwrite an unresolved journal: recover a proven orphan under the same lock, otherwise fail the new hook visibly and retain the journal. A hook may exclude only its verified OpenConnect owner and actual ancestors from the live-process guard; startup excludes nobody. Owner identity is boot plus PID plus process start time.
- Hook connect stores a durable journal before invoking vpnc-script. Observe all enabled network services so interface changes during setup do not defeat capture. Capture only valid DNS output; no heuristic range inference.
- Startup recovery checks actual OpenConnect, openconnect-lite and non-status Lemana connect processes before restoring, and checks again before writes. Missing/failed process evidence blocks mutation. Reused non-VPN PIDs do not block a legitimate old-boot recovery.
- Restore only recorded services in the same network location when current DNS exactly matches the expected VPN list; no-op if already original. If the user changed DNS since the session, preserve it. Keep failed restores for retry.
- Normal hook disconnect preserves custom DNS too: capture the desired value before upstream's unconditional DHCP reset, then restore it only if upstream left empty/expected DNS and no competing tunnel is active. Owner mismatch never consumes another session's journal.
- The disconnect exception requires exact owner and network-location match before upstream invocation. A failed upstream call retains the journal. Tests must exercise custom DNS through upstream's unconditional Empty, partial recovery followed by new connect, location mismatch, and concurrent hook/recovery.
- Snapshotless pre-upgrade leftovers cannot be proven foreign from a 10.x address alone. Automatic repair must skip them and document the limitation, not invent the prior setting.
- CLI startup uses sudo -n and fixed JSON result states. Old/missing helper is detected by a read-only protocol marker, never by executing an old helper with an unknown flag. The app surfaces upgrade/permission/recovery failures and blocks Connect while checking. Recovery never creates a VPN session.
## Task board and parallel scopes
1. Root: DNS wrapper/journal/hook implementation, CLI repair/capability/hook args, installer helper update/template, DNS fixture tests. No icon files edited by root during parallel work.
2. Icon agent (explicitly parallel): ONLY `app/Sources/LemanaVPN/VPNIcons.swift`, `LemanaVPNApp.swift`, `SessionTiming.swift`, `tests/icons.py`. Exact approved family from `artifacts/icon-family-v1/draw_icons.py`: 24-grid, 1.75 stroke, neutral 14% body, 60% bottom-up progress fill and colored perimeter; no demo 8h. No VPNManager/install/DNS/README edits. Return packaging instructions to root.
3. Root after icon merge: startup recovery lifecycle/UI messages in manager and menu, installer icon download, docs and full focused regressions. Credentials/auth/reconnect policy remain unchanged.
4. Read-only correctness/safety/maintainability reviewers; fix blockers; run shell checks, new DNS/icons tests, smoke, auth/config/import/menu/logging checks, Swift release build and diff whitespace check.
## Known limits
No software can restore a pre-upgrade custom DNS value that was never recorded. Unrecognised root journal or network-location changes are preserved for inspection. Native visual quality and actual reboot/network acceptance remain unproven without a separate live-test request.