1use std::{
2 collections::{hash_map::Entry, BTreeSet, HashMap},
3 hash::Hash,
4 net::{IpAddr, SocketAddr},
5 pin::Pin,
6 task::{Context, Poll},
7 time::Instant,
8};
9
10use futures_lite::stream::Stream;
11use iroh_base::key::NodeId;
12use iroh_metrics::inc;
13use parking_lot::Mutex;
14use serde::{Deserialize, Serialize};
15use stun_rs::TransactionId;
16use tracing::{debug, info, instrument, trace, warn};
17
18use self::{
19 best_addr::ClearReason,
20 node_state::{NodeState, Options, PingHandled},
21};
22use super::{
23 metrics::Metrics as MagicsockMetrics, ActorMessage, DiscoMessageSource, QuicMappedAddr,
24};
25use crate::{
26 disco::{CallMeMaybe, Pong, SendAddr},
27 key::PublicKey,
28 relay::RelayUrl,
29 stun, NodeAddr,
30};
31
32mod best_addr;
33mod node_state;
34mod path_state;
35mod udp_paths;
36
37pub use node_state::{ConnectionType, ControlMsg, DirectAddrInfo, RemoteInfo};
38pub(super) use node_state::{DiscoPingPurpose, PingAction, PingRole, SendPing};
39
40const MAX_INACTIVE_NODES: usize = 30;
43
44#[derive(Default, Debug)]
62pub(super) struct NodeMap {
63 inner: Mutex<NodeMapInner>,
64}
65
66#[derive(Default, Debug)]
67pub(super) struct NodeMapInner {
68 by_node_key: HashMap<NodeId, usize>,
69 by_ip_port: HashMap<IpPort, usize>,
70 by_quic_mapped_addr: HashMap<QuicMappedAddr, usize>,
71 by_id: HashMap<usize, NodeState>,
72 next_id: usize,
73}
74
75#[derive(Debug, Clone)]
80enum NodeStateKey {
81 Idx(usize),
82 NodeId(NodeId),
83 QuicMappedAddr(QuicMappedAddr),
84 IpPort(IpPort),
85}
86
87#[derive(Serialize, Deserialize, strum::Display, Debug, Clone, Eq, PartialEq, Hash)]
106#[strum(serialize_all = "kebab-case")]
107pub enum Source {
108 Saved,
110 Udp,
112 Relay,
114 App,
116 #[strum(serialize = "{name}")]
118 Discovery {
119 name: String,
121 },
122 #[strum(serialize = "{name}")]
124 NamedApp {
125 name: String,
127 },
128}
129
130impl NodeMap {
131 pub(super) fn load_from_vec(nodes: Vec<NodeAddr>) -> Self {
133 Self::from_inner(NodeMapInner::load_from_vec(nodes))
134 }
135
136 fn from_inner(inner: NodeMapInner) -> Self {
137 Self {
138 inner: Mutex::new(inner),
139 }
140 }
141
142 pub(super) fn add_node_addr(&self, node_addr: NodeAddr, source: Source) {
144 self.inner.lock().add_node_addr(node_addr, source)
145 }
146
147 pub(super) fn node_count(&self) -> usize {
149 self.inner.lock().node_count()
150 }
151
152 pub(super) fn receive_udp(&self, udp_addr: SocketAddr) -> Option<(PublicKey, QuicMappedAddr)> {
153 self.inner.lock().receive_udp(udp_addr)
154 }
155
156 pub(super) fn receive_relay(&self, relay_url: &RelayUrl, src: NodeId) -> QuicMappedAddr {
157 self.inner.lock().receive_relay(relay_url, src)
158 }
159
160 pub(super) fn notify_ping_sent(
161 &self,
162 id: usize,
163 dst: SendAddr,
164 tx_id: stun::TransactionId,
165 purpose: DiscoPingPurpose,
166 msg_sender: tokio::sync::mpsc::Sender<ActorMessage>,
167 ) {
168 if let Some(ep) = self.inner.lock().get_mut(NodeStateKey::Idx(id)) {
169 ep.ping_sent(dst, tx_id, purpose, msg_sender);
170 }
171 }
172
173 pub(super) fn notify_ping_timeout(&self, id: usize, tx_id: stun::TransactionId) {
174 if let Some(ep) = self.inner.lock().get_mut(NodeStateKey::Idx(id)) {
175 ep.ping_timeout(tx_id);
176 }
177 }
178
179 pub(super) fn get_quic_mapped_addr_for_node_key(
180 &self,
181 node_key: NodeId,
182 ) -> Option<QuicMappedAddr> {
183 self.inner
184 .lock()
185 .get(NodeStateKey::NodeId(node_key))
186 .map(|ep| *ep.quic_mapped_addr())
187 }
188
189 pub(super) fn handle_ping(
192 &self,
193 sender: PublicKey,
194 src: SendAddr,
195 tx_id: TransactionId,
196 ) -> PingHandled {
197 self.inner.lock().handle_ping(sender, src, tx_id)
198 }
199
200 pub(super) fn handle_pong(&self, sender: PublicKey, src: &DiscoMessageSource, pong: Pong) {
201 self.inner.lock().handle_pong(sender, src, pong)
202 }
203
204 #[must_use = "actions must be handled"]
205 pub(super) fn handle_call_me_maybe(
206 &self,
207 sender: PublicKey,
208 cm: CallMeMaybe,
209 ) -> Vec<PingAction> {
210 self.inner.lock().handle_call_me_maybe(sender, cm)
211 }
212
213 #[allow(clippy::type_complexity)]
214 pub(super) fn get_send_addrs(
215 &self,
216 addr: QuicMappedAddr,
217 have_ipv6: bool,
218 ) -> Option<(
219 PublicKey,
220 Option<SocketAddr>,
221 Option<RelayUrl>,
222 Vec<PingAction>,
223 )> {
224 let mut inner = self.inner.lock();
225 let ep = inner.get_mut(NodeStateKey::QuicMappedAddr(addr))?;
226 let public_key = *ep.public_key();
227 trace!(dest = %addr, node_id = %public_key.fmt_short(), "dst mapped to NodeId");
228 let (udp_addr, relay_url, msgs) = ep.get_send_addrs(have_ipv6);
229 Some((public_key, udp_addr, relay_url, msgs))
230 }
231
232 pub(super) fn notify_shutdown(&self) {
233 let mut inner = self.inner.lock();
234 for (_, ep) in inner.node_states_mut() {
235 ep.reset();
236 }
237 }
238
239 pub(super) fn reset_node_states(&self) {
240 let mut inner = self.inner.lock();
241 for (_, ep) in inner.node_states_mut() {
242 ep.note_connectivity_change();
243 }
244 }
245
246 pub(super) fn nodes_stayin_alive(&self) -> Vec<PingAction> {
247 let mut inner = self.inner.lock();
248 inner
249 .node_states_mut()
250 .flat_map(|(_idx, node_state)| node_state.stayin_alive())
251 .collect()
252 }
253
254 pub(super) fn list_remote_infos(&self, now: Instant) -> Vec<RemoteInfo> {
256 self.inner.lock().remote_infos_iter(now).collect()
261 }
262
263 pub(super) fn conn_type_stream(&self, node_id: NodeId) -> anyhow::Result<ConnectionTypeStream> {
273 self.inner.lock().conn_type_stream(node_id)
274 }
275
276 pub(super) fn remote_info(&self, node_id: NodeId) -> Option<RemoteInfo> {
278 self.inner.lock().remote_info(node_id)
279 }
280
281 pub(super) fn prune_inactive(&self) {
283 self.inner.lock().prune_inactive();
284 }
285
286 pub(crate) fn on_direct_addr_discovered(&self, discovered: BTreeSet<SocketAddr>) {
287 self.inner.lock().on_direct_addr_discovered(discovered);
288 }
289}
290
291impl NodeMapInner {
292 fn load_from_vec(nodes: Vec<NodeAddr>) -> Self {
294 let mut me = Self::default();
295 for node_addr in nodes {
296 me.add_node_addr(node_addr, Source::Saved);
297 }
298 me
299 }
300
301 #[instrument(skip_all, fields(node = %node_addr.node_id.fmt_short()))]
303 fn add_node_addr(&mut self, node_addr: NodeAddr, source: Source) {
304 let NodeAddr { node_id, info } = node_addr;
305
306 let source0 = source.clone();
307 let node_state = self.get_or_insert_with(NodeStateKey::NodeId(node_id), || Options {
308 node_id,
309 relay_url: info.relay_url.clone(),
310 active: false,
311 source,
312 });
313 node_state.update_from_node_addr(&info, source0);
314 let id = node_state.id();
315 for addr in &info.direct_addresses {
316 self.set_node_state_for_ip_port(*addr, id);
317 }
318 }
319
320 pub(super) fn on_direct_addr_discovered(&mut self, discovered: BTreeSet<SocketAddr>) {
322 for addr in discovered {
323 self.remove_by_ipp(addr.into(), ClearReason::MatchesOurLocalAddr)
324 }
325 }
326
327 fn remove_by_ipp(&mut self, ipp: IpPort, reason: ClearReason) {
329 if let Some(id) = self.by_ip_port.remove(&ipp) {
330 if let Entry::Occupied(mut entry) = self.by_id.entry(id) {
331 let node = entry.get_mut();
332 node.remove_direct_addr(&ipp, reason);
333 if node.direct_addresses().count() == 0 {
334 let node_id = node.public_key();
335 let mapped_addr = node.quic_mapped_addr();
336 self.by_node_key.remove(node_id);
337 self.by_quic_mapped_addr.remove(mapped_addr);
338 debug!(node_id=%node_id.fmt_short(), ?reason, "removing node");
339 entry.remove();
340 }
341 }
342 }
343 }
344
345 fn get_id(&self, id: NodeStateKey) -> Option<usize> {
346 match id {
347 NodeStateKey::Idx(id) => Some(id),
348 NodeStateKey::NodeId(node_key) => self.by_node_key.get(&node_key).copied(),
349 NodeStateKey::QuicMappedAddr(addr) => self.by_quic_mapped_addr.get(&addr).copied(),
350 NodeStateKey::IpPort(ipp) => self.by_ip_port.get(&ipp).copied(),
351 }
352 }
353
354 fn get_mut(&mut self, id: NodeStateKey) -> Option<&mut NodeState> {
355 self.get_id(id).and_then(|id| self.by_id.get_mut(&id))
356 }
357
358 fn get(&self, id: NodeStateKey) -> Option<&NodeState> {
359 self.get_id(id).and_then(|id| self.by_id.get(&id))
360 }
361
362 fn get_or_insert_with(
363 &mut self,
364 id: NodeStateKey,
365 f: impl FnOnce() -> Options,
366 ) -> &mut NodeState {
367 let id = self.get_id(id);
368 match id {
369 None => self.insert_node(f()),
370 Some(id) => self.by_id.get_mut(&id).expect("is not empty"),
371 }
372 }
373
374 fn node_count(&self) -> usize {
376 self.by_id.len()
377 }
378
379 fn receive_udp(&mut self, udp_addr: SocketAddr) -> Option<(NodeId, QuicMappedAddr)> {
381 let ip_port: IpPort = udp_addr.into();
382 let Some(node_state) = self.get_mut(NodeStateKey::IpPort(ip_port)) else {
383 info!(src=%udp_addr, "receive_udp: no node_state found for addr, ignore");
384 return None;
385 };
386 node_state.receive_udp(ip_port, Instant::now());
387 Some((*node_state.public_key(), *node_state.quic_mapped_addr()))
388 }
389
390 #[instrument(skip_all, fields(src = %src.fmt_short()))]
391 fn receive_relay(&mut self, relay_url: &RelayUrl, src: NodeId) -> QuicMappedAddr {
392 let node_state = self.get_or_insert_with(NodeStateKey::NodeId(src), || {
393 trace!("packets from unknown node, insert into node map");
394 Options {
395 node_id: src,
396 relay_url: Some(relay_url.clone()),
397 active: true,
398 source: Source::Relay,
399 }
400 });
401 node_state.receive_relay(relay_url, src, Instant::now());
402 *node_state.quic_mapped_addr()
403 }
404
405 fn node_states(&self) -> impl Iterator<Item = (&usize, &NodeState)> {
406 self.by_id.iter()
407 }
408
409 fn node_states_mut(&mut self) -> impl Iterator<Item = (&usize, &mut NodeState)> {
410 self.by_id.iter_mut()
411 }
412
413 fn remote_infos_iter(&self, now: Instant) -> impl Iterator<Item = RemoteInfo> + '_ {
415 self.node_states().map(move |(_, ep)| ep.info(now))
416 }
417
418 fn remote_info(&self, node_id: NodeId) -> Option<RemoteInfo> {
420 self.get(NodeStateKey::NodeId(node_id))
421 .map(|ep| ep.info(Instant::now()))
422 }
423
424 fn conn_type_stream(&self, node_id: NodeId) -> anyhow::Result<ConnectionTypeStream> {
434 match self.get(NodeStateKey::NodeId(node_id)) {
435 Some(ep) => Ok(ConnectionTypeStream {
436 initial: Some(ep.conn_type()),
437 inner: ep.conn_type_stream(),
438 }),
439 None => anyhow::bail!("No endpoint for {node_id:?} found"),
440 }
441 }
442
443 fn handle_pong(&mut self, sender: NodeId, src: &DiscoMessageSource, pong: Pong) {
444 if let Some(ns) = self.get_mut(NodeStateKey::NodeId(sender)).as_mut() {
445 let insert = ns.handle_pong(&pong, src.into());
446 if let Some((src, key)) = insert {
447 self.set_node_key_for_ip_port(src, &key);
448 }
449 trace!(?insert, "received pong")
450 } else {
451 warn!("received pong: node unknown, ignore")
452 }
453 }
454
455 #[must_use = "actions must be handled"]
456 fn handle_call_me_maybe(&mut self, sender: NodeId, cm: CallMeMaybe) -> Vec<PingAction> {
457 let ns_id = NodeStateKey::NodeId(sender);
458 if let Some(id) = self.get_id(ns_id.clone()) {
459 for number in &cm.my_numbers {
460 self.set_node_state_for_ip_port(*number, id);
462 }
463 }
464 match self.get_mut(ns_id) {
465 None => {
466 inc!(MagicsockMetrics, recv_disco_call_me_maybe_bad_disco);
467 debug!("received call-me-maybe: ignore, node is unknown");
468 vec![]
469 }
470 Some(ns) => {
471 debug!(endpoints = ?cm.my_numbers, "received call-me-maybe");
472
473 ns.handle_call_me_maybe(cm)
474 }
475 }
476 }
477
478 fn handle_ping(&mut self, sender: NodeId, src: SendAddr, tx_id: TransactionId) -> PingHandled {
479 let node_state = self.get_or_insert_with(NodeStateKey::NodeId(sender), || {
480 debug!("received ping: node unknown, add to node map");
481 let source = if src.is_relay() {
482 Source::Relay
483 } else {
484 Source::Udp
485 };
486 Options {
487 node_id: sender,
488 relay_url: src.relay_url(),
489 active: true,
490 source,
491 }
492 });
493
494 let handled = node_state.handle_ping(src.clone(), tx_id);
495 if let SendAddr::Udp(ref addr) = src {
496 if matches!(handled.role, PingRole::NewPath) {
497 self.set_node_key_for_ip_port(*addr, &sender);
498 }
499 }
500 handled
501 }
502
503 fn insert_node(&mut self, options: Options) -> &mut NodeState {
505 info!(
506 node = %options.node_id.fmt_short(),
507 relay_url = ?options.relay_url,
508 source = %options.source,
509 "inserting new node in NodeMap",
510 );
511 let id = self.next_id;
512 self.next_id = self.next_id.wrapping_add(1);
513 let node_state = NodeState::new(id, options);
514
515 self.by_quic_mapped_addr
517 .insert(*node_state.quic_mapped_addr(), id);
518 self.by_node_key.insert(*node_state.public_key(), id);
519
520 self.by_id.insert(id, node_state);
521 self.by_id.get_mut(&id).expect("just inserted")
522 }
523
524 fn set_node_key_for_ip_port(&mut self, ipp: impl Into<IpPort>, nk: &PublicKey) {
530 let ipp = ipp.into();
531 if let Some(id) = self.by_ip_port.get(&ipp) {
532 if !self.by_node_key.contains_key(nk) {
533 self.by_node_key.insert(*nk, *id);
534 }
535 self.by_ip_port.remove(&ipp);
536 }
537 if let Some(id) = self.by_node_key.get(nk) {
538 trace!("insert ip -> id: {:?} -> {}", ipp, id);
539 self.by_ip_port.insert(ipp, *id);
540 }
541 }
542
543 fn set_node_state_for_ip_port(&mut self, ipp: impl Into<IpPort>, id: usize) {
544 let ipp = ipp.into();
545 trace!(?ipp, ?id, "set endpoint for ip:port");
546 self.by_ip_port.insert(ipp, id);
547 }
548
549 fn prune_inactive(&mut self) {
551 let now = Instant::now();
552 let mut prune_candidates: Vec<_> = self
553 .by_id
554 .values()
555 .filter(|node| !node.is_active(&now))
556 .map(|node| (*node.public_key(), node.last_used()))
557 .collect();
558
559 let prune_count = prune_candidates.len().saturating_sub(MAX_INACTIVE_NODES);
560 if prune_count == 0 {
561 return;
563 }
564
565 prune_candidates.sort_unstable_by_key(|(_pk, last_used)| *last_used);
566 prune_candidates.truncate(prune_count);
567 for (public_key, last_used) in prune_candidates.into_iter() {
568 let node = public_key.fmt_short();
569 match last_used.map(|instant| instant.elapsed()) {
570 Some(last_used) => trace!(%node, ?last_used, "pruning inactive"),
571 None => trace!(%node, last_used=%"never", "pruning inactive"),
572 }
573
574 let Some(id) = self.by_node_key.remove(&public_key) else {
575 debug_assert!(false, "missing by_node_key entry for pk in by_id");
576 continue;
577 };
578
579 let Some(ep) = self.by_id.remove(&id) else {
580 debug_assert!(false, "missing by_id entry for id in by_node_key");
581 continue;
582 };
583
584 for ip_port in ep.direct_addresses() {
585 self.by_ip_port.remove(&ip_port);
586 }
587
588 self.by_quic_mapped_addr.remove(ep.quic_mapped_addr());
589 }
590 }
591}
592
593#[derive(Debug)]
595pub struct ConnectionTypeStream {
596 initial: Option<ConnectionType>,
597 inner: watchable::WatcherStream<ConnectionType>,
598}
599
600impl Stream for ConnectionTypeStream {
601 type Item = ConnectionType;
602
603 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
604 let this = &mut *self;
605 if let Some(initial_conn_type) = this.initial.take() {
606 return Poll::Ready(Some(initial_conn_type));
607 }
608 Pin::new(&mut this.inner).poll_next(cx)
609 }
610}
611
612#[derive(Debug, derive_more::Display, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
617#[display("{}", SocketAddr::from(*self))]
618pub struct IpPort {
619 ip: IpAddr,
620 port: u16,
621}
622
623impl From<SocketAddr> for IpPort {
624 fn from(socket_addr: SocketAddr) -> Self {
625 Self {
626 ip: socket_addr.ip(),
627 port: socket_addr.port(),
628 }
629 }
630}
631
632impl From<IpPort> for SocketAddr {
633 fn from(ip_port: IpPort) -> Self {
634 let IpPort { ip, port } = ip_port;
635 (ip, port).into()
636 }
637}
638
639impl IpPort {
640 pub fn ip(&self) -> &IpAddr {
641 &self.ip
642 }
643
644 pub fn port(&self) -> u16 {
645 self.port
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use std::net::Ipv4Addr;
652
653 use super::{node_state::MAX_INACTIVE_DIRECT_ADDRESSES, *};
654 use crate::key::SecretKey;
655
656 impl NodeMap {
657 #[track_caller]
658 fn add_test_addr(&self, node_addr: NodeAddr) {
659 self.add_node_addr(
660 node_addr,
661 Source::NamedApp {
662 name: "test".into(),
663 },
664 )
665 }
666 }
667
668 #[tokio::test]
670 async fn restore_from_vec() {
671 let _guard = iroh_test::logging::setup();
672
673 let node_map = NodeMap::default();
674
675 let node_a = SecretKey::generate().public();
676 let node_b = SecretKey::generate().public();
677 let node_c = SecretKey::generate().public();
678 let node_d = SecretKey::generate().public();
679
680 let relay_x: RelayUrl = "https://my-relay-1.com".parse().unwrap();
681 let relay_y: RelayUrl = "https://my-relay-2.com".parse().unwrap();
682
683 let direct_addresses_a = [addr(4000), addr(4001)];
684 let direct_addresses_c = [addr(5000)];
685
686 let node_addr_a = NodeAddr::new(node_a)
687 .with_relay_url(relay_x)
688 .with_direct_addresses(direct_addresses_a);
689 let node_addr_b = NodeAddr::new(node_b).with_relay_url(relay_y);
690 let node_addr_c = NodeAddr::new(node_c).with_direct_addresses(direct_addresses_c);
691 let node_addr_d = NodeAddr::new(node_d);
692
693 node_map.add_test_addr(node_addr_a);
694 node_map.add_test_addr(node_addr_b);
695 node_map.add_test_addr(node_addr_c);
696 node_map.add_test_addr(node_addr_d);
697
698 let mut addrs: Vec<NodeAddr> = node_map
699 .list_remote_infos(Instant::now())
700 .into_iter()
701 .filter_map(|info| {
702 let addr: NodeAddr = info.into();
703 if addr.info.is_empty() {
704 return None;
705 }
706 Some(addr)
707 })
708 .collect();
709 let loaded_node_map = NodeMap::load_from_vec(addrs.clone());
710
711 let mut loaded: Vec<NodeAddr> = loaded_node_map
712 .list_remote_infos(Instant::now())
713 .into_iter()
714 .filter_map(|info| {
715 let addr: NodeAddr = info.into();
716 if addr.info.is_empty() {
717 return None;
718 }
719 Some(addr)
720 })
721 .collect();
722
723 loaded.sort_unstable();
724 addrs.sort_unstable();
725
726 assert_eq!(addrs, loaded);
728 }
729
730 fn addr(port: u16) -> SocketAddr {
731 (std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), port).into()
732 }
733
734 #[test]
735 fn test_prune_direct_addresses() {
736 let _guard = iroh_test::logging::setup();
737
738 let node_map = NodeMap::default();
739 let public_key = SecretKey::generate().public();
740 let id = node_map
741 .inner
742 .lock()
743 .insert_node(Options {
744 node_id: public_key,
745 relay_url: None,
746 active: false,
747 source: Source::NamedApp {
748 name: "test".into(),
749 },
750 })
751 .id();
752
753 const LOCALHOST: IpAddr = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
754
755 info!("Adding active addresses");
759 for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES {
760 let addr = SocketAddr::new(LOCALHOST, 5000 + i as u16);
761 let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]);
762 node_map.add_test_addr(node_addr);
764 node_map.inner.lock().receive_udp(addr);
766 }
767
768 info!("Adding offline/inactive addresses");
769 for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES * 2 {
770 let addr = SocketAddr::new(LOCALHOST, 6000 + i as u16);
771 let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]);
772 node_map.add_test_addr(node_addr);
773 }
774
775 let mut node_map_inner = node_map.inner.lock();
776 let endpoint = node_map_inner.by_id.get_mut(&id).unwrap();
777
778 info!("Adding alive addresses");
779 for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES {
780 let addr = SendAddr::Udp(SocketAddr::new(LOCALHOST, 7000 + i as u16));
781 let txid = stun::TransactionId::from([i as u8; 12]);
782 endpoint.handle_ping(addr, txid);
785 }
786
787 info!("Pruning addresses");
788 endpoint.prune_direct_addresses();
789
790 assert_eq!(
793 endpoint.direct_addresses().count(),
794 MAX_INACTIVE_DIRECT_ADDRESSES * 3
795 );
796
797 assert_eq!(
799 endpoint
800 .direct_address_states()
801 .filter(|(_addr, state)| !state.is_active())
802 .count(),
803 MAX_INACTIVE_DIRECT_ADDRESSES * 2
804 )
805 }
806
807 #[test]
808 fn test_prune_inactive() {
809 let node_map = NodeMap::default();
810 let active_node = SecretKey::generate().public();
812 let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 167);
813 node_map.add_test_addr(NodeAddr::new(active_node).with_direct_addresses([addr]));
814 node_map.inner.lock().receive_udp(addr).expect("registered");
815
816 for _ in 0..MAX_INACTIVE_NODES + 1 {
817 let node = SecretKey::generate().public();
818 node_map.add_test_addr(NodeAddr::new(node));
819 }
820
821 assert_eq!(node_map.node_count(), MAX_INACTIVE_NODES + 2);
822 node_map.prune_inactive();
823 assert_eq!(node_map.node_count(), MAX_INACTIVE_NODES + 1);
824 node_map
825 .inner
826 .lock()
827 .get(NodeStateKey::NodeId(active_node))
828 .expect("should not be pruned");
829 }
830}