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
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum DiscoKeyMatch {
37 Active,
39 Inactive,
42}
43
44pub trait IndexedField: Debug + private::Sealed {
46 fn lookup(&self, db: &PeerDb) -> Option<PeerId>;
48}
49
50type Index<T> = HashMap<T, PeerId>;
51type PeerName = String;
52
53fn canon_name(name: &str) -> String {
60 name.strip_suffix('.').unwrap_or(name).to_ascii_lowercase()
61}
62
63#[derive(Default, Clone)]
71pub struct PeerDb {
72 peers: HashMap<PeerId, Node>,
73 index_state: IndexState,
74 next_id: u32,
75}
76
77impl Debug for PeerDb {
78 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
79 self.peers.fmt(f)
80 }
81}
82
83#[derive(Default, Clone)]
84struct IndexState {
85 nk_idx: Index<NodePublicKey>,
87 disco_idx: Index<DiscoPublicKey>,
93 inactive_disco_idx: Index<DiscoPublicKey>,
102 inactive_disco: HashMap<PeerId, DiscoPublicKey>,
105 stableid_idx: Index<StableNodeId>,
107 control_idx: Index<ts_control::NodeId>,
114 name_idx: Index<PeerName>,
116 ip_idx: ts_bart::Table<PeerId>,
118 route_idx: ts_bart::Table<smallvec::SmallVec<[PeerId; 2]>>,
123}
124
125impl PeerDb {
126 pub fn upsert(&mut self, new: &Node) -> PeerId {
130 let id = self
131 .index_state
132 .stableid_idx
133 .get(&new.stable_id)
134 .copied()
135 .unwrap_or_else(|| {
136 let id = self.next_id;
137 self.next_id += 1;
138
139 PeerId(id)
140 });
141
142 let old = self.peers.get(&id);
143
144 if old.is_some_and(|x| x == new) {
146 return id;
147 }
148
149 maybe_update_idx(new, old, |x| &x.node_key, &mut self.index_state.nk_idx, id);
150 maybe_update_idx(
151 new,
152 old,
153 |x| &x.stable_id,
154 &mut self.index_state.stableid_idx,
155 id,
156 );
157 maybe_update_idx(new, old, |x| &x.id, &mut self.index_state.control_idx, id);
158
159 maybe_update(
160 new,
161 old,
162 |x| &x.disco_key,
163 &mut self.index_state.disco_idx,
164 |old, idx| {
165 if let Some(key) = &old.disco_key {
166 if idx.get(key).is_some_and(|&x| x == id) {
171 idx.remove(key);
172 }
173 }
174 },
175 |new, idx| {
176 if let Some(key) = &new.disco_key {
177 idx.insert(*key, id);
178 }
179 },
180 );
181
182 maybe_update(
195 new,
196 old,
197 |x| (&x.hostname, &x.tailnet),
198 &mut self.index_state.name_idx,
199 |old, idx| {
200 let old_hostname = canon_name(&old.hostname);
201 if idx.get(&old_hostname).is_some_and(|&x| x == id) {
202 idx.remove(&old_hostname);
203 }
204
205 if let Some(fqdn) = old.fqdn_opt(false) {
206 let k = canon_name(&fqdn);
211 if idx.get(&k).is_some_and(|&x| x == id) {
212 idx.remove(&k);
213 }
214 }
215 },
216 |new, idx| {
217 idx.insert(canon_name(&new.hostname), id);
218
219 if let Some(fqdn) = new.fqdn_opt(false) {
220 idx.insert(canon_name(&fqdn), id);
221 }
222 },
223 );
224
225 maybe_update(
226 new,
227 old,
228 |x| &x.tailnet_address,
229 &mut self.index_state.ip_idx,
230 |old, idx| {
231 let ipv4: ipnet::IpNet = old.tailnet_address.ipv4.into();
235 let ipv6: ipnet::IpNet = old.tailnet_address.ipv6.into();
236
237 if idx.lookup_prefix_exact(ipv4).is_some_and(|&x| x == id) {
238 idx.remove(ipv4);
239 }
240 if idx.lookup_prefix_exact(ipv6).is_some_and(|&x| x == id) {
241 idx.remove(ipv6);
242 }
243 },
244 |new, idx| {
245 idx.insert(new.tailnet_address.ipv4.into(), id);
246 idx.insert(new.tailnet_address.ipv6.into(), id);
247 },
248 );
249
250 maybe_update(
251 new,
252 old,
253 |x| &x.accepted_routes,
254 &mut self.index_state,
255 |old, idx| {
256 for &route in &old.accepted_routes {
257 idx.remove_route(route, id);
258 }
259 },
260 |new, idx| {
261 for &route in &new.accepted_routes {
262 idx.route_idx.modify(route, |val| {
263 if let Some(val) = val {
264 val.push(id);
265 return RouteModification::Noop;
266 }
267
268 RouteModification::Insert(smallvec::smallvec![id])
269 });
270 }
271 },
272 );
273
274 self.peers.insert(id, new.clone());
275
276 id
277 }
278
279 pub fn remove(&mut self, field: &dyn IndexedField) -> Option<(PeerId, Node)> {
281 let id = field.lookup(self)?;
282
283 let node = self.peers.remove(&id)?;
284 self.index_state.remove(id, &node);
285
286 Some((id, node))
287 }
288
289 pub fn get(&self, field: &dyn IndexedField) -> Option<(PeerId, &Node)> {
291 let id = field.lookup(self)?;
292 let peer = self.peers.get(&id)?;
293
294 Some((id, peer))
295 }
296
297 pub fn get_route(&self, route: ipnet::IpNet) -> impl Iterator<Item = (PeerId, &Node)> {
299 self.index_state
302 .route_idx
303 .lookup_prefix(route)
304 .into_iter()
305 .flat_map(|x| x.iter())
306 .map(|&id| (id, self.peers.get(&id).unwrap()))
307 }
308
309 pub fn has(&self, field: &dyn IndexedField) -> Option<PeerId> {
311 field.lookup(self)
312 }
313
314 pub fn peer_by_known_disco_key(
328 &self,
329 key: &DiscoPublicKey,
330 ) -> Option<(PeerId, &Node, DiscoKeyMatch)> {
331 let (id, matched) = match self.index_state.disco_idx.get(key) {
332 Some(&id) => (id, DiscoKeyMatch::Active),
333 None => (
334 self.index_state.inactive_disco_idx.get(key).copied()?,
335 DiscoKeyMatch::Inactive,
336 ),
337 };
338
339 Some((id, self.peers.get(&id)?, matched))
340 }
341
342 pub fn set_inactive_disco_key(&mut self, id: PeerId, key: Option<DiscoPublicKey>) {
350 let idx = &mut self.index_state;
351
352 if let Some(previous) = idx.inactive_disco.remove(&id)
354 && idx
355 .inactive_disco_idx
356 .get(&previous)
357 .is_some_and(|&x| x == id)
358 {
359 idx.inactive_disco_idx.remove(&previous);
360 }
361
362 if let Some(key) = key {
363 idx.inactive_disco.insert(id, key);
364 idx.inactive_disco_idx.insert(key, id);
365 }
366 }
367
368 pub const fn peers(&self) -> &HashMap<PeerId, Node> {
370 &self.peers
371 }
372
373 pub fn retain(&mut self, mut predicate: impl FnMut(PeerId, &Node) -> bool) {
375 self.peers.retain(|&id, node| {
376 let retain = predicate(id, node);
377
378 if !retain {
379 self.index_state.remove(id, node);
380 }
381
382 retain
383 });
384 }
385}
386
387impl IndexState {
388 fn remove(&mut self, id: PeerId, node: &Node) {
389 self.nk_idx.remove(&node.node_key);
390 self.stableid_idx.remove(&node.stable_id);
391 self.control_idx.remove(&node.id);
392 self.ip_idx.remove(node.tailnet_address.ipv4.into());
393 self.ip_idx.remove(node.tailnet_address.ipv6.into());
394
395 let hostname = canon_name(&node.hostname);
396 if self.name_idx.get(&hostname).is_some_and(|&x| x == id) {
397 self.name_idx.remove(&hostname);
398 }
399
400 if let Some(fqdn) = node.fqdn_opt(false) {
401 self.name_idx.remove(&canon_name(&fqdn));
402 }
403
404 for route in &node.accepted_routes {
405 self.remove_route(*route, id);
406 }
407
408 if let Some(disco) = &node.disco_key {
409 self.disco_idx.remove(disco);
410 }
411
412 if let Some(key) = self.inactive_disco.remove(&id)
415 && self.inactive_disco_idx.get(&key).is_some_and(|&x| x == id)
416 {
417 self.inactive_disco_idx.remove(&key);
418 }
419 }
420
421 fn remove_route(&mut self, route: ipnet::IpNet, id: PeerId) {
423 self.route_idx.modify(route, |val| match val {
424 Some(val) => {
425 let mut some_matched = false;
426
427 val.retain(|&mut x| {
428 let ids_match = x == id;
429 if ids_match {
430 some_matched = true;
431 }
432
433 !ids_match
434 });
435
436 assert!(some_matched);
437
438 if val.is_empty() {
439 RouteModification::Remove
440 } else {
441 RouteModification::Noop
442 }
443 }
444 None => RouteModification::Noop,
445 });
446 }
447
448 #[cfg(test)]
449 fn is_empty(&self) -> bool {
450 self.nk_idx.is_empty()
451 && self.stableid_idx.is_empty()
452 && self.control_idx.is_empty()
453 && self.ip_idx.size() == 0
454 && self.name_idx.is_empty()
455 && self.route_idx.size() == 0
456 && self.disco_idx.is_empty()
457 && self.inactive_disco_idx.is_empty()
458 && self.inactive_disco.is_empty()
459 }
460}
461
462fn maybe_update<'n, T, Idx>(
472 new: &'n Node,
473 old: Option<&'n Node>,
474 accessor: impl Fn(&'n Node) -> T,
475 idx: &mut Idx,
476 mut remove: impl FnMut(&'n Node, &mut Idx),
477 mut insert: impl FnMut(&'n Node, &mut Idx),
478) where
479 T: PartialEq + 'n,
480{
481 match old {
482 Some(old) if accessor(old) == accessor(new) => {
483 return;
484 }
485 Some(x) => {
486 remove(x, idx);
487 }
488 None => {}
489 }
490
491 insert(new, idx)
492}
493
494fn maybe_update_idx<T>(
496 new: &Node,
497 old: Option<&Node>,
498 accessor: impl Fn(&Node) -> &T,
499 idx: &mut Index<T>,
500 new_id: PeerId,
501) where
502 T: Eq + Hash + Clone,
503{
504 maybe_update(
505 new,
506 old,
507 &accessor,
508 idx,
509 |old, idx| {
510 if idx.get(accessor(old)).is_some_and(|&x| x == new_id) {
514 idx.remove(accessor(old));
515 }
516 },
517 |new, idx| {
518 idx.insert(accessor(new).clone(), new_id);
519 },
520 )
521}
522
523impl IndexedField for PeerId {
524 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
525 if db.peers.contains_key(self) {
526 Some(*self)
527 } else {
528 None
529 }
530 }
531}
532
533impl IndexedField for NodePublicKey {
534 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
535 db.index_state.nk_idx.get(self).copied()
536 }
537}
538
539impl IndexedField for DiscoPublicKey {
540 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
541 db.index_state.disco_idx.get(self).copied()
542 }
543}
544
545impl IndexedField for StableNodeId {
546 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
547 db.index_state.stableid_idx.get(self).copied()
548 }
549}
550
551impl IndexedField for ts_control::NodeId {
552 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
553 db.index_state.control_idx.get(self).copied()
554 }
555}
556
557impl IndexedField for PeerName {
558 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
559 db.index_state.name_idx.get(&canon_name(self)).copied()
560 }
561}
562
563impl IndexedField for &str {
564 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
565 db.index_state.name_idx.get(&canon_name(self)).copied()
566 }
567}
568
569impl IndexedField for IpAddr {
570 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
571 db.index_state.ip_idx.lookup(*self).copied()
572 }
573}
574
575#[cfg(test)]
576mod test {
577 use std::{
578 collections::{HashMap, HashSet},
579 net::{Ipv4Addr, Ipv6Addr, SocketAddr},
580 num::NonZeroU32,
581 };
582
583 use proptest::{
584 collection::{hash_set, vec},
585 prelude::any,
586 strategy::Strategy,
587 };
588 use rand::{
589 RngExt,
590 distr::{Alphanumeric, SampleString},
591 };
592 use ts_control::TailnetAddress;
593
594 use super::*;
595
596 fn rand_string(rng: &mut dyn rand::Rng, max_len: usize) -> String {
597 let len = rng.random_range(1..max_len);
598 Alphanumeric.sample_string(rng, len)
599 }
600
601 fn rand_route(rng: &mut dyn rand::Rng) -> ipnet::IpNet {
602 if rng.random::<bool>() {
603 let ip = rand_ipv4(rng);
604 ipnet::Ipv4Net::new(ip, rand::random_range(0..=32))
605 .unwrap()
606 .trunc()
607 .into()
608 } else {
609 let ip = rand_ipv6(rng);
610 ipnet::Ipv6Net::new(ip, rand::random_range(0..=128))
611 .unwrap()
612 .trunc()
613 .into()
614 }
615 }
616
617 fn rand_ipv4(rng: &mut dyn rand::Rng) -> Ipv4Addr {
618 Ipv4Addr::from_octets(rng.random::<[u8; 4]>())
619 }
620
621 fn rand_ipv6(rng: &mut dyn rand::Rng) -> Ipv6Addr {
622 Ipv6Addr::from_segments(rng.random::<[u16; 8]>())
623 }
624
625 fn rand_node() -> Node {
626 let mut rng = rand::rng();
627 let tailnet_address = TailnetAddress {
628 ipv4: rand_ipv4(&mut rng).into(),
629 ipv6: rand_ipv6(&mut rng).into(),
630 };
631
632 Node {
633 stable_id: StableNodeId(rand_string(&mut rng, 32)),
634 addresses: vec![tailnet_address.ipv4.into(), tailnet_address.ipv6.into()],
635 tailnet_address,
636 node_key: rng.random::<[u8; 32]>().into(),
637 key_signature: vec![],
638 disco_key: rng
639 .random::<bool>()
640 .then_some(rng.random::<[u8; 32]>().into()),
641 machine_key: rng
642 .random::<bool>()
643 .then_some(rng.random::<[u8; 32]>().into()),
644 id: rng.random(),
645 accepted_routes: (0..rng.random_range(0..32))
646 .map(|_| rand_route(&mut rng))
647 .collect(),
648
649 hostname: rand_string(&mut rng, 32),
650 user_id: rng.random(),
651 tailnet: rng.random::<bool>().then_some(rand_string(&mut rng, 32)),
652
653 node_key_expiry: None,
654 expired: false,
655 online: None,
656 last_seen: None,
657 underlay_addresses: vec![],
658 derp_region: rng
659 .random::<bool>()
660 .then_some(ts_derp::RegionId(rng.random())),
661
662 tags: (0..rng.random_range(0..8))
663 .map(|_| rand_string(&mut rng, 32))
664 .collect(),
665
666 cap: Default::default(),
667 cap_map: Default::default(),
668 peerapi_port: None,
669 peerapi_dns_proxy: false,
670 is_wireguard_only: false,
671 exit_node_dns_resolvers: vec![],
672 peer_relay: false,
673 ssh_host_keys: vec![],
674 service_vips: Default::default(),
675 unsigned_peer_api_only: false,
676 }
677 }
678
679 fn validate_indices(db: &PeerDb, node: &Node, id: PeerId) {
680 let ipv4 = IpAddr::from(node.tailnet_address.ipv4.addr());
681 let ipv6 = IpAddr::from(node.tailnet_address.ipv6.addr());
682 let fqdn = node.fqdn_opt(false);
683
684 let mut keys: Vec<&dyn IndexedField> =
685 vec![&id, &node.node_key, &node.stable_id, &node.id, &ipv4, &ipv6];
686
687 if let Some(disco) = &node.disco_key {
688 keys.push(disco);
689 }
690
691 if let Some(fqdn) = &fqdn {
692 keys.push(fqdn);
693 }
694
695 for k in keys {
696 let lookup_id = k.lookup(db).unwrap();
697 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
698
699 let (lookup_id, lookup_node) = db.get(k).unwrap();
700 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
701 assert_eq!(lookup_node, node, "wrong node for key {k:?}");
702 }
703
704 node.hostname.lookup(db).unwrap();
706
707 for &route in &node.accepted_routes {
708 let routes = db.get_route(route).collect::<Vec<_>>();
713 assert!(!routes.is_empty());
714
715 for (found_id, found_node) in routes {
716 if found_id == id {
717 assert_eq!(found_node, node);
718 break;
719 }
720
721 let has_subset = found_node
722 .accepted_routes
723 .iter()
724 .any(|found_route| route.contains(found_route));
725
726 assert!(has_subset);
727 }
728 }
729 }
730
731 fn assert_has_routes_exact(db: &PeerDb, node: &Node, id: PeerId) {
734 for &route in &node.accepted_routes {
735 let match_exists = db
736 .get_route(route)
737 .any(|(found_id, found_node)| found_id == id && found_node == node);
738
739 assert!(match_exists);
740 }
741 }
742
743 #[test]
744 fn test_indices() {
745 let mut db = PeerDb::default();
746 let node = rand_node();
747 let id = db.upsert(&node);
748
749 validate_indices(&db, &node, id);
750 assert_has_routes_exact(&db, &node, id);
751 }
752
753 #[test]
754 fn test_names() {
755 let mut db = PeerDb::default();
756
757 let node1 = Node {
758 hostname: "test".to_string(),
759 tailnet: Some("ts.net".to_string()),
760 ..rand_node()
761 };
762 let node2 = Node {
763 hostname: "test".to_string(),
764 tailnet: Some("ts2.net".to_string()),
765 ..rand_node()
766 };
767 let node3 = Node {
768 hostname: "test".to_string(),
769 tailnet: None,
770 ..rand_node()
771 };
772
773 let id1 = db.upsert(&node1);
774 let id2 = db.upsert(&node2);
775 let id3 = db.upsert(&node3);
776
777 let nodes = [(id1, &node1), (id2, &node2), (id3, &node3)];
778
779 for (id, node) in &nodes {
780 validate_indices(&db, node, *id);
781 }
782
783 let (id, node) = db.get(&"test").unwrap();
784 assert!(nodes.iter().any(|(x, _node)| *x == id));
785
786 for &(x, curnode) in &nodes {
787 if x == id {
788 assert_eq!(node, curnode);
789 } else {
790 assert_ne!(node, curnode);
791 }
792 }
793
794 let (id, node) = db.get(&"test.ts.net").unwrap();
795 assert_eq!(id, id1);
796 assert_eq!(node, &node1);
797
798 let (id, node) = db.get(&"test.ts2.net").unwrap();
799 assert_eq!(id, id2);
800 assert_eq!(node, &node2);
801 }
802
803 #[test]
804 fn test_name_lookup_is_canonicalized() {
805 let mut db = PeerDb::default();
808
809 let node = Node {
810 hostname: "MixedCase".to_string(),
811 tailnet: Some("Tail-Scale.ts.net".to_string()),
812 ..rand_node()
813 };
814 let id = db.upsert(&node);
815
816 assert_eq!(db.get(&"mixedcase").unwrap().0, id);
818 assert_eq!(db.get(&"MIXEDCASE").unwrap().0, id);
819
820 assert_eq!(db.get(&"mixedcase.tail-scale.ts.net").unwrap().0, id);
822 assert_eq!(db.get(&"MixedCase.Tail-Scale.TS.NET").unwrap().0, id);
823 assert_eq!(db.get(&"mixedcase.tail-scale.ts.net.").unwrap().0, id);
824
825 db.remove(&id);
827 assert!(db.get(&"mixedcase").is_none());
828 assert!(db.get(&"mixedcase.tail-scale.ts.net").is_none());
829 assert!(db.index_state.is_empty());
830 }
831
832 #[test]
833 fn disco_key_reassigned_across_peers_no_panic() {
834 let mut db = PeerDb::default();
838
839 let disco: DiscoPublicKey = [7u8; 32].into();
840
841 let node_a = Node {
842 disco_key: Some(disco),
843 ..rand_node()
844 };
845 let id_a = db.upsert(&node_a);
846
847 let node_b = Node {
849 disco_key: Some(disco),
850 ..rand_node()
851 };
852 let id_b = db.upsert(&node_b);
853 assert_ne!(id_a, id_b);
854
855 let node_a2 = Node {
858 disco_key: None,
859 ..node_a.clone()
860 };
861 let id_a2 = db.upsert(&node_a2);
862 assert_eq!(id_a, id_a2);
863
864 assert_eq!(disco.lookup(&db), Some(id_b));
866 }
867
868 #[test]
874 fn either_known_disco_key_resolves_on_ingress() {
875 let mut db = PeerDb::default();
876
877 let active: DiscoPublicKey = [1u8; 32].into();
878 let inactive: DiscoPublicKey = [2u8; 32].into();
879 let stranger: DiscoPublicKey = [3u8; 32].into();
880
881 let node = Node {
882 disco_key: Some(active),
883 ..rand_node()
884 };
885 let id = db.upsert(&node);
886
887 assert!(
888 db.peer_by_known_disco_key(&inactive).is_none(),
889 "a peer with one key resolves only that key"
890 );
891
892 db.set_inactive_disco_key(id, Some(inactive));
893
894 let (got, _, matched) = db
895 .peer_by_known_disco_key(&active)
896 .expect("active resolves");
897 assert_eq!((got, matched), (id, DiscoKeyMatch::Active));
898
899 let (got, _, matched) = db
900 .peer_by_known_disco_key(&inactive)
901 .expect("the other known key resolves too");
902 assert_eq!((got, matched), (id, DiscoKeyMatch::Inactive));
903
904 assert!(
905 db.peer_by_known_disco_key(&stranger).is_none(),
906 "a key in neither slot is refused — the whole security value of the check"
907 );
908 assert_eq!(
909 inactive.lookup(&db),
910 None,
911 "and the send-side disco index still carries only the active key"
912 );
913
914 db.set_inactive_disco_key(id, None);
916 assert!(db.peer_by_known_disco_key(&inactive).is_none());
917
918 db.set_inactive_disco_key(id, Some(inactive));
920 db.remove(&id);
921 assert!(db.peer_by_known_disco_key(&inactive).is_none());
922 assert!(db.index_state.is_empty());
923 }
924
925 #[test]
933 fn a_key_claimed_by_two_peers_resolves_to_the_one_using_it() {
934 let mut db = PeerDb::default();
935
936 let shared: DiscoPublicKey = [9u8; 32].into();
937
938 let node_a = Node {
939 disco_key: Some([1u8; 32].into()),
940 ..rand_node()
941 };
942 let id_a = db.upsert(&node_a);
943 db.set_inactive_disco_key(id_a, Some(shared));
944
945 let node_b = Node {
947 disco_key: Some([2u8; 32].into()),
948 ..rand_node()
949 };
950 let id_b = db.upsert(&node_b);
951 db.set_inactive_disco_key(id_b, Some(shared));
952 assert_eq!(
953 db.peer_by_known_disco_key(&shared)
954 .map(|(id, _, m)| (id, m)),
955 Some((id_b, DiscoKeyMatch::Inactive))
956 );
957
958 db.set_inactive_disco_key(id_a, None);
960 assert_eq!(
961 db.peer_by_known_disco_key(&shared)
962 .map(|(id, _, m)| (id, m)),
963 Some((id_b, DiscoKeyMatch::Inactive)),
964 "retracting another peer's stale claim must not clobber the live one"
965 );
966
967 let node_c = Node {
969 disco_key: Some(shared),
970 ..rand_node()
971 };
972 let id_c = db.upsert(&node_c);
973 assert_eq!(
974 db.peer_by_known_disco_key(&shared)
975 .map(|(id, _, m)| (id, m)),
976 Some((id_c, DiscoKeyMatch::Active)),
977 "the peer sending under the key beats one that merely still knows it"
978 );
979 }
980
981 #[test]
982 fn ip_reassigned_across_peers_no_panic() {
983 let mut db = PeerDb::default();
986
987 let shared = TailnetAddress {
988 ipv4: Ipv4Addr::new(100, 64, 0, 1).into(),
989 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 1).into(),
990 };
991
992 let node_a = Node {
993 addresses: vec![shared.ipv4.into(), shared.ipv6.into()],
994 tailnet_address: shared.clone(),
995 ..rand_node()
996 };
997 let id_a = db.upsert(&node_a);
998
999 let node_b = Node {
1001 addresses: vec![shared.ipv4.into(), shared.ipv6.into()],
1002 tailnet_address: shared.clone(),
1003 ..rand_node()
1004 };
1005 let id_b = db.upsert(&node_b);
1006 assert_ne!(id_a, id_b);
1007
1008 let renumbered = TailnetAddress {
1011 ipv4: Ipv4Addr::new(100, 64, 0, 2).into(),
1012 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 2).into(),
1013 };
1014 let node_a2 = Node {
1015 addresses: vec![renumbered.ipv4.into(), renumbered.ipv6.into()],
1016 tailnet_address: renumbered,
1017 ..node_a.clone()
1018 };
1019 let id_a2 = db.upsert(&node_a2);
1020 assert_eq!(id_a, id_a2);
1021
1022 assert_eq!(
1024 IpAddr::from(Ipv4Addr::new(100, 64, 0, 1)).lookup(&db),
1025 Some(id_b)
1026 );
1027 assert_eq!(
1029 IpAddr::from(Ipv4Addr::new(100, 64, 0, 2)).lookup(&db),
1030 Some(id_a)
1031 );
1032 }
1033
1034 #[test]
1035 fn node_key_or_stableid_churn_no_panic() {
1036 let mut db = PeerDb::default();
1040
1041 let key: NodePublicKey = [9u8; 32].into();
1042
1043 let node_a = Node {
1044 node_key: key,
1045 ..rand_node()
1046 };
1047 let id_a = db.upsert(&node_a);
1048
1049 let node_b = Node {
1051 node_key: key,
1052 ..rand_node()
1053 };
1054 let id_b = db.upsert(&node_b);
1055 assert_ne!(id_a, id_b);
1056
1057 let node_a2 = Node {
1060 node_key: [10u8; 32].into(),
1061 ..node_a.clone()
1062 };
1063 let id_a2 = db.upsert(&node_a2);
1064 assert_eq!(id_a, id_a2);
1065
1066 assert_eq!(key.lookup(&db), Some(id_b));
1068 assert_eq!(NodePublicKey::from([10u8; 32]).lookup(&db), Some(id_a));
1069 }
1070
1071 proptest::prop_compose! {
1072 fn ipv4net()(
1073 addr: Ipv4Addr,
1074 pfx in 0u8..=32,
1075 ) -> ipnet::Ipv4Net {
1076 ipnet::Ipv4Net::new(addr, pfx).unwrap().trunc()
1077 }
1078 }
1079
1080 proptest::prop_compose! {
1081 fn ipv6net()(
1082 addr: Ipv6Addr,
1083 pfx in 0u8..=32,
1084 ) -> ipnet::Ipv6Net {
1085 ipnet::Ipv6Net::new(addr, pfx).unwrap().trunc()
1086 }
1087 }
1088
1089 fn ipnet() -> impl Strategy<Value = ipnet::IpNet> {
1090 proptest::prop_oneof![
1091 ipv4net().prop_map(ipnet::IpNet::from),
1092 ipv6net().prop_map(ipnet::IpNet::from)
1093 ]
1094 }
1095
1096 proptest::prop_compose! {
1097 fn domain_segment()(
1101 seg in "[a-z][a-z0-9]*"
1102 ) -> String {
1103 seg
1104 }
1105 }
1106
1107 proptest::prop_compose! {
1108 fn domain(max_count: usize)(
1109 segs in proptest::collection::vec(domain_segment(), 0..max_count)
1110 ) -> String {
1111 segs.join(".")
1112 }
1113 }
1114
1115 type Key = [u8; 32];
1116
1117 proptest::prop_compose! {
1118 fn nodes(n: usize)(
1121 id in hash_set(any::<i64>(), n),
1122 stable_id in hash_set(".+", n),
1123 tags in vec(hash_set(".+", 0..32), n),
1124 accepted_routes in vec(hash_set(ipnet(), 0..32), n),
1125 node_key in hash_set(any::<Key>(), n),
1126 machine_key in vec(any::<Option<Key>>(), n),
1127 disco_key in vec(any::<Option<Key>>(), n),
1128 ipv4 in hash_set(any::<Ipv4Addr>(), n),
1129 ipv6 in hash_set(any::<Ipv6Addr>(), n),
1130 name in hash_set(domain_segment(), n),
1131 tailnet in vec(domain(5), n),
1132 has_tailnet in vec(any::<bool>(), n),
1133 derp_region in vec(any::<Option<NonZeroU32>>(), n),
1134 underlay_addrs in vec(any::<HashSet<SocketAddr>>(), n),
1135 ) -> Vec<Node> {
1136 itertools::izip![
1137 id,
1138 stable_id,
1139 tags,
1140 accepted_routes,
1141 node_key,
1142 machine_key,
1143 disco_key,
1144 ipv4,
1145 ipv6,
1146 name,
1147 tailnet,
1148 has_tailnet,
1149 derp_region,
1150 underlay_addrs,
1151 ].map(|(
1152 id,
1153 stable_id,
1154 tags,
1155 mut accepted_routes,
1156 node_key,
1157 machine_key,
1158 disco_key,
1159 ipv4,
1160 ipv6,
1161 name,
1162 tailnet,
1163 has_tailnet,
1164 derp_region,
1165 underlay_addrs,
1166 )| {
1167 accepted_routes.insert(ipnet::Ipv4Net::from(ipv4).into());
1168 accepted_routes.insert(ipnet::Ipv6Net::from(ipv6).into());
1169
1170 Node {
1171 id,
1172 stable_id: StableNodeId(stable_id),
1173
1174 hostname: name,
1175 user_id: 0,
1176 tailnet: has_tailnet.then_some(tailnet),
1177
1178 node_key: node_key.into(),
1179 key_signature: vec![],
1180 disco_key: disco_key.map(Into::into),
1181 machine_key: machine_key.map(Into::into),
1182
1183 node_key_expiry: None,
1184 expired: false,
1185 online: None,
1186 last_seen: None,
1187
1188 addresses: vec![
1189 ipnet::IpNet::V4(ipv4.into()),
1190 ipnet::IpNet::V6(ipv6.into()),
1191 ],
1192 tailnet_address: TailnetAddress {
1193 ipv4: ipv4.into(),
1194 ipv6: ipv6.into(),
1195 },
1196 tags: tags.into_iter().collect(),
1197
1198 derp_region: derp_region.map(ts_derp::RegionId),
1199
1200 accepted_routes: accepted_routes.into_iter().collect(),
1201 underlay_addresses: underlay_addrs.into_iter().collect(),
1202
1203 cap: Default::default(),
1204 cap_map: Default::default(),
1205 peerapi_port: None,
1206 peerapi_dns_proxy: false,
1207 is_wireguard_only: false,
1208 exit_node_dns_resolvers: vec![],
1209 peer_relay: false,
1210 ssh_host_keys: vec![],
1211 service_vips: Default::default(),
1212 unsigned_peer_api_only: false,
1213 }
1214 })
1215 .collect()
1216 }
1217 }
1218
1219 proptest::proptest! {
1220 #[test]
1221 fn prop_one_node_indices(mut nodes in nodes(1)) {
1222 let node = nodes.pop().unwrap();
1223
1224 let mut db = PeerDb::default();
1225 let id = db.upsert(&node);
1226
1227 validate_indices(&db, &node, id);
1228 assert_has_routes_exact(&db, &node, id);
1229 }
1230
1231 #[test]
1232 fn prop_many_nodes_indexed(nodes in nodes(16)) {
1233 let mut db = PeerDb::default();
1234
1235 let mut nodes_by_id = HashMap::new();
1236
1237 for node in &nodes {
1238 let id = db.upsert(node);
1239 nodes_by_id.insert(id, node.clone());
1240 }
1241
1242 for (id, node) in &nodes_by_id {
1243 validate_indices(&db, node, *id);
1244 }
1245 }
1246
1247 #[test]
1248 fn prop_remove(nodes in nodes(16)) {
1249 let mut db = PeerDb::default();
1250
1251 let mut ids = vec![];
1252
1253 for node in &nodes {
1254 ids.push((db.upsert(node), node));
1255 }
1256
1257 for (id, node) in ids {
1258 let (removed_id, removed_node) = db.remove(&id).unwrap();
1259
1260 proptest::prop_assert_eq!(removed_id, id);
1261 proptest::prop_assert_eq!(&removed_node, node);
1262 }
1263
1264 proptest::prop_assert!(db.peers.is_empty());
1265 proptest::prop_assert!(db.index_state.is_empty());
1266 }
1267 }
1268}