Rask
Rask
v0.23.1-alpha.0.496
You're reading the Rask v0.23.1-alpha.0.496 guides. Updated .View source
All guides

ISignaling

The relay two peers trade an offer, an answer and their ICE candidates over.

  • Wraps: WebSocket (Rask's signaling relay)
  • MDN: WebSockets API
  • Home: Rask.Core.Browser (all hosts)
  • Shape: subscription
  • Availability: Web/Server ✅ · PWA/WASM ✅

IWebRtc deliberately doesn't pick a signaling channel — an app that already has one should use it. ISignaling is the channel for apps that don't, and it pairs with the Rask.Signaling package — which depends on nothing from Rask.Server, so any ASP.NET host can map it, including a static-file host serving a published WASM bundle.

The payload is an opaque string end to end. Serialize an RtcDescription or an RtcIceCandidate into it however you like; neither the relay nor this wrapper looks inside. That's deliberate — parsing attacker-controlled SDP server-side would be a lot of surface for no benefit, since only the two browsers need to understand it.

Server: hosting the relay

builder.Services.AddRaskSignaling(o =>
{
    // The framework can't know who belongs in which room. This is where you say.
    o.AuthorizeRoom = ctx => ctx.Services
        .GetRequiredService<IConversations>()
        .IsMember(ctx.User, ctx.Room);
});

app.MapRaskSignaling();

The host must have WebSocket support in the pipeline: Rask.Server's MapRask() calls app.UseWebSockets() for you, but a static-file host serving a published WASM bundle does not — call it yourself before mapping. The relay says so explicitly rather than refusing clients with a bare 400.

The relay's plain values — Path, RequireAuthorization, AllowedOrigins, MaxRooms, MaxPeersPerRoom and the message limits — are Rask:Signaling in appsettings.json, and the callback runs after them. AuthorizeRoom is a delegate, so it can only be set in code.

Authentication is required by default. A signaling relay anyone can join is a way to reach other people's browsers, so a public default would make that an accident rather than a decision. AuthorizeRoom is the per-room hook on top; its default lets any authenticated caller into any room, which is only right when every user of the app may talk to every other.

The socket opens for the host's own pages only. A browser sends the visitor's cookie with a WebSocket upgrade from any site it treats as the same one, so the relay answers 403 to an upgrade whose Origin is another host — before AuthorizeRoom is ever asked about a room as the signed-in visitor. When the relay is mapped on a different host from the app, name the app:

{ "Rask": { "Signaling": { "AllowedOrigins": [ "https://app.example.com" ] } } }

An origin is scheme and host, with a port when it is not the default and no path. A client that sends no Origin — anything that is not a browser — connects as before.

Client: joining a room

_signal = await signaling.Join("room-42", new SignalingHandlers
{
    // The peers already here are the ones WE offer to. A peer arriving later offers to us instead.
    OnJoined = async (self, peers) => { foreach (var p in peers) await OfferTo(p); },
    OnSignal = (from, payload) => ApplyAsync(from, payload),
    OnPeerLeft = id => Drop(id),
});

await _signal.Send(peerId, JsonSerializer.Serialize(offer));

That asymmetry in OnJoined matters: if both sides offer at once you get an SDP glare collision that neither browser resolves for you. "The one who arrives offers to those already there" is the simplest rule that avoids it.

What the relay guarantees

  • Peer ids are minted by the server, never taken from the client — otherwise a caller could impersonate another peer, or overwrite it.
  • A message reaches only a peer in the sender's own room, checked at delivery rather than trusted from the message.
  • Nothing is ever echoed to its sender, so the relay can't be aimed at itself.
  • Payload size, message rate, room size and room count are all capped (SignalingOptions).
  • A refused join says the same thing whether the room was full or you weren't allowed in — anything else would let a caller probe which rooms exist.

Limits worth knowing

Rooms are held in memory, per process. Signaling is short-lived, so there's nothing worth persisting — but a multi-instance deployment needs sticky routing for the signaling path, or two peers assigned to different instances never see each other.

The socket is separate from the live render socket, on both hosts. That one has its own frame contract, rate limits and shutdown-drain semantics, and signaling traffic has no business sharing them. It lives in the browser on both hosts too, so a Server-hosted app doesn't put its own server in the middle of a relay it is already running.

See also

Confirm