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 nodedescribe 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
DISCONNECTpacket 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
INACTIVEtoJOININGhappens 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_CHANNELbefore 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 |