Files

7.5 KiB

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.