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
533 Node {
534 stable_id: StableNodeId(rand_string(&mut rng, 32)),
535 tailnet_address: TailnetAddress {
536 ipv4: rand_ipv4(&mut rng).into(),
537 ipv6: rand_ipv6(&mut rng).into(),
538 },
539 node_key: rng.random::<[u8; 32]>().into(),
540 key_signature: vec![],
541 disco_key: rng
542 .random::<bool>()
543 .then_some(rng.random::<[u8; 32]>().into()),
544 machine_key: rng
545 .random::<bool>()
546 .then_some(rng.random::<[u8; 32]>().into()),
547 id: rng.random(),
548 accepted_routes: (0..rng.random_range(0..32))
549 .map(|_| rand_route(&mut rng))
550 .collect(),
551
552 hostname: rand_string(&mut rng, 32),
553 user_id: rng.random(),
554 tailnet: rng.random::<bool>().then_some(rand_string(&mut rng, 32)),
555
556 node_key_expiry: None,
557 online: None,
558 last_seen: None,
559 underlay_addresses: vec![],
560 derp_region: rng
561 .random::<bool>()
562 .then_some(ts_derp::RegionId(rng.random())),
563
564 tags: (0..rng.random_range(0..8))
565 .map(|_| rand_string(&mut rng, 32))
566 .collect(),
567
568 cap: Default::default(),
569 cap_map: Default::default(),
570 peerapi_port: None,
571 peerapi_dns_proxy: false,
572 is_wireguard_only: false,
573 exit_node_dns_resolvers: vec![],
574 peer_relay: false,
575 service_vips: Default::default(),
576 }
577 }
578
579 fn validate_indices(db: &PeerDb, node: &Node, id: PeerId) {
580 let ipv4 = IpAddr::from(node.tailnet_address.ipv4.addr());
581 let ipv6 = IpAddr::from(node.tailnet_address.ipv6.addr());
582 let fqdn = node.fqdn_opt(false);
583
584 let mut keys: Vec<&dyn IndexedField> =
585 vec![&id, &node.node_key, &node.stable_id, &node.id, &ipv4, &ipv6];
586
587 if let Some(disco) = &node.disco_key {
588 keys.push(disco);
589 }
590
591 if let Some(fqdn) = &fqdn {
592 keys.push(fqdn);
593 }
594
595 for k in keys {
596 let lookup_id = k.lookup(db).unwrap();
597 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
598
599 let (lookup_id, lookup_node) = db.get(k).unwrap();
600 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
601 assert_eq!(lookup_node, node, "wrong node for key {k:?}");
602 }
603
604 node.hostname.lookup(db).unwrap();
606
607 for &route in &node.accepted_routes {
608 let routes = db.get_route(route).collect::<Vec<_>>();
613 assert!(!routes.is_empty());
614
615 for (found_id, found_node) in routes {
616 if found_id == id {
617 assert_eq!(found_node, node);
618 break;
619 }
620
621 let has_subset = found_node
622 .accepted_routes
623 .iter()
624 .any(|found_route| route.contains(found_route));
625
626 assert!(has_subset);
627 }
628 }
629 }
630
631 fn assert_has_routes_exact(db: &PeerDb, node: &Node, id: PeerId) {
634 for &route in &node.accepted_routes {
635 let match_exists = db
636 .get_route(route)
637 .any(|(found_id, found_node)| found_id == id && found_node == node);
638
639 assert!(match_exists);
640 }
641 }
642
643 #[test]
644 fn test_indices() {
645 let mut db = PeerDb::default();
646 let node = rand_node();
647 let id = db.upsert(&node);
648
649 validate_indices(&db, &node, id);
650 assert_has_routes_exact(&db, &node, id);
651 }
652
653 #[test]
654 fn test_names() {
655 let mut db = PeerDb::default();
656
657 let node1 = Node {
658 hostname: "test".to_string(),
659 tailnet: Some("ts.net".to_string()),
660 ..rand_node()
661 };
662 let node2 = Node {
663 hostname: "test".to_string(),
664 tailnet: Some("ts2.net".to_string()),
665 ..rand_node()
666 };
667 let node3 = Node {
668 hostname: "test".to_string(),
669 tailnet: None,
670 ..rand_node()
671 };
672
673 let id1 = db.upsert(&node1);
674 let id2 = db.upsert(&node2);
675 let id3 = db.upsert(&node3);
676
677 let nodes = [(id1, &node1), (id2, &node2), (id3, &node3)];
678
679 for (id, node) in &nodes {
680 validate_indices(&db, node, *id);
681 }
682
683 let (id, node) = db.get(&"test").unwrap();
684 assert!(nodes.iter().any(|(x, _node)| *x == id));
685
686 for &(x, curnode) in &nodes {
687 if x == id {
688 assert_eq!(node, curnode);
689 } else {
690 assert_ne!(node, curnode);
691 }
692 }
693
694 let (id, node) = db.get(&"test.ts.net").unwrap();
695 assert_eq!(id, id1);
696 assert_eq!(node, &node1);
697
698 let (id, node) = db.get(&"test.ts2.net").unwrap();
699 assert_eq!(id, id2);
700 assert_eq!(node, &node2);
701 }
702
703 #[test]
704 fn test_name_lookup_is_canonicalized() {
705 let mut db = PeerDb::default();
708
709 let node = Node {
710 hostname: "MixedCase".to_string(),
711 tailnet: Some("Tail-Scale.ts.net".to_string()),
712 ..rand_node()
713 };
714 let id = db.upsert(&node);
715
716 assert_eq!(db.get(&"mixedcase").unwrap().0, id);
718 assert_eq!(db.get(&"MIXEDCASE").unwrap().0, id);
719
720 assert_eq!(db.get(&"mixedcase.tail-scale.ts.net").unwrap().0, id);
722 assert_eq!(db.get(&"MixedCase.Tail-Scale.TS.NET").unwrap().0, id);
723 assert_eq!(db.get(&"mixedcase.tail-scale.ts.net.").unwrap().0, id);
724
725 db.remove(&id);
727 assert!(db.get(&"mixedcase").is_none());
728 assert!(db.get(&"mixedcase.tail-scale.ts.net").is_none());
729 assert!(db.index_state.is_empty());
730 }
731
732 #[test]
733 fn disco_key_reassigned_across_peers_no_panic() {
734 let mut db = PeerDb::default();
738
739 let disco: DiscoPublicKey = [7u8; 32].into();
740
741 let node_a = Node {
742 disco_key: Some(disco),
743 ..rand_node()
744 };
745 let id_a = db.upsert(&node_a);
746
747 let node_b = Node {
749 disco_key: Some(disco),
750 ..rand_node()
751 };
752 let id_b = db.upsert(&node_b);
753 assert_ne!(id_a, id_b);
754
755 let node_a2 = Node {
758 disco_key: None,
759 ..node_a.clone()
760 };
761 let id_a2 = db.upsert(&node_a2);
762 assert_eq!(id_a, id_a2);
763
764 assert_eq!(disco.lookup(&db), Some(id_b));
766 }
767
768 #[test]
769 fn ip_reassigned_across_peers_no_panic() {
770 let mut db = PeerDb::default();
773
774 let shared = TailnetAddress {
775 ipv4: Ipv4Addr::new(100, 64, 0, 1).into(),
776 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 1).into(),
777 };
778
779 let node_a = Node {
780 tailnet_address: shared.clone(),
781 ..rand_node()
782 };
783 let id_a = db.upsert(&node_a);
784
785 let node_b = Node {
787 tailnet_address: shared.clone(),
788 ..rand_node()
789 };
790 let id_b = db.upsert(&node_b);
791 assert_ne!(id_a, id_b);
792
793 let node_a2 = Node {
796 tailnet_address: TailnetAddress {
797 ipv4: Ipv4Addr::new(100, 64, 0, 2).into(),
798 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 2).into(),
799 },
800 ..node_a.clone()
801 };
802 let id_a2 = db.upsert(&node_a2);
803 assert_eq!(id_a, id_a2);
804
805 assert_eq!(
807 IpAddr::from(Ipv4Addr::new(100, 64, 0, 1)).lookup(&db),
808 Some(id_b)
809 );
810 assert_eq!(
812 IpAddr::from(Ipv4Addr::new(100, 64, 0, 2)).lookup(&db),
813 Some(id_a)
814 );
815 }
816
817 #[test]
818 fn node_key_or_stableid_churn_no_panic() {
819 let mut db = PeerDb::default();
823
824 let key: NodePublicKey = [9u8; 32].into();
825
826 let node_a = Node {
827 node_key: key,
828 ..rand_node()
829 };
830 let id_a = db.upsert(&node_a);
831
832 let node_b = Node {
834 node_key: key,
835 ..rand_node()
836 };
837 let id_b = db.upsert(&node_b);
838 assert_ne!(id_a, id_b);
839
840 let node_a2 = Node {
843 node_key: [10u8; 32].into(),
844 ..node_a.clone()
845 };
846 let id_a2 = db.upsert(&node_a2);
847 assert_eq!(id_a, id_a2);
848
849 assert_eq!(key.lookup(&db), Some(id_b));
851 assert_eq!(NodePublicKey::from([10u8; 32]).lookup(&db), Some(id_a));
852 }
853
854 proptest::prop_compose! {
855 fn ipv4net()(
856 addr: Ipv4Addr,
857 pfx in 0u8..=32,
858 ) -> ipnet::Ipv4Net {
859 ipnet::Ipv4Net::new(addr, pfx).unwrap().trunc()
860 }
861 }
862
863 proptest::prop_compose! {
864 fn ipv6net()(
865 addr: Ipv6Addr,
866 pfx in 0u8..=32,
867 ) -> ipnet::Ipv6Net {
868 ipnet::Ipv6Net::new(addr, pfx).unwrap().trunc()
869 }
870 }
871
872 fn ipnet() -> impl Strategy<Value = ipnet::IpNet> {
873 proptest::prop_oneof![
874 ipv4net().prop_map(ipnet::IpNet::from),
875 ipv6net().prop_map(ipnet::IpNet::from)
876 ]
877 }
878
879 proptest::prop_compose! {
880 fn domain_segment()(
884 seg in "[a-z][a-z0-9]*"
885 ) -> String {
886 seg
887 }
888 }
889
890 proptest::prop_compose! {
891 fn domain(max_count: usize)(
892 segs in proptest::collection::vec(domain_segment(), 0..max_count)
893 ) -> String {
894 segs.join(".")
895 }
896 }
897
898 type Key = [u8; 32];
899
900 proptest::prop_compose! {
901 fn nodes(n: usize)(
904 id in hash_set(any::<i64>(), n),
905 stable_id in hash_set(".+", n),
906 tags in vec(hash_set(".+", 0..32), n),
907 accepted_routes in vec(hash_set(ipnet(), 0..32), n),
908 node_key in hash_set(any::<Key>(), n),
909 machine_key in vec(any::<Option<Key>>(), n),
910 disco_key in vec(any::<Option<Key>>(), n),
911 ipv4 in hash_set(any::<Ipv4Addr>(), n),
912 ipv6 in hash_set(any::<Ipv6Addr>(), n),
913 name in hash_set(domain_segment(), n),
914 tailnet in vec(domain(5), n),
915 has_tailnet in vec(any::<bool>(), n),
916 derp_region in vec(any::<Option<NonZeroU32>>(), n),
917 underlay_addrs in vec(any::<HashSet<SocketAddr>>(), n),
918 ) -> Vec<Node> {
919 itertools::izip![
920 id,
921 stable_id,
922 tags,
923 accepted_routes,
924 node_key,
925 machine_key,
926 disco_key,
927 ipv4,
928 ipv6,
929 name,
930 tailnet,
931 has_tailnet,
932 derp_region,
933 underlay_addrs,
934 ].map(|(
935 id,
936 stable_id,
937 tags,
938 mut accepted_routes,
939 node_key,
940 machine_key,
941 disco_key,
942 ipv4,
943 ipv6,
944 name,
945 tailnet,
946 has_tailnet,
947 derp_region,
948 underlay_addrs,
949 )| {
950 accepted_routes.insert(ipnet::Ipv4Net::from(ipv4).into());
951 accepted_routes.insert(ipnet::Ipv6Net::from(ipv6).into());
952
953 Node {
954 id,
955 stable_id: StableNodeId(stable_id),
956
957 hostname: name,
958 user_id: 0,
959 tailnet: has_tailnet.then_some(tailnet),
960
961 node_key: node_key.into(),
962 key_signature: vec![],
963 disco_key: disco_key.map(Into::into),
964 machine_key: machine_key.map(Into::into),
965
966 node_key_expiry: None,
967 online: None,
968 last_seen: None,
969
970 tailnet_address: TailnetAddress {
971 ipv4: ipv4.into(),
972 ipv6: ipv6.into(),
973 },
974 tags: tags.into_iter().collect(),
975
976 derp_region: derp_region.map(ts_derp::RegionId),
977
978 accepted_routes: accepted_routes.into_iter().collect(),
979 underlay_addresses: underlay_addrs.into_iter().collect(),
980
981 cap: Default::default(),
982 cap_map: Default::default(),
983 peerapi_port: None,
984 peerapi_dns_proxy: false,
985 is_wireguard_only: false,
986 exit_node_dns_resolvers: vec![],
987 peer_relay: false,
988 service_vips: Default::default(),
989 }
990 })
991 .collect()
992 }
993 }
994
995 proptest::proptest! {
996 #[test]
997 fn prop_one_node_indices(mut nodes in nodes(1)) {
998 let node = nodes.pop().unwrap();
999
1000 let mut db = PeerDb::default();
1001 let id = db.upsert(&node);
1002
1003 validate_indices(&db, &node, id);
1004 assert_has_routes_exact(&db, &node, id);
1005 }
1006
1007 #[test]
1008 fn prop_many_nodes_indexed(nodes in nodes(16)) {
1009 let mut db = PeerDb::default();
1010
1011 let mut nodes_by_id = HashMap::new();
1012
1013 for node in &nodes {
1014 let id = db.upsert(node);
1015 nodes_by_id.insert(id, node.clone());
1016 }
1017
1018 for (id, node) in &nodes_by_id {
1019 validate_indices(&db, node, *id);
1020 }
1021 }
1022
1023 #[test]
1024 fn prop_remove(nodes in nodes(16)) {
1025 let mut db = PeerDb::default();
1026
1027 let mut ids = vec![];
1028
1029 for node in &nodes {
1030 ids.push((db.upsert(node), node));
1031 }
1032
1033 for (id, node) in ids {
1034 let (removed_id, removed_node) = db.remove(&id).unwrap();
1035
1036 proptest::prop_assert_eq!(removed_id, id);
1037 proptest::prop_assert_eq!(&removed_node, node);
1038 }
1039
1040 proptest::prop_assert!(db.peers.is_empty());
1041 proptest::prop_assert!(db.index_state.is_empty());
1042 }
1043 }
1044}