diff --git a/CLAUDE.md b/CLAUDE.md index 9d3d03c..0472794 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,11 @@ Auto-loaded each session. This is the **map**: build commands, architecture at a where everything is. For the *working method* read [`AGENTS.md`](AGENTS.md); for *what's done and what's next* read [`PROGRESS.md`](PROGRESS.md); for *design* read [`docs/`](docs/). -> **One-line status:** M3 (multi-stream, per-channel tuning) is complete, plus a follow-up -> pass closing the device enumeration / VAD-PTT input gate / stereo playback / WASAPI loopback -> gaps it left open (`ctest --test-dir build/m1-dev` green — 12/12 tests, including -> `test_vad_ptt_devices`, and `vccli --voice`/`--list-devices` manually verified live). Real -> `webrtc-audio-processing` (AEC/NS/AGC) is still unbuilt — no working Windows/MSVC port -> upstream — so v1 ships a lightweight energy/RMS VAD instead. Next up is **M4** (native -> clients). See [`PROGRESS.md`](PROGRESS.md). +> **One-line status:** M4 (Windows WinForms C# client) is complete — connect, saved servers, +> TOFU identity pinning, channel tree, voice (VAD/PTT/always-on + VAD threshold slider + +> per-user tuning), text chat, device pickers, level meters. `ctest --preset m1-dev` green — +> 14/14 tests. `dotnet build` green — 0 warnings. macOS/iOS Swift client and M5 (moderation) +> are next. See [`PROGRESS.md`](PROGRESS.md). VoiceCat = self-hosted native voice & text chat (TeamSpeak/Mumble-style). Plain TCP (control) + UDP (media), no WebRTC, encrypted by default. A shared C++ core (`libvoicecat`) drives diff --git a/PROGRESS.md b/PROGRESS.md index 3dfce39..f9a6cd5 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -53,7 +53,14 @@ up instantly. Newest status at the top. (mutex lock, heap allocation, blocking `sendto` on the miniaudio real-time callback thread) — predates this work, documented but not fixed; fixing it needs the lock-free ring-buffer hand-off `docs/architecture.md §3` specifies, a separate, larger refactor. -- **Next:** **M4 — native clients** (Windows C#, macOS/iOS Swift). See `docs/roadmap.md §M4`. +- **Done:** **M4 — Windows WinForms C# client** ✓ complete (2026-06-17). Full details in the + M4 section below. `ctest --preset m1-dev` — **14/14 tests** green. `dotnet build` — 0 + warnings/errors across all three C# projects. Manually verified: saved servers, TOFU + first-connect dialog, channel tree, join, voice (VAD/PTT/always-on + sensitivity slider + + per-user gain/mute/NR), text chat (channel + private), device pickers, level meter. +- **Next:** macOS/iOS Swift client (M4 continued) and/or **M5** (admin UI, moderation, kick/ + ban). The C ABI is complete and stable through M4 — both directions are unblocked. See + `docs/roadmap.md §M4–M5`. --- @@ -63,7 +70,7 @@ up instantly. Newest status at the top. - [x] **M1 — Control plane** ✓ complete (2026-06-15) - [x] **M2 — Voice, single stream** ✓ complete (2026-06-16) - [x] **M3 — Multi-stream & per-channel tuning** ✓ complete (2026-06-16) -- [ ] **M4 — Native clients** (Windows C#, macOS/iOS Swift) ← next +- [~] **M4 — Native clients** — Windows WinForms ✓ (2026-06-17); macOS/iOS Swift pending - [ ] **M5 — Moderation, polish, beyond** (perms, bans, DRED; then file transfer, E2EE, …) --- @@ -349,6 +356,78 @@ flags), and `vccli --voice --input-mode vad` connecting + streaming without inci --- +## M4 — Windows WinForms C# client ✓ (completed 2026-06-17) + +**Exit criterion:** ✓ `ctest --preset m1-dev` — **14/14 tests** green (existing 12 + 2 new +C++ tests: `test_channel_user_list_abi`, `test_tofu_flow`). `dotnet build` — 0 warnings/errors. +Manually verified: connect, TOFU first-connect dialog, channel tree, join, voice, text, device +pickers, level meter. Accessibility: explicit `AccessibleName`/`AccessibleDescription` on every +control, `&` mnemonics on every button, activity-log `ListBox` as screen-reader record. + +**New C++ ABI surface** (all additive, backward-compatible): +- `vc_list_channels` / `vc_list_users` / `vc_list_user_streams` — pull-based snapshot getters + for the channel tree + user list; `session_model_mu_` added for cross-thread safety. + `SessionModel::apply_snapshot`/`apply_channel_event` fixed to populate `parent_id`, + `password_protected`, `max_users` (were permanently zeroed despite the struct declaring them). +- `vc_join_channel(channel_id, password)` — join with optional password; server replies via + new `VC_EVENT_JOIN_RESULT`. +- `VC_EVENT_SERVER_IDENTITY` + `vc_confirm_server_identity(accept)` — TOFU gate that blocks + `io_thread_` until the UI approves or rejects. Pins the TLS leaf-cert SHA-256 fingerprint + (verifiable directly at handshake), **not** the declared Ed25519 value (see docs/security.md + §1.1 for why — the TLS cert and Ed25519 key are generated independently, no binding). + `vc_get_server_identity_display` exposes the Ed25519 fingerprint for human-readable display. +- `vc_config::tofu_store_path` — optional per-user pin file path; defaults to a relative + `"./voicecat_tofu_pins.txt"` so existing tests need no change. +- `TcpAcceptor` now dual-stacks (IPv6 + IPv4 fallback) so `localhost` → `::1` on Windows + connects correctly without forcing users to type `127.0.0.1`. +- `VC_INPUT_ALWAYS_ON = 2` in `vc_input_mode` — transmit unconditionally (no VAD gate). +- `vc_set_vad_threshold(float)` — live VAD threshold update; `EnergyVadProcessor` stores it + atomically so the RT capture path reads without a lock or allocation. + +**New C++ tests:** +- `test_channel_user_list_abi` — snapshot getters, `parent_id`/`password_protected`/`max_users` + regression, per-user stream list, invalid-user-id error, double-free idempotency. +- `test_tofu_flow` — first-connect blocks until confirmed; reject doesn't persist; reconnect to + same identity reports `MATCHED`; rotated identity reports `MISMATCH`; `vc_confirm_*` with + nothing pending returns an error. + +**`windows-client` CMake preset** — Release, `VOICECAT_BUILD_SHARED=ON`, static MinGW runtime +(`-static-libgcc -static-libstdc++ -static -lwinpthread`), no tools/tests. Outputs +`build/windows-client/bin/voicecat.dll` with zero MinGW DLL dependencies (only Windows system +DLLs remain — verified via `objdump -p`). + +**C# solution** (`clients/windows/`, .NET 10 LTS `net10.0-windows`): +- `VoiceCat.Interop` — `[LibraryImport]` P/Invoke surface, `[UnmanagedCallersOnly]` callbacks, + `System.Threading.Channels.Channel` event delivery drained by 30ms WinForms + Timer; `VoiceCatClientHandle : SafeHandle` guarantees `vc_client_destroy`. +- `VoiceCat.App` — WinForms UI: + - `ConnectDialog` — saved-server `ListBox`, Add/Remove/Edit; servers persisted to + `%AppData%\VoiceCat\servers.json`; passwords DPAPI-encrypted (`ProtectedData`, opt-in). + - `ServerIdentityDialog` — shown only on `FIRST_CONNECT`/`MISMATCH` (never `MATCHED`); + mismatch text and button ordering are starkly different ("WARNING" framing, Cancel default). + - `MainForm` — `TreeView` channel tree, `ListBox` user list, `RichTextBox` chat, scope + `ComboBox` (Channel/Private), activity-log `ListBox`, voice panel with mic toggle, + mute/deafen checkboxes, VAD/PTT/Always-On radio group, VAD sensitivity `TrackBar` + (1–100, hidden for non-VAD modes), device `ComboBox` + refresh, level `ProgressBar`. + - `PerUserTuningDialog` — real-time gain `TrackBar` + mute/NR checkboxes; applied to all + of a user's streams immediately (no OK/Cancel round-trip). + - `PttKeyCaptureDialog` — focus-scoped PTT key capture. +- `VoiceCat.Interop.Tests` — xunit smoke test: connect → TOFU → guest auth → list channels + purely via P/Invoke against a live `voicecat-server.exe`. + +**Explicitly out of scope for this pass:** +- macOS/iOS Swift client — pending. +- Admin/moderation UI (kick/ban/permissions/account provisioning) — server-side dispatch for + these messages is M5's job; building the UI now would require building the server side too. +- PTT hotkey is **focus-scoped only** (works while VoiceCat window has focus). A system-wide + `WH_KEYBOARD_LL` hook would require escalated permissions and risk AV flagging — documented + limitation, not silently omitted. +- Receive-side noise reduction (`vc_set_remote_stream(..., noise_reduction)`) is end-to-end + plumbed but behaviorally a passthrough no-op (`ApmPassthrough`, no PCM modification) — + same as before M4. The per-user NR checkbox in `PerUserTuningDialog` is labeled accordingly. + +--- + ## Decisions log All architecture/scope decisions are settled and recorded in diff --git a/docs/architecture.md b/docs/architecture.md index 56fb6a5..faa8e4e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,7 +10,7 @@ stable **C ABI** (`voicecat.h`). ┌───────────────────────────────────────────┐ macOS / iOS (Swift) │ │ Windows (C#) ┌──────────────────┐ │ libvoicecat (C++) │ ┌──────────────────┐ - │ SwiftUI views │ │ ┌─────────────────────────────────────┐ │ │ WinUI/Avalonia │ + │ SwiftUI views │ │ ┌─────────────────────────────────────┐ │ │ WinForms (.NET 10│ │ AVAudioSession │──┼─▶│ C ABI (voicecat.h) │◀─┼──│ LibraryImport │ │ Swift↔C++ interop│ │ ├─────────────────────────────────────┤ │ │ P/Invoke │ └──────────────────┘ │ │ Session / Protocol state machine │ │ └──────────────────┘ @@ -150,10 +150,14 @@ Design notes: frames, so audio-only stays well within budget. It links a *minimal* slice of the core (Opus encode + media send), shares the session/credentials with the host app through an **App Group**, and re-derives its own media keys. This is detailed in [voice.md](voice.md) §9. -- **C# / Windows.** `LibraryImport` (source-generated P/Invoke, .NET 7+) over the C ABI. - Marshal the `on_event` callback as a `[UnmanagedCallersOnly]`/function-pointer to avoid - delegate lifetime pitfalls. UI in **WinUI 3** (most native) or **Avalonia** (if we later - want a single C# UI across desktop OSes). +- **C# / Windows.** `[LibraryImport]` (source-generated P/Invoke, .NET 7+) over the C ABI. + `[UnmanagedCallersOnly]` static methods for `on_event`/`on_level` to avoid delegate-lifetime + pitfalls. UI in **WinForms (.NET 10)** — chosen over WinUI 3/Avalonia for its mature, + predictable screen-reader (NVDA/JAWS/Narrator) UIA support (see roadmap.md §2). + Events are delivered via `System.Threading.Channels.Channel`, drained by a + 30ms `System.Windows.Forms.Timer` on the UI thread — simpler than a message-only HWND + + `PostMessage` with no meaningful latency cost. `VoiceCatClientHandle : SafeHandle` wraps + the `vc_client*` and guarantees `vc_client_destroy` runs on GC/Dispose. ## 5. Server architecture diff --git a/docs/roadmap.md b/docs/roadmap.md index 5f98dc0..0947bd8 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -38,13 +38,28 @@ exists from M1 so the protocol can be exercised long before any GUI. - **Exit:** a user shares mic + desktop audio; listeners control each independently. ### M4 — Native clients -- **Windows (C#/WinUI):** connect, saved-server list, channel tree, voice, text, device - pickers, meters, VAD/PTT + per-user NR controls. -- **macOS (Swift/SwiftUI):** same. -- **iOS (Swift):** AVAudioSession integration, mic permission, foreground voice; ReplayKit - broadcast extension for `SCREEN_AUDIO`. -- In-app **admin interface** (account provisioning, bans) for admin users. -- **Exit:** non-technical user installs a client, saves a server, and joins. + +**Windows (C#/WinForms, .NET 10) ✓ complete 2026-06-17:** +- Connect, saved-server list (JSON, DPAPI-encrypted passwords), TOFU identity dialog. +- Channel tree (`TreeView`), user list, join (incl. password-protected channels). +- Voice: mic start/stop, mute/deafen, VAD/PTT/**always-on** mode, **VAD sensitivity slider** + (live threshold update via `vc_set_vad_threshold`), device picker, level meter. +- Per-user gain/mute/NR tuning (`PerUserTuningDialog`). +- Channel + private text chat. Activity log (screen-reader primary path). +- Explicit `AccessibleName`/`AccessibleDescription` on every control; `&` mnemonics; + `AutomationNotification` curated live announcements. +- Focus-scoped PTT (documented limitation — no system-wide hook in v1). +- Admin/moderation UI **out of scope** — needs server-side dispatch first (M5). + +**macOS (Swift/SwiftUI) — pending:** +- Same feature set as Windows over the same C ABI (now stable and complete). +- AVAudioSession, mic permission, interruption/route-change handling. + +**iOS (Swift) — pending:** +- AVAudioSession, mic permission, foreground voice. +- ReplayKit broadcast extension for `SCREEN_AUDIO`. + +**Exit:** non-technical user installs a client, saves a server, and joins. ### M5 — Moderation, polish, and beyond - Permissions/roles, kick/ban/server-mute, channel passwords UI. @@ -84,6 +99,25 @@ Settled and reflected throughout the docs: the same `ApmProcessor` interface; there is **no AEC/NS/AGC implementation at all** yet. Real `webrtc-audio-processing` stays a tracked future swap (e.g. if/when a Linux build target exists). (voice.md §8, §11) +- **Windows client UI framework (2026-06-17):** **WinForms** (.NET 10), not WinUI 3 or + Avalonia. Reason: Win32 HWND controls have the most mature, predictable screen-reader + (NVDA/JAWS/Narrator) support of any current .NET UI stack. WinUI 3's accessibility UIA + tree has known rough edges on .NET 10; Avalonia's accessibility story is thinner still. + This overrides the earlier WinUI/Avalonia mention in `docs/tech-stack.md §2` and + `docs/architecture.md §4`. (clients/windows/) +- **TOFU pins TLS leaf cert, not Ed25519 (2026-06-17):** the original design said to pin the + server's declared Ed25519 identity fingerprint from `ServerHello`. This is circular — the + Ed25519 key and the TLS cert are generated independently with no cryptographic binding, so + accepting/rejecting based on a value sent *inside* the channel being trust-decided is + meaningless. **Decision:** pin the TLS leaf certificate's own SHA-256 fingerprint, which is + verifiable directly from the TLS handshake before any application data is trusted. The Ed25519 + value is still shown in the identity dialog for human-readable display only (informational). + See `docs/security.md §1.1`. (core/src/crypto/tofu_store.*, vc_confirm_server_identity) +- **PTT is focus-scoped in v1 (2026-06-17):** PTT hotkey capture uses `Form.KeyDown`/`KeyUp` + (works only while the VoiceCat window has focus), not a system-wide `WH_KEYBOARD_LL` hook. + Reason: a low-level keyboard hook requires escalated permissions, risks AV flagging, and is + disproportionate complexity for a v1 client. Documented in the UI as a known limitation. + Can be revisited for v2 if users request it. (clients/windows/VoiceCat.App/Forms/MainForm.cs) ## 3. Open questions diff --git a/docs/security.md b/docs/security.md index 888bdd6..e34583a 100644 --- a/docs/security.md +++ b/docs/security.md @@ -29,14 +29,26 @@ media channel. Plus server identity, authentication, accounts at rest, and anti- Self-hosting means most servers won't have a CA-signed cert for a hostname. We support both, advertised in `ServerHello`: -1. **TOFU (Trust On First Use)** — default for hobby servers. The server has a long-lived - **Ed25519 identity key**; its fingerprint is shown to the user on first connect (like - SSH host keys / TeamSpeak server keys) and pinned locally. Subsequent connects verify - the pin; a changed key warns loudly. The TLS cert is self-signed and bound to this - identity key. +1. **TOFU (Trust On First Use)** — default for hobby servers. On first connect the client + shows an identity dialog and, if accepted, pins the value locally. Subsequent connects + verify the pin silently; a changed value warns loudly (`MISMATCH`). 2. **PKI** — a server with a domain can use a normal CA-signed cert (e.g. Let's Encrypt); clients validate the chain conventionally. TOFU pinning still applies on top. +**What is actually pinned (M4 implementation):** the **TLS leaf certificate's SHA-256 +fingerprint** — verifiable directly from the TLS handshake before any application data is +trusted. The server also declares an Ed25519 identity fingerprint in `ServerHello`, but this +value is **display-only** and is *not* the value that is pinned or verified. Reason: the TLS +cert and the Ed25519 identity key are generated independently with no cryptographic binding +between them, so pinning the self-declared Ed25519 value (sent *inside* the channel being +trust-decided) would be circular — an attacker who impersonates the server at the TLS level +would supply whatever Ed25519 value they like. Pinning the TLS cert fingerprint is the only +value that is genuinely verifiable at the moment of trust decision. + +This is a known limitation of the current design. Closing it properly requires binding the +Ed25519 key into the TLS cert (e.g. as a SubjectAltName or extension), which is a planned +future improvement. Until then, clients display both values but gate on the cert fingerprint. + Client certificates are reserved for a future "key-based identity" option (see roadmap) but are not required in v1. diff --git a/docs/tech-stack.md b/docs/tech-stack.md index ea2527c..66c3938 100644 --- a/docs/tech-stack.md +++ b/docs/tech-stack.md @@ -33,14 +33,16 @@ explicit resampling (speexdsp/libsamplerate) is only needed when a device can't | Packaging | Swift Package + Xcode project | Core shipped as an XCFramework (device + simulator + macOS slices). | | Future | CallKit / PushKit | For background VoIP + incoming-call UX on iOS. Post-v1. | -### Windows — C# +### Windows — C# (shipped in M4, 2026-06-17) | Concern | Choice | Notes | |---------|--------|-------| -| Runtime | **.NET 8+** | LTS. | -| Interop | **`LibraryImport`** (source-gen P/Invoke) over the C ABI | Marshal the event callback as a function pointer (`[UnmanagedCallersOnly]`) to avoid delegate-lifetime bugs; keep the interface "chunky" not "chatty" to minimize managed↔native transitions. | -| UI | **WinUI 3** (most native) or **Avalonia** | WinUI for a first-class Windows look; Avalonia if we later want one C# UI across desktop OSes. | -| Audio | handled by the core (miniaudio/WASAPI) | C# only drives device selection + meters. | +| Runtime | **.NET 10 LTS** (`net10.0-windows`) | In-service until 2028. | +| Interop | **`[LibraryImport]`** (source-gen P/Invoke) over the C ABI | `[UnmanagedCallersOnly]` static methods for `on_event`/`on_level`; `VoiceCatClientHandle : SafeHandle` owns the `vc_client*` lifetime. | +| Event delivery | **`System.Threading.Channels.Channel`** | Single-writer/reader, unbounded; drained by a 30ms `System.Windows.Forms.Timer` on the UI thread. Simpler than a message-only HWND with no meaningful latency cost. | +| UI | **WinForms** | Chosen over WinUI 3 / Avalonia for mature, predictable NVDA/JAWS/Narrator UIA support. Win32 HWND controls have the most complete accessibility story on .NET 10 today. See roadmap.md §2. | +| Persistence | **`System.Text.Json`** (`servers.json`), **`ProtectedData`** (DPAPI) | Saved-server list in `%AppData%\VoiceCat\`; passwords DPAPI-encrypted at rest, opt-in, `CurrentUser` scope. | +| Audio | Handled by the core (miniaudio/WASAPI) | C# only drives device selection + meters. | ## 3. Server (`voicecat-server`)