diff --git a/.gitignore b/.gitignore index ec6bb11..8475c88 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ apt.dat.zip **/apt.dat **/apt.dat.zip cover.out + +# Local client research downloads/extracts (never redistribute third-party binaries) +.research/ diff --git a/docs/client-injector/research/vpilot-3.12.1.md b/docs/client-injector/research/vpilot-3.12.1.md new file mode 100644 index 0000000..cd2e12f --- /dev/null +++ b/docs/client-injector/research/vpilot-3.12.1.md @@ -0,0 +1,123 @@ +# vPilot 3.12.1 — reverse-engineering research notes + +**Date:** 2026-07-28 +**Installer:** `vPilot-Setup-3.12.1.exe` (NSIS) +**Legal posture:** Research notes + hashes only. **Never commit or redistribute** vPilot binaries. Users install from [vpilot.rosscarlson.dev](https://vpilot.rosscarlson.dev/Download). + +Tracked profile: `third_party/client-profiles/vpilot-3.12.1.yaml` + +## Fingerprints + +| Artifact | SHA-1 | SHA-256 | +|----------|-------|---------| +| Installer | `48820cb593c6cef8325a331c763316a8b33a501b` | `528a51bf0e71ada11103314d18a9fc0321afac94e2706f2fb13e0fbf5bb4beaa` | +| `vPilot.exe` | `7d95a7110392c15728143cc30e1f00899c686eb5` | `c743f204929309db6f49e8b543db2d661ad09c32a2a7bc71fe419c685d244bff` | + +Default install directory: `%LOCALAPPDATA%\vPilot\` (per official docs — no admin required). + +## Stack + +- WinForms/.NET Framework 4.7.2, PE32 (x86) managed assembly +- Network/Common assemblies Dotfuscator-obfuscated (`RossCarlson.Vatsim.Network*`) +- AFV via **GeoVR.*** (GeoVR.Client ~15MB Costura-packed NAudio; GeoVR.Connection REST+UDP; GeoVR.Shared DTOs) +- Plugin API: `RossCarlson.Vatsim.Vpilot.Plugins` (events for aircraft/network; **not** a supported open server switch) +- SimConnect for MSFS/FSX/P3D + +## Connection surfaces (what must be redirected for openfsd) + +| Surface | Stock value (3.12.1) | openfsd target | Notes | +|---------|----------------------|----------------|-------| +| FSD JWT auth | `https://auth.vatsim.net/api/fsd-jwt` | `https://{host}/api/v1/fsd-jwt` | Hardcoded #US string; TLS required | +| Network status / server list | Config `NetworkStatusURL` → `http://status.vatsim.net/` | `https://{host}/api/v1/data/status.txt` | 3DES-obfuscated in `vPilotConfig.xml` | +| Cached servers | `AUTOMATIC\|fsd.connect.vatsim.net` | e.g. `OPENFSD\|fsd.example.com` | Same config crypto; `NAME\|host` | +| Automatic server HTTP | `http://fsd.vatsim.net/` | optional / unused if cached list set | "best server" HTTP endpoint (changelog 3.4.10+) | +| AFV REST base | `https://voice1.vatsim.net` | openfsd `AFV_API_PUBLIC_BASE_URL` | Hardcoded #US; prior patch utilities **disabled** AFV instead of retargeting | +| Flight plan browser | `https://my.vatsim.net/pilots/flightplan` | optional openfsd/web | Cosmetic / UX | +| Model matching CDN | `vpilot.rosscarlson.dev/ModelMatchingData/*` | leave alone | Not openfsd-related | +| Updates | `vpilot.rosscarlson.dev` VersionCheck | leave alone | | + +### CLI (discovered) + +- `-serveraddressoverride` — override FSD server address (positional override; still need JWT + status for full openfsd path) +- `-novoice` / `/novoice` — disable voice connection (standalone AFV client path) + +### Config crypto (`vPilotConfig.xml`) + +Fields `NetworkStatusURL`, `CachedServers` (and login fields) are Base64(3DES-ECB-PKCS7). + +``` +key = MD5("5575ac09-f2de-4a1e-808b-e3398e17f8bf") // 16 bytes +3des_key = key || key[0:8] // 24 bytes +``` + +Source of truth for this algorithm: archived `renorris/vpilot-patch-utility` (`config/obfuscator.go`) and `openfsd-client-patch-utility` (`patch/vpilot_config.go`). Confirmed by decrypting shipping defaults. + +`Public.key` in install tree is an EC P-256 SPKI (licensing/update verify — not the config field cipher). + +## CLR #US / CIL ldstr map (3.12.1 `vPilot.exe`) + +`#US` stream file offset: `0xA22E8` (size `0x15E04`). + +| Logical string | #US heap offset (header) | Body file offset | ldstr CIL file offset(s) | Payload byte budget (UTF-16+term) | +|----------------|--------------------------|------------------|--------------------------|----------------------------------| +| `https://auth.vatsim.net/api/fsd-jwt` | `0x1569F` | `0xB7988` | `0x4BDB5` | 71 (odd ECMA-335) | +| `https://voice1.vatsim.net` | `0x6D8D` | `0xA9076` | `0x1F0A7` | 51 | +| `http://fsd.vatsim.net/` | `0x15751` | `0xB7A3A` | `0x4C0B2` | 45 | + +Second copy of fsd-jwt UTF-16 body at `0xBA44A` — header walk failed (likely resource/non-#US); treat primary #US entry + ldstr `0x4BDB5` as authoritative until second site proven live. + +**3.11.1 offsets are obsolete** (e.g. old fsd-jwt heap `0xD2A2` / ldstr `0x4B3C5` do not match 3.12.1). Profiles must be **version-pinned by binary hash**. + +### Prior-art AFV strategy + +`openfsd-client-patch-utility` example for vPilot 3.11.1 **disables AFV** (`ret` at voice connect handler) rather than rewriting voice URL. openfsd now has native `-afv`; design should prefer **retarget voice base URL** when space allows, with fallback disable. + +fsd-jwt stock URL length limits padded #US overwrite; longer openfsd URLs need either: +1. shorter public base host, or +2. ldstr remap to a longer unused #US slot (3.11.1 technique: `0x72 XX XX XX 70`), or +3. runtime string/hook injection without shrinking constraint. + +## Prior art (same author ecosystem) + +| Repo | Role | Status | +|------|------|--------| +| [renorris/vpilot-patch-utility](https://github.com/renorris/vpilot-patch-utility) | On-disk PE #US + config obfuscation for 3.11.1 | Archived | +| [renorris/openfsd-client-patch-utility](https://github.com/renorris/openfsd-client-patch-utility) | Multi-client static patcher (vPilot/xPilot/Euroscope/vatSys) | Active reference | +| openfsd wiki `Client-Connection.md` | Points operators at those tools | Exists | + +**Gap:** static on-disk patchers are CLI-centric, version-brittle, AFV-hostile (disable), and not a first-class openfsd module with a multi-client GUI. User request: **runtime injector** + **generic client picker GUI**. + +## Injection strategy spectrum (for design) + +1. **Config-only rewrite** — status URL + cached servers (3DES). Insufficient alone (JWT + AFV hardcoded). +2. **On-disk binary patch** — prior art; reversible backups; antivirus noise; requires quit vPilot. +3. **Launch-time ephemeral patch** — copy install tree or patch temp shadow; launch; optional restore. Borderline "runtime". +4. **True runtime** — process spawn under injector; rewrite PE before image map / or post-load memory #US / detour `HttpClient`/`Socket.connect` / hosts+local reverse proxy. +5. **Local control-plane proxy** — no PE touch: hosts or system proxy to fake `auth.vatsim.net`, `voice1.vatsim.net`, status; FSD via cached server IP. TLS MITM needs local CA (heavy UX). +6. **Plugin** — plugin surface is events, not network endpoint config; unlikely sufficient. + +Recommended design direction (to be confirmed in design doc): **adapter interface per client**, default vPilot adapter combining (a) config rewrite, (b) versioned #US/CIL profile for JWT+AFV, (c) optional AFV disable fallback; GUI is **client-agnostic** (select client → locate install → bind openfsd endpoints → Apply/Revert/Launch). + +## Legal / product constraints + +- Do not redistribute vPilot or derivative redistributable binaries. +- Track **hashes + profiles + research** in git. +- Assume user-owned licensed install on disk. +- Do not facilitate connecting patched clients **to the public VATSIM network** in a deceptive way — product framing is **private openfsd networks**. +- Prefer reversible operations; never delete user model-matching data. + +## Research method used + +- Downloaded official NSIS installer; extracted with 7-Zip. +- PE/CLR metadata walk for `#US`; UTF-16 string inventory; ldstr token search. +- openssl 3DES-ECB decrypt of default config fields. +- Cross-check with archived Go patch utilities and openfsd AFV/FSD docs. +- Full ILSpy decompilation not run in this environment (no `dotnet`/ilspycmd); GeoVR method names recovered via managed metadata strings (`ApiServerConnection`, `AddCallsign`, `ConnectToVoiceServer`, etc.). + +## Follow-ups for implementer + +- [ ] Confirm second fsd-jwt site and automatic-server HTTP usage path on live connect. +- [ ] Measure max openfsd URL length vs #US budgets; document remap free-slot catalog per version. +- [ ] Locate AFV connect handler CIL for 3.12.1 disable-fallback (`ret` site was 3.11.1-specific). +- [ ] User-settings store path beyond install dir (`%LOCALAPPDATA%` may hold runtime config copy). +- [ ] Antivirus / code-signing interaction when rewriting signed `vPilot.exe`. diff --git a/docs/design/client-runtime-injector.md b/docs/design/client-runtime-injector.md new file mode 100644 index 0000000..f46f2cd --- /dev/null +++ b/docs/design/client-runtime-injector.md @@ -0,0 +1,1260 @@ +# openfsd Client Setup (multi-client injector; vPilot first) + +| Field | Value | +|-------|--------| +| **Document** | openfsd Client Setup — multi-client framework + vPilot 3.12.1 adapter | +| **Author** | _(design author / implementer)_ | +| **Date** | 2026-07-28 | +| **Status** | **Draft** (rev 3 — re-review polish) | +| **Project** | openfsd | +| **Target land path** | `docs/design/client-runtime-injector.md` | +| **Related** | `Agents.md`, `docs/client-injector/research/vpilot-3.12.1.md`, `third_party/client-profiles/vpilot-3.12.1.yaml`, `wiki/Client-Connection.md`, `docs/design/afv-server.md`, `docs/authentication-token.md`, `docs/design/rest-api-versioning.md`, archived `renorris/vpilot-patch-utility`, `renorris/openfsd-client-patch-utility` | +| **Revision** | rev 3.1: fix `openfsd.example.com` host len parenthetical (19); prior rev 3 URL tables, `/j` A8, soft probe | + +--- + +## Overview + +**Product name: openfsd Client Setup** (binary `openfsd-client`). This is a **generic, cross-platform, minimal desktop GUI** (plus headless CLI) that configures **user-installed** third-party FSD/AFV clients so they connect to a private **openfsd** deployment. Phase 0 is an **on-disk + config patcher with reversible backups** — not a true in-memory runtime injector. “Runtime” strategies (shadow launch, process inject) are **later phases** only; do not market Phase 0 as memory-only rewriting. + +openfsd speaks modern VATSIM-shaped FSD and optional AFV, but third-party clients (vPilot, xPilot, Euroscope, vatSys, TrackAudio, …) ship with **hardcoded VATSIM endpoints** (JWT issuer, status/datafeed URLs, AFV voice base). Operators today depend on **external, CLI-centric, version-brittle patch utilities** that often **disable AFV** rather than retargeting it. + +**Critical product shape (client-agnostic):** + +1. User selects **which client** (vPilot today; xPilot, Euroscope, vatSys, TrackAudio later). +2. User points at the client’s **install location on disk** (or accepts auto-detect). +3. User configures openfsd endpoints (web base, FSD host/port, AFV voice base, etc.). +4. Tool runs the **adapter-specific** mutation strategy for that client version. +5. **Apply / Revert / Launch** are first-class. + +vPilot is **only the first adapter**. Architecture, GUI layout, and budget presentation must not hardcode `client_id == vpilot`. + +**Legal:** never redistribute proprietary clients. Assume the client is already installed legally. Git tracks hashes, YAML profiles, research notes, and our code — never third-party binaries (`.research/` gitignored). + +--- + +## Background & Motivation + +### Current state + +| Surface | openfsd path | Client need | +|---------|--------------|-------------| +| FSD JWT | `POST /api/v1/fsd-jwt` (`internal/web/auth.go` `getFsdJwt`) | Replace stock `https://auth.vatsim.net/api/fsd-jwt` | +| Classic status | `GET /api/v1/data/status.txt` (`internal/web/data.go`) | Replace stock status URL in client config/binary | +| Servers lists | `/api/v1/data/openfsd-servers.txt` (+ JSON variants) | Advertised via status.txt; cached server list often set directly | +| AFV REST | `AFV_API_PUBLIC_BASE_URL` (`internal/afv`, wiki Configuration) | Replace stock `https://voice1.vatsim.net` (or leave voice disabled via `-novoice`) | +| FSD TCP | default **6809** | Host (and optional port) via cached servers / CLI override | + +Wiki `wiki/Client-Connection.md` currently **defers** to external tools (`vpilot-patch-utility`, `openfsd-client-patch-utility`). Those work but are not monorepo-first-class, are CLI-only, pin old versions (vPilot 3.11.1), and **disable AFV** rather than retargeting openfsd `-afv`. + +### Research already performed (vPilot 3.12.1) + +| Artifact | Location | +|----------|----------| +| Fingerprint profile (schema v1 today) | `third_party/client-profiles/vpilot-3.12.1.yaml` | +| RE notes | `docs/client-injector/research/vpilot-3.12.1.md` | +| Local extract (gitignored) | `.research/vpilot/` | + +| Item | Value | +|------|--------| +| Installer SHA-256 | `528a51bf0e71ada11103314d18a9fc0321afac94e2706f2fb13e0fbf5bb4beaa` | +| `vPilot.exe` SHA-1 | `7d95a7110392c15728143cc30e1f00899c686eb5` | +| Default install | `%LOCALAPPDATA%\vPilot\` | +| Stack | .NET Framework 4.7.2 PE32 managed; Dotfuscator on network assemblies | +| AFV | GeoVR.* DLLs; stock base `https://voice1.vatsim.net` (confirm GeoVR does not re-hardcode — research gate) | +| Config crypto | Base64(3DES-ECB-PKCS7); key = MD5(GUID `5575ac09-f2de-4a1e-808b-e3398e17f8bf`) \|\| first 8 of MD5 | + +CLR `#US` (3.12.1) — heap file offset `0xA22E8`, size `0x15E04` (values live in version-pinned YAML only; Go must not hardcode except tests that load profiles): + +| Logical string | #US heap | Body file | ldstr CIL | UTF-16+term budget | +|----------------|----------|-----------|-----------|--------------------| +| `https://auth.vatsim.net/api/fsd-jwt` | `0x1569F` | `0xB7988` | `0x4BDB5` | 71 bytes | +| `https://voice1.vatsim.net` | `0x6D8D` | `0xA9076` | `0x1F0A7` | 51 bytes | +| `http://fsd.vatsim.net/` | `0x15751` | `0xB7A3A` | `0x4C0B2` | 45 bytes | + +**Also noted (open research):** second fsd-jwt UTF-16 body at file `0xBA44A` (header walk failed — may be resource/non-`#US`). Adapter completeness requires proving liveness or dual-writing (see Research gates). + +### Pain points + +1. Private openfsd networks need client surgery for modern pilot clients. +2. External tools lag 3.12.1 fingerprints already researched in-tree. +3. Prior art disables AFV; openfsd now has native `-afv`. +4. Half-patched installs need transactional bak + clear recovery. +5. Risk of vPilot-only product shape instead of multi-client platform. +6. **In-place JWT `#US` budget is extremely tight** for realistic hostnames (see below) — Phase 0 without free-slot remap or server short-path alias only works for **lab/short hosts**. + +### Constraints (non-negotiable) + +- **Legal:** never redistribute third-party client binaries/installers. +- **Import graph:** `internal/clientinject` must not import `server`, `postoffice`, `session`, `web`, `afv`, `db`, `cluster`, `sweatbox`, `metar`, `auth`, `serviceapi`. +- **Layout:** new **auxiliary cmd** `cmd/openfsd-client` (Agents.md “single binary `cmd/openfsd`” means FSD+web+AFV **colocation**, not “only one executable in the repo”). Pattern matches `cmd/openfsd-migrate-*`. +- **boring-web** = `internal/web` only; desktop GUI is separate (Fyne). +- **Product framing:** private openfsd networks only. + +--- + +## Goals & Non-Goals + +### Goals + +1. Generic **client adapter framework** — discover, verify hash, plan, apply, revert, health-check, launch. +2. **Version-pinned profiles** — refuse unknown PE hashes. +3. Pluggable mutations — config rewrite, `#US` patch, ldstr remap, later PE disable / runtime. +4. **AFV retarget preferred** when budgets and research allow; Phase 0 voice off via **`-novoice`** until PE disable site is known. +5. **Client-agnostic GUI** — no layout special-case on `client_id`; constraints come from `Plan` / adapter API. +6. Headless CLI parity. +7. Reversible by default (`.openfsd-bak` + manifest). +8. vPilot 3.12.1 path that is **honest about readiness** (see Implementation readiness). +9. First-class monorepo module (docs, embed-safe profiles, packages, PR plan). +10. Honest naming: Client Setup / on-disk apply vs later “runtime” phases. + +### Non-Goals + +- Redistributing proprietary clients. +- Facilitating deceptive connection to public VATSIM. +- Supporting every historical client version in v1. +- Embedding injector in `cmd/openfsd`. +- Full decompiler in-product. +- Cross-process memory inject in Phase 0–1. +- Native macOS pilot clients that do not exist (Windows primary; Wine path best-effort). +- Default TLS MITM reverse proxy. +- Browser automation tests for the desktop GUI. +- Claiming Phase 0 works for arbitrary long hostnames without free-slot remap **or** server short-path aliases. + +--- + +## Implementation readiness (MVP vs production hostnames) + +| Capability | Short-host lab MVP | Production hostnames | +|------------|--------------------|----------------------| +| Config status + CachedServers | **Yes** (Phase 0) | **Yes** | +| JWT `#US` in-place (host ≤ **12** chars for default path) | **Yes** | **No** for typical FQDNs | +| JWT via free-slot + ldstr remap | After **Research gate R1** | **Required** (unless A8 server alias) | +| JWT via server short-path alias (optional) | Optional openfsd web PR | Alternative to remap | +| AFV `#US` in-place (URL ≤ **25** chars) | **Yes** if short voice host | Same | +| AFV PE `ret` disable | **Out of Phase 0** until **R2** | Optional fallback | +| Voice off without PE patch | **`-novoice` / ForceDisableAFV launch flag** | Same | +| Residual stock JWT scan (incl. second body site) | After **R3** | **Required** for “adapter complete” | +| Config path multi-candidate | After **R4** | **Required** for “adapter complete” | +| GeoVR re-hardcode check | After **R5** | Required before promising AFV retarget | + +**Do not market** the first adapter CLI as “works for any openfsd hostname” until R1 (or A8) lands. + +--- + +## Proposed Design + +### High-level architecture + +```mermaid +flowchart TB + subgraph user [User machine] + GUI[openfsd-client GUI
Client Setup] + CLI[openfsd-client CLI] + ENGINE[internal/clientinject Engine] + AD[Adapter registry] + VP[vPilotAdapter] + XP[xPilotAdapter future] + PROF["embedded profiles
internal/clientinject/profiles"] + DISK[(User install tree)] + BAK[(.openfsd-bak + manifest)] + end + + subgraph openfsd [openfsd deployment] + WEB[web :8000
/api/v1/fsd-jwt
optional short aliases] + FSD[FSD TCP :6809] + AFV[AFV REST + UDP optional] + end + + GUI --> ENGINE + CLI --> ENGINE + ENGINE --> AD + AD --> VP + AD --> XP + ENGINE --> PROF + ENGINE -->|Plan/Apply/Revert| DISK + ENGINE --> BAK + DISK -.-> WEB + DISK -.-> FSD + DISK -.-> AFV +``` + +### Process / package layout (Agents.md ownership) + +**KD-1:** separate binary + library — not a subcommand of `openfsd`. + +Agents.md “single binary” means **one server process** can run FSD+web+AFV together. The repo **already has** auxiliary cmds (`openfsd-migrate-to-rqlite`, `aptdat2apt`, …). `openfsd-client` is another auxiliary cmd. + +| Package / path | Owns | Import constraints | +|----------------|------|--------------------| +| **`cmd/openfsd-client`** | Entry: GUI default, CLI subcommands | May import `internal/clientinject` + Fyne. **Not** server/web/afv/db/… | +| **`internal/clientinject`** | Engine: plan/apply/revert, backup, discover, preflight locks, launch | stdlib + `gopkg.in/yaml.v3` + pure subpackages | +| **`internal/clientinject/profiles/`** | **Embed root** for YAML profiles (`//go:embed *.yaml`) | Tracked in git; canonical runtime source | +| **`internal/clientinject/cilus`** | Pure CLR `#US` encode/decode | **stdlib only** | +| **`internal/clientinject/vpilotconfig`** | Pure vPilot 3DES + XML field rewrite | **stdlib only** | +| **`internal/clientinject/pepatch`** | Section map, overwrite, padded string | **stdlib only** | +| **`internal/clientinject/adapters`** | Per-client adapters | sibling pure packages + profiles | +| **`third_party/client-profiles/`** | Human/research **mirror** of fingerprints (may lag embed tree) | No binaries; **not** the `go:embed` root | +| **`docs/client-injector/`** | Research notes, operator guide | — | + +#### Profile embed policy (KD-17) + +Go `//go:embed` can only see files under the embedding package directory. Therefore: + +1. **Canonical embed path:** `internal/clientinject/profiles/*.yaml` +2. **Mirror:** `third_party/client-profiles/` kept for discoverability / non-Go consumers; CI or `scripts/check-client-profiles-sync.sh` may **diff** mirror ↔ embed (or generate one from the other in PR). +3. **openfsd-client never fetches** `installer.url` from profiles (user download instructions only). +4. Optional override: `--profiles-dir` for development. + +```text +cmd/openfsd-client/ + main.go + gui/ +internal/clientinject/ + engine.go + profile.go + backup.go + discover.go + preflight.go # running process / file lock + launch.go + launch_shadow.go # Phase 1 + adapter.go + kinds.go # MutationKind + YAML map + profiles/ # go:embed + vpilot-3.12.1.yaml + adapters/ + vpilot/ + registry.go + cilus/ + vpilotconfig/ + pepatch/ + testdata/ +third_party/client-profiles/ # mirror / research index + vpilot-3.12.1.yaml +docs/client-injector/research/ +``` + +**Import graph additions** (`scripts/check-import-graph.sh`): + +``` +internal/clientinject → forbid internal/{server,web,afv,db,postoffice,session,cluster,sweatbox,metar,auth,serviceapi} +internal/clientinject/{cilus,vpilotconfig,pepatch} → stdlib only +cmd/openfsd-client → only internal/clientinject (+ GUI module deps) +``` + +**Coverage floors** (wire when packages land; script already skips missing packages): + +| Scope | Floor | +|-------|-------| +| `internal/clientinject/cilus` | ≥98% hard | +| `internal/clientinject/vpilotconfig` | ≥98% hard | +| `internal/clientinject/pepatch` | ≥95% hard | +| `internal/clientinject` (engine + adapters) | ≥85% soft → hard after GUI PR | +| `cmd/openfsd-client` | excluded | + +### Core interfaces + +```go +package clientinject + +// Endpoints — user-configured openfsd connection surfaces (client-agnostic). +type Endpoints struct { + // Public HTTPS origin of openfsd web, no trailing slash. + // e.g. "https://fsd.example.com" + WebBaseURL string + + // FSD hostname only, or host:port if non-default port is required. + // See "FSD address shape" below. + FSDHost string + + // TCP port for FSD. Zero means default 6809 and omit from CachedServers + // unless IncludePortInServerList is true. + FSDPort int + + // If true, CachedServers uses "NAME|host:port" even when port is 6809. + IncludePortInServerList bool + + // Human label for cached server row, e.g. "OPENFSD". + FSDServerName string + + // AFV REST public base, e.g. "https://voice.example.com". Empty + ForceDisableAFV + // => launch with -novoice (Phase 0); PE ret only after research gate R2. + AFVBaseURL string + + // Prefer no voice (CLI -novoice / skip AFV #US retarget). + ForceDisableAFV bool + + // Prefer short JWT path templates if server short paths are available. + // When true, planner picks the shortest path that fits the #US budget from + // ShortJWTPathCandidates (default order below), after optional soft probe. + PreferShortJWTPath bool + + // Optional override of short-path candidates (empty = engine defaults). + // See A8: "/j" (preferred once fixed), "/api/fsd-jwt", "/fsd-jwt". + ShortJWTPathCandidates []string +} + +// Derived (engine helpers; path depends on PreferShortJWTPath + server support): +// JWTURL default: WebBaseURL + "/api/v1/fsd-jwt" +// JWTURL short: WebBaseURL + chosen short path (A8 / fixed POST /j) +// StatusURL: WebBaseURL + "/api/v1/data/status.txt" +// StatusJSON: WebBaseURL + "/api/v1/data/status.json" +// +// Default ShortJWTPathCandidates (shortest first for budget): +// "/j", "/fsd-jwt", "/api/fsd-jwt" +// PreferShortJWTPath without a working short route is operator error — soft-probe warns. + +// FSD address shape (normative for vPilot CachedServers / -serveraddressoverride): +// - Default port 6809: CachedServers entry "NAME|hostname" (hostname only), +// matching prior-art AUTOMATIC|fsd.connect.vatsim.net style. +// - Non-default port: "NAME|hostname:port". +// - -serveraddressoverride: same host or host:port string as FSDHost field +// after normalization (if FSDPort set and host has no port, append :port). +// Research gate R6 may refine if 3.12.1 rejects :port in CachedServers. + +type InstallCandidate struct { + ClientID string + RootDir string + PrimaryPE string + ConfigPaths []string // all discovered candidates (install + AppData, etc.) + DisplayHint string // e.g. "default LocalAppData install" +} + +type Install struct { + ClientID string + RootDir string + PrimaryPE string + // ConfigPaths that will be written (subset of candidates after resolve). + ConfigPaths []string + HashSHA1 string + ProfileID string +} + +// Constraint is a client-agnostic UI hint (GUI must not special-case vPilot). +type Constraint struct { + Field string // "JWTURL", "AFVBaseURL", "WebBaseURL", ... + MaxRunes int // 0 = unknown / N/A + Strategy string // "in_place", "remap_required", "server_alias", "n/a" + Description string +} + +type MutationKind string + +const ( + MutConfigRewrite MutationKind = "config_rewrite" + MutUSHeapString MutationKind = "cil_us_string" + MutLdstrRemap MutationKind = "cil_ldstr_remap" + MutRawOverwrite MutationKind = "raw_overwrite" + MutPaddedString MutationKind = "padded_string" + MutAFVDisablePE MutationKind = "afv_disable_pe" // only if profile has offset + MutLaunchFlag MutationKind = "launch_flag" // e.g. -novoice recorded in plan +) + +// YAML kind → Go kind (normative): +// vpilot_config, config_rewrite → config_rewrite +// cil_us_string → cil_us_string +// cil_us_or_remap → cil_us_string and/or cil_ldstr_remap (planner expands) +// cil_ldstr_remap → cil_ldstr_remap +// raw_overwrite, section_overwrite → raw_overwrite +// padded_string → padded_string +// afv_disable → afv_disable_pe if offset set, else launch_flag -novoice +// launch_flag → launch_flag + +type Mutation struct { + ID string + Kind MutationKind + Description string + TargetRel string + Detail MutationDetail // typed payload; see appendix +} + +type Plan struct { + Install Install + Endpoints Endpoints + Mutations []Mutation + // Constraints for generic budget UI (from adapter after dry plan). + Constraints []Constraint + Warnings []string + Blockers []string // non-empty => Apply refused +} + +type ApplyResult struct { + ManifestPath string + BackupRoot string + Applied []string +} + +// FileWriter abstracts disk for tests. +type FileWriter interface { + ReadFile(path string) ([]byte, error) + WriteFile(path string, data []byte) error + CopyFile(src, dst string) error + OpenReadWrite(path string) (ReadWriteSeekCloser, error) + Stat(path string) (fs.FileInfo, error) + Remove(path string) error +} + +type ReadWriteSeekCloser interface { + io.Reader + io.Writer + io.Seeker + io.Closer + Truncate(size int64) error +} + +// Profile is the loaded schema_version 2 document (fingerprint + mutations). +type Profile struct { + SchemaVersion int + ClientID string + DisplayName string + SupportedClientVersion string + PrimaryBinary PrimaryBinarySpec + ConfigFiles []ConfigFileSpec + CLR *CLRSpec + Strings map[string]StringSpec + USFreeSlots []USFreeSlot + Mutations []ProfileMutationSpec + Launch LaunchSpec + // ... see appendix for full field list +} + +// Adapter — per-client strategy; GUI/CLI stay generic. +type Adapter interface { + ClientID() string + DisplayName() string + SupportedProfiles() []string + + Discover(ctx context.Context) ([]InstallCandidate, error) + Verify(install Install, profile *Profile) error + + // EndpointConstraints returns static/profile hints before full plan + // (optional; may return nil and rely solely on Plan.Constraints). + EndpointConstraints(profile *Profile) []Constraint + + // Plan must not write disk. Must populate Constraints + Blockers. + Plan(install Install, profile *Profile, ep Endpoints) (*Plan, error) + + Apply(ctx context.Context, plan *Plan, w FileWriter) error + HealthCheck(install Install, ep Endpoints) error + LaunchArgs(install Install, ep Endpoints) []string +} +``` + +### Engine orchestration (transactional Apply) + +```text +1. Resolve adapter + profile by client_id + PE hash +2. PreflightRunningProcess(primary PE): + - Best-effort: Windows process list / try exclusive open + - If running or ERROR_SHARING_VIOLATION → abort with + "Quit the client completely, then Apply again" +3. plan := adapter.Plan(...) // includes URL budget / remap availability +4. if plan.Blockers != nil → abort (no writes) +5. BackupSession: + - Copy each target to .openfsd-bak + - Write .openfsd-inject-manifest.json (status=in_progress) +6. Apply mutations in order + - On any error → restore all from bak; status=failed; keep bak for forensics optional +7. HealthCheck (includes residual stock JWT scan when R3 done) + - On failure → auto-revert; report +8. status=applied +``` + +**Partial-write rule:** PE and config are separate files; if PE write fails after config write, restore **both** from bak (never leave config-only openfsd + stock JWT PE, or the reverse, without user confirmation). + +Revert restores bak files, verifies stock SHA of primary PE, removes/marks manifest. + +### Mutation strategies (phased) + +```mermaid +flowchart LR + subgraph phase0 [Phase 0 — ship first] + C[Config rewrite] + U["#US in-place overwrite"] + N["-novoice launch flag"] + end + subgraph phase05 [Phase 0.5 — research gates] + L[ldstr remap + free slots] + D2[Dual JWT body sites] + CFG[Config multi-path] + RET[AFV PE ret disable] + GV[GeoVR string audit] + end + subgraph phase1 [Phase 1] + S[Shadow PE + cwd install] + end + subgraph phase2 [Phase 2 optional] + M[True runtime hook] + P[Local reverse proxy] + end + C --> U --> N + N --> L + L --> D2 --> CFG --> RET --> GV + GV -.-> S + S -.-> M + S -.-> P +``` + +| Strategy | Phase | Notes | +|----------|-------|-------| +| Config rewrite | **0** | status + CachedServers | +| `#US` in-place | **0** | only if rendered URL fits budget | +| `-novoice` launch | **0** | voice off without PE disable | +| free-slot + ldstr remap | **0.5 / R1** | **required** for real JWT hostnames (or A8) | +| Dual/multi JWT site patch | **0.5 / R3** | residual stock JWT scan | +| Config multi-candidate | **0.5 / R4** | install dir vs AppData | +| AFV PE `ret` disable | **0.5 / R2** | not claimed until offset known | +| GeoVR inventory | **0.5 / R5** | may add DLL mutations | +| Shadow launch | **1** | hybrid: temp PE, cwd=install | +| True runtime / MITM | **2** | optional | + +### “Runtime” naming honesty + +| User-facing label | Implementation | +|-------------------|----------------| +| **openfsd Client Setup** | Product name | +| **Apply** | Durable on-disk PE + config (Phase 0) | +| **Launch patched (ephemeral)** | Phase 1 shadow PE | +| **Inject at launch** | Phase 2 only | + +UI copy must never claim Phase 0 “rewrites only in memory.” + +### JWT / AFV URL length budgeting (normative) + +`#US` payload budget is **odd** (UTF-16 bytes + terminal). Max characters ≈ `(budget − 1) / 2`. + +| Slot | Budget | Max chars | Stock | Stock len | +|------|--------|-----------|-------|-----------| +| fsd-jwt | 71 | **35** | `https://auth.vatsim.net/api/fsd-jwt` | 35 | +| AFV base | 51 | **25** | `https://voice1.vatsim.net` | 25 | +| auto fsd HTTP | 45 | **22** | `http://fsd.vatsim.net/` | 22 | + +**Path lengths** (`len` of ASCII path only) and **in-place max host** (JWT slot max **35** runes; `len("https://")` = **8**): + +| JWT path | Path len | max_host = 35 − 8 − path | Example host that fits | +|----------|---------:|-------------------------:|------------------------| +| VATSIM stock `/api/fsd-jwt` | 12 | 15 | (reference only) | +| openfsd default `/api/v1/fsd-jwt` | **15** | **12** | `fsd.ex.co` (9) | +| A8 `/api/fsd-jwt` | 12 | **15** | `fsd.example.com` (15) exact | +| A8 `/fsd-jwt` | 8 | **19** | `openfsd.example.com` (19) exact | +| Existing/fixed `POST /j` | **2** | **25** | most private FQDNs | + +```text +max_host(path) = 35 - len("https://") - len(path) +full_url = "https://" + host + path +# in-place requires len(full_url) ≤ 35 +``` + +**Verified full-URL lengths** (exact `len`; recompute with any language one-liner before editing this table): + +| URL | Length | In-place (≤35)? | +|-----|-------:|-----------------| +| `https://fsd.ex.co/api/v1/fsd-jwt` | **32** | Yes (host `fsd.ex.co` = 9) | +| `https://fsd.example.com/api/v1/fsd-jwt` | **38** | **No** — needs remap or short path | +| `https://fsd.example.com/api/fsd-jwt` | **35** | Yes (A8 `/api/fsd-jwt`) | +| `https://fsd.example.com/j` | **25** | Yes (fixed `/j`) | +| `https://openfsd.example.com/api/v1/fsd-jwt` | **42** | **No** | +| `https://openfsd.example.com/fsd-jwt` | **35** | Yes (A8 `/fsd-jwt`) | +| `https://openfsd.example.com/j` | **29** | Yes (fixed `/j`) | + +**In-place AFV max URL length:** 25 characters total (not host-only). Exact lengths: + +| URL | Length | In-place (≤25)? | +|-----|-------:|-----------------| +| `https://v.ex.co` | **15** | Yes | +| `https://voice.example.com` | **25** | Yes (at budget max) | +| `https://voice1.example.com` | **26** | **No** | + +**Phase 0 planner rules:** + +1. Render JWT/AFV URLs from endpoints (+ optional short path from PreferShortJWTPath). +2. If `len(url) ≤ max_chars` **and** encoded payload ≤ budget → in-place `#US`. +3. Else if `us_free_slots` non-empty **and** R1 complete → write free slot + ldstr remap. +4. Else if PreferShortJWTPath: choose shortest candidate path that yields `len(full_url) ≤ 35` (default order `/j`, `/fsd-jwt`, `/api/fsd-jwt`). **Soft-probe** that path (below); warn if probe fails; still allow Apply only with explicit override if probe fails. +5. Else if AFV only: ForceDisableAFV or empty AFV → plan `launch_flag -novoice` (no PE ret until R2). +6. Else **blocker** with explicit message: host must be ≤12 chars for default JWT path **or** free-slot remap not yet available **or** enable PreferShortJWTPath / shorten DNS. + +GUI shows **generic** `Plan.Constraints` / meters from dry-plan on keystroke — **not** hard-coded vPilot layout. + +### Optional server short-path JWT (A8) — includes existing `POST /j` + +**Problem:** PE remap is the hard path for long hosts. +**Alternative:** openfsd web exposes **short JWT paths** so in-place `#US` fits realistic private hostnames. + +#### Existing code (today) + +`internal/web/routes.go` already registers: + +```go +e.POST("/j", func(c *gin.Context) { + c.Redirect(http.StatusFound, "/api/v1/fsd-jwt") +}) +``` + +A **302 redirect is a poor JWT POST surface**: many HTTP clients do not re-POST the body after a redirect (or convert to GET). For Client Setup, **`POST /j` must invoke `getFsdJwt` directly** (same handler as `POST /api/v1/fsd-jwt`), not redirect. Do **not** rely on 307/308 unless a dedicated client compatibility matrix proves safe; direct handler binding is simpler and correct. + +Ultra-short path `/j` (2 chars) yields **max_host = 25** — better than `/api/fsd-jwt` (15) or `/fsd-jwt` (19) for in-place `#US`. + +#### A8 normative route set (PR-5) + +| Route | Behavior | max_host (in-place JWT) | +|-------|----------|------------------------:| +| `POST /j` | **Change** from 302 → **`s.getFsdJwt` directly** (fix existing short path) | **25** | +| `POST /api/v1/fsd-jwt` | Unchanged canonical | 12 | +| `POST /api/fsd-jwt` | **Add** alias → `getFsdJwt` (readable short) | 15 | +| `POST /fsd-jwt` | **Optional** alias → `getFsdJwt` | 19 | + +All return the same VATSIM-shaped body as today. Outside microversion reject. Document in `docs/authentication-token.md` + wiki. Open Q10 path-clash: `/j` already exists; `/fsd-jwt` must not collide with static file routes (prefer not adding if static mount is greedy). + +**Client PreferShortJWTPath trust model:** + +| Step | Behavior | +|------|----------| +| Soft probe (plan-time, optional network) | For each candidate path in order, issue **credential-free** request against `WebBaseURL+path` — prefer `OPTIONS` or `POST` with empty/`{}` body. Treat **401/400/200** with JSON body shape as “route exists”; **404/405/connection error** as missing. Do **not** send real CID/password. | +| Probe disabled | Offline/lab: skip probe; emit **warning** “PreferShortJWTPath set without connectivity check — ensure server has fixed `/j` or A8 aliases.” | +| Probe fails all candidates | **Warning** + plan **blocker** unless user sets override “I confirm short JWT path works.” | +| Mis-set flag without A8/fixed `/j` | Documented operator error; soft probe is the mitigation for the footgun next to the 12-char rule. | + +**KD-13 revised:** no **required** server changes for the client tool to exist; **optional** (and high-leverage) web changes: **fix `/j`**, add readable aliases. Not redistribution; not a DB migration. + +### Profile schema + +#### Migration: fingerprint v1 → schema v2 + +| Today | Target | +|-------|--------| +| `third_party/.../vpilot-3.12.1.yaml` with `profile_version: "1"` fingerprint-only | `schema_version: 2` full mutation profile in **embed** tree | + +**Loader policy (single breaking cut in PR that lands mutations):** + +1. Embed tree requires `schema_version: 2`. +2. Fingerprint-only v1 files are **not** loadable by the engine (research mirror may keep v1 until sync script rewrites). +3. One migration PR copies fingerprints into v2 embed YAML and updates `third_party/` mirror to match. +4. No dual-runtime support for v1 mutation application — avoids two planners. + +Illustrative embed profile fields (offsets stay in YAML only): + +```yaml +schema_version: 2 +client_id: vpilot +display_name: vPilot +supported_client_version: "3.12.1" +# ... installer fingerprints, primary_binary, clr.us_heap ... + +strings: + fsd_jwt: + stock: "https://auth.vatsim.net/api/fsd-jwt" + us_heap_offset: 0x1569F + body_file_offsets: [0xB7988] # extend after R3 if second site live + ldstr_file_offsets: [0x4BDB5] + payload_budget_bytes: 71 + template: "{{.JWTURL}}" + afv_base: + stock: "https://voice1.vatsim.net" + body_file_offsets: [0xA9076] + ldstr_file_offsets: [0x1F0A7] + payload_budget_bytes: 51 + template: "{{.AFVBaseURL}}" + +# Populated only after research gate R1 — empty means remap unavailable +us_free_slots: [] + +mutations: + - id: config_status_servers + kind: vpilot_config + fields: + network_status_url: "{{.StatusURL}}" + cached_servers: + - "{{.FSDServerName}}|{{.FSDAddress}}" # FSDAddress = host or host:port + clear_network_credentials: true + + - id: patch_fsd_jwt + kind: cil_us_or_remap + string_ref: fsd_jwt + on_too_long: error # JWT required; never silent skip + + - id: patch_afv_base + kind: cil_us_or_remap + string_ref: afv_base + on_too_long: launch_novoice # Phase 0: NOT pe_disable until R2 + + - id: afv_disable_pe + kind: raw_overwrite + file_offset: null # set only after R2 + new_bytes: [0x2A] + only_if: pe_disable_selected_and_offset_set +``` + +**Template vars:** `WebBaseURL`, `JWTURL`, `StatusURL`, `StatusJSON`, `FSDHost`, `FSDPort`, `FSDAddress` (normalized host or host:port), `FSDServerName`, `AFVBaseURL`. + +### vPilot 3.12.1 adapter + +#### Surfaces + +| Surface | Phase 0 strategy | +|---------|------------------| +| FSD JWT | `#US` in-place if fits; else blocker until R1/A8 | +| Network status | config 3DES `NetworkStatusURL` → openfsd status.txt | +| Cached servers | `NAME\|FSDAddress` obfuscated | +| Auto server HTTP | skip if cached list set | +| AFV base | `#US` if fits; else `-novoice` (not PE ret) | +| Model matching / updates | leave alone | + +#### Config crypto (normative) + +```text +guid = "5575ac09-f2de-4a1e-808b-e3398e17f8bf" +key16 = MD5(guid) +key24 = key16 || key16[0:8] +field = Base64(3DES-ECB-PKCS7(plaintext, key24)) +``` + +Clear `NetworkLogin` / `NetworkPassword` on Apply (KD-12). + +#### Config path discovery (R4 — required before “adapter complete”) + +Do **not** assume only `install/vPilotConfig.xml`. + +**Candidates (ordered, Windows):** + +1. `{installRoot}/vPilotConfig.xml` +2. `%LOCALAPPDATA%\vPilot\vPilotConfig.xml` (if install root differs) +3. Any path recorded in a previous manifest +4. Research may add more after ProcMon/IL notes + +**Behavior:** + +- Discover all existing candidates; show in GUI “Config files”. +- **Write set:** all candidates that exist **or** the install-dir path if none exist (create only if research confirms client creates there — default: rewrite existing only; if zero exist, blocker “run vPilot once to create config”). +- HealthCheck decrypts **each written** path and asserts status URL; if any written file still has stock status after apply → fail. +- Document operator validation: after Apply, connect once; if servers still VATSIM, report config path bug. + +#### Binary mutations + +1. Verify SHA-1/256 vs profile. +2. Preflight lock / not running. +3. Backup PE. +4. Patch primary JWT `#US` (+ all `body_file_offsets` after R3). +5. Patch AFV `#US` if retargeting. +6. Remap ldstr when using free slots (R1). +7. **Never** apply PE `ret` disable until profile `file_offset` non-null (R2). + +#### HealthCheck (strengthened) + +1. Decode primary `#US` slots asserted in profile. +2. **Scan entire PE** for UTF-16 stock JWT string `https://auth.vatsim.net/api/fsd-jwt` — **fail if any remain** after JWT mutation planned (catches second site `0xBA44A` if live). +3. Optionally scan for stock AFV base if AFV was retargeted. +4. Decrypt all written config paths; assert status + server list. +5. Soft: HTTP GET StatusURL (optional network). + +#### CLI launch + +- `-serveraddressoverride` with normalized FSDAddress. +- `-novoice` when ForceDisableAFV or AFV over-budget without PE disable. + +#### GeoVR (R5) + +String-inventory `GeoVR.Client.dll`, `GeoVR.Connection.dll`, `GeoVR.Shared.dll` for `voice1.vatsim.net` / AFV base. If found, either extend mutations to those DLLs or document that AFV retarget of `vPilot.exe` alone is insufficient (fall back to `-novoice` / PE disable of connect path). + +### Multi-client extension model + +Same adapter registry as before. **GUI rule:** no `if client_id == "vpilot"` for layout, budgets, or buttons. All per-client behavior flows from `Adapter` + `Plan.Constraints` + profile-driven mutation lists. + +xPilot / Euroscope use `padded_string` + `raw_overwrite` families from prior-art YAML as **research seeds**, re-verified per version hash. + +### GUI information architecture + +**Toolkit:** Fyne v2 (KD-2). Product title: **openfsd Client Setup**. + +```mermaid +flowchart TD + A[Home: Client picker] --> B[Install location] + B --> C[Version / hash + lock status] + C --> D[openfsd endpoints form] + D --> E[Plan preview + constraints] + E --> F{Action} + F -->|Apply| G[Progress + result] + F -->|Revert| H[Restore] + F -->|Launch| I[Start client] +``` + +Regions: + +1. Header — product name + legal one-liner. +2. **Client** dropdown (vPilot enabled; others “Coming soon”). +3. Install path + Detect. +4. Fingerprint panel + **“Client is running — quit before Apply”** if preflight fails. +5. Endpoints form — web base, FSD host, FSD port (optional, default 6809), server name, AFV base, force disable voice, prefer short JWT path. +6. **Constraints panel** — data-bound to last dry-plan (`Constraints` / blockers); generic meters (field name, max runes, strategy). +7. Config files list (multi-candidate). +8. Actions Apply / Revert / Launch. +9. Log + recovery text. +10. Advanced: ephemeral launch (Phase 1), profiles dir. + +**macOS:** may configure Windows paths (Wine/share); Launch best-effort. + +**Persistence:** last paths/endpoints in OS config dir — never passwords. + +**Every Apply:** soft banner “For private openfsd networks you are authorized to use.” Warn if `WebBaseURL` host is in known public VATSIM host set (`auth.vatsim.net`, `status.vatsim.net`, `voice1.vatsim.net`, `fsd.connect.vatsim.net`, …) — do not hard-block exotic private mirrors of those names without user override checkbox “I understand”. + +### CLI + +```text +openfsd-client # GUI when no subcommand +openfsd-client list-profiles +openfsd-client detect --client vpilot +openfsd-client plan|apply|revert|health|launch ... + --web-base URL --fsd-host HOST [--fsd-port 6809] + --afv-base URL [--force-disable-afv] [--prefer-short-jwt] + --install DIR +``` + +Exit codes: 0 ok, 1 usage, 2 hash mismatch, 3 apply failed (reverted), 4 revert failed, 5 client running / file locked, 6 plan blockers. + +### Backup & manifest + +`.openfsd-bak` siblings + `.openfsd-inject-manifest.json` as in rev 1; add `preflight_ok`, `config_paths[]`, `constraints_snapshot`. + +Recognize legacy `.orig` from external tools for soft migrate (optional Revert). + +### Relationship to external patch utilities + +**Reimplement** algorithms in monorepo; **supersede** for openfsd docs. External repos remain historical. Wiki points to `openfsd-client`. + +### Sequence: Apply (vPilot) + +```mermaid +sequenceDiagram + actor U as User + participant GUI as Client Setup + participant E as Engine + participant A as VPilotAdapter + participant FS as Install FS + + U->>GUI: Client, path, endpoints + GUI->>E: Preflight + Plan + E->>A: Verify hash + E->>FS: Lock probe / process check + alt running + E-->>GUI: refuse Apply + end + A-->>E: Plan + Constraints + Blockers? + GUI->>U: Preview + U->>GUI: Apply + E->>FS: bak + manifest + E->>A: Apply + A->>FS: config + PE + E->>A: HealthCheck residual scan + E-->>GUI: success or auto-revert +``` + +--- + +## API / Interface Changes + +### Client tool + +New binary `openfsd-client` only. + +### Optional openfsd web (A8 companion) — fix existing `POST /j` + +| Route | Behavior | +|-------|----------| +| `POST /j` | **Today:** 302 → `/api/v1/fsd-jwt` (**insufficient** for POST JWT). **A8:** bind `getFsdJwt` directly | +| `POST /api/v1/fsd-jwt` | Canonical (unchanged) | +| `POST /api/fsd-jwt` | New readable alias → `getFsdJwt` | +| `POST /fsd-jwt` | Optional alias → `getFsdJwt` (watch static route clashes) | + +No envelope change; same VATSIM-shaped body. Outside microversion reject. Document in `docs/authentication-token.md` + wiki. + +**Not required** for tool scaffolding; **recommended** before declaring production-hostname readiness if free-slot research slips. Fixing `/j` alone unlocks max_host **25** without free-slot remap. + +### Agents.md + +Add ownership rows for `cmd/openfsd-client`, `internal/clientinject*`; clarify auxiliary cmds; import forbids; coverage floors; hygiene PE check. + +### Wiki + +`Client-Connection.md` → primary openfsd Client Setup; note status.txt is openfsd’s VATSIM-shaped subset (`json3` / `url1` / `servers.live`); validate once in manual matrix with real 3.12.1. + +--- + +## Data Model Changes + +None in openfsd DB. User-machine bak/manifest/settings only. + +--- + +## Alternatives Considered + +### A1. Subcommand of `cmd/openfsd` — **Rejected** (GUI/deps/image bloat) + +### A2. Server distributes patched clients — **Rejected** (legal) + +### A3. Default TLS MITM proxy — **Non-default advanced only** + +### A4. Config/plugin only — **Insufficient for vPilot JWT/AFV** + +### A5. GUI stack — **Fyne v2 accepted** (not Wails/Qt) + +### A6. External patch utility only — **Supersede via reimplementation** + +### A7. True runtime-only Phase 0 — **Rejected** (EDR, slow) + +### A8. Server short-path JWT (fix `/j` + optional readable aliases) — **Accepted as optional high-leverage companion** + +**Pros:** `POST /j` already exists (must stop being a 302); direct `getFsdJwt` gives max_host **25**; optional `/api/fsd-jwt` for readability; few lines in `internal/web`; often avoids free-slot research for private FQDNs. +**Cons:** still finite host length; PE still needs **some** JWT string patch (stock VATSIM URL → openfsd URL); PreferShortJWTPath footgun without probe. +**Verdict:** Ship A8 as optional openfsd web PR (priority: **fix `/j`**); client PreferShortJWTPath + soft probe. Complements free-slot remap for ultra-long hosts. + +--- + +## Security & Privacy Considerations + +| Threat | Severity | Mitigation | +|--------|----------|------------| +| Misuse toward public VATSIM | High (ethics) | UI banner every Apply; warn on known VATSIM hosts; product framing; no profile that targets VATSIM | +| Malicious profile YAML | Med | PR review; schema validate; no shell from YAML | +| Path traversal | Med | Clean paths; write only under install + discovered config candidates | +| Credential theft | Med | Never store passwords; clear on Apply | +| Half-patched PE | High | Preflight lock; transactional bak; auto-revert | +| Running process write | High | Preflight refuse Apply | +| Accidental binary commit | Med | Hygiene PE magic check in CI | +| Installer URL fetch | Low | Tool never downloads `installer.url` by default | +| Supply chain (Fyne) | Med | Pin version; single go.mod (KD-16) | + +--- + +## Observability + +`slog` + GUI log; manifest audit; no telemetry. Debug checklist: hash, lock, bak missing, AV, URL budget, config path, residual JWT scan, TLS on openfsd. + +--- + +## Testing Strategy + +**Never** put vPilot binaries in git/CI. + +| Layer | Approach | +|-------|----------| +| cilus / vpilotconfig / pepatch | Goldens, table tests, ≥ floors | +| Profile self-consistency (CI) | `len(stock)` vs `payload_budget_bytes`; template required fields; schema_version=2 | +| Engine | Fake adapter; bak/revert; **lock failure** path (hold file handle); blocker paths | +| Residual JWT scanner | Synthetic PE with two UTF-16 copies of stock JWT | +| research tag | Local `.research/vpilot/extracted/vPilot.exe` offset confirmation | +| GUI | View-model / plan formatting unit tests; no Playwright | + +**PR-4 / adapter-complete checklist (maintainer):** + +1. `go test -tags=research ./internal/clientinject/...` green on machine with extract. +2. PR body pastes: PE SHA, confirmed offsets, config path(s) used, free-slot list or “short-host only”, residual scan result. +3. CI-only tests still green without extract. + +**CI binary guard:** extend `scripts/check-hygiene.sh` (or sibling) to fail if files match PE `MZ` magic or `*.exe`/`*.dll` under repo except documented allowlist (none today). `.research/` already gitignored — also fail if such files are staged. + +**Manual matrix:** short-host apply; long-host blocker until R1/A8; quit-client preflight; residual JWT; config multi-path; AFV short URL; `-novoice`; Revert; status feed connect once. + +--- + +## Research gates (must complete before claims below) + +| ID | Gate | Unblocks | +|----|------|----------| +| **R1** | Free `#US` slot catalog + ldstr remap on 3.12.1 | Production-length JWT hostnames without A8 | +| **R2** | AFV connect `ret` CIL file offset for 3.12.1 | PE disable fallback (not just `-novoice`) | +| **R3** | Second fsd-jwt body `0xBA44A` liveness; multi-site patch | “No residual VATSIM JWT” guarantee | +| **R4** | Config path resolution (install vs AppData) | Config Apply correctness | +| **R5** | GeoVR.* string inventory for voice base | AFV retarget confidence | +| **R6** | CachedServers `host:port` acceptance on 3.12.1 | Non-default FSD port UX | + +Gates are tracked under `docs/client-injector/research/`; profiles gain offsets only when gate closed. + +--- + +## Rollout Plan + +| Stage | Content | +|-------|---------| +| R0 | Design rev 3 + pure packages | +| R1a | Engine + CLI skeleton (short-host lab) | +| R1b | Research gates R1–R5 as needed | +| R2 | Adapter “complete” for production hostnames | +| R3 | GUI | +| R4 | Wiki + Windows release artifact | +| R5 | Phase 1 hybrid shadow | +| R6+ | Other client adapters | + +**Release:** Windows amd64 primary; macOS/Linux GUI optional for path management. Server Docker **unchanged**. + +**Effort (revised, indicative):** + +| Slice | Person-weeks | +|-------|----------------| +| Pure cilus + vpilotconfig + pepatch | **1–2** | +| Engine + preflight + backup + CLI | **1–1.5** | +| Research gates R1–R5 | **1–3** (unknown) | +| Adapter complete | **1–2** after gates | +| Optional A8 web aliases | **0.2–0.5** | +| GUI MVP | **1–2** | +| Shadow hybrid | **0.5–1** | +| Next client adapter | **0.5–1.5** each | + +--- + +## Open Questions + +1. ~~AFV disable site~~ → tracked as **R2** (not Phase 0). +2. ~~Free slots~~ → **R1**. +3. ~~Second JWT site~~ → **R3**. +4. ~~Config path~~ → **R4**. +5. Authenticode on 3.12.1 `vPilot.exe`? Affects SmartScreen copy severity. +6. Fyne packaging (`.exe` branding) for first Windows release? +7. TrackAudio config-only adapter early? +8. ~~go.mod nested vs main~~ → **KD-16: single main module**. +9. Ship A8 (fix `/j` first) before R1, in parallel, or only if R1 slips? **Recommendation:** fix `/j` early — high leverage, tiny PR. +10. Optional `/fsd-jwt` alias vs static-route clash — prefer **fixed `/j`** + **`/api/fsd-jwt`**; add `/fsd-jwt` only if no clash. + +--- + +## Key Decisions + +| ID | Decision | Rationale | +|----|----------|-----------| +| **KD-1** | Separate `cmd/openfsd-client` + `internal/clientinject` | Import isolation; auxiliary cmd pattern; not server flag | +| **KD-2** | Fyne v2 GUI; boring-web N/A | Pure Go; minimal elegant desktop UX | +| **KD-3** | Client-agnostic `Adapter` + Plan-driven constraints | Multi-client first; no vPilot-only GUI | +| **KD-4** | Phase 0 = durable on-disk + config; Phase 0.5 research gates; Phase 1 shadow; Phase 2 true runtime | Honest readiness; ship config+short-host early | +| **KD-5** | Version pin by PE hash | Offsets version-specific | +| **KD-6** | AFV retarget preferred; Phase 0 voice-off = **`-novoice`**; PE `ret` only after R2 | Do not promise PE disable without offset | +| **KD-7** | Reimplement prior art in monorepo; supersede external tools for docs | First-class module | +| **KD-8** | Pure stdlib `cilus` / `vpilotconfig` / `pepatch` | Coverage + hygiene culture | +| **KD-9** | Schema v2 template profiles; breaking cut from fingerprint v1 | One planner; embed tree is source of runtime truth | +| **KD-10** | Never redistribute client binaries; CI PE hygiene | Legal | +| **KD-11** | Transactional Apply + running-process preflight + auto-revert | Avoid half-patch / locked PE | +| **KD-12** | Clear vPilot credentials on Apply | Force openfsd credential re-entry | +| **KD-13** | No **required** server/DB changes for the tool; **optional A8** encouraged: **fix existing `POST /j` to `getFsdJwt`**, add readable aliases | PE tool stands alone; `/j` already shipped as 302 — wrong for JWT POST | +| **KD-14** | Synthetic CI + maintainer `research` tag gate for adapter complete | Offset correctness without shipping PE | +| **KD-15** | Import-graph hard-fail for clientinject → server/web/afv/db/… | Ownership forever | +| **KD-16** | **Single main `go.mod`**; pin Fyne when GUI PR lands | Avoid nested-module churn; accept download weight | +| **KD-17** | **Embed root** = `internal/clientinject/profiles/`; `third_party/client-profiles/` is mirror | `go:embed` path rules | +| **KD-18** | FSD default port **6809**; CachedServers `NAME\|host` without port when default; `host:port` when non-default or IncludePort | Prior-art shaped; R6 may refine | +| **KD-19** | Phase 0 production hostnames require **R1 free-slot remap and/or working short JWT path** (fixed `/j` or other A8); else short-host lab only (max JWT host **12** on default `/api/v1/fsd-jwt`) | Budget math; fixed `/j` → max host **25** | +| **KD-20** | HealthCheck residual UTF-16 scan for stock JWT after patch | Second-site safety | +| **KD-21** | Phase 1 shadow = **hybrid**: patch temp copy of PE (+ any DLLs mutated); **cwd = install root**; do not full-tree copy by default | DLLs/GeoVR/plugins resolve from install | +| **KD-22** | Product name **openfsd Client Setup**; “runtime injector” is roadmap language only | Naming honesty | + +--- + +## Risks + +| Risk | Severity | Mitigation | +|------|----------|------------| +| JWT over-budget without R1/A8 | **Critical** | Document 12-char rule; blockers; front-load R1; A8 | +| AFV PE disable unknown | **High** | `-novoice` only until R2 | +| Second JWT site live | **High** | R3 + residual scan KD-20 | +| Config path wrong | **High** | R4 multi-candidate + health | +| GeoVR re-hardcodes AFV | **Med** | R5 inventory | +| Client running / file lock | **High** | Preflight KD-11 | +| AV / SmartScreen | **High** | UX; Phase 1 hybrid; bak | +| Auto-update breaks hash | **Med** | Detect; re-apply prompt | +| Accidental binary commit | **Med** | Hygiene PE check | +| Status feed shape mismatch | **Low** | Manual connect once; docs note | + +--- + +## Appendix A — Interface & kind mapping (normative sketch) + +### YAML `kind` → `MutationKind` + +| YAML | Go constant | Notes | +|------|-------------|-------| +| `vpilot_config` | `config_rewrite` | Detail: network status + servers | +| `config_rewrite` | `config_rewrite` | Generic | +| `cil_us_string` | `cil_us_string` | In-place only | +| `cil_ldstr_remap` | `cil_ldstr_remap` | Free slot + ldstr bytes | +| `cil_us_or_remap` | expanded by planner | Prefer in-place else remap | +| `raw_overwrite` / `section_overwrite` | `raw_overwrite` | | +| `padded_string` | `padded_string` | xPilot-class | +| `afv_disable` | `afv_disable_pe` or `launch_flag` | PE only if offset set | +| `launch_flag` | `launch_flag` | e.g. `-novoice` | + +### `MutationDetail` (tagged union in practice) + +```go +type USStringDetail struct { + StringRef string + NewString string + HeapOff int64 + BodyOffs []int64 +} +type LdstrRemapDetail struct { + LdstrFileOff int64 + NewToken []byte // e.g. 72 xx xx xx 70 + Slot USFreeSlot +} +type RawOverwriteDetail struct { + FileOffset int64 + NewBytes []byte +} +type ConfigRewriteDetail struct { + Paths []string + NetworkStatusURL string + CachedServers []string + ClearCredentials bool +} +type LaunchFlagDetail struct { + Args []string +} +``` + +### `Profile` major fields + +`SchemaVersion`, `ClientID`, `DisplayName`, `Vendor`, `LicenseNote`, `SupportedClientVersion`, `Installer`, `PrimaryBinary` (path, globs, hashes, size, pe meta), `ConfigFiles[]`, `CLR.USHeap`, `Strings` map, `USFreeSlots[]`, `Mutations[]`, `Launch`, `RelatedBinaries[]` (for R5/shadow). + +--- + +## Appendix B — Phase 1 shadow launch (hybrid) + +**Default hybrid algorithm:** + +1. Require install root intact (DLLs, plugins, sounds, model data stay put). +2. Create temp dir `os.MkdirTemp("", "openfsd-client-shadow-*")`. +3. Copy **only** files that will be mutated (typically `vPilot.exe`; plus any DLL listed in mutations after R5). +4. Apply PE mutations to temp copies. +5. For config: **prefer durable install config rewrite** (user expects servers saved) **or** copy config to temp only if research shows PE-relative config load — default **durable config** + shadow PE. +6. Launch: `exec` temp PE with `Dir = installRoot` (Windows CWD for satellite DLL load). +7. On exit: delete temp PE copies (best-effort); leave install PE stock if only shadow mode was used. + +**Do not** full-tree copy (Costura/NAudio-sized GeoVR.Client ~15MB × tree cost, model matching paths). + +If hybrid fails (DLL loads by absolute path to install PE), fall back to durable Apply. + +--- + +## References + +- `docs/client-injector/research/vpilot-3.12.1.md` +- `third_party/client-profiles/vpilot-3.12.1.yaml` (mirror) +- `docs/authentication-token.md`, `docs/design/afv-server.md` +- `wiki/Client-Connection.md` +- `internal/web/auth.go` (`getFsdJwt`), `internal/web/data.go`, `data_templates/status.txt` +- Agents.md; ECMA-335 II.24.2.4 +- Prior art: `renorris/vpilot-patch-utility`, `renorris/openfsd-client-patch-utility` + +--- + +## PR Plan + +House gates when Go lands: + +```bash +go test -race ./internal/clientinject/... +gofmt -l . +bash scripts/check-import-graph.sh +bash scripts/check-hygiene.sh # include PE/binary guard when added +bash scripts/check-coverage.sh 80 +go build -o openfsd-client ./cmd/openfsd-client +``` + +### PR-1: Design rev 3 land + +| | | +|--|--| +| **Title** | `docs: client setup / injector design rev 3` | +| **Files** | `docs/design/client-runtime-injector.md` | +| **Deps** | None | +| **Description** | Land this document (incl. `/j` A8 notes, exact URL budgets). No runtime code. | + +### PR-2: Pure `cilus` + `vpilotconfig` + hygiene PE guard + +| | | +|--|--| +| **Title** | `clientinject: pure #US + vPilot config crypto; forbid PE commits` | +| **Files** | `internal/clientinject/cilus/*`, `vpilotconfig/*`, coverage floors, import-graph stdlib checks, `scripts/check-hygiene.sh` PE/`*.exe` guard, Agents.md rows (auxiliary cmd clarification) | +| **Deps** | PR-1 preferred | +| **Description** | ≥98% pure packages; CI rejects accidental binaries. No adapter yet. | + +### PR-3: `pepatch` + engine core (backup, plan types, preflight lock, profile load) + +| | | +|--|--| +| **Title** | `clientinject: pepatch, engine, backup, running-process preflight` | +| **Files** | `pepatch/*`, `engine.go`, `backup.go`, `preflight.go`, `profile.go`, `kinds.go`, `profiles/` embed skeleton, fake-adapter tests, optional `scripts/check-client-profiles-sync.sh` | +| **Deps** | PR-2 | +| **Description** | Transactional bak; file-lock preflight; schema_version 2 loader; YAML↔kind map. CLI optional stub. | + +### PR-4: Research gates R1–R5 (offsets, free slots, config path, GeoVR, second JWT) + +| | | +|--|--| +| **Title** | `docs/clientinject: vPilot 3.12.1 research gates (remap slots, config path, JWT sites)` | +| **Files** | `docs/client-injector/research/*`, embed profile updates when gates close, synthetic residual-scan fixtures | +| **Deps** | PR-3 (for fixture harness); can start research notes in parallel with PR-2 | +| **Description** | **Not** “ship adapter.” Close R1–R5 as far as possible; populate `us_free_slots`, multi `body_file_offsets`, config discovery notes, GeoVR inventory. May split into 4a/4b if large. | + +### PR-5 (optional companion): openfsd web short JWT paths (A8) + +| | | +|--|--| +| **Title** | `web: fix POST /j as fsd-jwt handler; add short-path aliases` | +| **Files** | `internal/web/routes.go`, PE/route tests (POST `/j` body reaches handler — **not** 302), `docs/authentication-token.md`, wiki | +| **Deps** | None (server-side); coordinates with client PreferShortJWTPath + soft probe | +| **Description** | (1) Change `POST /j` from 302 redirect to **`s.getFsdJwt` direct**. (2) Add `POST /api/fsd-jwt` alias. (3) Optional `POST /fsd-jwt` if no static clash. Document max_host table. Independently reviewable; high leverage for Client Setup budgets. | + +### PR-6: vPilot 3.12.1 adapter + CLI (honest readiness) + +| | | +|--|--| +| **Title** | `openfsd-client: vPilot 3.12.1 adapter CLI` | +| **Files** | `adapters/vpilot/*`, `cmd/openfsd-client` CLI, embed profile, residual JWT scan, config multi-path | +| **Deps** | PR-3; **PR-4 for production hostnames**; short-host lab path may land with blockers if R1 incomplete | +| **Description** | plan/apply/revert/health/launch. Acceptance: (a) short-host lab green; (b) production hostnames green **only if** R1 and/or PR-5 A8 available; (c) PE AFV disable **out of scope** until R2; (d) maintainer `go test -tags=research` evidence in PR body; (e) preflight running-client test. | + +### PR-7: Fyne GUI — openfsd Client Setup + +| | | +|--|--| +| **Title** | `openfsd-client: Fyne multi-client GUI (vPilot enabled)` | +| **Files** | `cmd/openfsd-client/gui/*`, Fyne in main go.mod (KD-16), legal banner, constraints panel from Plan | +| **Deps** | PR-6 | +| **Description** | Generic client picker + path + endpoints + constraints (no vPilot-only layout). Apply/Revert/Launch. | + +### PR-8: Wiki + operator guide + +| | | +|--|--| +| **Title** | `docs: Client Setup operator guide; update Client-Connection wiki` | +| **Files** | `wiki/Client-Connection.md`, `docs/client-injector/README.md`, README pointer | +| **Deps** | PR-6; PR-7 preferred | +| **Description** | 12-char rule, R1/A8, `-novoice`, quit-before-apply, status feed note, legal. Do **not** claim PE AFV disable until R2. | + +### PR-9: Phase 1 hybrid shadow launch + +| | | +|--|--| +| **Title** | `openfsd-client: hybrid shadow PE launch` | +| **Files** | `launch_shadow.go`, CLI `--ephemeral`, GUI advanced | +| **Deps** | PR-6 | +| **Description** | Temp PE (+ mutated DLLs only), cwd=install root, durable config default (Appendix B). | + +### PR-10: xPilot adapter (second client) + +| | | +|--|--| +| **Title** | `clientinject: xPilot version-pinned adapter` | +| **Files** | `adapters/xpilot/*`, embed profile, research notes, GUI enable | +| **Deps** | PR-3 engine; PR-7 GUI list | +| **Description** | Proves multi-client; re-verify offsets vs prior art for chosen hash. | + +### PR-11 (optional): Euroscope / vatSys / TrackAudio + +| | | +|--|--| +| **Title** | `clientinject: additional client adapters` | +| **Files** | per-client adapters + profiles + wiki | +| **Deps** | PR-10 pattern | +| **Description** | TrackAudio may be config-only. | + +--- + +*End of design document (rev 3).* diff --git a/third_party/client-profiles/vpilot-3.12.1.yaml b/third_party/client-profiles/vpilot-3.12.1.yaml new file mode 100644 index 0000000..c6ec18d --- /dev/null +++ b/third_party/client-profiles/vpilot-3.12.1.yaml @@ -0,0 +1,41 @@ +# openfsd client profile — metadata only. Do NOT ship vPilot binaries. +# Source installer: https://vpilot.rosscarlson.dev/Assets/Files/Installers/vPilot-Setup-3.12.1.exe +# Legal: user must install vPilot themselves; openfsd never redistributes it. + +client_id: vpilot +display_name: vPilot +vendor: Ross Alan Carlson +license_note: Proprietary; not redistributable by openfsd +profile_version: "1" +supported_client_version: "3.12.1" +released: "2026-02-15" +download_page: https://vpilot.rosscarlson.dev/Download +installer: + filename: vPilot-Setup-3.12.1.exe + url: https://vpilot.rosscarlson.dev/Assets/Files/Installers/vPilot-Setup-3.12.1.exe + size_bytes: 13984016 + sha256: 528a51bf0e71ada11103314d18a9fc0321afac94e2706f2fb13e0fbf5bb4beaa + sha1: 48820cb593c6cef8325a331c763316a8b33a501b + format: nsis +primary_binary: + relative_path: vPilot.exe # under install dir + # Default install: %LOCALAPPDATA%\vPilot\vPilot.exe + default_install_globs: + windows: + - "%LOCALAPPDATA%\\vPilot\\vPilot.exe" + sha1: 7d95a7110392c15728143cc30e1f00899c686eb5 + sha256: c743f204929309db6f49e8b543db2d661ad09c32a2a7bc71fe419c685d244bff + size_bytes: 1236416 + pe: + framework: ".NETFramework,Version=v4.7.2" + clr: true + architecture: x86 +config_files: + - relative_path: vPilotConfig.xml + format: xml_obfuscated_fields +related_binaries: + - GeoVR.Client.dll # AFV client stack (NAudio embedded via Costura) + - GeoVR.Connection.dll + - GeoVR.Shared.dll + - RossCarlson.Vatsim.Vpilot.Plugins.dll +prior_art_profile: vpilot-3.11.1 # openfsd-client-patch-utility