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 online: None,
655 last_seen: None,
656 underlay_addresses: vec![],
657 derp_region: rng
658 .random::<bool>()
659 .then_some(ts_derp::RegionId(rng.random())),
660
661 tags: (0..rng.random_range(0..8))
662 .map(|_| rand_string(&mut rng, 32))
663 .collect(),
664
665 cap: Default::default(),
666 cap_map: Default::default(),
667 peerapi_port: None,
668 peerapi_dns_proxy: false,
669 is_wireguard_only: false,
670 exit_node_dns_resolvers: vec![],
671 peer_relay: false,
672 ssh_host_keys: vec![],
673 service_vips: Default::default(),
674 unsigned_peer_api_only: false,
675 }
676 }
677
678 fn validate_indices(db: &PeerDb, node: &Node, id: PeerId) {
679 let ipv4 = IpAddr::from(node.tailnet_address.ipv4.addr());
680 let ipv6 = IpAddr::from(node.tailnet_address.ipv6.addr());
681 let fqdn = node.fqdn_opt(false);
682
683 let mut keys: Vec<&dyn IndexedField> =
684 vec![&id, &node.node_key, &node.stable_id, &node.id, &ipv4, &ipv6];
685
686 if let Some(disco) = &node.disco_key {
687 keys.push(disco);
688 }
689
690 if let Some(fqdn) = &fqdn {
691 keys.push(fqdn);
692 }
693
694 for k in keys {
695 let lookup_id = k.lookup(db).unwrap();
696 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
697
698 let (lookup_id, lookup_node) = db.get(k).unwrap();
699 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
700 assert_eq!(lookup_node, node, "wrong node for key {k:?}");
701 }
702
703 node.hostname.lookup(db).unwrap();
705
706 for &route in &node.accepted_routes {
707 let routes = db.get_route(route).collect::<Vec<_>>();
712 assert!(!routes.is_empty());
713
714 for (found_id, found_node) in routes {
715 if found_id == id {
716 assert_eq!(found_node, node);
717 break;
718 }
719
720 let has_subset = found_node
721 .accepted_routes
722 .iter()
723 .any(|found_route| route.contains(found_route));
724
725 assert!(has_subset);
726 }
727 }
728 }
729
730 fn assert_has_routes_exact(db: &PeerDb, node: &Node, id: PeerId) {
733 for &route in &node.accepted_routes {
734 let match_exists = db
735 .get_route(route)
736 .any(|(found_id, found_node)| found_id == id && found_node == node);
737
738 assert!(match_exists);
739 }
740 }
741
742 #[test]
743 fn test_indices() {
744 let mut db = PeerDb::default();
745 let node = rand_node();
746 let id = db.upsert(&node);
747
748 validate_indices(&db, &node, id);
749 assert_has_routes_exact(&db, &node, id);
750 }
751
752 #[test]
753 fn test_names() {
754 let mut db = PeerDb::default();
755
756 let node1 = Node {
757 hostname: "test".to_string(),
758 tailnet: Some("ts.net".to_string()),
759 ..rand_node()
760 };
761 let node2 = Node {
762 hostname: "test".to_string(),
763 tailnet: Some("ts2.net".to_string()),
764 ..rand_node()
765 };
766 let node3 = Node {
767 hostname: "test".to_string(),
768 tailnet: None,
769 ..rand_node()
770 };
771
772 let id1 = db.upsert(&node1);
773 let id2 = db.upsert(&node2);
774 let id3 = db.upsert(&node3);
775
776 let nodes = [(id1, &node1), (id2, &node2), (id3, &node3)];
777
778 for (id, node) in &nodes {
779 validate_indices(&db, node, *id);
780 }
781
782 let (id, node) = db.get(&"test").unwrap();
783 assert!(nodes.iter().any(|(x, _node)| *x == id));
784
785 for &(x, curnode) in &nodes {
786 if x == id {
787 assert_eq!(node, curnode);
788 } else {
789 assert_ne!(node, curnode);
790 }
791 }
792
793 let (id, node) = db.get(&"test.ts.net").unwrap();
794 assert_eq!(id, id1);
795 assert_eq!(node, &node1);
796
797 let (id, node) = db.get(&"test.ts2.net").unwrap();
798 assert_eq!(id, id2);
799 assert_eq!(node, &node2);
800 }
801
802 #[test]
803 fn test_name_lookup_is_canonicalized() {
804 let mut db = PeerDb::default();
807
808 let node = Node {
809 hostname: "MixedCase".to_string(),
810 tailnet: Some("Tail-Scale.ts.net".to_string()),
811 ..rand_node()
812 };
813 let id = db.upsert(&node);
814
815 assert_eq!(db.get(&"mixedcase").unwrap().0, id);
817 assert_eq!(db.get(&"MIXEDCASE").unwrap().0, id);
818
819 assert_eq!(db.get(&"mixedcase.tail-scale.ts.net").unwrap().0, id);
821 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
824 db.remove(&id);
826 assert!(db.get(&"mixedcase").is_none());
827 assert!(db.get(&"mixedcase.tail-scale.ts.net").is_none());
828 assert!(db.index_state.is_empty());
829 }
830
831 #[test]
832 fn disco_key_reassigned_across_peers_no_panic() {
833 let mut db = PeerDb::default();
837
838 let disco: DiscoPublicKey = [7u8; 32].into();
839
840 let node_a = Node {
841 disco_key: Some(disco),
842 ..rand_node()
843 };
844 let id_a = db.upsert(&node_a);
845
846 let node_b = Node {
848 disco_key: Some(disco),
849 ..rand_node()
850 };
851 let id_b = db.upsert(&node_b);
852 assert_ne!(id_a, id_b);
853
854 let node_a2 = Node {
857 disco_key: None,
858 ..node_a.clone()
859 };
860 let id_a2 = db.upsert(&node_a2);
861 assert_eq!(id_a, id_a2);
862
863 assert_eq!(disco.lookup(&db), Some(id_b));
865 }
866
867 #[test]
873 fn either_known_disco_key_resolves_on_ingress() {
874 let mut db = PeerDb::default();
875
876 let active: DiscoPublicKey = [1u8; 32].into();
877 let inactive: DiscoPublicKey = [2u8; 32].into();
878 let stranger: DiscoPublicKey = [3u8; 32].into();
879
880 let node = Node {
881 disco_key: Some(active),
882 ..rand_node()
883 };
884 let id = db.upsert(&node);
885
886 assert!(
887 db.peer_by_known_disco_key(&inactive).is_none(),
888 "a peer with one key resolves only that key"
889 );
890
891 db.set_inactive_disco_key(id, Some(inactive));
892
893 let (got, _, matched) = db
894 .peer_by_known_disco_key(&active)
895 .expect("active resolves");
896 assert_eq!((got, matched), (id, DiscoKeyMatch::Active));
897
898 let (got, _, matched) = db
899 .peer_by_known_disco_key(&inactive)
900 .expect("the other known key resolves too");
901 assert_eq!((got, matched), (id, DiscoKeyMatch::Inactive));
902
903 assert!(
904 db.peer_by_known_disco_key(&stranger).is_none(),
905 "a key in neither slot is refused — the whole security value of the check"
906 );
907 assert_eq!(
908 inactive.lookup(&db),
909 None,
910 "and the send-side disco index still carries only the active key"
911 );
912
913 db.set_inactive_disco_key(id, None);
915 assert!(db.peer_by_known_disco_key(&inactive).is_none());
916
917 db.set_inactive_disco_key(id, Some(inactive));
919 db.remove(&id);
920 assert!(db.peer_by_known_disco_key(&inactive).is_none());
921 assert!(db.index_state.is_empty());
922 }
923
924 #[test]
932 fn a_key_claimed_by_two_peers_resolves_to_the_one_using_it() {
933 let mut db = PeerDb::default();
934
935 let shared: DiscoPublicKey = [9u8; 32].into();
936
937 let node_a = Node {
938 disco_key: Some([1u8; 32].into()),
939 ..rand_node()
940 };
941 let id_a = db.upsert(&node_a);
942 db.set_inactive_disco_key(id_a, Some(shared));
943
944 let node_b = Node {
946 disco_key: Some([2u8; 32].into()),
947 ..rand_node()
948 };
949 let id_b = db.upsert(&node_b);
950 db.set_inactive_disco_key(id_b, Some(shared));
951 assert_eq!(
952 db.peer_by_known_disco_key(&shared)
953 .map(|(id, _, m)| (id, m)),
954 Some((id_b, DiscoKeyMatch::Inactive))
955 );
956
957 db.set_inactive_disco_key(id_a, None);
959 assert_eq!(
960 db.peer_by_known_disco_key(&shared)
961 .map(|(id, _, m)| (id, m)),
962 Some((id_b, DiscoKeyMatch::Inactive)),
963 "retracting another peer's stale claim must not clobber the live one"
964 );
965
966 let node_c = Node {
968 disco_key: Some(shared),
969 ..rand_node()
970 };
971 let id_c = db.upsert(&node_c);
972 assert_eq!(
973 db.peer_by_known_disco_key(&shared)
974 .map(|(id, _, m)| (id, m)),
975 Some((id_c, DiscoKeyMatch::Active)),
976 "the peer sending under the key beats one that merely still knows it"
977 );
978 }
979
980 #[test]
981 fn ip_reassigned_across_peers_no_panic() {
982 let mut db = PeerDb::default();
985
986 let shared = TailnetAddress {
987 ipv4: Ipv4Addr::new(100, 64, 0, 1).into(),
988 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 1).into(),
989 };
990
991 let node_a = Node {
992 addresses: vec![shared.ipv4.into(), shared.ipv6.into()],
993 tailnet_address: shared.clone(),
994 ..rand_node()
995 };
996 let id_a = db.upsert(&node_a);
997
998 let node_b = Node {
1000 addresses: vec![shared.ipv4.into(), shared.ipv6.into()],
1001 tailnet_address: shared.clone(),
1002 ..rand_node()
1003 };
1004 let id_b = db.upsert(&node_b);
1005 assert_ne!(id_a, id_b);
1006
1007 let renumbered = TailnetAddress {
1010 ipv4: Ipv4Addr::new(100, 64, 0, 2).into(),
1011 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 2).into(),
1012 };
1013 let node_a2 = Node {
1014 addresses: vec![renumbered.ipv4.into(), renumbered.ipv6.into()],
1015 tailnet_address: renumbered,
1016 ..node_a.clone()
1017 };
1018 let id_a2 = db.upsert(&node_a2);
1019 assert_eq!(id_a, id_a2);
1020
1021 assert_eq!(
1023 IpAddr::from(Ipv4Addr::new(100, 64, 0, 1)).lookup(&db),
1024 Some(id_b)
1025 );
1026 assert_eq!(
1028 IpAddr::from(Ipv4Addr::new(100, 64, 0, 2)).lookup(&db),
1029 Some(id_a)
1030 );
1031 }
1032
1033 #[test]
1034 fn node_key_or_stableid_churn_no_panic() {
1035 let mut db = PeerDb::default();
1039
1040 let key: NodePublicKey = [9u8; 32].into();
1041
1042 let node_a = Node {
1043 node_key: key,
1044 ..rand_node()
1045 };
1046 let id_a = db.upsert(&node_a);
1047
1048 let node_b = Node {
1050 node_key: key,
1051 ..rand_node()
1052 };
1053 let id_b = db.upsert(&node_b);
1054 assert_ne!(id_a, id_b);
1055
1056 let node_a2 = Node {
1059 node_key: [10u8; 32].into(),
1060 ..node_a.clone()
1061 };
1062 let id_a2 = db.upsert(&node_a2);
1063 assert_eq!(id_a, id_a2);
1064
1065 assert_eq!(key.lookup(&db), Some(id_b));
1067 assert_eq!(NodePublicKey::from([10u8; 32]).lookup(&db), Some(id_a));
1068 }
1069
1070 proptest::prop_compose! {
1071 fn ipv4net()(
1072 addr: Ipv4Addr,
1073 pfx in 0u8..=32,
1074 ) -> ipnet::Ipv4Net {
1075 ipnet::Ipv4Net::new(addr, pfx).unwrap().trunc()
1076 }
1077 }
1078
1079 proptest::prop_compose! {
1080 fn ipv6net()(
1081 addr: Ipv6Addr,
1082 pfx in 0u8..=32,
1083 ) -> ipnet::Ipv6Net {
1084 ipnet::Ipv6Net::new(addr, pfx).unwrap().trunc()
1085 }
1086 }
1087
1088 fn ipnet() -> impl Strategy<Value = ipnet::IpNet> {
1089 proptest::prop_oneof![
1090 ipv4net().prop_map(ipnet::IpNet::from),
1091 ipv6net().prop_map(ipnet::IpNet::from)
1092 ]
1093 }
1094
1095 proptest::prop_compose! {
1096 fn domain_segment()(
1100 seg in "[a-z][a-z0-9]*"
1101 ) -> String {
1102 seg
1103 }
1104 }
1105
1106 proptest::prop_compose! {
1107 fn domain(max_count: usize)(
1108 segs in proptest::collection::vec(domain_segment(), 0..max_count)
1109 ) -> String {
1110 segs.join(".")
1111 }
1112 }
1113
1114 type Key = [u8; 32];
1115
1116 proptest::prop_compose! {
1117 fn nodes(n: usize)(
1120 id in hash_set(any::<i64>(), n),
1121 stable_id in hash_set(".+", n),
1122 tags in vec(hash_set(".+", 0..32), n),
1123 accepted_routes in vec(hash_set(ipnet(), 0..32), n),
1124 node_key in hash_set(any::<Key>(), n),
1125 machine_key in vec(any::<Option<Key>>(), n),
1126 disco_key in vec(any::<Option<Key>>(), n),
1127 ipv4 in hash_set(any::<Ipv4Addr>(), n),
1128 ipv6 in hash_set(any::<Ipv6Addr>(), n),
1129 name in hash_set(domain_segment(), n),
1130 tailnet in vec(domain(5), n),
1131 has_tailnet in vec(any::<bool>(), n),
1132 derp_region in vec(any::<Option<NonZeroU32>>(), n),
1133 underlay_addrs in vec(any::<HashSet<SocketAddr>>(), n),
1134 ) -> Vec<Node> {
1135 itertools::izip![
1136 id,
1137 stable_id,
1138 tags,
1139 accepted_routes,
1140 node_key,
1141 machine_key,
1142 disco_key,
1143 ipv4,
1144 ipv6,
1145 name,
1146 tailnet,
1147 has_tailnet,
1148 derp_region,
1149 underlay_addrs,
1150 ].map(|(
1151 id,
1152 stable_id,
1153 tags,
1154 mut accepted_routes,
1155 node_key,
1156 machine_key,
1157 disco_key,
1158 ipv4,
1159 ipv6,
1160 name,
1161 tailnet,
1162 has_tailnet,
1163 derp_region,
1164 underlay_addrs,
1165 )| {
1166 accepted_routes.insert(ipnet::Ipv4Net::from(ipv4).into());
1167 accepted_routes.insert(ipnet::Ipv6Net::from(ipv6).into());
1168
1169 Node {
1170 id,
1171 stable_id: StableNodeId(stable_id),
1172
1173 hostname: name,
1174 user_id: 0,
1175 tailnet: has_tailnet.then_some(tailnet),
1176
1177 node_key: node_key.into(),
1178 key_signature: vec![],
1179 disco_key: disco_key.map(Into::into),
1180 machine_key: machine_key.map(Into::into),
1181
1182 node_key_expiry: None,
1183 online: None,
1184 last_seen: None,
1185
1186 addresses: vec![
1187 ipnet::IpNet::V4(ipv4.into()),
1188 ipnet::IpNet::V6(ipv6.into()),
1189 ],
1190 tailnet_address: TailnetAddress {
1191 ipv4: ipv4.into(),
1192 ipv6: ipv6.into(),
1193 },
1194 tags: tags.into_iter().collect(),
1195
1196 derp_region: derp_region.map(ts_derp::RegionId),
1197
1198 accepted_routes: accepted_routes.into_iter().collect(),
1199 underlay_addresses: underlay_addrs.into_iter().collect(),
1200
1201 cap: Default::default(),
1202 cap_map: Default::default(),
1203 peerapi_port: None,
1204 peerapi_dns_proxy: false,
1205 is_wireguard_only: false,
1206 exit_node_dns_resolvers: vec![],
1207 peer_relay: false,
1208 ssh_host_keys: vec![],
1209 service_vips: Default::default(),
1210 unsigned_peer_api_only: false,
1211 }
1212 })
1213 .collect()
1214 }
1215 }
1216
1217 proptest::proptest! {
1218 #[test]
1219 fn prop_one_node_indices(mut nodes in nodes(1)) {
1220 let node = nodes.pop().unwrap();
1221
1222 let mut db = PeerDb::default();
1223 let id = db.upsert(&node);
1224
1225 validate_indices(&db, &node, id);
1226 assert_has_routes_exact(&db, &node, id);
1227 }
1228
1229 #[test]
1230 fn prop_many_nodes_indexed(nodes in nodes(16)) {
1231 let mut db = PeerDb::default();
1232
1233 let mut nodes_by_id = HashMap::new();
1234
1235 for node in &nodes {
1236 let id = db.upsert(node);
1237 nodes_by_id.insert(id, node.clone());
1238 }
1239
1240 for (id, node) in &nodes_by_id {
1241 validate_indices(&db, node, *id);
1242 }
1243 }
1244
1245 #[test]
1246 fn prop_remove(nodes in nodes(16)) {
1247 let mut db = PeerDb::default();
1248
1249 let mut ids = vec![];
1250
1251 for node in &nodes {
1252 ids.push((db.upsert(node), node));
1253 }
1254
1255 for (id, node) in ids {
1256 let (removed_id, removed_node) = db.remove(&id).unwrap();
1257
1258 proptest::prop_assert_eq!(removed_id, id);
1259 proptest::prop_assert_eq!(&removed_node, node);
1260 }
1261
1262 proptest::prop_assert!(db.peers.is_empty());
1263 proptest::prop_assert!(db.index_state.is_empty());
1264 }
1265 }
1266}