Skip to main content

pb_mapper_protocol/
direct.rs

1//! Opt-in coordination messages. Legacy connections never emit these messages.
2
3use serde::{Deserialize, Serialize};
4use std::net::{IpAddr, SocketAddr};
5
6pub type SessionId = [u8; 16];
7pub fn random_session_id() -> SessionId {
8    rand::random()
9}
10
11pub const DIRECT_VERSION: u16 = 1;
12pub const MAX_CANDIDATES: usize = 8;
13pub const MAX_CERT_LEN: usize = 2048;
14pub const SETUP_TTL_MS: u64 = 8_000;
15
16#[derive(Clone, Copy, Serialize, Deserialize)]
17pub struct SessionSecret(pub [u8; 32]);
18
19impl PartialEq for SessionSecret {
20    fn eq(&self, other: &Self) -> bool {
21        use subtle::ConstantTimeEq;
22        bool::from(self.0.ct_eq(&other.0))
23    }
24}
25impl Eq for SessionSecret {}
26impl SessionSecret {
27    pub fn random() -> Self {
28        Self(rand::random())
29    }
30}
31
32impl std::fmt::Debug for SessionSecret {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.write_str("[redacted]")
35    }
36}
37
38#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
39pub struct DirectCapability {
40    pub version: u16,
41    /// An unspecified IP means the relay does not know its public address and
42    /// the client should use the one it reached the relay at; see [`stun_target`].
43    pub stun_addr: SocketAddr,
44}
45
46/// The STUN address a client should query, given what the relay advertised and
47/// the relay IP this client's control connection actually reached.
48///
49/// A relay behind 1:1 NAT or port forwarding cannot see its own public IP, so
50/// unless the operator names one it advertises only the port. The client
51/// already dialed that public address, which is the one its UDP probes must use.
52pub fn stun_target(advertised: SocketAddr, relay: IpAddr) -> SocketAddr {
53    if advertised.ip().is_unspecified() {
54        SocketAddr::new(relay.to_canonical(), advertised.port())
55    } else {
56        advertised
57    }
58}
59
60#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
61pub struct PeerInfo {
62    pub certificate: Vec<u8>,
63    pub candidates: Vec<SocketAddr>,
64}
65
66impl PeerInfo {
67    pub fn valid(&self) -> bool {
68        !self.certificate.is_empty()
69            && self.certificate.len() <= MAX_CERT_LEN
70            && self.candidates.len() <= MAX_CANDIDATES
71            && self.candidates.iter().all(valid_candidate)
72    }
73}
74
75pub fn valid_candidate(addr: &SocketAddr) -> bool {
76    addr.port() != 0
77        && !addr.ip().is_unspecified()
78        && !addr.ip().is_multicast()
79        && match addr {
80            SocketAddr::V4(addr) => !addr.ip().is_broadcast() && !addr.ip().is_link_local(),
81            SocketAddr::V6(addr) => !addr.ip().is_unicast_link_local(),
82        }
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize)]
86pub struct SessionGrant {
87    pub id: SessionId,
88    pub server_instance: SessionId,
89    pub generation: u64,
90    pub namespace: u64,
91    pub service: String,
92    pub is_datagram: bool,
93    pub consumer_key: u64,
94    pub provider_key: u64,
95    pub consumer: PeerInfo,
96    pub provider: PeerInfo,
97    pub secret: SessionSecret,
98    pub ttl_ms: u64,
99}
100
101#[derive(Clone, Debug, Serialize, Deserialize)]
102pub enum DirectClientMessage {
103    CancelOpen {
104        request: u64,
105    },
106    Open {
107        request: u64,
108        key: String,
109        is_datagram: bool,
110        peer: PeerInfo,
111    },
112    RelaySelect {
113        id: SessionId,
114    },
115    Cancel {
116        id: SessionId,
117    },
118    Report(SessionUpdate),
119    Ping,
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize)]
123pub enum DirectProviderMessage {
124    Accept { id: SessionId, peer: PeerInfo },
125    Reject { id: SessionId, reason: String },
126    RelaySelected { id: SessionId },
127    Report(SessionUpdate),
128}
129
130#[derive(Clone, Debug, Serialize, Deserialize)]
131pub enum DirectEvent {
132    Ready(DirectCapability),
133    Offer {
134        grant: SessionGrant,
135        stun_addr: SocketAddr,
136    },
137    Granted {
138        request: u64,
139        grant: SessionGrant,
140    },
141    LegacyRelay {
142        request: u64,
143    },
144    Rejected {
145        request: u64,
146        reason: String,
147    },
148    RelaySelect {
149        id: SessionId,
150    },
151    RelaySelected {
152        id: SessionId,
153    },
154    Cancel {
155        id: SessionId,
156    },
157    Pong,
158}
159
160#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
161#[serde(rename_all = "snake_case")]
162pub enum SessionPath {
163    Direct,
164    Relay,
165}
166
167#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
168#[serde(rename_all = "snake_case")]
169pub enum SessionState {
170    Active,
171    Closed,
172}
173
174#[derive(Clone, Debug, Serialize, Deserialize)]
175pub struct SessionReport {
176    pub id: SessionId,
177    pub seq: u64,
178    pub path: SessionPath,
179    pub state: SessionState,
180    pub reason: Option<String>,
181}
182
183/// Metadata claimed by an authenticated endpoint. It never authorizes setup.
184#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
185pub struct SessionSummary {
186    pub server_instance: SessionId,
187    pub id: SessionId,
188    pub namespace: u64,
189    pub service: String,
190    pub is_datagram: bool,
191    pub consumer_key: u64,
192    pub provider_key: u64,
193}
194impl From<&SessionGrant> for SessionSummary {
195    fn from(grant: &SessionGrant) -> Self {
196        Self {
197            server_instance: grant.server_instance,
198            id: grant.id,
199            namespace: grant.namespace,
200            service: grant.service.clone(),
201            is_datagram: grant.is_datagram,
202            consumer_key: grant.consumer_key,
203            provider_key: grant.provider_key,
204        }
205    }
206}
207#[derive(Clone, Debug, Serialize, Deserialize)]
208pub struct SessionUpdate {
209    pub session: SessionSummary,
210    pub secret: SessionSecret,
211    pub report: SessionReport,
212}
213
214#[derive(Clone, Debug, Serialize, Deserialize)]
215pub struct ObservedReport {
216    pub report: SessionReport,
217    pub age_ms: u64,
218}
219
220#[derive(Clone, Debug, Serialize, Deserialize)]
221pub struct SessionObservation {
222    pub server_instance: SessionId,
223    /// Restored endpoint claim after restart or retention expiry, without setup authority.
224    pub restored: bool,
225    pub id: SessionId,
226    pub service: String,
227    pub is_datagram: bool,
228    pub consumer: Option<ObservedReport>,
229    pub provider: Option<ObservedReport>,
230    pub stale: bool,
231    pub conflicting: bool,
232}
233
234macro_rules! wire_message {
235    ($($name:ty),+ $(,)?) => {$ (
236        impl crate::command::MessageSerializer for $name {
237            fn encode(&self) -> pb_mapper_core::error::Result<Vec<u8>> {
238                serde_json::to_vec(self).map_err(|e| crate::secure::protocol_error(e.to_string()))
239            }
240            fn decode(bytes: &[u8]) -> pb_mapper_core::error::Result<Self> {
241                serde_json::from_slice(bytes).map_err(|e| crate::secure::protocol_error(e.to_string()))
242            }
243        }
244    )+};
245}
246wire_message!(
247    DirectClientMessage,
248    DirectProviderMessage,
249    DirectEvent,
250    SessionGrant
251);
252
253#[cfg(test)]
254mod tests {
255    use crate::command::{
256        MessageSerializer, PbConnRequest, PbConnResponse, PbConnStatusReq, PbConnStatusResp,
257    };
258    #[test]
259    fn disabled_direct_fields_are_absent_from_existing_wire_messages() {
260        let request = PbConnRequest::Register {
261            direct: None,
262            key: "legacy".into(),
263            need_codec: false,
264            is_datagram: false,
265            protocol_version: None,
266            client_instance_id: None,
267            heartbeat_interval_ms: None,
268            heartbeat_tolerance_ms: None,
269        };
270        let expected = br#"{"Register":{"need_codec":false,"is_datagram":false,"key":"legacy"}}"#;
271        assert_eq!(request.encode().unwrap(), expected);
272        assert_eq!(
273            serde_json::to_vec(&PbConnStatusReq::Service {
274                key: "legacy".into(),
275                direct_query: None
276            })
277            .unwrap(),
278            br#"{"Service":{"key":"legacy"}}"#
279        );
280        let response = PbConnResponse::RegisterV2 {
281            conn_id: 1,
282            generation: 2,
283            lease_ttl_ms: 3000,
284            direct: None,
285        };
286        assert_eq!(
287            response.encode().unwrap(),
288            br#"{"RegisterV2":{"conn_id":1,"generation":2,"lease_ttl_ms":3000}}"#
289        );
290        let response = PbConnStatusResp::Service {
291            key: "legacy".into(),
292            connections: vec![],
293            direct: None,
294        };
295        assert_eq!(
296            serde_json::to_vec(&response).unwrap(),
297            br#"{"Service":{"key":"legacy","connections":[]}}"#
298        );
299    }
300
301    #[test]
302    fn unspecified_stun_address_takes_the_dialed_relay_ip() {
303        use super::stun_target;
304        let relay: std::net::IpAddr = "203.0.113.7".parse().unwrap();
305        assert_eq!(
306            stun_target("0.0.0.0:7666".parse().unwrap(), relay),
307            "203.0.113.7:7666".parse().unwrap()
308        );
309        assert_eq!(
310            stun_target("[::]:7666".parse().unwrap(), relay),
311            "203.0.113.7:7666".parse().unwrap()
312        );
313        // An IPv4 relay reached through a dual-stack socket keeps its v4 form.
314        assert_eq!(
315            stun_target(
316                "0.0.0.0:7666".parse().unwrap(),
317                "::ffff:203.0.113.7".parse().unwrap()
318            ),
319            "203.0.113.7:7666".parse().unwrap()
320        );
321        // An operator-supplied address always wins.
322        assert_eq!(
323            stun_target("198.51.100.1:10811".parse().unwrap(), relay),
324            "198.51.100.1:10811".parse().unwrap()
325        );
326    }
327}