Skip to main content

saorsa_core/
transport_address.rs

1//! Extensible address records for the versioned DHT address plane.
2//!
3//! The wire record deliberately stores numeric identifiers and an opaque,
4//! length-delimited payload rather than serialized Rust enums. A node that
5//! does not know a future transport or reachability identifier can still
6//! decode, retain, and forward the record without interpreting it.
7
8#[cfg(any(feature = "native", test))]
9use crate::AddressType;
10use crate::MultiAddr;
11use serde::{Deserialize, Serialize};
12
13/// Maximum encoded address payload accepted from the network.
14pub const MAX_TRANSPORT_ADDRESS_PAYLOAD: usize = 2 * 1024;
15
16/// Maximum number of records accepted in one complete address set.
17pub const MAX_TRANSPORT_ADDRESS_RECORDS: usize = 16;
18
19/// A transport understood by this implementation.
20///
21/// This enum is never serialized. Its stable numeric identifier is carried by
22/// [`TransportAddressRecord::transport`] so unknown future values remain
23/// decodable.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum KnownTransport {
26    /// Saorsa's native QUIC transport.
27    Quic,
28    /// Signaling-free, certificate-pinned WebRTC Direct.
29    WebRtcDirect,
30}
31
32impl KnownTransport {
33    /// Stable wire identifier for QUIC.
34    pub const QUIC_ID: u16 = 1;
35    /// Stable wire identifier for WebRTC Direct.
36    pub const WEBRTC_DIRECT_ID: u16 = 2;
37
38    /// Return the stable wire identifier.
39    #[must_use]
40    pub const fn id(self) -> u16 {
41        match self {
42            Self::Quic => Self::QUIC_ID,
43            Self::WebRtcDirect => Self::WEBRTC_DIRECT_ID,
44        }
45    }
46
47    /// Resolve a known identifier, leaving unknown identifiers opaque.
48    #[must_use]
49    pub const fn from_id(id: u16) -> Option<Self> {
50        match id {
51            Self::QUIC_ID => Some(Self::Quic),
52            Self::WEBRTC_DIRECT_ID => Some(Self::WebRtcDirect),
53            _ => None,
54        }
55    }
56}
57
58/// A reachability class understood by this implementation.
59///
60/// Like [`KnownTransport`], this is an in-memory helper rather than a wire
61/// enum. Numeric identifiers are stable and must never be reused.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum KnownReachability {
64    Relay,
65    Direct,
66    Unverified,
67    Lan,
68}
69
70impl KnownReachability {
71    pub const RELAY_ID: u16 = 1;
72    pub const DIRECT_ID: u16 = 2;
73    pub const UNVERIFIED_ID: u16 = 3;
74    pub const LAN_ID: u16 = 4;
75
76    #[must_use]
77    pub const fn id(self) -> u16 {
78        match self {
79            Self::Relay => Self::RELAY_ID,
80            Self::Direct => Self::DIRECT_ID,
81            Self::Unverified => Self::UNVERIFIED_ID,
82            Self::Lan => Self::LAN_ID,
83        }
84    }
85
86    #[must_use]
87    pub const fn from_id(id: u16) -> Option<Self> {
88        match id {
89            Self::RELAY_ID => Some(Self::Relay),
90            Self::DIRECT_ID => Some(Self::Direct),
91            Self::UNVERIFIED_ID => Some(Self::Unverified),
92            Self::LAN_ID => Some(Self::Lan),
93            _ => None,
94        }
95    }
96
97    #[must_use]
98    #[cfg(any(feature = "native", test))]
99    pub(crate) const fn from_legacy(value: AddressType) -> Self {
100        match value {
101            AddressType::Relay => Self::Relay,
102            AddressType::Direct => Self::Direct,
103            AddressType::Unverified => Self::Unverified,
104            AddressType::Lan => Self::Lan,
105        }
106    }
107
108    #[must_use]
109    #[cfg(any(feature = "native", test))]
110    pub(crate) const fn into_legacy(self) -> AddressType {
111        match self {
112            Self::Relay => AddressType::Relay,
113            Self::Direct => AddressType::Direct,
114            Self::Unverified => AddressType::Unverified,
115            Self::Lan => AddressType::Lan,
116        }
117    }
118}
119
120/// One address in the extensible V2 address plane.
121///
122/// `address` is Postcard-encoded [`MultiAddr`] for transports known today.
123/// Consumers must inspect `transport` before decoding it. Unknown transports
124/// retain opaque bytes so intermediaries can forward them unchanged.
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub struct TransportAddressRecord {
127    pub transport: u16,
128    pub reachability: u16,
129    #[serde(deserialize_with = "bounded_address")]
130    pub address: Vec<u8>,
131}
132
133impl TransportAddressRecord {
134    /// Encode a currently supported address.
135    ///
136    /// The supplied reachability applies to native QUIC. WebRTC Direct has no
137    /// relay transport today, so its reachability is always encoded as
138    /// [`KnownReachability::Unverified`].
139    pub fn from_multiaddr(
140        address: &MultiAddr,
141        reachability: KnownReachability,
142    ) -> Result<Option<Self>, postcard::Error> {
143        let transport = if address.is_quic() {
144            KnownTransport::Quic
145        } else if address.is_webrtc_direct() {
146            KnownTransport::WebRtcDirect
147        } else {
148            return Ok(None);
149        };
150        let reachability = match transport {
151            KnownTransport::Quic => reachability,
152            KnownTransport::WebRtcDirect => KnownReachability::Unverified,
153        };
154
155        Ok(Some(Self {
156            transport: transport.id(),
157            reachability: reachability.id(),
158            address: postcard::to_stdvec(address)?,
159        }))
160    }
161
162    /// Decode an address only when its transport identifier is known.
163    ///
164    /// Unknown identifiers return `Ok(None)` and remain safe to forward.
165    pub fn decode_known(&self) -> Result<Option<MultiAddr>, postcard::Error> {
166        let Some(transport) = KnownTransport::from_id(self.transport) else {
167            return Ok(None);
168        };
169        let address: MultiAddr = postcard::from_bytes(&self.address)?;
170        let matches_transport = match transport {
171            KnownTransport::Quic => address.is_quic(),
172            KnownTransport::WebRtcDirect => address.is_webrtc_direct(),
173        };
174        Ok(matches_transport.then_some(address))
175    }
176
177    #[must_use]
178    #[cfg(any(feature = "native", test))]
179    pub(crate) fn legacy_reachability(&self) -> Option<AddressType> {
180        KnownReachability::from_id(self.reachability).map(KnownReachability::into_legacy)
181    }
182
183    #[must_use]
184    #[cfg(any(feature = "native", test))]
185    pub(crate) fn is_within_wire_bounds(&self) -> bool {
186        !self.address.is_empty() && self.address.len() <= MAX_TRANSPORT_ADDRESS_PAYLOAD
187    }
188}
189
190fn bounded_address<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
191    crate::signed_address::bounded_vec::<_, _, MAX_TRANSPORT_ADDRESS_PAYLOAD>(d)
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::{PeerId, WebRtcCertificateHash, WebRtcDirectAddr};
198
199    #[test]
200    fn known_names_have_stable_open_wire_ids() {
201        assert_eq!(KnownTransport::Quic.id(), 1);
202        assert_eq!(KnownTransport::WebRtcDirect.id(), 2);
203        assert_eq!(KnownReachability::Relay.id(), 1);
204        assert_eq!(KnownReachability::Direct.id(), 2);
205        assert_eq!(KnownReachability::Unverified.id(), 3);
206        assert_eq!(KnownReachability::Lan.id(), 4);
207    }
208
209    #[test]
210    fn unknown_identifiers_round_trip_without_enum_decode_failure() {
211        let record = TransportAddressRecord {
212            transport: 900,
213            reachability: 901,
214            address: vec![1, 2, 3, 4],
215        };
216        let bytes = postcard::to_stdvec(&record).unwrap();
217        let decoded: TransportAddressRecord = postcard::from_bytes(&bytes).unwrap();
218
219        assert_eq!(decoded, record);
220        assert_eq!(decoded.decode_known().unwrap(), None);
221        assert_eq!(decoded.legacy_reachability(), None);
222    }
223
224    #[test]
225    fn quic_record_round_trips() {
226        let address: MultiAddr = "/ip4/203.0.113.9/udp/12000/quic".parse().unwrap();
227        let record = TransportAddressRecord::from_multiaddr(&address, KnownReachability::Direct)
228            .unwrap()
229            .unwrap();
230
231        assert_eq!(record.transport, KnownTransport::Quic.id());
232        assert_eq!(record.legacy_reachability(), Some(AddressType::Direct));
233        assert_eq!(record.decode_known().unwrap(), Some(address));
234    }
235
236    #[test]
237    fn webrtc_direct_record_is_always_unverified() {
238        let peer_id = PeerId::from_bytes([0x22; 32]);
239        let address = MultiAddr::webrtc_direct(
240            WebRtcDirectAddr::new(
241                "203.0.113.9:42768".parse().unwrap(),
242                WebRtcCertificateHash::new([0x33; 32]),
243            )
244            .unwrap(),
245        )
246        .with_peer_id(peer_id);
247        let record = TransportAddressRecord::from_multiaddr(&address, KnownReachability::Direct)
248            .unwrap()
249            .unwrap();
250
251        assert_eq!(record.transport, KnownTransport::WebRtcDirect.id());
252        assert_eq!(record.legacy_reachability(), Some(AddressType::Unverified));
253        assert_eq!(record.decode_known().unwrap(), Some(address));
254    }
255
256    #[test]
257    fn declared_transport_must_match_the_encoded_multiaddress() {
258        let address: MultiAddr = "/ip4/203.0.113.9/udp/12000/quic".parse().unwrap();
259        let mut record =
260            TransportAddressRecord::from_multiaddr(&address, KnownReachability::Direct)
261                .unwrap()
262                .unwrap();
263        record.transport = KnownTransport::WebRtcDirect.id();
264
265        assert_eq!(record.decode_known().unwrap(), None);
266    }
267}