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