Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration and limits

There is no configuration file and no environment variable. Everything is Rust code, and the values that are not code are constants rather than settings.

The reason is in Boundaries: everything an application would want to vary at runtime, names, trees, who hears whom, belongs to the render. What is left is the handful of values the Mumble handshake demands before any view exists.

GatewayConfig

#![allow(unused)]
fn main() {
let config = GatewayConfig {
    bind: "0.0.0.0:64738".parse()?,
    welcome_text: "Arena. Double-click a channel to choose.".to_owned(),
    max_users: 500,
    ..GatewayConfig::default()
};
}
fielddefaulteffect
bind0.0.0.0:64738TLS control plane. UDP binds the same port
welcome_textemptysent in ServerSync. Empty omits the field
max_bandwidth72_000advertised bits per second
max_users100advertised, and the admission ceiling
recording_allowedtrueadvisory
allow_htmltruewhether HTML is allowed in text
message_length5_000maximum text message length
version(1, 5, 0)advertised version

Two of these are advertised rather than enforced. The bandwidth ceiling is sent to clients and not applied server-side. The recording flag is a request the client is free to ignore.

max_users is the exception among the advertised values: connections past it are refused, rather than admitted into a runtime that announced a smaller number.

The default version is 1.5.0 because the protobuf UDP format was introduced there, and a real client negotiates it on that basis.

The certificate

#![allow(unused)]
fn main() {
let identity = Identity::self_signed(vec!["localhost".to_owned()])?;
}

Self-signed generation exists for local development. A deployment supplies a persistent certificate instead. Clients key their local preferences on the certificate hash, so a fresh certificate at every restart looks like a new server each time.

A client certificate is requested but not required. When present it proves identity rather than authority, and its lowercase SHA-1 hash reaches the router in ConnectionIdentity. What that identity is worth is the application’s decision.

TLS 1.2 is pinned. A widely deployed client build crashes on a TLS 1.3 session.

Limits that are not settable

Each of these has a cost behind it rather than a preference.

limitvalueconsequence of exceeding
scope depth4 segmentsthe render is refused
scopes observed per connection4the render is refused
context actions per connection64the render is refused
delta history retained256 versionsthe connection is closed
output queue per connection1024 messagesthe connection is closed
minimum interval between turns50 msturns are coalesced

The two closures are worth distinguishing from the three refusals. A refused render keeps the previous view and closes nothing, because the fault is in the application’s own code and the committed view is still correct. A connection falling off the tail of the journal or overflowing its queue has no correct view left to keep, and closing it is the only alternative to letting it drift.

Running out of scopes is a modelling signal rather than a configuration problem. See Scope or overlay.

Data-plane limits

limitvalue
maximum voice datagram1024 bytes
minimum voice datagram2 bytes
sustained voice packets per connection200 per second
voice burst allowance400 packets
text messages per connection1 per second, burst of 5

A client at the usual 10 ms framing emits 100 packets per second, so the voice budget leaves headroom for a faster codec setting while bounding a flood to twice the legitimate worst case. A connection exceeding it has packets dropped rather than being disconnected.

The text allowance is the same one the reference server applies. A flood refused at the socket never crosses a mailbox nor wakes a shard.

The datagram bounds match the reference server exactly, which keeps tunnelled and direct audio on one rule.

Two entry points

serve covers the common case:

#![allow(unused)]
fn main() {
serve(config, identity, |runtime| {
    let lobby = runtime.create_shard(|_handle| Lobby::new());
    AlwaysAttach(lobby.shard())
})
.await
}

Gateway::bind followed by Gateway::serve splits the two, which is what a test binding an ephemeral port needs in order to read the address back:

#![allow(unused)]
fn main() {
let gateway = Gateway::bind(config, identity).await?;
let address = gateway.address();
gateway.serve(router).await
}