VNOX-Server/docs/01-architecture.md

300 lines
10 KiB
Markdown

# Architecture
## High-level layout
```
┌─────────────────────────────────────────────────────┐
│ VNOX Client │
│ (Rust + egui + wgpu) │
└────────────────────┬────────────────────────────────┘
│ LNEx v1
┌──────────┴──────────┐
│ TCP │ UDP
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Gateway │ │ Voice Node │
│ (Rust/Tokio) │ │ (Rust) │
│ │ │ │
│ auth │ │ Opus encode │
│ channels │ │ UDP relay │
│ sessions │ │ jitter buffer │
│ permissions │ │ packet seq │
│ federation │ │ audio routing │
└────────┬────────┘ └──────────────────┘
│
│ LNEx Federation (Phase 3)
▼
┌─────────────────┐
│ Other nodes │
│ (federated) │
└─────────────────┘
```
---
## Hybrid networking model
VNOX uses two transports simultaneously. They serve different purposes and
are never mixed.
### TCP — control plane
Used for everything that requires ordering and reliability:
- authentication and session setup
- channel join / leave events
- text chat messages
- permission checks
- federation routing signals
### UDP — data plane
Used for everything where latency matters more than reliability:
- voice packets (Opus frames)
- realtime presence state
Voice packets that are lost are not retransmitted. Loss is handled by
Opus packet loss concealment and the jitter buffer.
### LNEx layer
LNEx sits above both transports. It defines:
- packet format and framing
- compression
- encryption (ChaCha20, see `02-protocol/security.md`)
- routing between nodes
- federation identity resolution
LNEx is not tied to a specific transport. The same packet schema is used
over both TCP and UDP, with transport-appropriate flags.
---
## Modules
### gateway
Language: Rust
Runtime: Tokio (async)
Responsibilities:
- client authentication via identity keypair
- channel and session management
- permission enforcement
- federation routing (Phase 3)
- text chat delivery
Entry point for all TCP connections from clients.
### voice-node
Language: Rust
Responsibilities:
- receive UDP voice packets from clients
- decode Opus frames
- apply jitter buffer
- relay to other clients in the same channel
- handle packet sequencing and reordering
Can run on the same machine as the gateway or separately.
### client
Language: Rust
UI: egui
Renderer: wgpu
Audio: cpal + rodio + opus
Networking: tokio
The desktop application. Connects to a gateway via TCP (LNEx) and to a
voice-node via UDP (LNEx). Handles all UI, audio capture, Opus encoding,
and local identity management.
Web client: not planned. Native only.
### overlay
Language: Rust (separate process or injected)
Status: Phase 2
In-game HUD showing:
- who is speaking
- current channel
- latency
- hotkey state
### plugins
Language: TypeScript / JavaScript
Runtime: Deno or QuickJS (not finalized)
API: WebSocket RPC
Sandboxed plugin environment. Plugins cannot access internals directly —
only through the documented RPC API.
### federation (Phase 3)
Handles cross-node communication:
- node discovery
- identity bridging
- voice relay across nodes
- channel bridging
---
## Tech stack summary
| Component | Language | Key deps |
|-----------|----------|----------|
| Gateway | Rust | Tokio, serde, sqlx |
| Voice node | Rust | opus, tokio |
| Client | Rust | egui, wgpu, cpal, tokio |
| Overlay | Rust | TBD |
| Plugins | TypeScript | Deno / QuickJS |
### Why Rust
- async performance via Tokio: handles thousands of concurrent connections
- memory safety without GC: no pauses during voice transmission
- strong UDP networking ecosystem
- cross-platform native binary: Windows, macOS, Linux from one codebase
- no Electron: the client is a real native application
### Serialization
JSON for Phase 1 LNEx packets — simple, debuggable, no build-time codegen.
Protobuf schemas live in `protocol/` and will replace JSON in Phase 2
without changing the wire framing (header stays identical).
---
## File structure
```
vnox/
├── client/ # Desktop client (Rust + egui)
├── gateway/ # TCP gateway (Rust + Tokio)
├── voice-node/ # UDP voice relay (Rust)
├── federation/ # Federation layer (Phase 3)
├── protocol/ # LNEx .proto schemas + spec
├── plugins/ # Plugin runtime + example plugins
├── sdk/ # Client SDK
└── docs/ # This documentation
```
---
## Source layout conventions
### Rules
- **≤ 200 lines per file.** If a file grows past that, split it.
- **≤ 6 files per directory.** If a directory has more, introduce a subdirectory.
- **KISS / DRY / SOLID.** One file = one clear responsibility.
- Subdirectory always has a `mod.rs` that re-exports the public surface.
Internal files are `pub(super)` or `pub(crate)` — never `pub` unless
they are part of the module's public API.
### gateway/src
```
gateway/src/
├── main.rs # startup: config, storage, TCP listener, spawn tasks
├── domain/ # pure business logic, no I/O
│ ├── mod.rs
│ ├── auth.rs # Ed25519 verify, challenge generation
│ ├── channels.rs # ChannelStore: join/leave/members
│ ├── config.rs # Config structs + load()
│ ├── session.rs # SessionStore: create/get/remove
│ └── storage.rs # SQLite via sqlx: messages, users, bans
├── net/ # network I/O
│ ├── mod.rs
│ ├── handshake.rs # HELLO → AUTH → SESSION exchange
│ ├── io.rs # send_packet / read_packet / send_error
│ └── state.rs # State (shared Arc clone) + BroadcastMsg
├── proto/ # wire protocol
│ ├── mod.rs # re-exports everything
│ ├── packet.rs # PacketId, ErrorCode, PacketHeader
│ ├── payloads.rs # all JSON payload structs
│ └── framing.rs # encode_packet, to_payload
└── handler/ # per-connection request handling
├── mod.rs # run_session loop + dispatch + deliver
├── channel.rs # JOIN_CHANNEL / LEAVE_CHANNEL / broadcast_leave
└── chat.rs # CHAT_MESSAGE persist + broadcast
```
**Dependency direction:** `main` → `net` → `domain` + `proto`.
`handler` → `net` + `domain` + `proto`. No cycles.
### voice-node/src
```
voice-node/src/
├── main.rs # config, UdpSocket::bind, recv loop
├── relay.rs # relay_packet, add_member, remove_member
└── jitter.rs # JitterBuffer (reorder by voice_seq)
```
Small crate — no subdirectories needed yet.
### client/src
```
client/src/
├── main.rs # tokio runtime, identity load, eframe::run_native
├── identity.rs # keypair gen/load, Identity struct
├── net/ # LNEx TCP + UDP networking
│ ├── mod.rs # NetHandle, spawn(), session_loop
│ ├── types.rs # NetCommand, NetEvent, MemberInfo, ChatMsg
│ ├── wire.rs # PID_* constants + all wire payload structs
│ ├── framing.rs # read() / write() raw packets over TcpStream
│ ├── handshake.rs # HELLO → AUTH → SESSION
│ ├── dispatch.rs # incoming packet → NetEvent mapping
│ └── voice.rs # build_packet(), spawn_recv() UDP loop
├── audio/ # cpal + Opus pipeline
│ ├── mod.rs # start(), AudioPipeline, EncodedFrame, DecodedFrame
│ ├── config.rs # find() best StreamConfig for device
│ ├── capture.rs # cpal input → Opus encode → EncodedFrame channel
│ └── playback.rs # Opus decode → cpal output
└── ui/ # egui layout
├── mod.rs # VnoxApp, eframe::App impl, poll_net()
├── state.rs # UiState, ConnState, Channel, ChatMessage
├── theme.rs # color constants (BG_BASE, ACCENT, …)
├── widgets.rs # channel_row(), message_row(), fmt_ts()
├── sidebar.rs # node switcher strip + channel list panel
├── chat.rs # chat area: header, scroll, input box
└── connect.rs # connect screen (shown when disconnected)
```
**Dependency direction:** `main` → `net` + `audio` + `ui`.
`ui` → `net::types` (read-only). `audio` is standalone.
`net` → `identity`. No cycles.
### Adding a new feature — checklist
1. Decide which crate owns it (`gateway`, `voice-node`, `client`).
2. Decide which layer it belongs to:
- pure logic with no I/O → `domain/`
- network I/O → `net/`
- UI rendering → `ui/`
- audio → `audio/`
3. Create a new file in the right subdirectory.
4. If the subdirectory now has > 6 files, split into a deeper level.
5. Re-export from `mod.rs` only what callers actually need.
6. Keep the file under 200 lines. If it grows, extract a helper module.
7. Run `cargo check` before committing.
### What goes where — quick reference
| Thing | File |
|-------|------|
| New packet type | `gateway/src/proto/payloads.rs` + `client/src/net/wire.rs` |
| New gateway handler | `gateway/src/handler/` (new file if > 200 lines) |
| New domain rule (ban, rate limit…) | `gateway/src/domain/` |
| New UI panel | `client/src/ui/` (new file) |
| New audio processing step | `client/src/audio/` (new file) |
| Config field | `gateway/src/domain/config.rs` |
| DB schema change | `gateway/src/domain/storage.rs` → `migrate()` |