Expand description
Odoo’s Selective Forwarding Unit (SFU) for audio/video calls.
o-sfu is a multi-tenant SFU designed to be compatible with the Odoo saas/.sh
architecture, which means that a single o-sfu server can serve
independent (individually authenticated) “rooms” which can be used by many
independent tenants (like odoo saas “databases”).
§Reading Map
- Start or embed the server:
run,Runtimeandconfig. - Understand admission:
auth,httpandwebsocket. - Change room and media behavior:
core::server::room::Room, [o_sfu_router::Router] andcore::server::transport::MediaTransport. - Integrate a browser client:
SfuClientexposes the Odoo-facing API.o_sfu_protocol::host::ProtocolCoresupplies its signaling state machine.
§Core Concepts
o-sfu separates the control plane for admission and room policy, the
routing plane for user-to-connection placement and the packet plane
for RTP forwarding on worker loops.
Runtime: Owns the process lifecycle, the HTTP/WebSocket servers and graceful shutdown.core::server::room::Room: The control plane boundary for a set of participants. It commits membership and media relationships.- [
o_sfu_router::Router]: The routing plane, a pure, sans-I/O engine owning the placement graph that maps users to connections. core::prelude::MediaSession: Orchestrates a user’s connection, bridging room intent to transport effects.core::server::transport::MediaTransport: Owns the media workers and abstracts their threading model. Each worker holds a packet loop and applies projected routes to incoming datagrams.
§Architecture
Control and routing decisions happen above the packet loops, which apply the resulting transport state to UDP datagrams.
HTTP control API ------------------------------> RoomManager
|
WebSocket session -> SfuCore -> MediaSession |
\ /
+----> Room <----+
|
+<----> Router
|
v
MediaTransport
| | |
v v v
RTC workers / packet loops
(one thread per worker)
| | |
UDP socket UDP socket UDP socket
| | |
v v v
fanout fanout fanout
UDP OUT UDP OUT UDP OUT§Admission Edge
Applications provision rooms through
RoomManager::serve_room.
WebSocket clients join through
SfuCore::admit_user. Both paths require
JWT authentication before admission.
Upgraded sockets retain their connection permits. Before authentication, they have a first-frame deadline and global plus per-origin caps (one bucket per IPv4 address or IPv6 /64).
Incoming TCP connection
|
v
connection cap + HTTP header deadline
|
v
Axum Router
|
+-> GET /v1/noop
| public liveness -> noop
|
+-> GET /v1/channel
| Authorization JWT -> VerifiedRoomRequest -> room
|
+-> POST /v1/disconnect
| request-body JWT -> VerifiedDisconnectClaims -> disconnect
|
+-> WebSocket /
| upgrade -> first-frame JWT -> admit_user -> session
|
+-> GET /v1/stats, /metrics and /internal/diagnostics/...
authorize_operator -> stats, metrics or diagnostics- HTTP: Parses server-to-server requests using
http::CreateRoomQuery. Verifiesauth::HttpRoomClaims. The request that creates the current room fixes its signing key. - WebSocket: Client connection frames are decoded by
websocket::decode_auth_payload_text. A hint selects a candidate key. Claims are verified, normalized intoauth::WebSocketConnectClaimsand trusted for access.
§Security Model
o-sfu secures two planes independently. Application-layer JWTs gate room
admission on the control plane. str0m encrypts media on the packet plane
with DTLS-SRTP. An external reverse proxy provides TLS for HTTP and
WebSocket signaling.
control plane JWT HS256 admission trust
packet plane DTLS-SRTP (str0m) media confidentiality
signaling wire TLS (via proxy) transport confidentiality§JWT Admission
Tokens are HS256 only. auth::verify rejects any other alg, checks the
HMAC in constant time and requires an unexpired exp. It validates nbf
and the iat future-skew bound when present. Current Odoo does not issue
iat. Tokens without exp fail with auth::AuthenticationError::MissingExpiry.
It caps token size at
auth::MAX_JWT_TOKEN_BYTES.
There are two different keys:
- Server-to-server key:
AUTH_KEY(base64, at least 32 decoded bytes) verifies the HTTPhttp::CreateRoomQuerypath throughauth::HttpRoomClaimsandauth::HttpDisconnectClaims. Seeconfig. - Per-room key: the request that creates the current room pins the signing
key from the
keyorkeySeedclaim inauth::HttpRoomClaims. A direct key must decode to at least 32 bytes or room creation returns400.keySeedinstead derives the room’s signing bytes fromAUTH_KEY:WebSocketroom_key = HMAC-SHA256( key = Base64Decode(AUTH_KEY), message = Base64Decode(keySeed) )auth::WebSocketConnectClaimsverify against that room key, never againstAUTH_KEY. Runtime construction decodes the global key once. Room creation retains decoded secret bytes, so verification does not decode stored keys and equivalent base64 encodings match the same reservation.
HTTP room creation uses the
Authorization header, HTTP disconnect uses the request body and the
WebSocket client sends a first-frame auth envelope decoded by
websocket::decode_auth_payload_text. An unverified room id selects only a
candidate key, then the same token is verified once against it. Modern
auth::WebSocketConnectClaims must name the selected room. Legacy Odoo
tokens select it through the auth envelope’s channel and are normalized
only after verification with that room’s key. Odoo may send both
session_id and account user_id. The session takes precedence as the
participant identity. Tokens without either identity are rejected.
Admission establishes identity and room scope. It does not enforce the
per-user permissions claim provided by each tenant.
§Media Transport
str0m handles ICE, DTLS and SRTP over UDP. o-sfu builds and drives the
str0m sessions to forward RTP between participants.
- Keying: the DTLS handshake derives SRTP keys per RFC 5764 DTLS-SRTP.
- Certificate:
str0mgenerates a self-signed certificate when each RTC session is built and advertises its SHA-256 fingerprint in the SDP offer. Accepting the answer stores the expected remote fingerprint. The DTLS handshake verifies it against the peer certificate. - ICE:
o-sfuruns ICE-lite witha=setup:actpassand advertisesANNOUNCED_IP, so media UDP must reach the host directly.
§Signaling Transport
HTTPS and WSS are expected to be terminated by an external reverse proxy.
Setting PROXY=true in config requires TRUSTED_PROXIES CIDRs.
Forwarded headers are honored only when the TCP peer belongs to that set.
The public edge must overwrite client-supplied forwarded host and protocol
headers. See http::resolve_request_origin for origin
resolution and http for operator route access.
§Room and Router Ownership
Room transitions produce typed commits while holding short exclusive state
locks. RoomEffects consumes deferred transport, source-policy and WebSocket
output work after the lock is released.
room state lock held lock released
+--------------------------------+ +------------------------------+
| validate user and connection | | MediaTransport commands |
| commit room topology | | source-policy turn |
| capture transition commit |--->| websocket output |
| | | idempotent teardown |
+--------------------------------+ +------------------------------+[o_sfu_router::Router] owns exact user-to-connection placement. Receiver shadows are foreign local sessions derived from active consumer dependencies, disappearing with their final consumer.
Absent subscription targets are capped per receiver. Eviction drops the oldest absent target’s intent. Present members do not consume that allowance.
§Signaling and Client Bundle
Browsers use SfuClient for connection, publication, subscription and room
control. Signaling state stays in o_sfu_protocol::host::ProtocolCore and
yields ordered o_sfu_protocol::host::Command values. The WASM bridge
serializes commands for BrowserRuntime, which executes effects through
browser WebSocket, RTCPeerConnection and timer APIs. Protocol events are
mapped to Odoo bundle updates during command serialization. BrowserRuntime
applies those updates to client state and notifies the application.
Outbound overflow closes with 4110 (Overloaded), allowing reconnect and
intent replay. Replacement or explicit removal uses terminal 4108 (Kicked).
SfuClient (public API)
|
v
BrowserRuntime
|
v
ProtocolCore (sans-I/O) -> Vec<Command>
|
v
WASM serialization -> TypeScript command union
|
v
BrowserRuntime -> WebSocket, RTCPeerConnection, timers
^ |
+--------------- browser events ---------+§Packet Path
core::server::transport::MediaTransport owns the media workers, which hold the packet loops. These loops receive UDP datagrams, drive WebRTC state (str0m), apply route tables and forward RTP.
UDP datagram
|
v
worker ingress and `str0m` drain
|
v
packet facts (source, RID and codec)
|
+-> origin packet sinks
|
+-> source and destination packet gates
|
+-> relay fanout
+-> local RTC -> RTP identity and codec rewriteRegistered origin packet sinks observe publisher packets before route gates, including publishers without active receivers. Source, relay and receiver gates then narrow routed fanout. Same-process relays share payload data with another worker for local delivery.
Worker BWE and audio observations feed into room source policy, which updates route gates for later packets.
worker BWE and audio observations
|
v
room source policy
|
v
route gates for later packets§Observability
Monitored through the o_sfu_telemetry sub-crate. See http::telemetry for the HTTP contracts.
- Metrics:
http::telemetry::metricsexposes Prometheus text exposition. - Diagnostics:
http::telemetry::diagnosticsexposes JSON state summaries.
§Scaling
Rooms use one [o_sfu_router::Router] facade and default to one local router.
config::RoomWorkerPolicy can enable additional same-process local routers.
Only running workers are eligible. Joins prefer assigned workers with a known
delay below the configured threshold. When none qualifies, a join may attach
an unused healthy worker within the router cap and worker count. Otherwise,
joins reuse a running assigned worker even when it exceeds the delay threshold.
Placements on failed workers do not count toward the cap. If no assigned worker
is running, a later join can attach a fresh placement on another running worker
even under the single-router policy. Admission fails when no worker is running.
§Feature Flags
Core media behavior is configured at runtime through config, not Cargo features.
The default feature otel-tracing enables OpenTelemetry tracing support through o_sfu_telemetry::TraceExportConfig.
Other features are only used for tests and benchmarking.
§Sub-crates
| Crate | Role |
|---|---|
o_sfu_rfc | RFC-backed JWT, RTP, RTCP, SDP and WebRTC consts/types |
o-sfu-model | Shared call data (o_sfu_protocol::wire::UserId, etc.) |
[o_sfu_router] | Sans-I/O [o_sfu_router::Router] facade for room placement and routed media lifetimes |
o_sfu_core | Room engine, core::prelude::SourcePolicy, recording taps and core::server::transport::MediaTransport projection |
o_sfu_protocol | Sans-I/O o_sfu_protocol::host::ProtocolCore and typed commands |
o_sfu_telemetry | Tracing setup, metrics and diagnostics response types |
Modules§
- application 🔒
- auth
- config
- core
- http
- HTTP route and payload contracts.
- runtime 🔒
- Wires process services and drains them during shutdown.
- websocket
Structs§
- Runtime
- Process services and lifecycle configuration.
Enums§
- Serve
Error - Failure to serve or fully drain a
Runtime.
Functions§
- run
- Errors