mumble_server_runtime_gateway/router.rs
1//! Where a connection goes when it has just authenticated.
2//!
3//! A connection that has just proven who it is belongs to no shard yet, and the
4//! decision cannot come from a shard: none of them knows about it, and asking
5//! them all would be a broadcast for every arrival. So it is a runtime-level
6//! policy, and this is its whole surface.
7//!
8//! It runs **on the connection's own task**, never on a shard's, which is what
9//! makes it free to await: validating a token, calling out to a service, or
10//! reading a database blocks one arrival rather than a whole shard.
11//!
12//! REF: docs/design/guide-implementation.md 10.1
13
14use mumble_server_runtime_shard::{ConnectionId, ShardId};
15
16/// What the gateway knows about a connection at the moment it must be routed.
17///
18/// Everything here comes from the client, so all of it is a claim. The one piece
19/// that carries cryptographic weight is [`ConnectionIdentity::certificate_hash`]:
20/// TLS proved possession of the matching private key. It still proves identity
21/// and not authority - what a certificate is *allowed* to do is the router's
22/// business, not the gateway's.
23#[derive(Debug, Clone)]
24pub struct ConnectionIdentity {
25 /// The name the client proposed. A suggestion: the server is authoritative
26 /// on what a user is finally called (spec 10.5).
27 pub name: String,
28 /// Lowercase SHA-1 of the client certificate, as Murmur presents it.
29 pub certificate_hash: Option<String>,
30 /// The `Authenticate.password` field, treated as an opaque credential. This
31 /// is where a token flow plugs in.
32 pub credential: Option<String>,
33}
34
35/// The routing verdict.
36#[derive(Debug, Clone)]
37pub enum RouteDecision {
38 /// Attach to this shard. The shard must exist; if it has been destroyed in
39 /// the meantime the connection is refused rather than stranded.
40 Attach(ShardId),
41 /// Refuse, with a reason the client is shown.
42 Reject(String),
43}
44
45/// The policy that answers "which shard?".
46///
47/// Generic rather than `dyn` on purpose: an `async fn` in a trait is not
48/// dyn-compatible, and boxing every routing decision to work around that would
49/// buy nothing. A deployment that genuinely needs runtime polymorphism writes
50/// one router that dispatches internally.
51pub trait ConnectionRouter: Send + Sync + 'static {
52 /// Decide where this connection belongs.
53 ///
54 /// Runs on the connection's task. It may await, and it may take its time:
55 /// the cost is borne by the arriving client alone.
56 ///
57 /// The identifier is handed over as well as the claim, because this is the
58 /// only moment where the two meet. A shard's [`mumble_server_runtime_shard::VoiceEvent`]
59 /// carries a `ConnectionId` and nothing else - deliberately, since the
60 /// runtime has no opinion on what a user *is* - so an application that wants
61 /// its flavor to know a name records the pair here.
62 fn route(
63 &self,
64 connection: ConnectionId,
65 identity: &ConnectionIdentity,
66 ) -> impl std::future::Future<Output = RouteDecision> + Send;
67}
68
69/// A router that sends everyone to the same shard.
70///
71/// The honest default for a runtime with one entry point, and what a lobby
72/// looks like: the *flavor* decides where anyone goes next, which is a
73/// migration rather than a routing decision.
74#[derive(Debug, Clone, Copy)]
75pub struct AlwaysAttach(pub ShardId);
76
77impl ConnectionRouter for AlwaysAttach {
78 async fn route(
79 &self,
80 _connection: ConnectionId,
81 _identity: &ConnectionIdentity,
82 ) -> RouteDecision {
83 RouteDecision::Attach(self.0)
84 }
85}