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

9.8 KiB

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