VNOX-Server/docs/02-protocol/state-machine.md

217 lines
9.8 KiB
Markdown

# State Machine
> **Phase 1 note:** the gateway and voice-node do not share membership state.
> The voice node registers a client on the first UDP packet and drops idle addresses
> after 30 seconds. Diagrams that show `gateway notify voice node` describe the Phase 2 target.
Client and server maintain synchronized state machines over the LNEx connection.
This document describes the states, transitions, and what triggers them.
---
## Client states
```
┌─────────────────┐
│ DISCONNECTED │ ◄─── initial state / after disconnect
└────────┬────────┘
│ connect(address)
▼
┌─────────────────┐
│ CONNECTING │ TCP handshake in progress
└────────┬────────┘
│ TCP established
▼
┌─────────────────┐
│ HANDSHAKING │ HELLO received, sending AUTH
└────────┬────────┘
│ SESSION received
▼
┌─────────────────┐
│ CONNECTED │ ◄─── idle, no channel joined
└────────┬────────┘
│ join_channel()
▼
┌─────────────────┐
┌────►│ IN_CHANNEL │ text + voice available
│ └────────┬────────┘
│ │ leave_channel() / switch channel
└──────────────┘
│ disconnect() / TCP drop
▼
┌─────────────────┐
│ RECONNECTING │ exponential backoff (Phase 2)
└────────┬────────┘
│ max retries exceeded
▼
┌─────────────────┐
│ DISCONNECTED │
└─────────────────┘
```
Any state can transition to `DISCONNECTED` on:
- TCP connection drop
- `DISCONNECT` packet received
- auth failure (`ERR_AUTH_FAILED`)
- version mismatch (`ERR_VERSION_MISMATCH`)
---
## Server session states
Per-client session on the gateway:
```
┌─────────────────┐
│ ACCEPTING │ TCP connection received, sending HELLO
└────────┬────────┘
│ AUTH received
▼
┌─────────────────┐
│ AUTHENTICATING │ verifying Ed25519 signature
└────────┬────────┘
│ signature valid + not banned
▼
┌─────────────────┐
│ ESTABLISHED │ SESSION sent, client is authenticated
└────────┬────────┘
│ JOIN_CHANNEL received
▼
┌─────────────────┐
┌────►│ IN_CHANNEL │ forwarding messages + voice state
│ └────────┬────────┘
│ │ LEAVE_CHANNEL / JOIN different channel
└──────────────┘
│ TCP drop / DISCONNECT / idle timeout
▼
┌─────────────────┐
│ TERMINATED │ session cleaned up, resources freed
└─────────────────┘
```
### Auth failure paths
```
AUTHENTICATING
│ invalid signature → send ERR_AUTH_FAILED → TERMINATED
│ banned pubkey → send ERR_AUTH_FAILED → TERMINATED
│ unsupported version → send ERR_VERSION_MISMATCH → TERMINATED
│ rate limit on auth → send ERR_RATE_LIMITED → TERMINATED
```
---
## Voice session states (per user, on voice-node)
> **Phase 1:** transition from `INACTIVE` to `JOINING` happens when the first UDP
> packet arrives from a client address, not when the gateway sends a signal.
```
┌─────────────────┐
│ INACTIVE │ user not in a voice channel
└────────┬────────┘
│ first UDP packet from client (Phase 1)
│ gateway signals join (Phase 2, not implemented)
▼
┌─────────────────┐
│ JOINING │ UDP path being established
└────────┬────────┘
│ first UDP packet received from client
▼
┌─────────────────┐
│ SILENT │ in channel, not transmitting
└────────┬────────┘
│ voice packets arriving
▼
┌─────────────────┐
┌────►│ SPEAKING │ relaying to other channel members
│ └────────┬────────┘
│ │ DTX / push-to-talk released / silence
└──────────────┘
┌─────────────────┐
│ MUTED │ server-side mute (moderator action)
└────────┬────────┘
│ unmuted by moderator
▼
┌─────────────────┐
│ SILENT │
└─────────────────┘
```
Voice state is broadcast to all channel members as `VOICE_STATE` packets
on every transition: `SILENT → SPEAKING`, `SPEAKING → SILENT`, `MUTED`, `DEAFENED`.
---
## Channel join sequence (detailed)
Full flow from `join_channel()` call to receiving audio:
```
Client Gateway Voice Node
│ │ │
│── JOIN_CHANNEL ─────────►│ │
│ │ check permissions │
│ │ check channel exists │
│◄─ CHANNEL_STATE ─────────│ │
│ { members, │ │
│ voice_endpoint, │ (Phase 2: gateway │
│ channel_id } │ notifies voice node) │
│ │ │
│ [start UDP] │ │
│── UDP VOICE_PACKET ──────────────────────────────── ►│
│ │ │ relay to others
│◄─ UDP VOICE_PACKET ───────────────────────────────── │
│ │ │
│◄─ USER_JOIN broadcast ───│ │
│ (sent to all members) │ │
```
If `ERR_PERMISSION_DENIED` is returned on `JOIN_CHANNEL`, the client
stays in `CONNECTED` state and does not attempt UDP.
---
## Reconnect logic (Phase 2)
On unexpected TCP drop from `IN_CHANNEL` or `CONNECTED`:
```
disconnect detected
│
▼
RECONNECTING state
│
├── attempt 1: wait 1s
├── attempt 2: wait 2s
├── attempt 3: wait 4s
├── attempt 4: wait 8s
├── attempt 5: wait 16s
└── attempt 6+: wait 30s (cap)
```
On successful reconnect:
- full auth exchange (new session token)
- if user was `IN_CHANNEL` before disconnect: auto-rejoin same channel
On reconnect failure after N attempts (configurable, default 10):
- transition to `DISCONNECTED`
- notify user in UI
---
## Packet validity by state
Packets received in an unexpected state are dropped with `ERR_INVALID_PACKET`.
| Packet | Valid in states |
|--------|----------------|
| `HELLO` | server sends in `ACCEPTING` |
| `AUTH` | client sends in `HANDSHAKING` |
| `SESSION` | server sends in `AUTHENTICATING` |
| `JOIN_CHANNEL` | `CONNECTED`, `IN_CHANNEL` |
| `LEAVE_CHANNEL` | `IN_CHANNEL` |
| `CHAT_MESSAGE` | `IN_CHANNEL` |
| `VOICE_PACKET` | UDP, user in voice channel |
| `PING` / `PONG` | any authenticated state |
| `DISCONNECT` | any state |