Skip to main content

iroh_base/
endpoint_addr.rs

1//! Addressing for iroh endpoints.
2//!
3//! This module contains some common addressing types for iroh.  An endpoint is uniquely
4//! identified by the [`EndpointId`] but that does not make it addressable on the network layer.
5//! For this the addition of a [`RelayUrl`] and/or direct addresses are required.
6//!
7//! The primary way of addressing an endpoint is by using the [`EndpointAddr`].
8
9use std::{collections::BTreeSet, fmt, net::SocketAddr};
10
11use data_encoding::HEXLOWER;
12use n0_error::stack_error;
13use serde::{Deserialize, Serialize};
14
15use crate::{EndpointId, PublicKey, RelayUrl};
16
17/// Network-level addressing information for an iroh endpoint.
18///
19/// This combines an endpoint's identifier with network-level addressing information of how to
20/// contact the endpoint.
21///
22/// To establish a network connection to an endpoint both the [`EndpointId`] and one or more network
23/// paths are needed.  The network paths can come from various sources, current sources can come from
24///
25/// - An [Address Lookup] service which can provide routing information for a given [`EndpointId`].
26///
27/// - A [`RelayUrl`] of the endpoint's [home relay], this allows establishing the connection via
28///   the Relay server and is very reliable.
29///
30/// - One or more *IP based addresses* on which the endpoint might be reachable.  Depending on the
31///   network location of both endpoints it might not be possible to establish a direct
32///   connection without the help of a [Relay server].
33///
34/// This structure will always contain the required [`EndpointId`] and will contain an optional
35/// number of other addressing information.  It is a generic addressing type used whenever a connection
36/// to other endpoints needs to be established.
37///
38/// [Address Lookup]: https://docs.rs/iroh/*/iroh/index.html#address-lookup
39/// [home relay]: https://docs.rs/iroh/*/iroh/relay/index.html
40/// [Relay server]: https://docs.rs/iroh/*/iroh/index.html#relay-servers
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub struct EndpointAddr {
43    /// The endpoint's identifier.
44    pub id: EndpointId,
45    /// The endpoint's addresses
46    pub addrs: BTreeSet<TransportAddr>,
47}
48
49/// Available address types.
50#[derive(
51    derive_more::Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
52)]
53#[non_exhaustive]
54pub enum TransportAddr {
55    /// Relays
56    #[debug("Relay({_0})")]
57    Relay(RelayUrl),
58    /// IP based addresses
59    Ip(SocketAddr),
60    /// Custom transport address
61    Custom(CustomAddr),
62}
63
64impl TransportAddr {
65    /// Whether this is a transport address via a relay server.
66    pub fn is_relay(&self) -> bool {
67        matches!(self, Self::Relay(_))
68    }
69
70    /// Whether this is an IP transport address.
71    pub fn is_ip(&self) -> bool {
72        matches!(self, Self::Ip(_))
73    }
74
75    /// Whether this is a custom transport address.
76    pub fn is_custom(&self) -> bool {
77        matches!(self, Self::Custom(_))
78    }
79}
80
81impl fmt::Display for TransportAddr {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Self::Relay(url) => write!(f, "relay:{url}"),
85            Self::Ip(addr) => write!(f, "ip:{addr}"),
86            Self::Custom(addr) => write!(f, "custom:{addr}"),
87        }
88    }
89}
90
91impl EndpointAddr {
92    /// Creates a new [`EndpointAddr`] with no network level addresses.
93    ///
94    /// This still is usable with e.g. an address lookup service to establish a connection,
95    /// depending on the situation.
96    pub fn new(id: PublicKey) -> Self {
97        EndpointAddr {
98            id,
99            addrs: Default::default(),
100        }
101    }
102
103    /// Creates a new [`EndpointAddr`] from its parts.
104    pub fn from_parts(id: PublicKey, addrs: impl IntoIterator<Item = TransportAddr>) -> Self {
105        Self {
106            id,
107            addrs: addrs.into_iter().collect(),
108        }
109    }
110
111    /// Adds a [`RelayUrl`] address.
112    pub fn with_relay_url(mut self, relay_url: RelayUrl) -> Self {
113        self.addrs.insert(TransportAddr::Relay(relay_url));
114        self
115    }
116
117    /// Adds an IP based address.
118    pub fn with_ip_addr(mut self, addr: SocketAddr) -> Self {
119        self.addrs.insert(TransportAddr::Ip(addr));
120        self
121    }
122
123    /// Adds a list of addresses.
124    pub fn with_addrs(mut self, addrs: impl IntoIterator<Item = TransportAddr>) -> Self {
125        for addr in addrs.into_iter() {
126            self.addrs.insert(addr);
127        }
128        self
129    }
130
131    /// Returns true, if only a [`EndpointId`] is present.
132    pub fn is_empty(&self) -> bool {
133        self.addrs.is_empty()
134    }
135
136    /// Returns a list of IP addresses of this peer.
137    pub fn ip_addrs(&self) -> impl Iterator<Item = &SocketAddr> {
138        self.addrs.iter().filter_map(|addr| match addr {
139            TransportAddr::Ip(addr) => Some(addr),
140            _ => None,
141        })
142    }
143
144    /// Returns a list of relay urls of this peer.
145    ///
146    ///  In practice this is expected to be zero or one home relay for all known cases currently.
147    pub fn relay_urls(&self) -> impl Iterator<Item = &RelayUrl> {
148        self.addrs.iter().filter_map(|addr| match addr {
149            TransportAddr::Relay(url) => Some(url),
150            _ => None,
151        })
152    }
153}
154
155impl From<EndpointId> for EndpointAddr {
156    fn from(endpoint_id: EndpointId) -> Self {
157        EndpointAddr::new(endpoint_id)
158    }
159}
160
161/// A custom transport address consisting of a transport id and opaque address data.
162///
163/// This is a generic address type that allows external crates to implement custom
164/// transports for iroh.
165///
166/// Transport ids are freely chosen u64 numbers. A registry for well-known transport ids
167/// is maintained at <https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md>.
168/// The opaque address data is not validated or size-limited in any way.
169///
170/// # String encoding
171///
172/// Used by [`Display`] and [`FromStr`] implementations.
173/// Format: `<id>_<data>` where `<id>` is the transport id as lowercase hex (no `0x`
174/// prefix, no leading zeros) and `<data>` is the address bytes as lowercase hex,
175/// separated by `_`.
176///
177/// # Binary encoding
178///
179/// Used by [`Self::to_vec`] and [`Self::from_bytes`].
180/// Format: 8-byte little-endian `u64` transport id, followed by raw address data bytes.
181/// The minimum valid length is 8 bytes (id only with empty data).
182///
183/// [`Display`]: std::fmt::Display
184/// [`FromStr`]: std::str::FromStr
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
186pub struct CustomAddr {
187    /// The transport id.
188    id: u64,
189    /// Opaque address data for this transport.
190    data: CustomAddrBytes,
191}
192
193impl fmt::Display for CustomAddr {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        write!(f, "{:x}_{}", self.id, HEXLOWER.encode(self.data.as_bytes()))
196    }
197}
198
199impl std::str::FromStr for CustomAddr {
200    type Err = CustomAddrParseError;
201
202    fn from_str(s: &str) -> Result<Self, Self::Err> {
203        let Some((id_str, data_str)) = s.split_once('_') else {
204            return Err(CustomAddrParseError::MissingSeparator);
205        };
206        let Ok(id) = u64::from_str_radix(id_str, 16) else {
207            return Err(CustomAddrParseError::InvalidId);
208        };
209        let Ok(data) = HEXLOWER.decode(data_str.as_bytes()) else {
210            return Err(CustomAddrParseError::InvalidData);
211        };
212        Ok(Self::from_parts(id, &data))
213    }
214}
215
216/// Error returned when parsing a [`CustomAddr`] from its string encoding fails.
217///
218/// Parsing a string into a [`CustomAddr`] represents just the first part of
219/// validation. Even if the string is well-formed, the resulting [`CustomAddr`] might
220/// still have an invalid data size or format for the transport type.
221#[stack_error(derive)]
222#[allow(missing_docs)]
223pub enum CustomAddrParseError {
224    /// Missing `_` separator between id and data.
225    #[error("missing '_' separator")]
226    MissingSeparator,
227    /// Invalid hex-encoded id.
228    #[error("invalid id")]
229    InvalidId,
230    /// Invalid hex-encoded data.
231    #[error("invalid data")]
232    InvalidData,
233}
234
235#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
236enum CustomAddrBytes {
237    Inline { size: u8, data: [u8; 30] },
238    Heap(Box<[u8]>),
239}
240
241impl fmt::Debug for CustomAddrBytes {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        if !f.alternate() {
244            write!(f, "[{}]", HEXLOWER.encode(self.as_bytes()))
245        } else {
246            let bytes = self.as_bytes();
247            match self {
248                Self::Inline { .. } => write!(f, "Inline[{}]", HEXLOWER.encode(bytes)),
249                Self::Heap(_) => write!(f, "Heap[{}]", HEXLOWER.encode(bytes)),
250            }
251        }
252    }
253}
254
255impl From<(u64, &[u8])> for CustomAddr {
256    fn from((id, data): (u64, &[u8])) -> Self {
257        Self::from_parts(id, data)
258    }
259}
260
261impl CustomAddrBytes {
262    fn len(&self) -> usize {
263        match self {
264            Self::Inline { size, .. } => *size as usize,
265            Self::Heap(data) => data.len(),
266        }
267    }
268
269    fn as_bytes(&self) -> &[u8] {
270        match self {
271            Self::Inline { size, data } => &data[..*size as usize],
272            Self::Heap(data) => data,
273        }
274    }
275
276    fn copy_from_slice(data: &[u8]) -> Self {
277        if data.len() <= 30 {
278            let mut inline = [0u8; 30];
279            inline[..data.len()].copy_from_slice(data);
280            Self::Inline {
281                size: data.len() as u8,
282                data: inline,
283            }
284        } else {
285            Self::Heap(data.to_vec().into_boxed_slice())
286        }
287    }
288}
289
290impl CustomAddr {
291    /// Creates a new [`CustomAddr`] from a transport id and raw address data.
292    pub fn from_parts(id: u64, data: &[u8]) -> Self {
293        Self {
294            id,
295            data: CustomAddrBytes::copy_from_slice(data),
296        }
297    }
298
299    /// Returns the transport id.
300    ///
301    /// You can freely choose this. There is a table of reserved custom transport ids in
302    /// <https://github.com/n0-computer/iroh/blob/main/TRANSPORTS.md>, where you could
303    /// submit your transport for registration to get a reserved id.
304    ///
305    /// But this is only relevant if you care for interop.
306    pub fn id(&self) -> u64 {
307        self.id
308    }
309
310    /// Returns the opaque address data for this transport.
311    ///
312    /// Below a certain size (currently 30 bytes) this is stored inline, otherwise on the heap.
313    ///
314    /// Note that there are no guarantees about the size of this data. When parsing custom
315    /// addresses you must be prepared to handle unexpected sizes here.
316    pub fn data(&self) -> &[u8] {
317        self.data.as_bytes()
318    }
319
320    /// Serializes to the binary encoding.
321    ///
322    /// See [`CustomAddr`] docs for details on the encoding.
323    pub fn to_vec(&self) -> Vec<u8> {
324        let mut out = vec![0u8; 8 + self.data.len()];
325        out[..8].copy_from_slice(&self.id().to_le_bytes());
326        out[8..].copy_from_slice(self.data());
327        out
328    }
329
330    /// Parses from the binary encoding.
331    ///
332    /// See [`CustomAddr`] docs for details on the encoding.
333    pub fn from_bytes(data: &[u8]) -> Result<Self, &'static str> {
334        if data.len() < 8 {
335            return Err("data too short");
336        }
337        let id = u64::from_le_bytes(data[..8].try_into().expect("data length checked above"));
338        let data = &data[8..];
339        Ok(Self::from_parts(id, data))
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
348    #[non_exhaustive]
349    enum NewAddrType {
350        /// Relays
351        Relay(RelayUrl),
352        /// IP based addresses
353        Ip(SocketAddr),
354        /// New addr type for testing
355        Cool(u16),
356    }
357
358    #[test]
359    fn test_roundtrip_new_addr_type() {
360        let old = vec![
361            TransportAddr::Ip("127.0.0.1:9".parse().unwrap()),
362            TransportAddr::Relay("https://example.com".parse().unwrap()),
363        ];
364        let old_ser = postcard::to_stdvec(&old).unwrap();
365        let old_back: Vec<TransportAddr> = postcard::from_bytes(&old_ser).unwrap();
366        assert_eq!(old, old_back);
367
368        let new = vec![
369            NewAddrType::Ip("127.0.0.1:9".parse().unwrap()),
370            NewAddrType::Relay("https://example.com".parse().unwrap()),
371            NewAddrType::Cool(4),
372        ];
373        let new_ser = postcard::to_stdvec(&new).unwrap();
374        let new_back: Vec<NewAddrType> = postcard::from_bytes(&new_ser).unwrap();
375
376        assert_eq!(new, new_back);
377
378        // serialize old into new
379        let old_new_back: Vec<NewAddrType> = postcard::from_bytes(&old_ser).unwrap();
380
381        assert_eq!(
382            old_new_back,
383            vec![
384                NewAddrType::Ip("127.0.0.1:9".parse().unwrap()),
385                NewAddrType::Relay("https://example.com".parse().unwrap()),
386            ]
387        );
388    }
389
390    #[test]
391    fn test_custom_addr_roundtrip() {
392        // Small id, small data (e.g., Bluetooth MAC)
393        let addr = CustomAddr::from_parts(1, &[0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6]);
394        let s = addr.to_string();
395        assert_eq!(s, "1_a1b2c3d4e5f6");
396        let parsed: CustomAddr = s.parse().unwrap();
397        assert_eq!(addr, parsed);
398
399        // Larger id, 32-byte data (e.g., Tor pubkey)
400        let addr = CustomAddr::from_parts(42, &[0xab; 32]);
401        let s = addr.to_string();
402        assert_eq!(
403            s,
404            "2a_abababababababababababababababababababababababababababababababab"
405        );
406        let parsed: CustomAddr = s.parse().unwrap();
407        assert_eq!(addr, parsed);
408
409        // Zero id, empty data
410        let addr = CustomAddr::from_parts(0, &[]);
411        let s = addr.to_string();
412        assert_eq!(s, "0_");
413        let parsed: CustomAddr = s.parse().unwrap();
414        assert_eq!(addr, parsed);
415
416        // Large id
417        let addr = CustomAddr::from_parts(0xdeadbeef, &[0x01, 0x02]);
418        let s = addr.to_string();
419        assert_eq!(s, "deadbeef_0102");
420        let parsed: CustomAddr = s.parse().unwrap();
421        assert_eq!(addr, parsed);
422    }
423
424    #[test]
425    fn test_custom_addr_parse_errors() {
426        // Missing separator
427        assert!("abc123".parse::<CustomAddr>().is_err());
428
429        // Invalid id (not hex)
430        assert!("xyz_0102".parse::<CustomAddr>().is_err());
431
432        // Invalid data (not hex)
433        assert!("1_ghij".parse::<CustomAddr>().is_err());
434
435        // Odd-length hex data
436        assert!("1_abc".parse::<CustomAddr>().is_err());
437    }
438}