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 delete_if_owned(idx, key, id);
167 }
168 },
169 |new, idx| {
170 if let Some(key) = &new.disco_key {
171 idx.insert(*key, id);
172 }
173 },
174 );
175
176 maybe_update(
189 new,
190 old,
191 |x| (&x.hostname, &x.tailnet),
192 &mut self.index_state.name_idx,
193 |old, idx| {
194 delete_if_owned(idx, &canon_name(&old.hostname), id);
195
196 if let Some(fqdn) = old.fqdn_opt(false) {
197 delete_if_owned(idx, &canon_name(&fqdn), id);
198 }
199 },
200 |new, idx| {
201 idx.insert(canon_name(&new.hostname), id);
202
203 if let Some(fqdn) = new.fqdn_opt(false) {
204 idx.insert(canon_name(&fqdn), id);
205 }
206 },
207 );
208
209 maybe_update(
210 new,
211 old,
212 |x| &x.tailnet_address,
213 &mut self.index_state.ip_idx,
214 |old, idx| {
215 delete_ip_if_owned(idx, old.tailnet_address.ipv4.into(), id);
216 delete_ip_if_owned(idx, old.tailnet_address.ipv6.into(), id);
217 },
218 |new, idx| {
219 idx.insert(new.tailnet_address.ipv4.into(), id);
220 idx.insert(new.tailnet_address.ipv6.into(), id);
221 },
222 );
223
224 maybe_update(
225 new,
226 old,
227 |x| &x.accepted_routes,
228 &mut self.index_state,
229 |old, idx| {
230 for &route in &old.accepted_routes {
231 idx.remove_route(route, id);
232 }
233 },
234 |new, idx| {
235 for &route in &new.accepted_routes {
236 idx.route_idx.modify(route, |val| {
237 if let Some(val) = val {
238 val.push(id);
239 return RouteModification::Noop;
240 }
241
242 RouteModification::Insert(smallvec::smallvec![id])
243 });
244 }
245 },
246 );
247
248 self.peers.insert(id, new.clone());
249
250 id
251 }
252
253 pub fn remove(&mut self, field: &dyn IndexedField) -> Option<(PeerId, Node)> {
255 let id = field.lookup(self)?;
256
257 let node = self.peers.remove(&id)?;
258 self.index_state.remove(id, &node);
259
260 Some((id, node))
261 }
262
263 pub fn get(&self, field: &dyn IndexedField) -> Option<(PeerId, &Node)> {
265 let id = field.lookup(self)?;
266 let peer = self.peers.get(&id)?;
267
268 Some((id, peer))
269 }
270
271 pub fn get_route(&self, route: ipnet::IpNet) -> impl Iterator<Item = (PeerId, &Node)> {
273 self.index_state
276 .route_idx
277 .lookup_prefix(route)
278 .into_iter()
279 .flat_map(|x| x.iter())
280 .map(|&id| (id, self.peers.get(&id).unwrap()))
281 }
282
283 pub fn has(&self, field: &dyn IndexedField) -> Option<PeerId> {
285 field.lookup(self)
286 }
287
288 pub fn peer_by_known_disco_key(
302 &self,
303 key: &DiscoPublicKey,
304 ) -> Option<(PeerId, &Node, DiscoKeyMatch)> {
305 let (id, matched) = match self.index_state.disco_idx.get(key) {
306 Some(&id) => (id, DiscoKeyMatch::Active),
307 None => (
308 self.index_state.inactive_disco_idx.get(key).copied()?,
309 DiscoKeyMatch::Inactive,
310 ),
311 };
312
313 Some((id, self.peers.get(&id)?, matched))
314 }
315
316 pub fn set_inactive_disco_key(&mut self, id: PeerId, key: Option<DiscoPublicKey>) {
324 let idx = &mut self.index_state;
325
326 if let Some(previous) = idx.inactive_disco.remove(&id)
328 && idx
329 .inactive_disco_idx
330 .get(&previous)
331 .is_some_and(|&x| x == id)
332 {
333 idx.inactive_disco_idx.remove(&previous);
334 }
335
336 if let Some(key) = key {
337 idx.inactive_disco.insert(id, key);
338 idx.inactive_disco_idx.insert(key, id);
339 }
340 }
341
342 pub const fn peers(&self) -> &HashMap<PeerId, Node> {
344 &self.peers
345 }
346
347 pub fn retain(&mut self, mut predicate: impl FnMut(PeerId, &Node) -> bool) {
349 self.peers.retain(|&id, node| {
350 let retain = predicate(id, node);
351
352 if !retain {
353 self.index_state.remove(id, node);
354 }
355
356 retain
357 });
358 }
359}
360
361impl IndexState {
362 fn remove(&mut self, id: PeerId, node: &Node) {
372 delete_if_owned(&mut self.nk_idx, &node.node_key, id);
373 delete_if_owned(&mut self.stableid_idx, &node.stable_id, id);
374 delete_if_owned(&mut self.control_idx, &node.id, id);
375
376 delete_ip_if_owned(&mut self.ip_idx, node.tailnet_address.ipv4.into(), id);
377 delete_ip_if_owned(&mut self.ip_idx, node.tailnet_address.ipv6.into(), id);
378
379 delete_if_owned(&mut self.name_idx, &canon_name(&node.hostname), id);
380
381 if let Some(fqdn) = node.fqdn_opt(false) {
382 delete_if_owned(&mut self.name_idx, &canon_name(&fqdn), id);
383 }
384
385 for route in &node.accepted_routes {
386 self.remove_route(*route, id);
387 }
388
389 if let Some(disco) = &node.disco_key {
390 delete_if_owned(&mut self.disco_idx, disco, id);
391 }
392
393 if let Some(key) = self.inactive_disco.remove(&id) {
394 delete_if_owned(&mut self.inactive_disco_idx, &key, id);
395 }
396 }
397
398 fn remove_route(&mut self, route: ipnet::IpNet, id: PeerId) {
400 self.route_idx.modify(route, |val| match val {
401 Some(val) => {
402 let mut some_matched = false;
403
404 val.retain(|&mut x| {
405 let ids_match = x == id;
406 if ids_match {
407 some_matched = true;
408 }
409
410 !ids_match
411 });
412
413 assert!(some_matched);
414
415 if val.is_empty() {
416 RouteModification::Remove
417 } else {
418 RouteModification::Noop
419 }
420 }
421 None => RouteModification::Noop,
422 });
423 }
424
425 #[cfg(test)]
426 fn is_empty(&self) -> bool {
427 self.nk_idx.is_empty()
428 && self.stableid_idx.is_empty()
429 && self.control_idx.is_empty()
430 && self.ip_idx.size() == 0
431 && self.name_idx.is_empty()
432 && self.route_idx.size() == 0
433 && self.disco_idx.is_empty()
434 && self.inactive_disco_idx.is_empty()
435 && self.inactive_disco.is_empty()
436 }
437}
438
439fn delete_if_owned<T: Eq + Hash>(idx: &mut Index<T>, key: &T, id: PeerId) {
448 if idx.get(key).is_some_and(|&x| x == id) {
449 idx.remove(key);
450 }
451}
452
453fn delete_ip_if_owned(idx: &mut ts_bart::Table<PeerId>, prefix: ipnet::IpNet, id: PeerId) {
458 if idx.lookup_prefix_exact(prefix).is_some_and(|&x| x == id) {
459 idx.remove(prefix);
460 }
461}
462
463fn maybe_update<'n, T, Idx>(
473 new: &'n Node,
474 old: Option<&'n Node>,
475 accessor: impl Fn(&'n Node) -> T,
476 idx: &mut Idx,
477 mut remove: impl FnMut(&'n Node, &mut Idx),
478 mut insert: impl FnMut(&'n Node, &mut Idx),
479) where
480 T: PartialEq + 'n,
481{
482 match old {
483 Some(old) if accessor(old) == accessor(new) => {
484 return;
485 }
486 Some(x) => {
487 remove(x, idx);
488 }
489 None => {}
490 }
491
492 insert(new, idx)
493}
494
495fn maybe_update_idx<T>(
497 new: &Node,
498 old: Option<&Node>,
499 accessor: impl Fn(&Node) -> &T,
500 idx: &mut Index<T>,
501 new_id: PeerId,
502) where
503 T: Eq + Hash + Clone,
504{
505 maybe_update(
506 new,
507 old,
508 &accessor,
509 idx,
510 |old, idx| {
511 delete_if_owned(idx, accessor(old), new_id);
512 },
513 |new, idx| {
514 idx.insert(accessor(new).clone(), new_id);
515 },
516 )
517}
518
519impl IndexedField for PeerId {
520 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
521 if db.peers.contains_key(self) {
522 Some(*self)
523 } else {
524 None
525 }
526 }
527}
528
529impl IndexedField for NodePublicKey {
530 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
531 db.index_state.nk_idx.get(self).copied()
532 }
533}
534
535impl IndexedField for DiscoPublicKey {
536 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
537 db.index_state.disco_idx.get(self).copied()
538 }
539}
540
541impl IndexedField for StableNodeId {
542 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
543 db.index_state.stableid_idx.get(self).copied()
544 }
545}
546
547impl IndexedField for ts_control::NodeId {
548 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
549 db.index_state.control_idx.get(self).copied()
550 }
551}
552
553impl IndexedField for PeerName {
554 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
555 db.index_state.name_idx.get(&canon_name(self)).copied()
556 }
557}
558
559impl IndexedField for &str {
560 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
561 db.index_state.name_idx.get(&canon_name(self)).copied()
562 }
563}
564
565impl IndexedField for IpAddr {
566 fn lookup(&self, db: &PeerDb) -> Option<PeerId> {
567 db.index_state.ip_idx.lookup(*self).copied()
568 }
569}
570
571#[cfg(test)]
572mod test {
573 use std::{
574 collections::{HashMap, HashSet},
575 net::{Ipv4Addr, Ipv6Addr, SocketAddr},
576 num::NonZeroU32,
577 };
578
579 use proptest::{
580 collection::{hash_set, vec},
581 prelude::any,
582 strategy::Strategy,
583 };
584 use rand::{
585 RngExt,
586 distr::{Alphanumeric, SampleString},
587 };
588 use ts_control::TailnetAddress;
589
590 use super::*;
591
592 fn rand_string(rng: &mut dyn rand::Rng, max_len: usize) -> String {
593 let len = rng.random_range(1..max_len);
594 Alphanumeric.sample_string(rng, len)
595 }
596
597 fn rand_route(rng: &mut dyn rand::Rng) -> ipnet::IpNet {
598 if rng.random::<bool>() {
599 let ip = rand_ipv4(rng);
600 ipnet::Ipv4Net::new(ip, rand::random_range(0..=32))
601 .unwrap()
602 .trunc()
603 .into()
604 } else {
605 let ip = rand_ipv6(rng);
606 ipnet::Ipv6Net::new(ip, rand::random_range(0..=128))
607 .unwrap()
608 .trunc()
609 .into()
610 }
611 }
612
613 fn rand_ipv4(rng: &mut dyn rand::Rng) -> Ipv4Addr {
614 Ipv4Addr::from_octets(rng.random::<[u8; 4]>())
615 }
616
617 fn rand_ipv6(rng: &mut dyn rand::Rng) -> Ipv6Addr {
618 Ipv6Addr::from_segments(rng.random::<[u16; 8]>())
619 }
620
621 fn rand_node() -> Node {
622 let mut rng = rand::rng();
623 let tailnet_address = TailnetAddress {
624 ipv4: rand_ipv4(&mut rng).into(),
625 ipv6: rand_ipv6(&mut rng).into(),
626 };
627
628 Node {
629 stable_id: StableNodeId(rand_string(&mut rng, 32)),
630 addresses: vec![tailnet_address.ipv4.into(), tailnet_address.ipv6.into()],
631 tailnet_address,
632 node_key: rng.random::<[u8; 32]>().into(),
633 key_signature: vec![],
634 disco_key: rng
635 .random::<bool>()
636 .then_some(rng.random::<[u8; 32]>().into()),
637 machine_key: rng
638 .random::<bool>()
639 .then_some(rng.random::<[u8; 32]>().into()),
640 id: rng.random(),
641 accepted_routes: (0..rng.random_range(0..32))
642 .map(|_| rand_route(&mut rng))
643 .collect(),
644
645 hostname: rand_string(&mut rng, 32),
646 user_id: rng.random(),
647 tailnet: rng.random::<bool>().then_some(rand_string(&mut rng, 32)),
648
649 node_key_expiry: None,
650 expired: false,
651 online: None,
652 last_seen: None,
653 underlay_addresses: vec![],
654 derp_region: rng
655 .random::<bool>()
656 .then_some(ts_derp::RegionId(rng.random())),
657
658 tags: (0..rng.random_range(0..8))
659 .map(|_| rand_string(&mut rng, 32))
660 .collect(),
661
662 cap: Default::default(),
663 cap_map: Default::default(),
664 peerapi_port: None,
665 peerapi_dns_proxy: false,
666 is_wireguard_only: false,
667 exit_node_dns_resolvers: vec![],
668 peer_relay: false,
669 ssh_host_keys: vec![],
670 service_vips: Default::default(),
671 unsigned_peer_api_only: false,
672 }
673 }
674
675 fn validate_indices(db: &PeerDb, node: &Node, id: PeerId) {
676 let ipv4 = IpAddr::from(node.tailnet_address.ipv4.addr());
677 let ipv6 = IpAddr::from(node.tailnet_address.ipv6.addr());
678 let fqdn = node.fqdn_opt(false);
679
680 let mut keys: Vec<&dyn IndexedField> =
681 vec![&id, &node.node_key, &node.stable_id, &node.id, &ipv4, &ipv6];
682
683 if let Some(disco) = &node.disco_key {
684 keys.push(disco);
685 }
686
687 if let Some(fqdn) = &fqdn {
688 keys.push(fqdn);
689 }
690
691 for k in keys {
692 let lookup_id = k.lookup(db).unwrap();
693 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
694
695 let (lookup_id, lookup_node) = db.get(k).unwrap();
696 assert_eq!(lookup_id, id, "wrong id for key {k:?}");
697 assert_eq!(lookup_node, node, "wrong node for key {k:?}");
698 }
699
700 node.hostname.lookup(db).unwrap();
702
703 for &route in &node.accepted_routes {
704 let routes = db.get_route(route).collect::<Vec<_>>();
709 assert!(!routes.is_empty());
710
711 for (found_id, found_node) in routes {
712 if found_id == id {
713 assert_eq!(found_node, node);
714 break;
715 }
716
717 let has_subset = found_node
718 .accepted_routes
719 .iter()
720 .any(|found_route| route.contains(found_route));
721
722 assert!(has_subset);
723 }
724 }
725 }
726
727 fn assert_has_routes_exact(db: &PeerDb, node: &Node, id: PeerId) {
730 for &route in &node.accepted_routes {
731 let match_exists = db
732 .get_route(route)
733 .any(|(found_id, found_node)| found_id == id && found_node == node);
734
735 assert!(match_exists);
736 }
737 }
738
739 #[test]
740 fn test_indices() {
741 let mut db = PeerDb::default();
742 let node = rand_node();
743 let id = db.upsert(&node);
744
745 validate_indices(&db, &node, id);
746 assert_has_routes_exact(&db, &node, id);
747 }
748
749 #[test]
750 fn test_names() {
751 let mut db = PeerDb::default();
752
753 let node1 = Node {
754 hostname: "test".to_string(),
755 tailnet: Some("ts.net".to_string()),
756 ..rand_node()
757 };
758 let node2 = Node {
759 hostname: "test".to_string(),
760 tailnet: Some("ts2.net".to_string()),
761 ..rand_node()
762 };
763 let node3 = Node {
764 hostname: "test".to_string(),
765 tailnet: None,
766 ..rand_node()
767 };
768
769 let id1 = db.upsert(&node1);
770 let id2 = db.upsert(&node2);
771 let id3 = db.upsert(&node3);
772
773 let nodes = [(id1, &node1), (id2, &node2), (id3, &node3)];
774
775 for (id, node) in &nodes {
776 validate_indices(&db, node, *id);
777 }
778
779 let (id, node) = db.get(&"test").unwrap();
780 assert!(nodes.iter().any(|(x, _node)| *x == id));
781
782 for &(x, curnode) in &nodes {
783 if x == id {
784 assert_eq!(node, curnode);
785 } else {
786 assert_ne!(node, curnode);
787 }
788 }
789
790 let (id, node) = db.get(&"test.ts.net").unwrap();
791 assert_eq!(id, id1);
792 assert_eq!(node, &node1);
793
794 let (id, node) = db.get(&"test.ts2.net").unwrap();
795 assert_eq!(id, id2);
796 assert_eq!(node, &node2);
797 }
798
799 #[test]
800 fn test_name_lookup_is_canonicalized() {
801 let mut db = PeerDb::default();
804
805 let node = Node {
806 hostname: "MixedCase".to_string(),
807 tailnet: Some("Tail-Scale.ts.net".to_string()),
808 ..rand_node()
809 };
810 let id = db.upsert(&node);
811
812 assert_eq!(db.get(&"mixedcase").unwrap().0, id);
814 assert_eq!(db.get(&"MIXEDCASE").unwrap().0, id);
815
816 assert_eq!(db.get(&"mixedcase.tail-scale.ts.net").unwrap().0, id);
818 assert_eq!(db.get(&"MixedCase.Tail-Scale.TS.NET").unwrap().0, id);
819 assert_eq!(db.get(&"mixedcase.tail-scale.ts.net.").unwrap().0, id);
820
821 db.remove(&id);
823 assert!(db.get(&"mixedcase").is_none());
824 assert!(db.get(&"mixedcase.tail-scale.ts.net").is_none());
825 assert!(db.index_state.is_empty());
826 }
827
828 #[test]
829 fn disco_key_reassigned_across_peers_no_panic() {
830 let mut db = PeerDb::default();
834
835 let disco: DiscoPublicKey = [7u8; 32].into();
836
837 let node_a = Node {
838 disco_key: Some(disco),
839 ..rand_node()
840 };
841 let id_a = db.upsert(&node_a);
842
843 let node_b = Node {
845 disco_key: Some(disco),
846 ..rand_node()
847 };
848 let id_b = db.upsert(&node_b);
849 assert_ne!(id_a, id_b);
850
851 let node_a2 = Node {
854 disco_key: None,
855 ..node_a.clone()
856 };
857 let id_a2 = db.upsert(&node_a2);
858 assert_eq!(id_a, id_a2);
859
860 assert_eq!(disco.lookup(&db), Some(id_b));
862 }
863
864 #[test]
870 fn either_known_disco_key_resolves_on_ingress() {
871 let mut db = PeerDb::default();
872
873 let active: DiscoPublicKey = [1u8; 32].into();
874 let inactive: DiscoPublicKey = [2u8; 32].into();
875 let stranger: DiscoPublicKey = [3u8; 32].into();
876
877 let node = Node {
878 disco_key: Some(active),
879 ..rand_node()
880 };
881 let id = db.upsert(&node);
882
883 assert!(
884 db.peer_by_known_disco_key(&inactive).is_none(),
885 "a peer with one key resolves only that key"
886 );
887
888 db.set_inactive_disco_key(id, Some(inactive));
889
890 let (got, _, matched) = db
891 .peer_by_known_disco_key(&active)
892 .expect("active resolves");
893 assert_eq!((got, matched), (id, DiscoKeyMatch::Active));
894
895 let (got, _, matched) = db
896 .peer_by_known_disco_key(&inactive)
897 .expect("the other known key resolves too");
898 assert_eq!((got, matched), (id, DiscoKeyMatch::Inactive));
899
900 assert!(
901 db.peer_by_known_disco_key(&stranger).is_none(),
902 "a key in neither slot is refused — the whole security value of the check"
903 );
904 assert_eq!(
905 inactive.lookup(&db),
906 None,
907 "and the send-side disco index still carries only the active key"
908 );
909
910 db.set_inactive_disco_key(id, None);
912 assert!(db.peer_by_known_disco_key(&inactive).is_none());
913
914 db.set_inactive_disco_key(id, Some(inactive));
916 db.remove(&id);
917 assert!(db.peer_by_known_disco_key(&inactive).is_none());
918 assert!(db.index_state.is_empty());
919 }
920
921 #[test]
929 fn a_key_claimed_by_two_peers_resolves_to_the_one_using_it() {
930 let mut db = PeerDb::default();
931
932 let shared: DiscoPublicKey = [9u8; 32].into();
933
934 let node_a = Node {
935 disco_key: Some([1u8; 32].into()),
936 ..rand_node()
937 };
938 let id_a = db.upsert(&node_a);
939 db.set_inactive_disco_key(id_a, Some(shared));
940
941 let node_b = Node {
943 disco_key: Some([2u8; 32].into()),
944 ..rand_node()
945 };
946 let id_b = db.upsert(&node_b);
947 db.set_inactive_disco_key(id_b, Some(shared));
948 assert_eq!(
949 db.peer_by_known_disco_key(&shared)
950 .map(|(id, _, m)| (id, m)),
951 Some((id_b, DiscoKeyMatch::Inactive))
952 );
953
954 db.set_inactive_disco_key(id_a, None);
956 assert_eq!(
957 db.peer_by_known_disco_key(&shared)
958 .map(|(id, _, m)| (id, m)),
959 Some((id_b, DiscoKeyMatch::Inactive)),
960 "retracting another peer's stale claim must not clobber the live one"
961 );
962
963 let node_c = Node {
965 disco_key: Some(shared),
966 ..rand_node()
967 };
968 let id_c = db.upsert(&node_c);
969 assert_eq!(
970 db.peer_by_known_disco_key(&shared)
971 .map(|(id, _, m)| (id, m)),
972 Some((id_c, DiscoKeyMatch::Active)),
973 "the peer sending under the key beats one that merely still knows it"
974 );
975 }
976
977 #[test]
978 fn ip_reassigned_across_peers_no_panic() {
979 let mut db = PeerDb::default();
982
983 let shared = TailnetAddress {
984 ipv4: Ipv4Addr::new(100, 64, 0, 1).into(),
985 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 1).into(),
986 };
987
988 let node_a = Node {
989 addresses: vec![shared.ipv4.into(), shared.ipv6.into()],
990 tailnet_address: shared.clone(),
991 ..rand_node()
992 };
993 let id_a = db.upsert(&node_a);
994
995 let node_b = Node {
997 addresses: vec![shared.ipv4.into(), shared.ipv6.into()],
998 tailnet_address: shared.clone(),
999 ..rand_node()
1000 };
1001 let id_b = db.upsert(&node_b);
1002 assert_ne!(id_a, id_b);
1003
1004 let renumbered = TailnetAddress {
1007 ipv4: Ipv4Addr::new(100, 64, 0, 2).into(),
1008 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 2).into(),
1009 };
1010 let node_a2 = Node {
1011 addresses: vec![renumbered.ipv4.into(), renumbered.ipv6.into()],
1012 tailnet_address: renumbered,
1013 ..node_a.clone()
1014 };
1015 let id_a2 = db.upsert(&node_a2);
1016 assert_eq!(id_a, id_a2);
1017
1018 assert_eq!(
1020 IpAddr::from(Ipv4Addr::new(100, 64, 0, 1)).lookup(&db),
1021 Some(id_b)
1022 );
1023 assert_eq!(
1025 IpAddr::from(Ipv4Addr::new(100, 64, 0, 2)).lookup(&db),
1026 Some(id_a)
1027 );
1028 }
1029
1030 #[test]
1040 fn a_departing_peer_does_not_evict_its_successors_index_rows() {
1041 let mut db = PeerDb::default();
1042
1043 let shared_ips = TailnetAddress {
1044 ipv4: Ipv4Addr::new(100, 64, 0, 7).into(),
1045 ipv6: Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 7).into(),
1046 };
1047 let shared_disco: DiscoPublicKey = [4u8; 32].into();
1048 let shared_node_key: NodePublicKey = [5u8; 32].into();
1049 let shared_control_id = 4242;
1050
1051 let departing = Node {
1052 stable_id: StableNodeId("departing".to_string()),
1053 id: shared_control_id,
1054 hostname: "churny".to_string(),
1055 tailnet: Some("ts.net".to_string()),
1056 addresses: vec![shared_ips.ipv4.into(), shared_ips.ipv6.into()],
1057 tailnet_address: shared_ips.clone(),
1058 node_key: shared_node_key,
1059 disco_key: Some(shared_disco),
1060 accepted_routes: Vec::new(),
1061 ..rand_node()
1062 };
1063 let departing_id = db.upsert(&departing);
1064
1065 let successor = Node {
1068 stable_id: StableNodeId("successor".to_string()),
1069 ..departing.clone()
1070 };
1071 let successor_id = db.upsert(&successor);
1072 assert_ne!(departing_id, successor_id);
1073
1074 let (removed_id, _node) = db
1075 .remove(&departing.stable_id)
1076 .expect("the departing peer is still in the db under its own stable id");
1077 assert_eq!(removed_id, departing_id);
1078
1079 assert_eq!(db.peers().len(), 1, "only the departing peer is gone");
1080 assert_eq!(
1081 departing.stable_id.lookup(&db),
1082 None,
1083 "and its own stable id no longer resolves"
1084 );
1085
1086 for (what, got) in [
1087 (
1088 "tailnet ipv4",
1089 IpAddr::from(Ipv4Addr::new(100, 64, 0, 7)).lookup(&db),
1090 ),
1091 (
1092 "tailnet ipv6",
1093 IpAddr::from(Ipv6Addr::new(0xfd7a, 0, 0, 0, 0, 0, 0, 7)).lookup(&db),
1094 ),
1095 ("hostname", "churny".lookup(&db)),
1096 ("fqdn", "churny.ts.net".lookup(&db)),
1097 ("disco key", shared_disco.lookup(&db)),
1098 ("node key", shared_node_key.lookup(&db)),
1099 ("control node id", shared_control_id.lookup(&db)),
1100 ("stable id", successor.stable_id.lookup(&db)),
1101 ] {
1102 assert_eq!(
1103 got,
1104 Some(successor_id),
1105 "the departing peer evicted the successor's {what}"
1106 );
1107 }
1108 }
1109
1110 #[test]
1114 fn a_departing_peers_inactive_disco_claim_does_not_evict_the_successors() {
1115 let mut db = PeerDb::default();
1116
1117 let shared: DiscoPublicKey = [6u8; 32].into();
1118
1119 let departing = Node {
1120 stable_id: StableNodeId("departing".to_string()),
1121 disco_key: Some([1u8; 32].into()),
1122 accepted_routes: Vec::new(),
1123 ..rand_node()
1124 };
1125 let departing_id = db.upsert(&departing);
1126 db.set_inactive_disco_key(departing_id, Some(shared));
1127
1128 let successor = Node {
1130 stable_id: StableNodeId("successor".to_string()),
1131 disco_key: Some(shared),
1132 accepted_routes: Vec::new(),
1133 ..rand_node()
1134 };
1135 let successor_id = db.upsert(&successor);
1136
1137 db.remove(&departing_id).expect("departing peer removed");
1138
1139 assert_eq!(
1140 db.peer_by_known_disco_key(&shared)
1141 .map(|(id, _, m)| (id, m)),
1142 Some((successor_id, DiscoKeyMatch::Active)),
1143 "the departing peer's stale inactive claim must not take the live path with it"
1144 );
1145 }
1146
1147 #[test]
1150 fn a_peer_removed_while_it_owns_its_rows_leaves_no_index_entries() {
1151 let mut db = PeerDb::default();
1152
1153 let node = Node {
1154 hostname: "solo".to_string(),
1155 tailnet: Some("ts.net".to_string()),
1156 disco_key: Some([3u8; 32].into()),
1157 accepted_routes: vec!["192.0.2.0/24".parse().unwrap()],
1158 ..rand_node()
1159 };
1160 let id = db.upsert(&node);
1161 db.set_inactive_disco_key(id, Some([4u8; 32].into()));
1162 validate_indices(&db, &node, id);
1163
1164 db.remove(&id).expect("the peer is in the db");
1165
1166 assert!(db.peers().is_empty());
1167 assert_eq!(node.node_key.lookup(&db), None);
1168 assert_eq!(node.stable_id.lookup(&db), None);
1169 assert_eq!(node.id.lookup(&db), None);
1170 assert_eq!(
1171 IpAddr::from(node.tailnet_address.ipv4.addr()).lookup(&db),
1172 None
1173 );
1174 assert_eq!(
1175 IpAddr::from(node.tailnet_address.ipv6.addr()).lookup(&db),
1176 None
1177 );
1178 assert_eq!("solo".lookup(&db), None);
1179 assert_eq!("solo.ts.net".lookup(&db), None);
1180 assert_eq!(node.disco_key.unwrap().lookup(&db), None);
1181 assert!(db.peer_by_known_disco_key(&[4u8; 32].into()).is_none());
1182 assert_eq!(db.get_route("192.0.2.0/24".parse().unwrap()).count(), 0);
1183 assert!(
1184 db.index_state.is_empty(),
1185 "an owned row must still be retracted — the guard is conditional, not a no-op"
1186 );
1187 }
1188
1189 #[test]
1190 fn node_key_or_stableid_churn_no_panic() {
1191 let mut db = PeerDb::default();
1195
1196 let key: NodePublicKey = [9u8; 32].into();
1197
1198 let node_a = Node {
1199 node_key: key,
1200 ..rand_node()
1201 };
1202 let id_a = db.upsert(&node_a);
1203
1204 let node_b = Node {
1206 node_key: key,
1207 ..rand_node()
1208 };
1209 let id_b = db.upsert(&node_b);
1210 assert_ne!(id_a, id_b);
1211
1212 let node_a2 = Node {
1215 node_key: [10u8; 32].into(),
1216 ..node_a.clone()
1217 };
1218 let id_a2 = db.upsert(&node_a2);
1219 assert_eq!(id_a, id_a2);
1220
1221 assert_eq!(key.lookup(&db), Some(id_b));
1223 assert_eq!(NodePublicKey::from([10u8; 32]).lookup(&db), Some(id_a));
1224 }
1225
1226 proptest::prop_compose! {
1227 fn ipv4net()(
1228 addr: Ipv4Addr,
1229 pfx in 0u8..=32,
1230 ) -> ipnet::Ipv4Net {
1231 ipnet::Ipv4Net::new(addr, pfx).unwrap().trunc()
1232 }
1233 }
1234
1235 proptest::prop_compose! {
1236 fn ipv6net()(
1237 addr: Ipv6Addr,
1238 pfx in 0u8..=32,
1239 ) -> ipnet::Ipv6Net {
1240 ipnet::Ipv6Net::new(addr, pfx).unwrap().trunc()
1241 }
1242 }
1243
1244 fn ipnet() -> impl Strategy<Value = ipnet::IpNet> {
1245 proptest::prop_oneof![
1246 ipv4net().prop_map(ipnet::IpNet::from),
1247 ipv6net().prop_map(ipnet::IpNet::from)
1248 ]
1249 }
1250
1251 proptest::prop_compose! {
1252 fn domain_segment()(
1256 seg in "[a-z][a-z0-9]*"
1257 ) -> String {
1258 seg
1259 }
1260 }
1261
1262 proptest::prop_compose! {
1263 fn domain(max_count: usize)(
1264 segs in proptest::collection::vec(domain_segment(), 0..max_count)
1265 ) -> String {
1266 segs.join(".")
1267 }
1268 }
1269
1270 type Key = [u8; 32];
1271
1272 proptest::prop_compose! {
1273 fn nodes(n: usize)(
1276 id in hash_set(any::<i64>(), n),
1277 stable_id in hash_set(".+", n),
1278 tags in vec(hash_set(".+", 0..32), n),
1279 accepted_routes in vec(hash_set(ipnet(), 0..32), n),
1280 node_key in hash_set(any::<Key>(), n),
1281 machine_key in vec(any::<Option<Key>>(), n),
1282 disco_key in vec(any::<Option<Key>>(), n),
1283 ipv4 in hash_set(any::<Ipv4Addr>(), n),
1284 ipv6 in hash_set(any::<Ipv6Addr>(), n),
1285 name in hash_set(domain_segment(), n),
1286 tailnet in vec(domain(5), n),
1287 has_tailnet in vec(any::<bool>(), n),
1288 derp_region in vec(any::<Option<NonZeroU32>>(), n),
1289 underlay_addrs in vec(any::<HashSet<SocketAddr>>(), n),
1290 ) -> Vec<Node> {
1291 itertools::izip![
1292 id,
1293 stable_id,
1294 tags,
1295 accepted_routes,
1296 node_key,
1297 machine_key,
1298 disco_key,
1299 ipv4,
1300 ipv6,
1301 name,
1302 tailnet,
1303 has_tailnet,
1304 derp_region,
1305 underlay_addrs,
1306 ].map(|(
1307 id,
1308 stable_id,
1309 tags,
1310 mut accepted_routes,
1311 node_key,
1312 machine_key,
1313 disco_key,
1314 ipv4,
1315 ipv6,
1316 name,
1317 tailnet,
1318 has_tailnet,
1319 derp_region,
1320 underlay_addrs,
1321 )| {
1322 accepted_routes.insert(ipnet::Ipv4Net::from(ipv4).into());
1323 accepted_routes.insert(ipnet::Ipv6Net::from(ipv6).into());
1324
1325 Node {
1326 id,
1327 stable_id: StableNodeId(stable_id),
1328
1329 hostname: name,
1330 user_id: 0,
1331 tailnet: has_tailnet.then_some(tailnet),
1332
1333 node_key: node_key.into(),
1334 key_signature: vec![],
1335 disco_key: disco_key.map(Into::into),
1336 machine_key: machine_key.map(Into::into),
1337
1338 node_key_expiry: None,
1339 expired: false,
1340 online: None,
1341 last_seen: None,
1342
1343 addresses: vec![
1344 ipnet::IpNet::V4(ipv4.into()),
1345 ipnet::IpNet::V6(ipv6.into()),
1346 ],
1347 tailnet_address: TailnetAddress {
1348 ipv4: ipv4.into(),
1349 ipv6: ipv6.into(),
1350 },
1351 tags: tags.into_iter().collect(),
1352
1353 derp_region: derp_region.map(ts_derp::RegionId),
1354
1355 accepted_routes: accepted_routes.into_iter().collect(),
1356 underlay_addresses: underlay_addrs.into_iter().collect(),
1357
1358 cap: Default::default(),
1359 cap_map: Default::default(),
1360 peerapi_port: None,
1361 peerapi_dns_proxy: false,
1362 is_wireguard_only: false,
1363 exit_node_dns_resolvers: vec![],
1364 peer_relay: false,
1365 ssh_host_keys: vec![],
1366 service_vips: Default::default(),
1367 unsigned_peer_api_only: false,
1368 }
1369 })
1370 .collect()
1371 }
1372 }
1373
1374 proptest::proptest! {
1375 #[test]
1376 fn prop_one_node_indices(mut nodes in nodes(1)) {
1377 let node = nodes.pop().unwrap();
1378
1379 let mut db = PeerDb::default();
1380 let id = db.upsert(&node);
1381
1382 validate_indices(&db, &node, id);
1383 assert_has_routes_exact(&db, &node, id);
1384 }
1385
1386 #[test]
1387 fn prop_many_nodes_indexed(nodes in nodes(16)) {
1388 let mut db = PeerDb::default();
1389
1390 let mut nodes_by_id = HashMap::new();
1391
1392 for node in &nodes {
1393 let id = db.upsert(node);
1394 nodes_by_id.insert(id, node.clone());
1395 }
1396
1397 for (id, node) in &nodes_by_id {
1398 validate_indices(&db, node, *id);
1399 }
1400 }
1401
1402 #[test]
1403 fn prop_remove(nodes in nodes(16)) {
1404 let mut db = PeerDb::default();
1405
1406 let mut ids = vec![];
1407
1408 for node in &nodes {
1409 ids.push((db.upsert(node), node));
1410 }
1411
1412 for (id, node) in ids {
1413 let (removed_id, removed_node) = db.remove(&id).unwrap();
1414
1415 proptest::prop_assert_eq!(removed_id, id);
1416 proptest::prop_assert_eq!(&removed_node, node);
1417 }
1418
1419 proptest::prop_assert!(db.peers.is_empty());
1420 proptest::prop_assert!(db.index_state.is_empty());
1421 }
1422 }
1423}