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}