Skip to main content

iroh_relay/
relay_map.rs

1//! based on tailscale/tailcfg/derpmap.go
2
3use std::{
4    collections::BTreeMap,
5    fmt,
6    sync::{Arc, RwLock},
7};
8
9use iroh_base::{RelayUrl, RelayUrlParseError};
10use serde::{Deserialize, Serialize};
11
12use crate::defaults::DEFAULT_RELAY_QUIC_PORT;
13
14/// List of relay server configurations to be used in an iroh endpoint.
15///
16/// A [`RelayMap`] can be constructed from an iterator of [`RelayConfig`] or [`RelayUrl]`,
17/// or by creating an empty relay map with [`RelayMap::empty`] and then adding entries with
18/// [`RelayMap::insert`].
19///
20/// Example:
21/// ```
22/// # use std::str::FromStr;
23/// # use iroh_base::RelayUrl;
24/// # use iroh_relay::RelayMap;
25/// let relay1 = RelayUrl::from_str("https://relay1.example.org").unwrap();
26/// let relay2 = RelayUrl::from_str("https://relay2.example.org").unwrap();
27/// let map = RelayMap::from_iter(vec![relay1, relay2]);
28/// ```
29#[derive(Debug, Clone)]
30pub struct RelayMap {
31    /// A map of the different relay IDs to the [`RelayConfig`] information
32    relays: Arc<RwLock<BTreeMap<RelayUrl, Arc<RelayConfig>>>>,
33}
34
35impl PartialEq for RelayMap {
36    fn eq(&self, other: &Self) -> bool {
37        let this = self.relays.read().expect("poisoned");
38        let that = other.relays.read().expect("poisoned");
39        this.eq(&*that)
40    }
41}
42
43impl Eq for RelayMap {}
44
45impl RelayMap {
46    /// Creates an empty relay map.
47    pub fn empty() -> Self {
48        Self {
49            relays: Default::default(),
50        }
51    }
52
53    /// Creates a [`RelayMap`] from an iterator.
54    ///
55    /// The conversion from a URL to a [`RelayConfig`] is done the same as when parsing it directly,
56    /// which means it is assumed to run QUIC on default settings as defined in [`RelayQuicConfig::default`].
57    ///
58    /// # Example
59    /// ```rust
60    /// # use iroh_relay::RelayMap;
61    /// let map =
62    ///     RelayMap::try_from_iter(["https://relay_0.cool.com", "https://relay_1.cool.com"]).unwrap();
63    /// ```
64    pub fn try_from_iter<'a, T: IntoIterator<Item = &'a str>>(
65        urls: T,
66    ) -> Result<Self, RelayUrlParseError> {
67        let relays: BTreeMap<RelayUrl, Arc<RelayConfig>> = urls
68            .into_iter()
69            .map(|t| {
70                t.parse()
71                    .map(|url: RelayUrl| (url.clone(), Arc::new(RelayConfig::from(url))))
72            })
73            .collect::<Result<_, _>>()?;
74        Ok(Self {
75            relays: Arc::new(RwLock::new(relays)),
76        })
77    }
78
79    /// Returns the URLs of all servers in this relay map.
80    ///
81    /// This function is generic over the container to collect into. If you simply want a list
82    /// of URLs, call this with `map.urls::<Vec<_>>()` to get a `Vec<RelayUrl>`.
83    pub fn urls<T>(&self) -> T
84    where
85        T: FromIterator<RelayUrl>,
86    {
87        self.relays
88            .read()
89            .expect("poisoned")
90            .keys()
91            .cloned()
92            .collect::<T>()
93    }
94
95    /// Returns a list with the [`RelayConfig`] for each relay in this relay map.
96    ///
97    /// This function is generic over the container to collect into. If you simply want a list
98    /// of URLs, call this with `map.relays::<Vec<_>>()` to get a `Vec<RelayConfig>`.
99    pub fn relays<T>(&self) -> T
100    where
101        T: FromIterator<Arc<RelayConfig>>,
102    {
103        self.relays
104            .read()
105            .expect("poisoned")
106            .values()
107            .cloned()
108            .collect::<T>()
109    }
110
111    /// Returns `true` if a relay with `url` is contained in this this relay map.
112    pub fn contains(&self, url: &RelayUrl) -> bool {
113        self.relays.read().expect("poisoned").contains_key(url)
114    }
115
116    /// Returns the config for a relay.
117    pub fn get(&self, url: &RelayUrl) -> Option<Arc<RelayConfig>> {
118        self.relays.read().expect("poisoned").get(url).cloned()
119    }
120
121    /// Returns the number of relays in this relay map.
122    pub fn len(&self) -> usize {
123        self.relays.read().expect("poisoned").len()
124    }
125
126    /// Returns `true` if this relay map is empty.
127    pub fn is_empty(&self) -> bool {
128        self.relays.read().expect("poisoned").is_empty()
129    }
130
131    /// Inserts a new relay into the relay map.
132    pub fn insert(&self, url: RelayUrl, endpoint: Arc<RelayConfig>) -> Option<Arc<RelayConfig>> {
133        self.relays.write().expect("poisoned").insert(url, endpoint)
134    }
135
136    /// Removes an existing relay by its URL.
137    pub fn remove(&self, url: &RelayUrl) -> Option<Arc<RelayConfig>> {
138        self.relays.write().expect("poisoned").remove(url)
139    }
140
141    /// Extends this `RelayMap` with another one.
142    pub fn extend(&self, other: &RelayMap) {
143        let mut a = self.relays.write().expect("poisoned");
144        let b = other.relays.read().expect("poisoned");
145        a.extend(b.iter().map(|(a, b)| (a.clone(), b.clone())));
146    }
147
148    /// Sets an authorization token for all relays configured in this relay map.
149    ///
150    /// This applies [`RelayConfig::with_auth_token`] to all current entries in this relay map.
151    /// Any entries added to this relay map *after* calling this will not have the token set.
152    ///
153    /// See [`RelayConfig::with_auth_token`] for details.
154    pub fn with_auth_token(self, auth_token: impl Into<String>) -> Self {
155        let auth_token = auth_token.into();
156        for config in self.relays.write().expect("poisoned").values_mut() {
157            *config = Arc::new(config.as_ref().clone().with_auth_token(auth_token.clone()));
158        }
159        self
160    }
161}
162
163impl FromIterator<RelayConfig> for RelayMap {
164    fn from_iter<T: IntoIterator<Item = RelayConfig>>(iter: T) -> Self {
165        Self::from_iter(iter.into_iter().map(Arc::new))
166    }
167}
168
169impl FromIterator<Arc<RelayConfig>> for RelayMap {
170    fn from_iter<T: IntoIterator<Item = Arc<RelayConfig>>>(iter: T) -> Self {
171        Self {
172            relays: Arc::new(RwLock::new(
173                iter.into_iter()
174                    .map(|config| (config.url.clone(), config))
175                    .collect(),
176            )),
177        }
178    }
179}
180
181impl From<RelayUrl> for RelayMap {
182    /// Creates a [`RelayMap`] from a [`RelayUrl`].
183    ///
184    /// The [`RelayConfig`]s in the [`RelayMap`] will have the default QUIC address
185    /// discovery ports.
186    fn from(value: RelayUrl) -> Self {
187        Self {
188            relays: Arc::new(RwLock::new(
189                [(value.clone(), Arc::new(value.into()))].into(),
190            )),
191        }
192    }
193}
194
195impl From<RelayConfig> for RelayMap {
196    fn from(value: RelayConfig) -> Self {
197        Self {
198            relays: Arc::new(RwLock::new([(value.url.clone(), Arc::new(value))].into())),
199        }
200    }
201}
202
203impl FromIterator<RelayUrl> for RelayMap {
204    /// Creates a [`RelayMap`] from an iterator of [`RelayUrl`].
205    ///
206    /// The [`RelayConfig`]s in the [`RelayMap`] will have the default QUIC address
207    /// discovery ports.
208    fn from_iter<T: IntoIterator<Item = RelayUrl>>(iter: T) -> Self {
209        Self {
210            relays: Arc::new(RwLock::new(
211                iter.into_iter()
212                    .map(|url| (url.clone(), Arc::new(url.into())))
213                    .collect(),
214            )),
215        }
216    }
217}
218
219impl fmt::Display for RelayMap {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        fmt::Debug::fmt(&self, f)
222    }
223}
224
225/// Information on a specific relay server.
226///
227/// Includes the Url where it can be dialed.
228// Please note that this is documented in the `iroh.computer` repository under
229// `src/app/docs/reference/config/page.mdx`.  Any changes to this need to be updated there.
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
231#[non_exhaustive]
232pub struct RelayConfig {
233    /// The [`RelayUrl`] where this relay server can be dialed.
234    pub url: RelayUrl,
235    /// Configuration to speak to the QUIC endpoint on the relay server.
236    ///
237    /// When `None`, we will not attempt to do QUIC address discovery
238    /// with this relay server.
239    #[serde(default = "quic_config")]
240    pub quic: Option<RelayQuicConfig>,
241    /// Optional authorization token sent to the relay.
242    ///
243    /// Set via [`RelayConfig::with_auth_token`].
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub auth_token: Option<String>,
246}
247
248impl RelayConfig {
249    /// Creates a new relay configuration with the given URL and optional QUIC config.
250    pub fn new(url: RelayUrl, quic: Option<RelayQuicConfig>) -> Self {
251        Self {
252            url,
253            quic,
254            auth_token: None,
255        }
256    }
257
258    /// Sets an authorization token for this relay.
259    ///
260    /// On native targets, the token is sent as an `Authorization: Bearer TOKEN`
261    /// header on the WebSocket upgrade request.
262    ///
263    /// When compiled to WebAssembly the token is sent as a `?token=TOKEN`
264    /// query parameter on the upgrade URL, since browsers don't allow setting
265    /// headers on WebSocket requests.
266    pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
267        self.auth_token = Some(token.into());
268        self
269    }
270}
271
272impl From<RelayUrl> for RelayConfig {
273    fn from(value: RelayUrl) -> Self {
274        Self {
275            url: value,
276            quic: quic_config(),
277            auth_token: None,
278        }
279    }
280}
281
282fn quic_config() -> Option<RelayQuicConfig> {
283    Some(RelayQuicConfig::default())
284}
285
286/// Configuration for speaking to the QUIC endpoint on the relay
287/// server to do QUIC address discovery.
288///
289/// Defaults to using [`DEFAULT_RELAY_QUIC_PORT`].
290#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
291#[non_exhaustive]
292pub struct RelayQuicConfig {
293    /// The port on which the connection should be bound to.
294    pub port: u16,
295}
296
297impl RelayQuicConfig {
298    /// Creates a new QUIC address discovery configuration with the given port.
299    pub fn new(port: u16) -> Self {
300        Self { port }
301    }
302}
303
304impl Default for RelayQuicConfig {
305    fn default() -> Self {
306        Self {
307            port: DEFAULT_RELAY_QUIC_PORT,
308        }
309    }
310}
311
312impl fmt::Display for RelayConfig {
313    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
314        write!(f, "{}", self.url)
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use std::str::FromStr;
321
322    use super::*;
323
324    #[test]
325    fn relay_map_extend() {
326        let urls1 = vec![
327            RelayUrl::from_str("https://hello-a-01.com").unwrap(),
328            RelayUrl::from_str("https://hello-b-01.com").unwrap(),
329            RelayUrl::from_str("https://hello-c-01-.com").unwrap(),
330        ];
331
332        let urls2 = vec![
333            RelayUrl::from_str("https://hello-a-02.com").unwrap(),
334            RelayUrl::from_str("https://hello-b-02.com").unwrap(),
335            RelayUrl::from_str("https://hello-c-02-.com").unwrap(),
336        ];
337
338        let map1 = RelayMap::from_iter(urls1.clone().into_iter().map(RelayConfig::from));
339        let map2 = RelayMap::from_iter(urls2.clone().into_iter().map(RelayConfig::from));
340
341        assert_ne!(map1, map2);
342
343        // combine
344
345        let map3 = RelayMap::from_iter(
346            map1.relays::<Vec<_>>()
347                .into_iter()
348                .chain(map2.relays::<Vec<_>>()),
349        );
350
351        assert_eq!(map3.len(), 6);
352
353        map1.extend(&map2);
354        assert_eq!(map3, map1);
355    }
356}