iroh_relay/server/
clients.rs

1//! The "Server" side of the client. Uses the `ClientConnManager`.
2// Based on tailscale/derp/derp_server.go
3
4use std::{collections::HashSet, sync::Arc};
5
6use dashmap::DashMap;
7use iroh_base::EndpointId;
8use n0_future::IterExt;
9use tokio::sync::mpsc::error::TrySendError;
10use tracing::{debug, trace};
11
12use super::{
13    ConnectionId, OnDisconnectGuard,
14    client::{Client, Config, ForwardPacketError},
15};
16use crate::{
17    protos::{
18        relay::{Datagrams, Status},
19        streams::BytesStreamSink,
20    },
21    server::{client::SendError, metrics::Metrics},
22};
23
24/// Registry of connected relay clients.
25///
26/// This type manages the collection of active client connections and
27/// handles routing messages between them.
28#[derive(Debug, Clone, Default)]
29pub struct Clients(Arc<Inner>);
30
31#[derive(Debug, Default)]
32struct Inner {
33    /// The list of all currently connected clients.
34    clients: DashMap<EndpointId, ClientState>,
35    /// Map of which client has sent where
36    sent_to: DashMap<EndpointId, HashSet<EndpointId>>,
37}
38
39#[derive(Debug)]
40struct ClientState {
41    active: Client,
42    inactive: Vec<Client>,
43}
44
45impl ClientState {
46    async fn shutdown_all(mut self) {
47        [self.active]
48            .into_iter()
49            .chain(self.inactive.drain(..))
50            .map(Client::shutdown)
51            .join_all()
52            .await;
53    }
54}
55
56impl Clients {
57    /// Shuts down all connected clients.
58    ///
59    /// This method gracefully disconnects all active client connections managed by
60    /// this registry. It will wait for all clients to complete their shutdown before
61    /// returning.
62    pub async fn shutdown(&self) {
63        let keys: Vec<_> = self.0.clients.iter().map(|x| *x.key()).collect();
64        trace!("shutting down {} clients", keys.len());
65        let clients = keys.into_iter().filter_map(|k| self.0.clients.remove(&k));
66        n0_future::join_all(clients.map(|(_, state)| state.shutdown_all())).await;
67    }
68
69    /// Builds the client handler and starts the read & write loops for the connection.
70    ///
71    /// Once the client disconnects, the [`OnDisconnectGuard`] set in `config` will be dropped,
72    /// allowing callers to be notified of the disconnect.
73    pub fn register<S>(&self, client_config: Config<S>, metrics: Arc<Metrics>)
74    where
75        S: BytesStreamSink + Send + 'static,
76    {
77        let endpoint_id = client_config.guard.endpoint_id;
78        trace!(remote_endpoint = %endpoint_id.fmt_short(), "registering client");
79
80        let client = Client::new(client_config, self, metrics.clone());
81        match self.0.clients.entry(endpoint_id) {
82            dashmap::Entry::Occupied(mut entry) => {
83                let state = entry.get_mut();
84                let old_client = std::mem::replace(&mut state.active, client);
85                debug!(
86                    remote_endpoint = %endpoint_id.fmt_short(),
87                    "multiple connections found, deactivating old connection",
88                );
89                old_client
90                    .try_send_health(Status::SameEndpointIdConnected)
91                    .ok();
92                state.inactive.push(old_client);
93                metrics.clients_inactive_added.inc();
94            }
95            dashmap::Entry::Vacant(entry) => {
96                entry.insert(ClientState {
97                    active: client,
98                    inactive: Vec::new(),
99                });
100            }
101        }
102    }
103
104    /// Removes the client from the map of clients, & sends a notification
105    /// to each client that peers has sent data to, to let them know that
106    /// peer is gone from the network.
107    ///
108    /// Must be passed a matching connection_id.
109    pub(super) fn unregister(&self, guard: OnDisconnectGuard, metrics: &Metrics) {
110        let endpoint_id = guard.endpoint_id;
111        let connection_id = guard.connection_id;
112        trace!(
113            endpoint_id = %endpoint_id.fmt_short(),
114            %connection_id, "unregistering client"
115        );
116
117        let mut notify_peers = None;
118
119        self.0.clients.remove_if_mut(&endpoint_id, |_id, state| {
120            if state.active.connection_id() == connection_id {
121                // The unregistering client is the currently active client
122                if let Some(last_inactive_client) = state.inactive.pop() {
123                    metrics.clients_inactive_removed.inc();
124                    // There is an inactive client, promote to active again.
125                    state.active = last_inactive_client;
126                    // Inform the old client that it is healthy again.
127                    state.active.try_send_health(Status::Healthy).ok();
128                    // Don't remove the entry from client map.
129                    false
130                } else {
131                    // No inactive clients: collect sent_to set for peer-gone notifications.
132                    notify_peers = self.0.sent_to.remove(&endpoint_id).map(|(_, peers)| peers);
133                    // Remove entry from the client map.
134                    true
135                }
136            } else {
137                // The unregistering client is already inactive. Remove from the list of inactive clients.
138                state
139                    .inactive
140                    .retain(|client| client.connection_id() != connection_id);
141                metrics.clients_inactive_removed.inc();
142                // Active client is unmodified: keep entry in map.
143                false
144            }
145        });
146
147        // Inform peers that this endpoint is gone.
148        // Done outside the remove_if_mut closure to avoid DashMap deadlocks.
149        if let Some(peers) = notify_peers {
150            for peer_id in peers {
151                if let Some(peer) = self.0.clients.get(&peer_id) {
152                    match peer.active.try_send_peer_gone(endpoint_id) {
153                        Ok(_) => {}
154                        Err(TrySendError::Full(_)) => {
155                            debug!(
156                                dst = %peer_id.fmt_short(),
157                                "client too busy to receive peer gone notification, dropping"
158                            );
159                        }
160                        Err(TrySendError::Closed(_)) => {
161                            debug!(
162                                dst = %peer_id.fmt_short(),
163                                "can no longer write to client, dropping peer gone notification"
164                            );
165                        }
166                    }
167                }
168            }
169        }
170    }
171
172    /// Disconnects connections registered for `endpoint_id`.
173    ///
174    /// With `Some(connection_id)`, disconnects only that connection (active or
175    /// an inactive duplicate). With `None`, disconnects every connection for the
176    /// endpoint. Returns `true` if a matching connection was found, or `false`
177    /// otherwise.
178    ///
179    /// Shutdown happens asynchronously: each per-connection actor exits its run
180    /// loop and unregisters itself after this call returns.
181    pub fn disconnect(&self, endpoint_id: EndpointId, connection_id: Option<ConnectionId>) -> bool {
182        let Some(state) = self.0.clients.get(&endpoint_id) else {
183            return false;
184        };
185        let mut clients = state.inactive.iter().chain([&state.active]);
186        if let Some(id) = connection_id {
187            let Some(client) = clients.find(|c| c.connection_id() == id) else {
188                return false;
189            };
190            client.start_shutdown();
191        } else {
192            for client in clients {
193                client.start_shutdown();
194            }
195        }
196        true
197    }
198
199    /// Attempt to send a packet to client with [`EndpointId`] `dst`.
200    pub(super) fn send_packet(
201        &self,
202        dst: EndpointId,
203        data: Datagrams,
204        src: EndpointId,
205        metrics: &Metrics,
206    ) -> Result<(), ForwardPacketError> {
207        let Some(client) = self.0.clients.get(&dst) else {
208            debug!(dst = %dst.fmt_short(), "no connected client, dropped packet");
209            metrics.send_packets_dropped.inc();
210            return Ok(());
211        };
212        match client.active.try_send_packet(src, data) {
213            Ok(_) => {
214                // Record sent_to relationship
215                self.0.sent_to.entry(src).or_default().insert(dst);
216                Ok(())
217            }
218            Err(TrySendError::Full(_)) => {
219                debug!(
220                    dst = %dst.fmt_short(),
221                    "client too busy to receive packet, dropping packet"
222                );
223                Err(ForwardPacketError::new(SendError::Full))
224            }
225            Err(TrySendError::Closed(_)) => {
226                debug!(
227                    dst = %dst.fmt_short(),
228                    "can no longer write to client, dropping message and pruning connection"
229                );
230                client.active.start_shutdown();
231                Err(ForwardPacketError::new(SendError::Closed))
232            }
233        }
234    }
235
236    #[cfg(test)]
237    fn active_connection_id(&self, endpoint_id: EndpointId) -> Option<ConnectionId> {
238        self.0
239            .clients
240            .get(&endpoint_id)
241            .map(|s| s.active.connection_id())
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use std::time::Duration;
248
249    use iroh_base::SecretKey;
250    use n0_error::{Result, StdResultExt};
251    use n0_future::{Stream, StreamExt};
252    use n0_tracing_test::traced_test;
253    use rand::{RngExt, SeedableRng};
254
255    use super::*;
256    use crate::{
257        client::conn::Conn,
258        http::ProtocolVersion,
259        protos::{common::FrameType, relay::RelayToClientMsg, streams::WsBytesFramed},
260        server::streams::{MaybeTlsStream, RateLimited, ServerRelayedStream},
261    };
262
263    async fn recv_frame<
264        E: std::error::Error + Sync + Send + 'static,
265        S: Stream<Item = Result<RelayToClientMsg, E>> + Unpin,
266    >(
267        frame_type: FrameType,
268        mut stream: S,
269    ) -> Result<RelayToClientMsg> {
270        match stream.next().await {
271            Some(Ok(frame)) => {
272                if frame_type != frame.typ() {
273                    n0_error::bail_any!(
274                        "Unexpected frame, got {:?}, but expected {:?}",
275                        frame.typ(),
276                        frame_type
277                    );
278                }
279                Ok(frame)
280            }
281            Some(Err(err)) => Err(err).anyerr(),
282            None => n0_error::bail_any!("Unexpected EOF, expected frame {frame_type:?}"),
283        }
284    }
285
286    fn test_client_builder(
287        key: EndpointId,
288    ) -> (Config<WsBytesFramed<RateLimited<MaybeTlsStream>>>, Conn) {
289        let (server, client) = tokio::io::duplex(1024);
290        let guard = OnDisconnectGuard::empty(key);
291        let protocol_version = ProtocolVersion::default();
292        let mut config = Config::new(guard, ServerRelayedStream::test(server), protocol_version);
293        config.write_timeout = Duration::from_secs(1);
294        config.channel_capacity = 10;
295        (config, Conn::test(client, protocol_version))
296    }
297
298    #[tokio::test]
299    #[traced_test]
300    async fn test_clients() -> Result {
301        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
302        let a_key = SecretKey::from_bytes(&rng.random()).public();
303        let b_key = SecretKey::from_bytes(&rng.random()).public();
304
305        let (builder_a, mut a_rw) = test_client_builder(a_key);
306
307        let clients = Clients::default();
308        let metrics = Arc::new(Metrics::default());
309        clients.register(builder_a, metrics.clone());
310
311        // send packet
312        let data = b"hello world!";
313        clients.send_packet(a_key, Datagrams::from(&data[..]), b_key, &metrics)?;
314        let frame = recv_frame(FrameType::RelayToClientDatagram, &mut a_rw).await?;
315        assert_eq!(
316            frame,
317            RelayToClientMsg::Datagrams {
318                remote_endpoint_id: b_key,
319                datagrams: data.to_vec().into(),
320            }
321        );
322
323        {
324            let client = clients.0.clients.get(&a_key).unwrap();
325            // shutdown client a, this should trigger the removal from the clients list
326            client.active.start_shutdown();
327        }
328
329        // need to wait a moment for the removal to be processed
330        let c = clients.clone();
331        tokio::time::timeout(Duration::from_secs(1), async move {
332            loop {
333                if !c.0.clients.contains_key(&a_key) {
334                    break;
335                }
336                tokio::time::sleep(Duration::from_millis(100)).await;
337            }
338        })
339        .await
340        .std_context("timeout")?;
341        clients.shutdown().await;
342
343        Ok(())
344    }
345
346    #[tokio::test]
347    #[traced_test]
348    async fn test_clients_same_endpoint_id() -> Result {
349        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
350        let a_key = SecretKey::from_bytes(&rng.random()).public();
351        let b_key = SecretKey::from_bytes(&rng.random()).public();
352
353        let (a1_builder, mut a1_rw) = test_client_builder(a_key);
354
355        let clients = Clients::default();
356        let metrics = Arc::new(Metrics::default());
357
358        // register client a
359        clients.register(a1_builder, metrics.clone());
360        let a1_conn_id = clients.active_connection_id(a_key).unwrap();
361
362        // send packet and verify it is send to a1
363        let data = b"hello world!";
364        clients.send_packet(a_key, Datagrams::from(&data[..]), b_key, &metrics)?;
365        let frame = recv_frame(FrameType::RelayToClientDatagram, &mut a1_rw).await?;
366        assert_eq!(
367            frame,
368            RelayToClientMsg::Datagrams {
369                remote_endpoint_id: b_key,
370                datagrams: data.to_vec().into(),
371            }
372        );
373
374        // register new client with same endpoint id
375        let (a2_builder, mut a2_rw) = test_client_builder(a_key);
376        clients.register(a2_builder, metrics.clone());
377        let a2_conn_id = clients.active_connection_id(a_key).unwrap();
378        assert!(a2_conn_id != a1_conn_id);
379
380        // a1 is marked inactive and should receive a health frame
381        let frame = recv_frame(FrameType::Status, &mut a1_rw).await?;
382        assert_eq!(
383            frame,
384            RelayToClientMsg::Status(Status::SameEndpointIdConnected)
385        );
386
387        // send packet and verify it is send to a2
388        clients.send_packet(a_key, Datagrams::from(&data[..]), b_key, &metrics)?;
389        let frame = recv_frame(FrameType::RelayToClientDatagram, &mut a2_rw).await?;
390        assert_eq!(
391            frame,
392            RelayToClientMsg::Datagrams {
393                remote_endpoint_id: b_key,
394                datagrams: data.to_vec().into(),
395            }
396        );
397
398        // disconnect a2
399        clients
400            .0
401            .clients
402            .get(&a_key)
403            .unwrap()
404            .active
405            .start_shutdown();
406
407        // need to wait a moment for the removal to be processed
408        tokio::time::timeout(Duration::from_secs(1), {
409            let clients = clients.clone();
410            async move {
411                // wait until the active connection is no longer a2 (which we unregistered)
412                while clients.active_connection_id(a_key) == Some(a2_conn_id) {
413                    tokio::time::sleep(Duration::from_millis(100)).await;
414                }
415            }
416        })
417        .await
418        .std_context("timeout")?;
419
420        // a1 should be marked active again now
421        assert_eq!(clients.active_connection_id(a_key), Some(a1_conn_id));
422
423        // a1 is marked active again and should receive a health frame
424        let frame = recv_frame(FrameType::Status, &mut a1_rw).await?;
425        assert_eq!(frame, RelayToClientMsg::Status(Status::Healthy));
426
427        // a1 should receive packets
428        clients.send_packet(a_key, Datagrams::from(&data[..]), b_key, &metrics)?;
429        let frame = recv_frame(FrameType::RelayToClientDatagram, &mut a1_rw).await?;
430        assert_eq!(
431            frame,
432            RelayToClientMsg::Datagrams {
433                remote_endpoint_id: b_key,
434                datagrams: data.to_vec().into(),
435            }
436        );
437
438        // after shutting down the now-active client, there should no longer be an entry for that endpoint id
439        clients
440            .0
441            .clients
442            .get(&a_key)
443            .unwrap()
444            .active
445            .start_shutdown();
446
447        // need to wait a moment for the removal to be processed
448        tokio::time::timeout(Duration::from_secs(1), {
449            let clients = clients.clone();
450            async move {
451                // wait until the active connection is no longer a2 (which we unregistered)
452                while clients.0.clients.contains_key(&a_key) {
453                    tokio::time::sleep(Duration::from_millis(100)).await;
454                }
455            }
456        })
457        .await
458        .std_context("timeout")?;
459
460        clients.shutdown().await;
461
462        Ok(())
463    }
464
465    #[tokio::test]
466    #[traced_test]
467    async fn test_peer_gone_notification() -> Result {
468        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64);
469        let a_key = SecretKey::from_bytes(&rng.random()).public();
470        let b_key = SecretKey::from_bytes(&rng.random()).public();
471
472        let clients = Clients::default();
473        let metrics = Arc::new(Metrics::default());
474
475        // Register both clients
476        let (builder_a, _a_rw) = test_client_builder(a_key);
477        let (builder_b, mut b_rw) = test_client_builder(b_key);
478        clients.register(builder_a, metrics.clone());
479        clients.register(builder_b, metrics.clone());
480
481        // A sends a packet to B (records sent_to[A] = {B})
482        let data = b"hello b!";
483        clients.send_packet(b_key, Datagrams::from(&data[..]), a_key, &metrics)?;
484
485        // B receives the packet
486        let frame = recv_frame(FrameType::RelayToClientDatagram, &mut b_rw).await?;
487        assert_eq!(
488            frame,
489            RelayToClientMsg::Datagrams {
490                remote_endpoint_id: a_key,
491                datagrams: data.to_vec().into(),
492            }
493        );
494
495        // Disconnect A
496        {
497            let client = clients.0.clients.get(&a_key).unwrap();
498            client.active.start_shutdown();
499        }
500
501        // Wait for A to unregister
502        tokio::time::timeout(Duration::from_secs(1), {
503            let clients = clients.clone();
504            async move {
505                while clients.0.clients.contains_key(&a_key) {
506                    tokio::time::sleep(Duration::from_millis(100)).await;
507                }
508            }
509        })
510        .await
511        .std_context("timeout waiting for A to unregister")?;
512
513        // B should receive EndpointGone(a_key): notifying B that A is gone
514        let frame = recv_frame(FrameType::EndpointGone, &mut b_rw).await?;
515        assert_eq!(frame, RelayToClientMsg::EndpointGone(a_key));
516
517        clients.shutdown().await;
518        Ok(())
519    }
520}