1use std::{
2 collections::HashMap,
3 fmt::{Debug, Formatter},
4 hash::Hash,
5 net::IpAddr,
6};
7
8use ts_bart::{RouteModification, RoutingTable, RoutingTableExt};
9use ts_control::{Node, StableNodeId};
10use ts_keys::{DiscoPublicKey, NodePublicKey};
11use ts_transport::PeerId;
12
13mod private {
14 use super::*;
15
16 pub trait Sealed {}
17
18 impl Sealed for PeerId {}
19 impl Sealed for NodePublicKey {}
20 impl Sealed for DiscoPublicKey {}
21 impl Sealed for StableNodeId {}
22 impl Sealed for ts_control::NodeId {}
23 impl Sealed for PeerName {}
24 impl Sealed for &str {}
25 impl Sealed for IpAddr {}
26 impl Sealed for ipnet::IpNet {}
27}
28
29pub trait IndexedField: Debug + private::Sealed {
31 fn lookup(&self, db: &PeerDb) -> Option<PeerId>;
33}
34
35type Index<T> = HashMap<T, PeerId>;
36type PeerName = String;
37
38fn canon_name(name: &str) -> String {
45 name.strip_suffix('.').unwrap_or(name).to_ascii_lowercase()
46}
47
48#[derive(Default, Clone)]
56pub struct PeerDb {
57 peers: HashMap<PeerId, Node>,
58 index_state: IndexState,
59 next_id: u32,
60}
61
62impl Debug for PeerDb {
63 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
64 self.peers.fmt(f)
65 }
66}
67
68#[derive(Default, Clone)]
69struct IndexState {
70 nk_idx: Index<NodePublicKey>,
72 disco_idx: Index<DiscoPublicKey>,
74 stableid_idx: Index<StableNodeId>,
76 control_idx: Index<ts_control::NodeId>,
83 name_idx: Index<PeerName>,
85 ip_idx: ts_bart::Table<PeerId>,
87 route_idx: ts_bart::Table<smallvec::SmallVec<[PeerId; 2]>>,
92}
93
94impl PeerDb {
95 pub fn upsert(&mut self, new: &Node) -> PeerId {
99 let id = self
100 .index_state
101 .stableid_idx
102 .get(&new.stable_id)
103 .copied()
104 .unwrap_or_else(|| {
105 let id = self.next_id;
106 self.next_id += 1;
107
108 PeerId(id)
109 });
110
111 let old = self.peers.get(&id);
112
113 if old.is_some_and(|x| x == new) {
115 return id;
116 }
117
118 maybe_update_idx(new, old, |x| &x.node_key, &mut self.index_state.nk_idx, id);
119 maybe_update_idx(
120 new,
121 old,
122 |x| &x.stable_id,
123 &mut self.index_state.stableid_idx,
124 id,
125 );
126 maybe_update_idx(new, old, |x| &x.id, &mut self.index_state.control_idx, id);
127
128 maybe_update(
129 new,
130 old,
131 |x| &x.disco_key,
132 &mut self.index_state.disco_idx,
133 |old, idx| {
134 if let Some(key) = &old.disco_key {
135 if idx.get(key).is_some_and(|&x| x == id) {
140 idx.remove(key);
141 }
142 }
143 },
144 |new, idx| {
145 if let Some(key) = &new.disco_key {
146 idx.insert(*key, id);
147 }
148 },
149 );
150
151 maybe_update(
164 new,
165 old,
166 |x| (&x.hostname, &x.tailnet),
167 &mut self.index_state.name_idx,
168 |old, idx| {
169 let old_hostname = canon_name(&old.hostname);
170 if idx.get(&old_hostname).is_some_and(|&x| x == id) {
171 idx.remove(&old_hostname);
172 }
173
174 if let Some(fqdn) = old.fqdn_opt(false) {
175 let k = canon_name(&fqdn);
180 if idx.get(&k).is_some_and(|&x| x == id) {
181 idx.remove(&k);
182 }
183 }
184 },
185 |new, idx| {
186 idx.insert(canon_name(&new.hostname), id);
187
188 if let Some(fqdn) = new.fqdn_opt(false) {
189 idx.insert(canon_name(&fqdn), id);
190 }
191 },
192 );
193
194 maybe_update(
195 new,
196 old,
197 |x| &x.tailnet_address,
198 &mut self.index_state.ip_idx,
199 |old, idx| {
200 let ipv4: ipnet::IpNet = old.tailnet_address.ipv4.into();
204 let ipv6: ipnet::IpNet = old.tailnet_address.ipv6.into();
205
206 if idx.lookup_prefix_exact(ipv4).is_some_and(|&x| x == id) {
207 idx.remove(ipv4);
208 }
209 if idx.lookup_prefix_exact(ipv6).is_some_and(|&x| x == id) {
210 idx.remove(ipv6);
211 }
212 },
213 |new, idx| {
214 idx.insert(new.tailnet_address.ipv4.into(), id);
215 idx.insert(new.tailnet_address.ipv6.into(), id);
216 },
217 );
218
219 maybe_update(
220 new,
221 old,
222 |x| &x.accepted_routes,
223 &mut self.index_state,
224 |old, idx| {
225 for &route in &old.accepted_routes {
226 idx.remove_route(route, id);
227 }
228 },
229 |new, idx| {
230 for &route in &new.accepted_routes {
231 idx.route_idx.modify(route, |val| {
232 if let Some(val) = val {
233 val.push(id);
234 return RouteModification::Noop;
235 }
236
237 RouteModification::Insert(smallvec::smallvec![id])
238 });
239 }
240 },
241 );
242
243 self.peers.insert(id, new.clone());
244
245 id
246 }
247
248 pub fn remove(&mut self, field: &dyn IndexedField) -> Option<(PeerId, Node)> {
250 let id = field.lookup(self)?;
251
252 let node = self.peers.remove(&id)?;
253 self.index_state.remove(id, &node);
254
255 Some((id, node))
256 }
257
258 pub fn get(&self, field: &dyn IndexedField) -> Option<(PeerId, &Node)> {
260 let id = field.lookup(self)?;
261 let peer = self.peers.get(&id)?;
262
263 Some((id, peer))
264 }
265
266 pub fn get_route(&self, route: ipnet::IpNet) -> impl Iterator<Item = (PeerId, &Node)> {
268 self.index_state
271 .route_idx
272 .lookup_prefix(route)
273 .into_iter()
274 .flat_map(|x| x.iter())
275 .map(|&id| (id, self.peers.get(&id).unwrap()))
276 }
277
278 pub fn has(&self, field: &dyn IndexedField) -> Option<PeerId> {
280 field.lookup(self)
281 }
282
283 pub const fn peers(&self) -> &HashMap<PeerId, Node> {
285 &self.peers
286 }
287
288 pub fn retain(&mut self, mut predicate: impl FnMut(PeerId, &Node) -> bool) {
290 self.peers.retain(|&id, node| {
291 let retain = predicate(id, node);
292
293 if !retain {
294 self.index_state.remove(id, node);
295 }
296
297 retain
298 });
299 }
300}
301
302impl IndexState {
303 fn remove(&mut self, id: PeerId, node: &Node) {
304 self.nk_idx.remove(&node.node_key);
305 self.stableid_idx.remove(&node.stable_id);
306 self.control_idx.remove(&node.id);
307 self.ip_idx.remove(node.tailnet_address.ipv4.into());
308 self.ip_idx.remove(node.tailnet_address.ipv6.into());
309
310 let hostname = canon_name(&node.hostname);
311 if self.name_idx.get(&hostname).is_some_and(|&x| x == id) {
312 self.name_idx.remove(&hostname);
313 }
314
315 if let Some(fqdn) = node.fqdn_opt(false) {
316 self.name_idx.remove(&canon_name(&fqdn));
317 }
318
319 for route in &node.accepted_routes {
320 self.remove_route(*route, id);
321 }
322
323 if let Some(disco) = &node.disco_key {
324 self.disco_idx.remove(disco);
325 }
326 }
327
328 fn remove_route(&mut self, route: ipnet::IpNet, id: PeerId) {
330 self.route_idx.modify(route, |val| match val {
331 Some(val) => {
332 let mut some_matched = false;
333
334 val.retain(|&mut x| {
335 let ids_match = x == id;
336 if ids_match {
337 some_matched = true;
338 }
339
340 !ids_match
341 });
342
343 assert!(some_matched);
344
345 if val.is_empty() {
346 RouteModification::Remove
347 } else {
348 RouteModification::Noop
349 }
350 }
351 None => RouteModification::Noop,
352 });
353 }
354
355 #[cfg(test)]
356 fn is_empty(&self) -> bool {
357 self.nk_idx.is_empty()
358 && self.stableid_idx.is_empty()
359 && self.control_idx.is_empty()
360 && self.ip_idx.size() == 0
361 && self.name_idx.is_empty()
362 && self.route_idx.size() == 0
363 && self.disco_idx.is_empty()
364 }
365}
366
367fn maybe_update<'n, T, Idx>(
377 new: &'n Node,
378 old: Option<&'n Node>,
379 accessor: impl Fn(&'n Node) -> T,
380 idx: &mut Idx,
381 mut remove: impl FnMut(&'n Node, &mut Idx),
382 mut insert: impl FnMut(&'n Node, &mut Idx),
383) where
384 T: PartialEq + 'n,
385{
386 match old {
387 Some(old) if accessor(old) == accessor(new) => {
388 return;
389 }
390 Some(x) => {
391 remove(x, idx);
392 }
393 None => {}
394 }
395
396 insert(new, idx)
397}
398
399fn maybe_update_idx<T>(
401 new: &Node,
402 old: Option<&Node>,
403 accessor: impl Fn(&Node) -> &T,
404 idx: &mut Index<T>,
405 new_id: PeerId,
406) where
407 T: Eq + Hash + Clone,
408{
409 maybe_update(
410 new,
411 old,
412 &accessor,
413 idx,
414 |old, idx| {
415 if idx.get(accessor(old)).is_some_and(|&x| x == new_id) {
419 idx.remove(accessor(old));
420 }
421 },
422 |new, idx| {
423 idx.insert(accessor(new).clone(), new_id);
424 },
425 )
426}
427
428impl IndexedField for PeerId {
429 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
430 if db.peers.contains_key(self) {
431 Some(*self)
432 } else {
433 None
434 }
435 }
436}
437
438impl IndexedField for NodePublicKey {
439 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
440 db.index_state.nk_idx.get(self).copied()
441 }
442}
443
444impl IndexedField for DiscoPublicKey {
445 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
446 db.index_state.disco_idx.get(self).copied()
447 }
448}
449
450impl IndexedField for StableNodeId {
451 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
452 db.index_state.stableid_idx.get(self).copied()
453 }
454}
455
456impl IndexedField for ts_control::NodeId {
457 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
458 db.index_state.control_idx.get(self).copied()
459 }
460}
461
462impl IndexedField for PeerName {
463 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
464 db.index_state.name_idx.get(&canon_name(self)).copied()
465 }
466}
467
468impl IndexedField for &str {
469 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
470 db.index_state.name_idx.get(&canon_name(self)).copied()
471 }
472}
473
474impl IndexedField for IpAddr {
475 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
476 db.index_state.ip_idx.lookup(*self).copied()
477 }
478}
479
480#[cfg(test)]
481mod test {
482 use std::{
483 collections::{HashMap, HashSet},
484 net::{Ipv4Addr, Ipv6Addr, SocketAddr},
485 num::NonZeroU32,
486 };
487
488 use proptest::{
489 collection::{hash_set, vec},
490 prelude::any,
491 strategy::Strategy,
492 };
493 use rand::{
494 RngExt,
495 distr::{Alphanumeric, SampleString},
496 };
497 use ts_control::TailnetAddress;
498
499 use super::*;
500
501 fn rand_string(rng: &mut dyn rand::Rng, max_len: usize) -> String {
502 let len = rng.random_range(1..max_len);
503 Alphanumeric.sample_string(rng, len)
504 }
505
506 fn rand_route(rng: &mut dyn rand::Rng) -> ipnet::IpNet {
507 if rng.random::<bool>() {
508 let ip = rand_ipv4(rng);
509 ipnet::Ipv4Net::new(ip, rand::random_range(0..=32))
510 .unwrap()
511 .trunc()
512 .into()
513 } else {
514 let ip = rand_ipv6(rng);
515 ipnet::Ipv6Net::new(ip, rand::random_range(0..=128))
516 .unwrap()
517 .trunc()
518 .into()
519 }
520 }
521
522 fn rand_ipv4(rng: &mut dyn rand::Rng) -> Ipv4Addr {
523 Ipv4Addr::from_octets(rng.random::<[u8; 4]>())
524 }
525
526 fn rand_ipv6(rng: &mut dyn rand::Rng) -> Ipv6Addr {
527 Ipv6Addr::from_segments(rng.random::<[u16; 8]>())
528 }
529
530 fn rand_node() -> Node {
531 let mut rng = rand::rng();
532 let tailnet_address = TailnetAddress {
533 ipv4: rand_ipv4(&mut rng).into(),
534 ipv6: rand_ipv6(&mut rng).into(),
535 };
536
537 Node {
538 stable_id: StableNodeId(rand_string(&mut rng, 32)),
539 addresses: vec![tailnet_address.ipv4.into(), tailnet_address.ipv6.into()],
540 tailnet_address,
541 node_key: rng.random::<[u8; 32]>().into(),
542 key_signature: vec![],
543 disco_key: rng
544 .random::<bool>()
545 .then_some(rng.random::<[u8; 32]>().into()),
546 machine_key: rng
547 .random::<bool>()
548 .then_some(rng.random::<[u8; 32]>().into()),
549 id: rng.random(),
550 accepted_routes: (0..rng.random_range(0..32))
551 .map(|_| rand_route(&mut rng))
552 .collect(),
553
554 hostname: rand_string(&mut rng, 32),
555 user_id: rng.random(),
556 tailnet: rng.random::<bool>().then_some(rand_string(&mut rng, 32)),
557
558 node_key_expiry: None,
559 online: None,
560 last_seen: None,
561 underlay_addresses: vec![],
562 derp_region: rng
563 .random::<bool>()
564 .then_some(ts_derp::RegionId(rng.random())),
565
566 tags: (0..rng.random_range(0..8))
567 .map(|_| rand_string(&mut rng, 32))
568 .collect(),
569
570 cap: Default::default(),
571 cap_map: Default::default(),
572 peerapi_port: None,
573 peerapi_dns_proxy: false,
574 is_wireguard_only: false,
575 exit_node_dns_resolvers: vec![],
576 peer_relay: false,
577 ssh_host_keys: vec![],
578 service_vips: Default::default(),
579 unsigned_peer_api_only: false,
580 }
581 }
582
583 fn validate_indices(db: &PeerDb, node: &Node, id: PeerId) {
584 let ipv4 = IpAddr::from(node.tailnet_address.ipv4.addr());
585 let ipv6 = IpAddr::from(node.tailnet_address.ipv6.addr());
586 let fqdn = node.fqdn_opt(false);
587
588 let mut keys: Vec<&dyn IndexedField> =
589 vec![&id, &node.node_key, &node.stable_id, &node.id, &ipv4, &ipv6];
590
591 if let Some(disco) = &node.disco_key {
592 keys.push(disco);
593 }
594
595 if let Some(fqdn) = &fqdn {
596 keys.push(fqdn);
597 }
598
599 for k in keys {
600 let lookup_id = k.lookup(db).unwrap();
601 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
602
603 let (lookup_id, lookup_node) = db.get(k).unwrap();
604 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
605 assert_eq!(lookup_node, node, "wrong node for key {k:?}");
606 }
607
608 node.hostname.lookup(db).unwrap();
610
611 for &route in &node.accepted_routes {
612 let routes = db.get_route(route).collect::<Vec<_>>();
617 assert!(!routes.is_empty());
618
619 for (found_id, found_node) in routes {
620 if found_id == id {
621 assert_eq!(found_node, node);
622 break;
623 }
624
625 let has_subset = found_node
626 .accepted_routes
627 .iter()
628 .any(|found_route| route.contains(found_route));
629
630 assert!(has_subset);
631 }
632 }
633 }
634
635 fn assert_has_routes_exact(db: &PeerDb, node: &Node, id: PeerId) {
638 for &route in &node.accepted_routes {
639 let match_exists = db
640 .get_route(route)
641 .any(|(found_id, found_node)| found_id == id && found_node == node);
642
643 assert!(match_exists);
644 }
645 }
646
647 #[test]
648 fn test_indices() {
649 let mut db = PeerDb::default();
650 let node = rand_node();
651 let id = db.upsert(&node);
652
653 validate_indices(&db, &node, id);
654 assert_has_routes_exact(&db, &node, id);
655 }
656
657 #[test]
658 fn test_names() {
659 let mut db = PeerDb::default();
660
661 let node1 = Node {
662 hostname: "test".to_string(),
663 tailnet: Some("ts.net".to_string()),
664 ..rand_node()
665 };
666 let node2 = Node {
667 hostname: "test".to_string(),
668 tailnet: Some("ts2.net".to_string()),
669 ..rand_node()
670 };
671 let node3 = Node {
672 hostname: "test".to_string(),
673 tailnet: None,
674 ..rand_node()
675 };
676
677 let id1 = db.upsert(&node1);
678 let id2 = db.upsert(&node2);
679 let id3 = db.upsert(&node3);
680
681 let nodes = [(id1, &node1), (id2, &node2), (id3, &node3)];
682
683 for (id, node) in &nodes {
684 validate_indices(&db, node, *id);
685 }
686
687 let (id, node) = db.get(&"test").unwrap();
688 assert!(nodes.iter().any(|(x, _node)| *x == id));
689
690 for &(x, curnode) in &nodes {
691 if x == id {
692 assert_eq!(node, curnode);
693 } else {
694 assert_ne!(node, curnode);
695 }
696 }
697
698 let (id, node) = db.get(&"test.ts.net").unwrap();
699 assert_eq!(id, id1);
700 assert_eq!(node, &node1);
701
702 let (id, node) = db.get(&"test.ts2.net").unwrap();
703 assert_eq!(id, id2);
704 assert_eq!(node, &node2);
705 }
706
707 #[test]
708 fn test_name_lookup_is_canonicalized() {
709 let mut db = PeerDb::default();
712
713 let node = Node {
714 hostname: "MixedCase".to_string(),
715 tailnet: Some("Tail-Scale.ts.net".to_string()),
716 ..rand_node()
717 };
718 let id = db.upsert(&node);
719
720 assert_eq!(db.get(&"mixedcase").unwrap().0, id);
722 assert_eq!(db.get(&"MIXEDCASE").unwrap().0, id);
723
724 assert_eq!(db.get(&"mixedcase.tail-scale.ts.net").unwrap().0, id);
726 assert_eq!(db.get(&"MixedCase.Tail-Scale.TS.NET").unwrap().0, id);
727 assert_eq!(db.get(&"mixedcase.tail-scale.ts.net.").unwrap().0, id);
728
729 db.remove(&id);
731 assert!(db.get(&"mixedcase").is_none());
732 assert!(db.get(&"mixedcase.tail-scale.ts.net").is_none());
733 assert!(db.index_state.is_empty());
734 }
735
736 #[test]
737 fn disco_key_reassigned_across_peers_no_panic() {
738 let mut db = PeerDb::default();
742
743 let disco: DiscoPublicKey = [7u8; 32].into();
744
745 let node_a = Node {
746 disco_key: Some(disco),
747 ..rand_node()
748 };
749 let id_a = db.upsert(&node_a);
750
751 let node_b = Node {
753 disco_key: Some(disco),
754 ..rand_node()
755 };
756 let id_b = db.upsert(&node_b);
757 assert_ne!(id_a, id_b);
758
759 let node_a2 = Node {
762 disco_key: None,
763 ..node_a.clone()
764 };
765 let id_a2 = db.upsert(&node_a2);
766 assert_eq!(id_a, id_a2);
767
768 assert_eq!(disco.lookup(&db), Some(id_b));
770 }
771
772 #[test]
773 fn ip_reassigned_across_peers_no_panic() {
774 let mut db = PeerDb::default();
777
778 let shared = TailnetAddress {
779 ipv4: Ipv4Addr::new(100, 64, 0, 1).into(),
780 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 1).into(),
781 };
782
783 let node_a = Node {
784 addresses: vec![shared.ipv4.into(), shared.ipv6.into()],
785 tailnet_address: shared.clone(),
786 ..rand_node()
787 };
788 let id_a = db.upsert(&node_a);
789
790 let node_b = Node {
792 addresses: vec![shared.ipv4.into(), shared.ipv6.into()],
793 tailnet_address: shared.clone(),
794 ..rand_node()
795 };
796 let id_b = db.upsert(&node_b);
797 assert_ne!(id_a, id_b);
798
799 let renumbered = TailnetAddress {
802 ipv4: Ipv4Addr::new(100, 64, 0, 2).into(),
803 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 2).into(),
804 };
805 let node_a2 = Node {
806 addresses: vec![renumbered.ipv4.into(), renumbered.ipv6.into()],
807 tailnet_address: renumbered,
808 ..node_a.clone()
809 };
810 let id_a2 = db.upsert(&node_a2);
811 assert_eq!(id_a, id_a2);
812
813 assert_eq!(
815 IpAddr::from(Ipv4Addr::new(100, 64, 0, 1)).lookup(&db),
816 Some(id_b)
817 );
818 assert_eq!(
820 IpAddr::from(Ipv4Addr::new(100, 64, 0, 2)).lookup(&db),
821 Some(id_a)
822 );
823 }
824
825 #[test]
826 fn node_key_or_stableid_churn_no_panic() {
827 let mut db = PeerDb::default();
831
832 let key: NodePublicKey = [9u8; 32].into();
833
834 let node_a = Node {
835 node_key: key,
836 ..rand_node()
837 };
838 let id_a = db.upsert(&node_a);
839
840 let node_b = Node {
842 node_key: key,
843 ..rand_node()
844 };
845 let id_b = db.upsert(&node_b);
846 assert_ne!(id_a, id_b);
847
848 let node_a2 = Node {
851 node_key: [10u8; 32].into(),
852 ..node_a.clone()
853 };
854 let id_a2 = db.upsert(&node_a2);
855 assert_eq!(id_a, id_a2);
856
857 assert_eq!(key.lookup(&db), Some(id_b));
859 assert_eq!(NodePublicKey::from([10u8; 32]).lookup(&db), Some(id_a));
860 }
861
862 proptest::prop_compose! {
863 fn ipv4net()(
864 addr: Ipv4Addr,
865 pfx in 0u8..=32,
866 ) -> ipnet::Ipv4Net {
867 ipnet::Ipv4Net::new(addr, pfx).unwrap().trunc()
868 }
869 }
870
871 proptest::prop_compose! {
872 fn ipv6net()(
873 addr: Ipv6Addr,
874 pfx in 0u8..=32,
875 ) -> ipnet::Ipv6Net {
876 ipnet::Ipv6Net::new(addr, pfx).unwrap().trunc()
877 }
878 }
879
880 fn ipnet() -> impl Strategy<Value = ipnet::IpNet> {
881 proptest::prop_oneof![
882 ipv4net().prop_map(ipnet::IpNet::from),
883 ipv6net().prop_map(ipnet::IpNet::from)
884 ]
885 }
886
887 proptest::prop_compose! {
888 fn domain_segment()(
892 seg in "[a-z][a-z0-9]*"
893 ) -> String {
894 seg
895 }
896 }
897
898 proptest::prop_compose! {
899 fn domain(max_count: usize)(
900 segs in proptest::collection::vec(domain_segment(), 0..max_count)
901 ) -> String {
902 segs.join(".")
903 }
904 }
905
906 type Key = [u8; 32];
907
908 proptest::prop_compose! {
909 fn nodes(n: usize)(
912 id in hash_set(any::<i64>(), n),
913 stable_id in hash_set(".+", n),
914 tags in vec(hash_set(".+", 0..32), n),
915 accepted_routes in vec(hash_set(ipnet(), 0..32), n),
916 node_key in hash_set(any::<Key>(), n),
917 machine_key in vec(any::<Option<Key>>(), n),
918 disco_key in vec(any::<Option<Key>>(), n),
919 ipv4 in hash_set(any::<Ipv4Addr>(), n),
920 ipv6 in hash_set(any::<Ipv6Addr>(), n),
921 name in hash_set(domain_segment(), n),
922 tailnet in vec(domain(5), n),
923 has_tailnet in vec(any::<bool>(), n),
924 derp_region in vec(any::<Option<NonZeroU32>>(), n),
925 underlay_addrs in vec(any::<HashSet<SocketAddr>>(), n),
926 ) -> Vec<Node> {
927 itertools::izip![
928 id,
929 stable_id,
930 tags,
931 accepted_routes,
932 node_key,
933 machine_key,
934 disco_key,
935 ipv4,
936 ipv6,
937 name,
938 tailnet,
939 has_tailnet,
940 derp_region,
941 underlay_addrs,
942 ].map(|(
943 id,
944 stable_id,
945 tags,
946 mut accepted_routes,
947 node_key,
948 machine_key,
949 disco_key,
950 ipv4,
951 ipv6,
952 name,
953 tailnet,
954 has_tailnet,
955 derp_region,
956 underlay_addrs,
957 )| {
958 accepted_routes.insert(ipnet::Ipv4Net::from(ipv4).into());
959 accepted_routes.insert(ipnet::Ipv6Net::from(ipv6).into());
960
961 Node {
962 id,
963 stable_id: StableNodeId(stable_id),
964
965 hostname: name,
966 user_id: 0,
967 tailnet: has_tailnet.then_some(tailnet),
968
969 node_key: node_key.into(),
970 key_signature: vec![],
971 disco_key: disco_key.map(Into::into),
972 machine_key: machine_key.map(Into::into),
973
974 node_key_expiry: None,
975 online: None,
976 last_seen: None,
977
978 addresses: vec![
979 ipnet::IpNet::V4(ipv4.into()),
980 ipnet::IpNet::V6(ipv6.into()),
981 ],
982 tailnet_address: TailnetAddress {
983 ipv4: ipv4.into(),
984 ipv6: ipv6.into(),
985 },
986 tags: tags.into_iter().collect(),
987
988 derp_region: derp_region.map(ts_derp::RegionId),
989
990 accepted_routes: accepted_routes.into_iter().collect(),
991 underlay_addresses: underlay_addrs.into_iter().collect(),
992
993 cap: Default::default(),
994 cap_map: Default::default(),
995 peerapi_port: None,
996 peerapi_dns_proxy: false,
997 is_wireguard_only: false,
998 exit_node_dns_resolvers: vec![],
999 peer_relay: false,
1000 ssh_host_keys: vec![],
1001 service_vips: Default::default(),
1002 unsigned_peer_api_only: false,
1003 }
1004 })
1005 .collect()
1006 }
1007 }
1008
1009 proptest::proptest! {
1010 #[test]
1011 fn prop_one_node_indices(mut nodes in nodes(1)) {
1012 let node = nodes.pop().unwrap();
1013
1014 let mut db = PeerDb::default();
1015 let id = db.upsert(&node);
1016
1017 validate_indices(&db, &node, id);
1018 assert_has_routes_exact(&db, &node, id);
1019 }
1020
1021 #[test]
1022 fn prop_many_nodes_indexed(nodes in nodes(16)) {
1023 let mut db = PeerDb::default();
1024
1025 let mut nodes_by_id = HashMap::new();
1026
1027 for node in &nodes {
1028 let id = db.upsert(node);
1029 nodes_by_id.insert(id, node.clone());
1030 }
1031
1032 for (id, node) in &nodes_by_id {
1033 validate_indices(&db, node, *id);
1034 }
1035 }
1036
1037 #[test]
1038 fn prop_remove(nodes in nodes(16)) {
1039 let mut db = PeerDb::default();
1040
1041 let mut ids = vec![];
1042
1043 for node in &nodes {
1044 ids.push((db.upsert(node), node));
1045 }
1046
1047 for (id, node) in ids {
1048 let (removed_id, removed_node) = db.remove(&id).unwrap();
1049
1050 proptest::prop_assert_eq!(removed_id, id);
1051 proptest::prop_assert_eq!(&removed_node, node);
1052 }
1053
1054 proptest::prop_assert!(db.peers.is_empty());
1055 proptest::prop_assert!(db.index_state.is_empty());
1056 }
1057 }
1058}