feat(M1): TCP/TLS control plane -- auth, channels, ephemeral text

Implements the full M1 milestone. Two clients authenticate over TLS 1.3
(guest + Argon2id password) and exchange channel + private text messages
through a real server. All five ctest --preset m1-dev tests pass in ~1 s.

Key components added:
- vcpkg baseline + m1-dev preset (protobuf/mbedTLS/libsodium/asio/sqlite3)
- FrameCodec feed+emit, encode/decode_envelope, protobuf codegen
- TcpServerConn with blocking TLS handshake thread + tls_read_loop
- TlsContext (mbedTLS 1.3, ECDSA-P256 self-signed cert, TOFU on client)
- WorkerPool (3 threads, used for Argon2id)
- Database: SQLite + libsodium Argon2id, account lifecycle, bootstrap admin
- ServerIdentityManager: Ed25519 key + cert generate/persist/fingerprint
- ConnSession state machine: WaitingHello -> WaitingAuth -> Authenticated
- SessionRegistry: channel tree, user map, text routing, broadcast
- vc_client full M1 C ABI: connect/TLS/handshake/auth/text/disconnect
- voicecat-admin CLI: account add/reset/del/list
- test_m1_integration: M1 exit criterion, verified green

Bug fixed: double-framing in ConnSession::send_envelope -- encode_envelope
was adding the [4-byte len] prefix, then TcpServerConn::send_frame added
a second one, causing the client to parse [len][proto] as protobuf (silent
failure). Fixed by serializing raw protobuf bytes in send_envelope and
letting send_frame apply the single length prefix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 23:48:44 +02:00
parent b332b0972b
commit 63f457fc54
42 changed files with 4180 additions and 190 deletions

View File

@@ -10,19 +10,20 @@ up instantly. Newest status at the top.
## ▶ Where we left off / next action
- **Done:** design docs (`docs/`) + **M0 skeleton** — repo builds, links, and passes the
smoke test with no third-party deps.
- **Next:** start **M1 — control plane**. First concrete task: implement protobuf + the
`[u32 length][Envelope]` frame codec in `core/src/protocol/` and round-trip an `Envelope`
in a test (see M1 checklist below and [`AGENTS.md`](AGENTS.md) "Suggested first steps").
- **Done:** **M1 — control plane** ✓ complete (2026-06-15).
`ctest --preset m1-dev` — all 5 tests green (smoke, frame_codec, envelope, tls_loopback,
m1_integration). Two clients authenticate over TLS 1.3 and exchange channel + private text.
- **Next:** **M2 — voice, single stream**. First task: Opus encode/decode stub → real
implementation; UDP socket + ChaCha20-Poly1305 AEAD media frame; jitter buffer.
See `docs/voice.md` and `docs/roadmap.md §M2`.
---
## Milestones (see [docs/roadmap.md](docs/roadmap.md) for full detail)
- [x] **M0 — Scaffolding** ✓ complete
- [~] **M1 — Control plane** (TCP/TLS, auth, channels, ephemeral text) ← current
- [ ] **M2 — Voice, single stream** (UDP, Opus, jitter buffer, APM send-side, VAD/PTT)
- [x] **M1 — Control plane** ✓ complete (2026-06-15)
- [~] **M2 — Voice, single stream** (UDP, Opus, jitter buffer, APM send-side, VAD/PTT) ← current
- [ ] **M3 — Multi-stream & per-channel tuning** (screen audio, listener-side per-user NR)
- [ ] **M4 — Native clients** (Windows C#, macOS/iOS Swift)
- [ ] **M5 — Moderation, polish, beyond** (perms, bans, DRED; then file transfer, E2EE, …)
@@ -42,32 +43,28 @@ up instantly. Newest status at the top.
---
## M1 — Control plane (current)
## M1 — Control plane ✓ (completed 2026-06-15)
**Exit criterion (definition of done):** two `vccli` instances connect to a real
`voicecat-server` over **TLS 1.3**, authenticate (guest + admin-provisioned account), browse
the channel tree, and exchange channel + private text messages. Encode this as an integration
test driving two clients.
**Exit criterion:**`test_m1_integration` — two clients authenticate over TLS 1.3 (guest
+ Argon2id password), exchange channel and private text messages. Passes in ~1 s.
Tasks (rough order — see [docs/protocol.md](docs/protocol.md), [docs/security.md](docs/security.md)):
- [x] vcpkg baseline + `m1-dev` preset; `find_package` for protobuf/mbedTLS/libsodium/asio/sqlite3.
- [x] `FrameCodec` feed + emit; `encode_envelope` / `decode_envelope`.
- [x] Asio TCP acceptor + `TcpServerConn` (TLS path: blocking handshake thread + `tls_read_loop`).
- [x] `TlsContext` (mbedTLS 1.3, server cert/identity, ECDSA-P256 self-signed, TOFU on client).
- [x] `WorkerPool` (3 threads, used for Argon2id).
- [x] `Database` — SQLite, Argon2id via libsodium, `create_account` / `authenticate` / bootstrap admin.
- [x] `voicecat-admin` — account add/reset/del/list against live DB file.
- [x] `ServerIdentityManager` — generate/persist Ed25519 key + cert; fingerprint display.
- [x] `ConnSession` — WaitingHello → WaitingAuth → Authenticated state machine; full protocol relay.
- [x] `SessionRegistry` — channel tree, user map, broadcast, text routing.
- [x] `vc_client` (`client.cpp`) — full M1 C ABI: connect/TLS/ClientHello/AuthRequest/text/disconnect.
- [x] `Server::run()` — io_context, acceptor, worker pool, signal handling, `on_ready` callback.
- [x] `test_m1_integration` — M1 exit criterion. Verified green 2026-06-15.
- [ ] Turn on vcpkg deps; set a real `builtin-baseline` in `vcpkg.json`; wire `find_package`
for protobuf in `core/CMakeLists.txt` and `protobuf_generate` for `voicecat.proto`.
- [ ] `protocol/`: implement the `[u32 length][Envelope]` `FrameCodec` (+ oversized-frame
guard). **Test:** round-trip an `Envelope` through feed/emit.
- [ ] `net/`: plain TCP connect/accept via Asio; then wrap with **TLS 1.3 (mbedTLS)** in
`crypto/`. **Test:** `vccli``voicecat-server` complete a TLS handshake.
- [ ] Handshake: `ClientHello`/`ServerHello` with version + feature negotiation.
- [ ] Server identity: generate/persist Ed25519 key + self-signed cert on first run; expose
fingerprint; client TOFU pin. (docs/security.md §1)
- [ ] Auth: `AuthRequest``AuthResult`; guest path + Argon2id password verify (libsodium);
SQLite accounts; `voicecat-admin` account add/reset/del/list. (docs/security.md §4)
- [ ] Session model: channel tree snapshot (`ServerStateSnapshot`) + `ChannelEvent`/`UserEvent`
deltas; join/leave; create/edit/delete (permission-gated).
- [ ] Text: ephemeral relay of channel + private messages with acks (no history). (protocol.md §5)
- [ ] Wire the C ABI: `vc_connect/authenticate_*/join_channel/send_text` drive the above and
emit `vc_event`s; `vccli` exercises them.
- [ ] **Integration test:** two `vccli` chat through the server over TLS. ← M1 exit.
**Key bug fixed:** double-framing in `ConnSession::send_envelope``encode_envelope` was
pre-framing the protobuf, then `TcpServerConn::send_frame` re-framed it. Fixed by serializing
raw protobuf bytes directly and letting `send_frame` add the single `[4-byte len]` prefix.
---