use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, fmt::Debug, net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::{ Arc, Weak, atomic::{AtomicBool, AtomicU32, Ordering}, }, time::{Duration, Instant, SystemTime}, }; use arc_swap::ArcSwap; use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr, Ipv6Inet}; use crossbeam::atomic::AtomicCell; use dashmap::DashMap; use ordered_hash_map::OrderedHashMap; use parking_lot::{RwLock, lock_api::RwLockUpgradableReadGuard}; use petgraph::{ Directed, algo::dijkstra, graph::{Graph, NodeIndex}, visit::{EdgeRef, IntoNodeReferences}, }; use prefix_trie::PrefixMap; use prost::Message; use prost_reflect::{DynamicMessage, ReflectMessage}; use tokio::{ select, sync::Mutex, task::{JoinHandle, JoinSet}, }; use crate::{ common::{ PeerId, config::NetworkIdentity, constants::EASYTIER_VERSION, global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, shrink_dashmap, stun::StunInfoCollectorTrait, }, peers::route_trait::{Route, RouteInterfaceBox}, proto::{ acl::GroupIdentity, common::{Ipv4Inet, NatType, StunInfo}, peer_rpc::{ ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, OspfRouteRpc, OspfRouteRpcClientFactory, OspfRouteRpcServer, PeerGroupInfo, PeerIdVersion, PeerIdentityType, PublicIpv6AddrRpcServer, RouteForeignNetworkInfos, RouteForeignNetworkSummary, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoError, SyncRouteInfoRequest, SyncRouteInfoResponse, TrustedCredentialPubkey, TrustedCredentialPubkeyProof, route_foreign_network_infos, route_foreign_network_summary, sync_route_info_request::ConnInfo, }, rpc_types::{ self, controller::{BaseController, Controller}, }, }, use_global_var, }; use super::{ PeerPacketFilter, graph_algo::dijkstra_with_first_hop, peer_rpc::PeerRpcManager, public_ipv6::{ PublicIpv6PeerRouteInfo, PublicIpv6RouteControl, PublicIpv6Service, PublicIpv6SyncTrigger, }, route_trait::{ DefaultRouteCostCalculator, ForeignNetworkRouteInfoMap, NextHopPolicy, RouteCostCalculator, RouteCostCalculatorInterface, }, }; use atomic_shim::AtomicU64; static SERVICE_ID: u32 = 7; static UPDATE_PEER_INFO_PERIOD: Duration = Duration::from_secs(3600); static REMOVE_DEAD_PEER_INFO_AFTER: Duration = Duration::from_secs(3660); // the cost (latency between two peers) is i32, i32::MAX is large enough. static AVOID_RELAY_COST: usize = i32::MAX as usize; static FORCE_USE_CONN_LIST: AtomicBool = AtomicBool::new(false); // if a peer is unreachable for `REMOVE_UNREACHABLE_PEER_INFO_AFTER` time, we can remove it because // 1. all the ospf sessions between two zone are already destroy, new created session will resend the peer info. // 2. all the dst_saved_peer_info_version in all sessions already remove the peer info, the peer info will be propagated // in another zone when two zone restore the conneciton. static REMOVE_UNREACHABLE_PEER_INFO_AFTER: Duration = Duration::from_secs(90); type Version = u32; /// Check if `child` CIDR is a subset of `parent` CIDR. /// Returns true if `child` is contained within `parent`, or if they are equal. fn cidr_is_subset(child: &IpCidr, parent: &IpCidr) -> bool { match (child, parent) { (IpCidr::V4(c), IpCidr::V4(p)) => { p.first_address() <= c.first_address() && c.last_address() <= p.last_address() } (IpCidr::V6(c), IpCidr::V6(p)) => { p.first_address() <= c.first_address() && c.last_address() <= p.last_address() } _ => false, // mixed v4/v6 } } /// Check if `child` CIDR is a subset of `parent` CIDR (both as string representations). fn cidr_is_subset_str(child: &str, parent: &str) -> bool { let Ok(child_cidr) = child.parse::() else { return false; }; let Ok(parent_cidr) = parent.parse::() else { return false; }; cidr_is_subset(&child_cidr, &parent_cidr) } /// Patch specific fields in a raw DynamicMessage from a decoded RoutePeerInfo, /// preserving all other fields (including unknown ones). fn patch_raw_from_info(raw: &mut DynamicMessage, info: &RoutePeerInfo, fields: &[&str]) { let mut decoded_raw = DynamicMessage::new(RoutePeerInfo::default().descriptor()); decoded_raw.transcode_from(info).unwrap(); for field_name in fields { if let Some(value) = decoded_raw.get_field_by_name(field_name) { raw.set_field_by_name(field_name, value.into_owned()); } } } fn raw_credential_bytes_from_route_info( raw_route_info: &DynamicMessage, proof_idx: usize, ) -> Option> { raw_route_info .get_field_by_name("trusted_credential_pubkeys")? .as_list()? .get(proof_idx)? .as_message()? .get_field_by_name("credential")? .as_message() .map(|credential| credential.encode_to_vec()) } fn route_peer_inst_id(info: &RoutePeerInfo) -> Option { info.inst_id.map(Into::into) } #[derive(Debug, Clone)] struct AtomicVersion(Arc); impl AtomicVersion { fn new() -> Self { AtomicVersion(Arc::new(AtomicU32::new(0))) } fn get(&self) -> Version { self.0.load(Ordering::Relaxed) } fn set(&self, version: Version) { self.0.store(version, Ordering::Relaxed); } fn inc(&self) -> Version { self.0.fetch_add(1, Ordering::Relaxed) + 1 } fn set_if_larger(&self, version: Version) -> bool { // return true if the version is set. self.0.fetch_max(version, Ordering::Relaxed) < version } } impl From for AtomicVersion { fn from(version: Version) -> Self { AtomicVersion(Arc::new(AtomicU32::new(version))) } } fn is_foreign_network_info_newer( next: &ForeignNetworkRouteInfoEntry, prev: &ForeignNetworkRouteInfoEntry, ) -> Option { Some( SystemTime::try_from(next.last_update?).ok()? > SystemTime::try_from(prev.last_update?).ok()?, ) } impl RoutePeerInfo { #[allow(deprecated)] pub fn new() -> Self { Self { peer_id: 0, inst_id: Some(uuid::Uuid::nil().into()), cost: 0, ipv4_addr: None, proxy_cidrs: Vec::new(), hostname: None, udp_nat_type: 0, tcp_nat_type: 0, // ensure this is updated when the peer_infos/conn_info/foreign_network lock is acquired. // else we may assign a older timestamp than iterate time. last_update: None, version: 0, easytier_version: EASYTIER_VERSION.to_string(), feature_flag: None, peer_route_id: 0, network_length: 24, ipv6_addr: None, groups: Vec::new(), quic_port: None, noise_static_pubkey: Vec::new(), trusted_credential_pubkeys: Vec::new(), ipv6_public_addr_prefix: None, ipv6_public_addr_lease: None, } } /// Creates a new `RoutePeerInfo` instance with updated information from the given context. /// /// # Parameters /// - `my_peer_id`: The unique identifier for the peer. /// - `peer_route_id`: The route identifier associated with the peer. /// - `global_ctx`: Reference to the global context containing configuration and state. /// /// # Returns /// A new `RoutePeerInfo` instance initialized with values from the provided context and parameters. pub fn new_updated_self( my_peer_id: PeerId, peer_route_id: u64, global_ctx: &ArcGlobalCtx, public_ipv6_addr_lease: Option, ) -> Self { let stun_info = global_ctx.get_stun_info_collector().get_stun_info(); let noise_static_pubkey = global_ctx .config .get_secure_mode() .and_then(|cfg| cfg.public_key().ok()) .map(|pk| pk.as_bytes().to_vec()) .unwrap_or_default(); Self { peer_id: my_peer_id, inst_id: Some(global_ctx.get_id().into()), cost: 0, ipv4_addr: global_ctx.get_ipv4().map(|x| x.address().into()), proxy_cidrs: global_ctx .config .get_proxy_cidrs() .iter() .map(|x| x.mapped_cidr.unwrap_or(x.cidr)) .chain(global_ctx.get_vpn_portal_cidr()) .map(|x| x.to_string()) .collect(), hostname: Some(global_ctx.get_hostname()), udp_nat_type: stun_info.udp_nat_type, tcp_nat_type: stun_info.tcp_nat_type, // these two fields should not participate in comparison. last_update: None, version: 0, easytier_version: EASYTIER_VERSION.to_string(), feature_flag: Some(global_ctx.get_feature_flags()), peer_route_id, network_length: global_ctx .get_ipv4() .map(|x| x.network_length() as u32) .unwrap_or(24), ipv6_addr: global_ctx.get_ipv6().map(|x| x.into()), ipv6_public_addr_prefix: global_ctx.get_advertised_ipv6_public_addr_prefix().map( |prefix| { Ipv6Inet::new(prefix.first_address(), prefix.network_length()) .unwrap() .into() }, ), ipv6_public_addr_lease: public_ipv6_addr_lease.map(Into::into), groups: global_ctx.get_acl_groups(my_peer_id), noise_static_pubkey, // Only admin nodes (holding network_secret) publish trusted credential pubkeys trusted_credential_pubkeys: if let Some(network_secret) = global_ctx.get_network_identity().network_secret { global_ctx .get_credential_manager() .get_trusted_pubkeys(&network_secret) } else { Vec::new() }, ..Default::default() } } /// Attempts to update the `new` RoutePeerInfo based on the `old` RoutePeerInfo. /// /// An update is triggered if any fields in `new` differ from `old`, or if the time since /// `old.last_update` exceeds the `UPDATE_PEER_INFO_PERIOD`. /// /// If an update occurs, `new.last_update` is set to the current time and `new.version` is incremented. /// Otherwise, `new.last_update` and `new.version` are copied from `old` without modification. /// /// Returns `true` if an update was performed (fields changed or periodic update required), /// or `false` if no update was necessary. pub fn try_update_new_peer_info(old: &RoutePeerInfo, new: &mut RoutePeerInfo) -> bool { let need_update_periodically = if let Ok(Ok(d)) = SystemTime::try_from(old.last_update.unwrap_or_default()).map(|x| x.elapsed()) { d > UPDATE_PEER_INFO_PERIOD } else { true }; // these two fields should not participate in comparison. new.version = old.version; new.last_update = old.last_update; if *new != *old || need_update_periodically { new.version += 1; true } else { false } } } impl From for crate::proto::api::instance::Route { fn from(val: RoutePeerInfo) -> Self { let network_length = if val.network_length == 0 { 24 } else { val.network_length }; crate::proto::api::instance::Route { peer_id: val.peer_id, ipv4_addr: val.ipv4_addr.map(|ipv4_addr| Ipv4Inet { address: Some(ipv4_addr), network_length, }), next_hop_peer_id: 0, // next_hop_peer_id is calculated in RouteTable. cost: 0, // cost is calculated in RouteTable. path_latency: 0, // path_latency is calculated in RouteTable. proxy_cidrs: val.proxy_cidrs.clone(), hostname: val.hostname.unwrap_or_default(), stun_info: { let mut stun_info = StunInfo::default(); if let Ok(udp_nat_type) = NatType::try_from(val.udp_nat_type) { stun_info.set_udp_nat_type(udp_nat_type); } if let Ok(tcp_nat_type) = NatType::try_from(val.tcp_nat_type) { stun_info.set_tcp_nat_type(tcp_nat_type); } Some(stun_info) }, inst_id: val.inst_id.map(|x| x.to_string()).unwrap_or_default(), version: val.easytier_version, feature_flag: val.feature_flag, next_hop_peer_id_latency_first: None, cost_latency_first: None, path_latency_latency_first: None, ipv6_addr: val.ipv6_addr, public_ipv6_addr: val.ipv6_public_addr_lease, ipv6_public_addr_prefix: val.ipv6_public_addr_prefix, } } } type RouteConnBitmap = crate::proto::peer_rpc::RouteConnBitmap; type RouteConnPeerList = crate::proto::peer_rpc::RouteConnPeerList; type PeerConnInfo = crate::proto::peer_rpc::route_conn_peer_list::PeerConnInfo; impl RouteConnBitmap { fn get_bit(&self, idx: usize) -> bool { let byte_idx = idx / 8; let bit_idx = idx % 8; let byte = self.bitmap[byte_idx]; (byte >> bit_idx) & 1 == 1 } fn get_connected_peers(&self, peer_idx: usize) -> BTreeSet { let mut connected_peers = BTreeSet::new(); for (idx, peer_id_version) in self.peer_ids.iter().enumerate() { if self.get_bit(peer_idx * self.peer_ids.len() + idx) { connected_peers.insert(peer_id_version.peer_id); } } connected_peers } } type Error = SyncRouteInfoError; #[derive(Debug, Clone)] struct RouteConnInfo { connected_peers: BTreeSet, version: AtomicVersion, last_update: SystemTime, } impl Default for RouteConnInfo { fn default() -> Self { Self { connected_peers: BTreeSet::new(), version: AtomicVersion::new(), last_update: SystemTime::now(), } } } #[derive(Debug, Default)] struct InterfacePeerSnapshot { generation: u64, peers: BTreeSet, identity_types: BTreeMap>, } // constructed with all infos synced from all peers. struct SyncedRouteInfo { peer_infos: RwLock>, // prost doesn't support unknown fields, so we use DynamicMessage to store raw infos and propagate them to other peers. raw_peer_infos: DashMap, conn_map: RwLock>, foreign_network: DashMap, group_trust_map: DashMap>>, group_trust_map_cache: DashMap>>, // cache for group trust map, should sync with group_trust_map // Aggregated trusted credential pubkeys from all admin nodes // Maps pubkey bytes -> TrustedCredentialPubkey trusted_credential_pubkeys: DashMap, TrustedCredentialPubkey>, // Tracks the currently accepted peer for non-reusable credentials. // Maps credential pubkey bytes -> peer_id. non_reusable_credential_owners: DashMap, PeerId>, version: AtomicVersion, } impl Debug for SyncedRouteInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SyncedRouteInfo") .field("peer_infos", &self.peer_infos) .field("conn_map", &self.conn_map) .field("foreign_network", &self.foreign_network) .field("group_trust_map", &self.group_trust_map) .field("version", &self.version.get()) .finish() } } impl SyncedRouteInfo { fn set_peer_groups(&self, peer_id: PeerId, groups: HashMap>) { if groups.is_empty() { self.group_trust_map.remove(&peer_id); self.group_trust_map_cache.remove(&peer_id); return; } let group_names = groups.keys().cloned().collect(); self.group_trust_map.insert(peer_id, groups); self.group_trust_map_cache .insert(peer_id, Arc::new(group_names)); } fn get_proof_groups(&self, peer_id: PeerId) -> HashMap> { self.group_trust_map .get(&peer_id) .map(|groups| { groups .iter() .filter(|(_, proof)| !proof.is_empty()) .map(|(group, proof)| (group.clone(), proof.clone())) .collect() }) .unwrap_or_default() } fn mark_credential_peer(info: &mut RoutePeerInfo, is_credential_peer: bool) { let mut feature_flag = info.feature_flag.unwrap_or_default(); feature_flag.is_credential_peer = is_credential_peer; info.feature_flag = Some(feature_flag); } fn is_credential_peer_info(info: &RoutePeerInfo) -> bool { info.feature_flag .as_ref() .map(|x| x.is_credential_peer) .unwrap_or(false) } fn credential_is_reusable(info: &TrustedCredentialPubkey) -> bool { info.reusable.unwrap_or(true) } fn credential_proof_is_valid( &self, raw_route_info: Option<&DynamicMessage>, proof_idx: usize, proof: &TrustedCredentialPubkeyProof, network_secret: Option<&str>, ) -> bool { network_secret .map(|secret| { raw_route_info .and_then(|raw| raw_credential_bytes_from_route_info(raw, proof_idx)) .map(|raw_credential_bytes| { proof.verify_credential_hmac_with_bytes(&raw_credential_bytes, secret) }) .unwrap_or_else(|| proof.verify_credential_hmac(secret)) }) .unwrap_or(true) } fn collect_trusted_credentials( &self, peer_infos: &OrderedHashMap, network_secret: Option<&str>, now: i64, ) -> ( HashMap, TrustedCredentialPubkey>, HashMap, crate::common::global_ctx::TrustedKeyMetadata>, ) { use crate::common::global_ctx::{TrustedKeyMetadata, TrustedKeySource}; let mut all_trusted = HashMap::new(); let mut global_trusted_keys = HashMap::new(); for (peer_id, info) in peer_infos.iter() { if !self.is_admin_peer(info) { continue; } if !info.noise_static_pubkey.is_empty() { global_trusted_keys.insert( info.noise_static_pubkey.clone(), TrustedKeyMetadata { source: TrustedKeySource::OspfNode, expiry_unix: None, }, ); } let raw_route_info = self.raw_peer_infos.get(peer_id); let raw_route_info = raw_route_info.as_deref(); for (proof_idx, proof) in info.trusted_credential_pubkeys.iter().enumerate() { if !self.credential_proof_is_valid(raw_route_info, proof_idx, proof, network_secret) { continue; } let Some(credential) = proof.credential.as_ref() else { continue; }; if credential.expiry_unix <= now { continue; } all_trusted .entry(credential.pubkey.clone()) .or_insert_with(|| credential.clone()); global_trusted_keys.insert( credential.pubkey.clone(), TrustedKeyMetadata { source: TrustedKeySource::OspfCredential, expiry_unix: Some(credential.expiry_unix), }, ); } } (all_trusted, global_trusted_keys) } fn replace_trusted_credential_pubkeys( &self, all_trusted: &HashMap, TrustedCredentialPubkey>, ) -> HashSet> { let prev_trusted = self .trusted_credential_pubkeys .iter() .map(|entry| entry.key().clone()) .collect(); self.trusted_credential_pubkeys.clear(); for (pubkey, credential) in all_trusted { self.trusted_credential_pubkeys .insert(pubkey.clone(), credential.clone()); } prev_trusted } fn collect_non_reusable_credential_owners( &self, peer_infos: &OrderedHashMap, all_trusted: &HashMap, TrustedCredentialPubkey>, mut is_peer_active: F, ) -> (HashMap, PeerId>, BTreeSet) where F: FnMut(PeerId) -> bool, { let mut candidates: BTreeMap, BTreeSet> = BTreeMap::new(); for (peer_id, info) in peer_infos.iter() { if info.noise_static_pubkey.is_empty() { continue; } let Some(credential) = all_trusted.get(&info.noise_static_pubkey) else { continue; }; if Self::credential_is_reusable(credential) { continue; } if !is_peer_active(*peer_id) { continue; } candidates .entry(info.noise_static_pubkey.clone()) .or_default() .insert(*peer_id); } let mut active_owners = HashMap::new(); let mut duplicate_untrusted_peers = BTreeSet::new(); for (pubkey, candidate_peer_ids) in candidates { let Some(owner_peer_id) = candidate_peer_ids.iter().next().copied() else { continue; }; active_owners.insert(pubkey, owner_peer_id); duplicate_untrusted_peers.extend( candidate_peer_ids .into_iter() .filter(|peer_id| *peer_id != owner_peer_id), ); } (active_owners, duplicate_untrusted_peers) } fn replace_non_reusable_credential_owners(&self, active_owners: HashMap, PeerId>) { self.non_reusable_credential_owners .retain(|pubkey, _| active_owners.contains_key(pubkey)); for (pubkey, peer_id) in active_owners { self.non_reusable_credential_owners.insert(pubkey, peer_id); } } fn update_credential_groups( &self, peer_infos: &OrderedHashMap, all_trusted: &HashMap, TrustedCredentialPubkey>, ) { for (_, info) in peer_infos.iter() { if info.noise_static_pubkey.is_empty() { continue; } let Some(credential) = all_trusted.get(&info.noise_static_pubkey) else { continue; }; let mut group_map = self.get_proof_groups(info.peer_id); for group in &credential.groups { group_map.entry(group.clone()).or_default(); } self.set_peer_groups(info.peer_id, group_map); } } fn collect_revoked_credential_peers( peer_infos: &OrderedHashMap, prev_trusted: &HashSet>, all_trusted: &HashMap, TrustedCredentialPubkey>, ) -> BTreeSet { let mut untrusted_peers = BTreeSet::new(); for (peer_id, info) in peer_infos.iter() { if info.noise_static_pubkey.is_empty() || info.version == 0 { continue; } if prev_trusted.contains(&info.noise_static_pubkey) && !all_trusted.contains_key(&info.noise_static_pubkey) { untrusted_peers.insert(*peer_id); } } untrusted_peers } fn get_connected_peers>(&self, peer_id: PeerId) -> Option { self.conn_map .read() .get(&peer_id) .map(|x| x.connected_peers.iter().copied().collect()) } fn remove_peer(&self, peer_id: PeerId) { self.remove_peers([peer_id]); } fn remove_peers(&self, peer_ids: I) where I: IntoIterator, { let peer_ids: HashSet<_> = peer_ids.into_iter().collect(); if peer_ids.is_empty() { return; } for peer_id in &peer_ids { tracing::warn!(?peer_id, "remove_peer from synced_route_info"); } { let mut peer_infos = self.peer_infos.write(); let mut conn_map = self.conn_map.write(); for peer_id in &peer_ids { peer_infos.remove(peer_id); conn_map.remove(peer_id); } } for peer_id in &peer_ids { self.raw_peer_infos.remove(peer_id); self.group_trust_map.remove(peer_id); self.group_trust_map_cache.remove(peer_id); } self.foreign_network .retain(|k, _| !peer_ids.contains(&k.peer_id)); shrink_dashmap(&self.raw_peer_infos, None); shrink_dashmap(&self.foreign_network, None); shrink_dashmap(&self.group_trust_map, None); shrink_dashmap(&self.group_trust_map_cache, None); self.version.inc(); } fn fill_empty_peer_info(&self, peer_ids: &BTreeSet) { let mut need_inc_version = false; for peer_id in peer_ids { let guard = self.peer_infos.upgradable_read(); if !guard.contains_key(peer_id) { let mut peer_info = RoutePeerInfo::new(); let mut guard = RwLockUpgradableReadGuard::upgrade(guard); peer_info.last_update = Some(SystemTime::now().into()); guard.insert(*peer_id, peer_info); need_inc_version = true; } else { drop(guard); } let guard = self.conn_map.upgradable_read(); if !guard.contains_key(peer_id) { let mut guard = RwLockUpgradableReadGuard::upgrade(guard); guard.insert(*peer_id, RouteConnInfo::default()); need_inc_version = true; } else { drop(guard); } } if need_inc_version { self.version.inc(); } } fn get_peer_info_version_with_default(&self, peer_id: PeerId) -> Version { self.peer_infos .read() .get(&peer_id) .map(|x| x.version) .unwrap_or(0) } fn get_avoid_relay_data(&self, peer_id: PeerId) -> bool { // if avoid relay, just set all outgoing edges to a large value: AVOID_RELAY_COST. self.peer_infos .read() .get(&peer_id) .and_then(|x| x.feature_flag) .map(|x| x.avoid_relay_data) .unwrap_or_default() } fn check_duplicate_peer_id( &self, my_peer_id: PeerId, my_peer_route_id: u64, dst_peer_id: PeerId, dst_peer_route_id: Option, info: &RoutePeerInfo, ) -> Result<(), Error> { // 1. check if we are duplicated. if info.peer_id == my_peer_id { if info.peer_route_id != my_peer_route_id && info.version > self.get_peer_info_version_with_default(info.peer_id) { // if dst peer send to us with higher version info of my peer, our peer id is duplicated // TODO: handle this better. restart peer manager? panic!("my peer id is duplicated"); // return Err(Error::DuplicatePeerId); } } else if info.peer_id == dst_peer_id { let Some(dst_peer_route_id) = dst_peer_route_id else { return Ok(()); }; if dst_peer_route_id != info.peer_route_id && info.version < self.get_peer_info_version_with_default(info.peer_id) { // if dst peer send to us with lower version info of dst peer, dst peer id is duplicated return Err(Error::DuplicatePeerId); } } Ok(()) } fn update_peer_infos( &self, my_peer_id: PeerId, my_peer_route_id: u64, dst_peer_id: PeerId, peer_infos: &[RoutePeerInfo], raw_peer_infos: &[DynamicMessage], ) -> Result<(), Error> { let mut need_inc_version = false; for (idx, route_info) in peer_infos.iter().enumerate() { let mut route_info = route_info.clone(); let raw_route_info = &raw_peer_infos[idx]; self.check_duplicate_peer_id( my_peer_id, my_peer_route_id, dst_peer_id, if route_info.peer_id == dst_peer_id { self.peer_infos .read() .get(&dst_peer_id) .map(|x| x.peer_route_id) } else { None }, &route_info, )?; let peer_id_raw = raw_route_info .get_field_by_name("peer_id") .unwrap() .as_u32() .unwrap(); assert_eq!(peer_id_raw, route_info.peer_id); let mut guard = self.peer_infos.write(); // time between peers may not be synchronized, so update last_update to local now. // note only last_update with larger version will be updated to local saved peer info. route_info.last_update = Some(SystemTime::now().into()); if guard .get_mut(&route_info.peer_id) .is_none_or(|old| route_info.version > old.version) { self.raw_peer_infos .insert(route_info.peer_id, raw_route_info.clone()); guard.insert(route_info.peer_id, route_info); need_inc_version = true; } } if need_inc_version { self.version.inc(); } Ok(()) } fn update_conn_info_one_peer( &self, peer_id_version: &PeerIdVersion, connected_peers: BTreeSet, ) -> bool { let mut guard = self.conn_map.write(); if guard .get_mut(&peer_id_version.peer_id) .is_none_or(|old| peer_id_version.version > old.version.get()) { guard.insert( peer_id_version.peer_id, RouteConnInfo { connected_peers, version: peer_id_version.version.into(), last_update: SystemTime::now(), }, ); return true; } false } fn update_conn_info_with_bitmap(&self, conn_bitmap: &RouteConnBitmap) { self.fill_empty_peer_info(&conn_bitmap.peer_ids.iter().map(|x| x.peer_id).collect()); let mut need_inc_version = false; for (peer_idx, peer_id_version) in conn_bitmap.peer_ids.iter().enumerate() { let connceted_peers = conn_bitmap.get_connected_peers(peer_idx); self.fill_empty_peer_info(&connceted_peers); need_inc_version |= self.update_conn_info_one_peer(peer_id_version, connceted_peers); } if need_inc_version { self.version.inc(); } } fn update_conn_info_with_list(&self, conn_peer_list: &RouteConnPeerList) { let mut need_inc_version = false; for peer_conn_info in &conn_peer_list.peer_conn_infos { let Some(peer_id_version) = peer_conn_info.peer_id else { continue; }; let connected_peers: BTreeSet = peer_conn_info.connected_peer_ids.iter().copied().collect(); self.fill_empty_peer_info(&connected_peers); need_inc_version |= self.update_conn_info_one_peer(&peer_id_version, connected_peers); } if need_inc_version { self.version.inc(); } } fn update_conn_info(&self, conn_info: &ConnInfo) { match conn_info { ConnInfo::ConnBitmap(conn_bitmap) => { self.update_conn_info_with_bitmap(conn_bitmap); } ConnInfo::ConnPeerList(conn_peer_list) => { self.update_conn_info_with_list(conn_peer_list); } } } fn update_foreign_network(&self, foreign_network: &RouteForeignNetworkInfos) -> bool { let mut changed = false; for item in foreign_network.infos.iter().map(Clone::clone) { let Some(key) = item.key else { continue; }; let Some(mut entry) = item.value else { continue; }; entry.last_update = Some(SystemTime::now().into()); self.foreign_network .entry(key.clone()) .and_modify(|old_entry| { if entry.version > old_entry.version { *old_entry = entry.clone(); changed = true; } }) .or_insert_with(|| { changed = true; entry.clone() }); } changed } fn update_my_peer_info( &self, my_peer_id: PeerId, my_peer_route_id: u64, global_ctx: &ArcGlobalCtx, public_ipv6_addr_lease: Option, ) -> bool { let mut new = RoutePeerInfo::new_updated_self( my_peer_id, my_peer_route_id, global_ctx, public_ipv6_addr_lease, ); let mut guard = self.peer_infos.upgradable_read(); let old = guard.get(&my_peer_id); let new_version = old.map(|x| x.version).unwrap_or(0) + 1; let need_insert_new = if let Some(old) = old { RoutePeerInfo::try_update_new_peer_info(old, &mut new) } else { true }; if need_insert_new { let acl_groups = if old.map(|x| x.groups != new.groups).unwrap_or(true) { Some(new.groups.clone()) } else { None }; guard.with_upgraded(|peer_infos| { new.last_update = Some(SystemTime::now().into()); new.version = new_version; peer_infos.insert(my_peer_id, new) }); drop(guard); if let Some(acl_groups) = acl_groups { self.update_my_group_trusts(my_peer_id, &acl_groups); } self.version.inc(); true } else { false } } fn update_my_conn_info(&self, my_peer_id: PeerId, connected_peers: BTreeSet) -> bool { self.fill_empty_peer_info(&connected_peers); let guard = self.conn_map.upgradable_read(); let my_conn_info = guard.get(&my_peer_id); let new_version = my_conn_info.map(|x| x.version.get()).unwrap_or(0) + 1; if my_conn_info.is_none_or(|old| old.connected_peers != connected_peers) { let mut guard = RwLockUpgradableReadGuard::upgrade(guard); guard.insert( my_peer_id, RouteConnInfo { connected_peers, version: new_version.into(), last_update: SystemTime::now(), }, ); self.version.inc(); true } else { false } } fn update_my_foreign_network( &self, my_peer_id: PeerId, foreign_networks: ForeignNetworkRouteInfoMap, ) -> bool { let now = SystemTime::now(); let now_version = now .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs() as Version; let mut updated = false; for mut item in self .foreign_network .iter_mut() .filter(|x| x.key().peer_id == my_peer_id) { let (key, entry) = item.pair_mut(); if let Some(mut new_entry) = foreign_networks.get_mut(key) { assert!(!new_entry.foreign_peer_ids.is_empty()); if let Some(is_newer) = is_foreign_network_info_newer(&new_entry, entry) { let need_renew = is_newer || now .duration_since(entry.last_update.unwrap().try_into().unwrap()) .unwrap_or(Duration::from_secs(0)) > UPDATE_PEER_INFO_PERIOD; if need_renew { new_entry.version = std::cmp::max(new_entry.version + 1, now_version); *entry = new_entry.clone(); updated = true; } } drop(new_entry); foreign_networks.remove(key).unwrap(); } else if !item.foreign_peer_ids.is_empty() { item.foreign_peer_ids.clear(); item.last_update = Some(SystemTime::now().into()); item.version = std::cmp::max(item.version + 1, now_version); updated = true; } } for item in foreign_networks.iter() { assert!(!item.value().foreign_peer_ids.is_empty()); self.foreign_network .entry(item.key().clone()) .and_modify(|old_entry| { if item.value().version > old_entry.version { *old_entry = item.value().clone(); } }) .or_insert_with(|| { let mut v = item.value().clone(); v.version = now_version; v }); updated = true; } if updated { self.version.inc(); } updated } fn get_next_last_sync_succ_timestamp(&self) -> SystemTime { let _peer_info_lock = self.peer_infos.read(); let _conn_info_lock = self.conn_map.read(); // TODO: add conn and foreign network lock SystemTime::now() } fn verify_and_update_group_trusts( &self, peer_infos: &[RoutePeerInfo], local_group_declarations: &[GroupIdentity], trust_admin_groups_without_proof: bool, ) { let local_group_declarations = local_group_declarations .iter() .map(|g| (g.group_name.as_str(), g.group_secret.as_str())) .collect::>(); let verify_groups = |info: &RoutePeerInfo| -> HashMap> { let mut trusted_groups_for_peer: HashMap> = HashMap::new(); for group_proof in &info.groups { let name = &group_proof.group_name; let proof_bytes = group_proof.group_proof.clone(); if let Some(&local_secret) = local_group_declarations.get(group_proof.group_name.as_str()) { if group_proof.verify(local_secret, info.peer_id) { trusted_groups_for_peer.insert(name.clone(), proof_bytes); } else { tracing::warn!( peer_id = info.peer_id, group = %group_proof.group_name, "Group proof verification failed" ); } } } if trust_admin_groups_without_proof && self.is_admin_peer(info) { for group_proof in &info.groups { trusted_groups_for_peer .entry(group_proof.group_name.clone()) .or_default(); } } trusted_groups_for_peer }; for info in peer_infos { match self.group_trust_map.entry(info.peer_id) { dashmap::mapref::entry::Entry::Occupied(mut entry) => { let trusted_groups_for_peer = verify_groups(info); if trusted_groups_for_peer.is_empty() { entry.remove(); self.group_trust_map_cache.remove(&info.peer_id); } else { let group_names = trusted_groups_for_peer.keys().cloned().collect(); self.group_trust_map_cache .insert(info.peer_id, Arc::new(group_names)); *entry.get_mut() = trusted_groups_for_peer; } } dashmap::mapref::entry::Entry::Vacant(entry) => { let trusted_groups_for_peer = verify_groups(info); if !trusted_groups_for_peer.is_empty() { let group_names = trusted_groups_for_peer.keys().cloned().collect(); self.group_trust_map_cache .insert(info.peer_id, Arc::new(group_names)); entry.insert(trusted_groups_for_peer); } } } } } fn update_my_group_trusts(&self, my_peer_id: PeerId, groups: &[PeerGroupInfo]) { let mut my_group_map = HashMap::new(); for group in groups.iter() { my_group_map.insert(group.group_name.clone(), group.group_proof.clone()); } self.set_peer_groups(my_peer_id, my_group_map); } /// Collect trusted credential pubkeys from admin nodes (network_secret holders) /// and verify credential peers. Returns set of peer_ids that should be removed. /// Also returns a HashMap of trusted keys for synchronization to GlobalCtx. fn verify_and_update_credential_trusts( &self, network_secret: Option<&str>, ) -> ( Vec, HashMap, crate::common::global_ctx::TrustedKeyMetadata>, ) { self.verify_and_update_credential_trusts_with_active_peers(network_secret, |_| true) } fn verify_and_update_credential_trusts_with_active_peers( &self, network_secret: Option<&str>, is_peer_active: F, ) -> ( Vec, HashMap, crate::common::global_ctx::TrustedKeyMetadata>, ) where F: FnMut(PeerId) -> bool, { self.verify_and_update_credential_trusts_with_active_peers_protecting( network_secret, is_peer_active, None, ) } fn verify_and_update_credential_trusts_with_active_peers_protecting( &self, network_secret: Option<&str>, is_peer_active: F, protected_peer_id: Option, ) -> ( Vec, HashMap, crate::common::global_ctx::TrustedKeyMetadata>, ) where F: FnMut(PeerId) -> bool, { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; let peer_infos = self.peer_infos.read(); let (all_trusted, global_trusted_keys) = self.collect_trusted_credentials(&peer_infos, network_secret, now); let prev_trusted = self.replace_trusted_credential_pubkeys(&all_trusted); let (active_non_reusable_owners, duplicate_untrusted_peers) = self.collect_non_reusable_credential_owners(&peer_infos, &all_trusted, is_peer_active); self.replace_non_reusable_credential_owners(active_non_reusable_owners); self.update_credential_groups(&peer_infos, &all_trusted); let mut untrusted_peers = Self::collect_revoked_credential_peers(&peer_infos, &prev_trusted, &all_trusted); untrusted_peers.extend(duplicate_untrusted_peers); if let Some(protected_peer_id) = protected_peer_id { untrusted_peers.remove(&protected_peer_id); } // Remove untrusted peers from peer_infos so they won't appear in route graph if !untrusted_peers.is_empty() { drop(peer_infos); // release read lock before writing for peer_id in &untrusted_peers { tracing::warn!(?peer_id, "removing untrusted peer from route info"); } self.remove_peers(untrusted_peers.iter().copied()); } (untrusted_peers.into_iter().collect(), global_trusted_keys) } fn is_admin_peer(&self, info: &RoutePeerInfo) -> bool { if info.version == 0 { return false; } !Self::is_credential_peer_info(info) } fn is_credential_peer(&self, peer_id: PeerId) -> bool { let peer_infos = self.peer_infos.read(); peer_infos .get(&peer_id) .map(Self::is_credential_peer_info) .unwrap_or(false) } fn get_credential_info_by_pubkey(&self, peer_pubkey: &[u8]) -> Option { if peer_pubkey.is_empty() { return None; } self.trusted_credential_pubkeys .get(peer_pubkey) .map(|r| r.value().clone()) } } type PeerGraph = Graph; type PeerIdToNodexIdxMap = DashMap; #[derive(Debug, Clone, Copy)] struct NextHopInfo { next_hop_peer_id: PeerId, path_latency: i32, path_len: usize, // path includes src and dst. version: Version, } // dst_peer_id -> (next_hop_peer_id, cost, path_len) type NextHopMap = DashMap; // computed with SyncedRouteInfo. used to get next hop. #[derive(Debug)] struct RouteTable { peer_infos: DashMap, next_hop_map: NextHopMap, ipv4_peer_id_map: DashMap, ipv6_peer_id_map: DashMap, cidr_peer_id_map: ArcSwap>, cidr_v6_peer_id_map: ArcSwap>, next_hop_map_version: AtomicVersion, } impl RouteTable { fn new() -> Self { RouteTable { peer_infos: DashMap::new(), next_hop_map: DashMap::new(), ipv4_peer_id_map: DashMap::new(), ipv6_peer_id_map: DashMap::new(), cidr_peer_id_map: ArcSwap::new(Arc::new(PrefixMap::new())), cidr_v6_peer_id_map: ArcSwap::new(Arc::new(PrefixMap::new())), next_hop_map_version: AtomicVersion::new(), } } fn get_next_hop(&self, dst_peer_id: PeerId) -> Option { let cur_version = self.next_hop_map_version.get(); self.next_hop_map.get(&dst_peer_id).and_then(|x| { if x.version >= cur_version { Some(*x) } else { None } }) } fn peer_reachable(&self, peer_id: PeerId) -> bool { self.get_next_hop(peer_id).is_some() } fn get_udp_nat_type(&self, peer_id: PeerId) -> Option { self.peer_infos .get(&peer_id) .map(|x| NatType::try_from(x.udp_nat_type).unwrap_or_default()) } // return graph and start node index (node of my peer id). fn build_peer_graph_from_synced_info( my_peer_id: PeerId, synced_info: &SyncedRouteInfo, cost_calc: &T, ) -> (PeerGraph, NodeIndex) { let mut graph: PeerGraph = PeerGraph::new(); let mut start_node_idx = None; let peer_id_to_node_index: PeerIdToNodexIdxMap = DashMap::new(); for (peer_id, info) in synced_info.peer_infos.read().iter() { let peer_id = *peer_id; if info.version == 0 { continue; } let node_idx = graph.add_node(peer_id); peer_id_to_node_index.insert(peer_id, node_idx); if peer_id == my_peer_id { start_node_idx = Some(node_idx); } } if start_node_idx.is_none() { return (graph, NodeIndex::end()); } for item in peer_id_to_node_index.iter() { let src_peer_id = item.key(); let src_node_idx = item.value(); let connected_peers: BTreeSet<_> = synced_info .get_connected_peers(*src_peer_id) .unwrap_or_default(); // if avoid relay, just set all outgoing edges to a large value: AVOID_RELAY_COST. let peer_avoid_relay_data = synced_info.get_avoid_relay_data(*src_peer_id); for dst_peer_id in connected_peers.iter() { let Some(dst_node_idx) = peer_id_to_node_index.get(dst_peer_id) else { continue; }; let mut cost = cost_calc.calculate_cost(*src_peer_id, *dst_peer_id) as usize; if peer_avoid_relay_data { cost += AVOID_RELAY_COST; } graph.add_edge(*src_node_idx, *dst_node_idx, cost); } } (graph, start_node_idx.unwrap()) } fn clean_expired_route_info(&self) { let cur_version = self.next_hop_map_version.get(); self.next_hop_map.retain(|_, v| { // remove next hop map for peers we cannot reach. v.version >= cur_version }); self.peer_infos.retain(|k, _| { // remove peer info for peers we cannot reach. self.next_hop_map.contains_key(k) }); self.ipv4_peer_id_map.retain(|_, v| { // remove ipv4 map for peers we cannot reach. self.next_hop_map.contains_key(&v.peer_id) }); self.ipv6_peer_id_map.retain(|_, v| { // remove ipv6 map for peers we cannot reach. self.next_hop_map.contains_key(&v.peer_id) }); shrink_dashmap(&self.peer_infos, None); shrink_dashmap(&self.next_hop_map, None); shrink_dashmap(&self.ipv4_peer_id_map, None); shrink_dashmap(&self.ipv6_peer_id_map, None); } fn gen_next_hop_map_with_least_hop( &self, graph: &PeerGraph, start_node: &NodeIndex, version: Version, ) { if graph.node_weight(*start_node).is_none() { tracing::warn!( ?start_node, version, "invalid start node for least-hop route rebuild" ); return; } let normalize_edge_cost = |e: petgraph::graph::EdgeReference| { if *e.weight() >= AVOID_RELAY_COST { AVOID_RELAY_COST + 1 } else { 1 } }; // Step 1: 第一次 Dijkstra - 计算最短跳数 let path_len_map = dijkstra(&graph, *start_node, None, normalize_edge_cost); // Step 2: 构建最短跳数子图(只保留属于最短路径和 AVOID RELAY 的边) let mut subgraph: PeerGraph = PeerGraph::new(); let mut start_node_idx = None; for (node_idx, peer_id) in graph.node_references() { let new_node_idx = subgraph.add_node(*peer_id); if node_idx == *start_node { start_node_idx = Some(new_node_idx); } } for edge in graph.edge_references() { let (src, tgt) = graph.edge_endpoints(edge.id()).unwrap(); let Some(src_path_len) = path_len_map.get(&src) else { continue; }; let Some(tgt_path_len) = path_len_map.get(&tgt) else { continue; }; if *src_path_len + normalize_edge_cost(edge) == *tgt_path_len { subgraph.add_edge(src, tgt, *edge.weight()); } } // Step 3: 第二次 Dijkstra - 在子图上找代价最小的路径 self.gen_next_hop_map_with_least_cost(&subgraph, &start_node_idx.unwrap(), version); } fn gen_next_hop_map_with_least_cost( &self, graph: &PeerGraph, start_node: &NodeIndex, version: Version, ) { if graph.node_weight(*start_node).is_none() { tracing::warn!( ?start_node, version, "invalid start node for least-cost route rebuild" ); return; } let (costs, next_hops) = dijkstra_with_first_hop(&graph, *start_node, |e| *e.weight()); for (dst, (next_hop, path_len)) in next_hops.iter() { let info = NextHopInfo { next_hop_peer_id: *graph.node_weight(*next_hop).unwrap(), path_latency: (*costs.get(dst).unwrap() % AVOID_RELAY_COST) as i32, path_len: { *path_len }, version, }; let dst_peer_id = *graph.node_weight(*dst).unwrap(); self.next_hop_map .entry(dst_peer_id) .and_modify(|x| { if x.version < version { *x = info; } }) .or_insert(info); } self.next_hop_map_version.set_if_larger(version); } fn build_from_synced_info( &self, my_peer_id: PeerId, synced_info: &SyncedRouteInfo, policy: NextHopPolicy, cost_calc: &T, ) { let version = synced_info.version.get(); let local_proxy_cidrs = synced_info .peer_infos .read() .get(&my_peer_id) .into_iter() .flat_map(|info| &info.proxy_cidrs) .filter_map(|cidr| cidr.parse::().ok()) .collect::>(); // build next hop map let (graph, start_node) = Self::build_peer_graph_from_synced_info(my_peer_id, synced_info, cost_calc); if graph.node_count() == 0 { tracing::warn!("no peer in graph, cannot build next hop map"); self.next_hop_map_version.set_if_larger(version); self.clean_expired_route_info(); return; } if start_node == NodeIndex::end() { tracing::warn!( ?my_peer_id, version, "my peer id is missing in graph, skip next-hop rebuild this round" ); self.next_hop_map_version.set_if_larger(version); self.clean_expired_route_info(); return; } if matches!(policy, NextHopPolicy::LeastHop) { self.gen_next_hop_map_with_least_hop(&graph, &start_node, version); } else { self.gen_next_hop_map_with_least_cost(&graph, &start_node, version); }; let mut new_cidr_prefix_trie = PrefixMap::new(); let mut new_cidr_v6_prefix_trie = PrefixMap::new(); // build peer_infos, ipv4_peer_id_map, cidr_peer_id_map // only set map for peers we can reach. for item in self.next_hop_map.iter() { if item.version < version { // skip if the next hop entry is outdated. (peer is unreachable) continue; } let peer_id = item.key(); let Some(info) = synced_info.peer_infos.read().get(peer_id).cloned() else { continue; }; self.peer_infos.insert(*peer_id, info.clone()); let peer_id_and_version = PeerIdVersion { peer_id: *peer_id, version, }; let is_new_peer_better = |old_peer: &PeerIdVersion| -> bool { if peer_id_and_version.version > old_peer.version { return true; } if peer_id_and_version.peer_id == old_peer.peer_id { return false; } let old_next_hop = self.get_next_hop(old_peer.peer_id); let new_next_hop = item.value(); old_next_hop.is_none() || new_next_hop.path_len < old_next_hop.unwrap().path_len }; if let Some(ipv4_addr) = info.ipv4_addr { self.ipv4_peer_id_map .entry(ipv4_addr.into()) .and_modify(|v| { if is_new_peer_better(v) { *v = peer_id_and_version; } }) .or_insert(peer_id_and_version); } if let Some(ipv6_addr) = info.ipv6_addr.and_then(|x| x.address) { self.ipv6_peer_id_map .entry(ipv6_addr.into()) .and_modify(|v| { if is_new_peer_better(v) { *v = peer_id_and_version; } }) .or_insert(peer_id_and_version); } if let Some(ipv6_addr) = info .ipv6_public_addr_lease .as_ref() .and_then(|addr| addr.address) { self.ipv6_peer_id_map .entry(ipv6_addr.into()) .and_modify(|v| { if is_new_peer_better(v) { *v = peer_id_and_version; } }) .or_insert(peer_id_and_version); } for cidr in info.proxy_cidrs.iter() { let Ok(cidr) = cidr.parse::() else { tracing::warn!("invalid proxy cidr: {:?}, from peer: {:?}", cidr, peer_id); continue; }; if *peer_id != my_peer_id && local_proxy_cidrs .iter() .any(|local_cidr| cidr_is_subset(&cidr, local_cidr)) { tracing::debug!( ?peer_id, ?my_peer_id, ?local_proxy_cidrs, ?cidr, "skip remote proxy cidr covered by local announced proxy cidr while building route table" ); continue; } match cidr { IpCidr::V4(cidr) => { new_cidr_prefix_trie .entry(cidr) .and_modify(|e| { // if ourself has same cidr, ensure here put my peer id, so we can know deadloop may happen. if *peer_id == my_peer_id || is_new_peer_better(e) { *e = peer_id_and_version; } }) .or_insert(peer_id_and_version); } IpCidr::V6(cidr) => { new_cidr_v6_prefix_trie .entry(cidr) .and_modify(|e| { // if ourself has same cidr, ensure here put my peer id, so we can know deadloop may happen. if *peer_id == my_peer_id || is_new_peer_better(e) { *e = peer_id_and_version; } }) .or_insert(peer_id_and_version); } } tracing::debug!( "add cidr: {:?} to peer: {:?}, my peer id: {:?}", cidr, peer_id, my_peer_id ); } } self.cidr_peer_id_map.store(Arc::new(new_cidr_prefix_trie)); self.cidr_v6_peer_id_map .store(Arc::new(new_cidr_v6_prefix_trie)); tracing::trace!( my_peer_id = my_peer_id, cidrs = ?self.cidr_peer_id_map.load(), cidrs_v6 = ?self.cidr_v6_peer_id_map.load(), "update peer cidr map" ); } fn get_peer_id_for_proxy(&self, ip: &IpAddr) -> Option { match ip { IpAddr::V4(ipv4) => self .cidr_peer_id_map .load() .get_lpm(&Ipv4Cidr::new(*ipv4, 32).unwrap()) .map(|x| x.1.peer_id), IpAddr::V6(ipv6) => self .cidr_v6_peer_id_map .load() .get_lpm(&Ipv6Cidr::new(*ipv6, 128).unwrap()) .map(|x| x.1.peer_id), } } } type SessionId = u64; type AtomicSessionId = atomic_shim::AtomicU64; struct SessionTask { my_peer_id: PeerId, task: Arc>>>, } impl SessionTask { fn new(my_peer_id: PeerId) -> Self { SessionTask { my_peer_id, task: Arc::new(std::sync::Mutex::new(None)), } } fn set_task(&self, task: JoinHandle<()>) { if let Some(old) = self.task.lock().unwrap().replace(task) { old.abort(); } } fn is_running(&self) -> bool { if let Some(task) = self.task.lock().unwrap().as_ref() { !task.is_finished() } else { false } } } impl Drop for SessionTask { fn drop(&mut self) { if let Some(task) = self.task.lock().unwrap().take() { task.abort(); } tracing::debug!(my_peer_id = self.my_peer_id, "drop SessionTask"); } } impl Debug for SessionTask { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SessionTask") .field("is_running", &self.is_running()) .finish() } } #[derive(Debug)] struct VersionAndTouchTime { version: AtomicVersion, touch_time: AtomicCell, } impl Default for VersionAndTouchTime { fn default() -> Self { VersionAndTouchTime { version: AtomicVersion::new(), touch_time: AtomicCell::new(Instant::now()), } } } impl VersionAndTouchTime { fn touch(&self) { self.touch_time.store(Instant::now()); } fn get(&self) -> Version { self.version.get() } fn set_if_larger(&self, version: Version) { self.version.set_if_larger(version); } fn is_expired(&self) -> bool { self.touch_time.load().elapsed() > Duration::from_secs(60) } } // if we need to sync route info with one peer, we create a SyncRouteSession with that peer. #[derive(Debug)] struct SyncRouteSession { my_peer_id: PeerId, dst_peer_id: PeerId, dst_saved_peer_info_versions: DashMap, dst_saved_conn_info_version: DashMap, dst_saved_foreign_network_versions: DashMap, // we don't want to send unreachable peer infos / conn infos to peer, so we keep track of them. unreachable_peers_for_peer_info: parking_lot::Mutex>, unreachable_peers_for_conn_info: parking_lot::Mutex>, last_sync_succ_timestamp: AtomicCell>, my_session_id: AtomicSessionId, dst_session_id: AtomicSessionId, // every node should have exactly one initator session to one other non-initiator peer. we_are_initiator: AtomicBool, dst_is_initiator: AtomicBool, need_sync_initiator_info: AtomicBool, rpc_tx_count: AtomicU32, rpc_rx_count: AtomicU32, task: SessionTask, lock: parking_lot::Mutex<()>, } impl SyncRouteSession { fn new(my_peer_id: PeerId, dst_peer_id: PeerId) -> Self { SyncRouteSession { my_peer_id, dst_peer_id, dst_saved_peer_info_versions: DashMap::new(), dst_saved_conn_info_version: DashMap::new(), dst_saved_foreign_network_versions: DashMap::new(), unreachable_peers_for_peer_info: parking_lot::Mutex::new(BTreeMap::new()), unreachable_peers_for_conn_info: parking_lot::Mutex::new(BTreeMap::new()), last_sync_succ_timestamp: AtomicCell::new(None), my_session_id: AtomicSessionId::new(rand::random()), dst_session_id: AtomicSessionId::new(0), we_are_initiator: AtomicBool::new(false), dst_is_initiator: AtomicBool::new(false), need_sync_initiator_info: AtomicBool::new(false), rpc_tx_count: AtomicU32::new(0), rpc_rx_count: AtomicU32::new(0), task: SessionTask::new(my_peer_id), lock: parking_lot::Mutex::new(()), } } fn check_saved_peer_info_update_to_date(&self, peer_id: PeerId, version: Version) -> bool { if version == 0 || peer_id == self.dst_peer_id { // never send version 0 peer info to dst peer. return true; } self.dst_saved_peer_info_versions .get(&peer_id) .map(|v| { v.touch(); v.get() >= version }) .unwrap_or(false) } fn check_saved_conn_version_update_to_date(&self, peer_id: PeerId, version: Version) -> bool { if version == 0 || peer_id == self.dst_peer_id { // never send version 0 conn bitmap to dst peer. return true; } self.dst_saved_conn_info_version .get(&peer_id) .map(|v| { v.touch(); v.get() >= version }) .unwrap_or(false) } fn check_saved_foreign_network_version_update_to_date( &self, foreign_network_key: &ForeignNetworkRouteInfoKey, version: Version, ) -> bool { if version == 0 || foreign_network_key.peer_id == self.dst_peer_id { // never send version 0 foreign network to dst peer. return true; } self.dst_saved_foreign_network_versions .get(foreign_network_key) .map(|x| { x.touch(); x.get() >= version }) .unwrap_or(false) } fn update_dst_saved_peer_info_version(&self, infos: &[RoutePeerInfo], dst_peer_id: PeerId) { for info in infos.iter() { if info.peer_id == dst_peer_id { // we never send dst peer info to dst peer, so no need to store it. continue; } self.dst_saved_peer_info_versions .entry(info.peer_id) .or_default() .set_if_larger(info.version); } } fn update_dst_saved_conn_bitmap_version( &self, conn_bitmap: &RouteConnBitmap, dst_peer_id: PeerId, ) { for peer_id_version in conn_bitmap.peer_ids.iter() { if peer_id_version.peer_id == dst_peer_id { continue; } self.dst_saved_conn_info_version .entry(peer_id_version.peer_id) .or_default() .set_if_larger(peer_id_version.version); } } fn update_dst_saved_conn_peer_list_version( &self, conn_peer_list: &RouteConnPeerList, dst_peer_id: PeerId, ) { for peer_conn_info in &conn_peer_list.peer_conn_infos { let Some(peer_id_version) = peer_conn_info.peer_id else { continue; }; if peer_id_version.peer_id == dst_peer_id { continue; } self.dst_saved_conn_info_version .entry(peer_id_version.peer_id) .or_default() .set_if_larger(peer_id_version.version); } } fn update_dst_saved_conn_info_version(&self, conn_info: &ConnInfo, dst_peer_id: PeerId) { match conn_info { ConnInfo::ConnBitmap(conn_bitmap) => { self.update_dst_saved_conn_bitmap_version(conn_bitmap, dst_peer_id); } ConnInfo::ConnPeerList(peer_list) => { self.update_dst_saved_conn_peer_list_version(peer_list, dst_peer_id); } } } fn update_dst_saved_foreign_network_version( &self, foreign_network: &RouteForeignNetworkInfos, dst_peer_id: PeerId, ) { for item in foreign_network.infos.iter() { if item.key.as_ref().unwrap().peer_id == dst_peer_id { continue; } self.dst_saved_foreign_network_versions .entry(item.key.clone().unwrap()) .or_default() .set_if_larger(item.value.as_ref().unwrap().version); } } fn update_initiator_flag(&self, is_initiator: bool) { self.we_are_initiator.store(is_initiator, Ordering::Relaxed); self.need_sync_initiator_info.store(true, Ordering::Relaxed); } // return whether session id is updated fn update_dst_session_id(&self, session_id: SessionId) { if session_id != self.dst_session_id.load(Ordering::Relaxed) { tracing::warn!(?self, ?session_id, "session id mismatch, clear saved info."); self.dst_session_id.store(session_id, Ordering::Relaxed); self.dst_saved_conn_info_version.clear(); self.dst_saved_peer_info_versions.clear(); // update_dst_session_id is always called with session lock held, so clear // last_sync_succ_timestamp and unreachable_peers non-atomic is safe. self.last_sync_succ_timestamp.store(None); self.unreachable_peers_for_peer_info.lock().clear(); self.unreachable_peers_for_conn_info.lock().clear(); } } fn clean_dst_saved_map(&self) { self.dst_saved_peer_info_versions .retain(|_, v| !v.is_expired()); self.dst_saved_peer_info_versions.shrink_to_fit(); self.dst_saved_conn_info_version .retain(|_, v| !v.is_expired()); self.dst_saved_conn_info_version.shrink_to_fit(); self.dst_saved_foreign_network_versions .retain(|_, v| !v.is_expired()); self.dst_saved_foreign_network_versions.shrink_to_fit(); } fn update_last_sync_succ_timestamp(&self, next_last_sync_succ_timestamp: SystemTime) { let _ = self.last_sync_succ_timestamp.fetch_update(|x| { if x.is_none_or(|old| old < next_last_sync_succ_timestamp) { Some(Some(next_last_sync_succ_timestamp)) } else { None } }); } fn short_debug_string(&self) -> String { format!( "session_dst_peer: {:?}, my_session_id: {:?}, dst_session_id: {:?}, we_are_initiator: {:?}, dst_is_initiator: {:?}, rpc_tx_count: {:?}, rpc_rx_count: {:?}, task: {:?}", self.dst_peer_id, self.my_session_id, self.dst_session_id, self.we_are_initiator, self.dst_is_initiator, self.rpc_tx_count, self.rpc_rx_count, self.task ) } } impl Drop for SyncRouteSession { fn drop(&mut self) { tracing::debug!(?self, "drop SyncRouteSession"); } } struct PeerRouteServiceImpl { my_peer_id: PeerId, my_peer_route_id: u64, global_ctx: ArcGlobalCtx, sessions: DashMap>, interface: Mutex>, cost_calculator: std::sync::RwLock>, route_table: RouteTable, route_table_with_cost: RouteTable, foreign_network_owner_map: DashMap>, foreign_network_my_peer_id_map: DashMap<(String, PeerId), PeerId>, synced_route_info: SyncedRouteInfo, public_ipv6_service: std::sync::Mutex>, self_public_ipv6_addr_lease: std::sync::Mutex>, cached_local_conn_map: std::sync::Mutex, cached_local_conn_map_version: AtomicVersion, cached_interface_peer_snapshot: std::sync::Mutex>, interface_peers_generation: AtomicU64, applied_interface_peers_generation: AtomicU64, last_update_my_foreign_network: AtomicCell>, peer_info_last_update: AtomicCell, } impl Debug for PeerRouteServiceImpl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PeerRouteServiceImpl") .field("my_peer_id", &self.my_peer_id) .field("my_peer_route_id", &self.my_peer_route_id) .field("network", &self.global_ctx.get_network_identity()) .field("sessions", &self.sessions) .field("route_table", &self.route_table) .field("route_table_with_cost", &self.route_table_with_cost) .field("synced_route_info", &self.synced_route_info) .field("foreign_network_owner_map", &self.foreign_network_owner_map) .field( "foreign_network_my_peer_id_map", &self.foreign_network_my_peer_id_map, ) .field( "cached_local_conn_map", &self.cached_local_conn_map.lock().unwrap(), ) .finish() } } impl PeerRouteServiceImpl { fn new(my_peer_id: PeerId, global_ctx: ArcGlobalCtx) -> Self { PeerRouteServiceImpl { my_peer_id, my_peer_route_id: rand::random(), global_ctx, sessions: DashMap::new(), interface: Mutex::new(None), cost_calculator: std::sync::RwLock::new(Some(Box::new(DefaultRouteCostCalculator))), route_table: RouteTable::new(), route_table_with_cost: RouteTable::new(), foreign_network_owner_map: DashMap::new(), foreign_network_my_peer_id_map: DashMap::new(), synced_route_info: SyncedRouteInfo { peer_infos: RwLock::new(OrderedHashMap::new()), raw_peer_infos: DashMap::new(), conn_map: RwLock::new(OrderedHashMap::new()), foreign_network: DashMap::new(), group_trust_map: DashMap::new(), group_trust_map_cache: DashMap::new(), trusted_credential_pubkeys: DashMap::new(), non_reusable_credential_owners: DashMap::new(), version: AtomicVersion::new(), }, public_ipv6_service: std::sync::Mutex::new(Weak::new()), self_public_ipv6_addr_lease: std::sync::Mutex::new(None), cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::default()), cached_local_conn_map_version: AtomicVersion::new(), cached_interface_peer_snapshot: std::sync::Mutex::new(Arc::new( InterfacePeerSnapshot::default(), )), interface_peers_generation: AtomicU64::new(1), applied_interface_peers_generation: AtomicU64::new(0), last_update_my_foreign_network: AtomicCell::new(None), peer_info_last_update: AtomicCell::new(std::time::Instant::now()), } } fn get_my_secret_digest(&self) -> Option> { let ni = self.global_ctx.get_network_identity(); ni.network_secret_digest.map(|d| d.to_vec()) } fn is_active_non_reusable_credential_peer(&self, peer_id: PeerId) -> bool { peer_id == self.my_peer_id || self.sessions.contains_key(&peer_id) || self.route_table.peer_reachable(peer_id) } fn is_credential_node(&self) -> bool { self.global_ctx .get_network_identity() .network_secret .is_none() && self .global_ctx .config .get_secure_mode() .map(|c| c.enabled) .unwrap_or(false) } fn set_public_ipv6_service(&self, service: Weak) { *self.public_ipv6_service.lock().unwrap() = service; } fn public_ipv6_service(&self) -> Option> { self.public_ipv6_service.lock().unwrap().upgrade() } fn notify_public_ipv6_route_change(&self) -> bool { self.public_ipv6_service() .map(|service| service.handle_route_change()) .unwrap_or(false) } fn get_or_create_session(&self, dst_peer_id: PeerId) -> Arc { self.sessions .entry(dst_peer_id) .or_insert_with(|| Arc::new(SyncRouteSession::new(self.my_peer_id, dst_peer_id))) .value() .clone() } fn get_session(&self, dst_peer_id: PeerId) -> Option> { self.sessions.get(&dst_peer_id).map(|x| x.value().clone()) } fn remove_session(&self, dst_peer_id: PeerId) { self.sessions.remove(&dst_peer_id); shrink_dashmap(&self.sessions, None); } fn list_session_peers(&self) -> Vec { self.sessions.iter().map(|x| *x.key()).collect() } pub fn mark_interface_peers_dirty(&self) { self.interface_peers_generation .fetch_add(1, Ordering::Relaxed); } async fn interface_peer_snapshot_uncached(&self) -> InterfacePeerSnapshot { let interface = self.interface.lock().await; let interface = interface.as_ref().unwrap(); let peers: BTreeSet<_> = interface.list_peers().await.into_iter().collect(); let mut identity_types = BTreeMap::new(); for peer_id in peers.iter().copied() { identity_types.insert(peer_id, interface.get_peer_identity_type(peer_id).await); } InterfacePeerSnapshot { generation: 0, peers, identity_types, } } async fn interface_peer_snapshot(&self) -> Arc { loop { let start_generation = self.interface_peers_generation.load(Ordering::Acquire); { let cached = self.cached_interface_peer_snapshot.lock().unwrap(); if cached.generation == start_generation { return cached.clone(); } } let mut snapshot = self.interface_peer_snapshot_uncached().await; let end_generation = self.interface_peers_generation.load(Ordering::Acquire); if start_generation == end_generation { snapshot.generation = end_generation; let snapshot = Arc::new(snapshot); *self.cached_interface_peer_snapshot.lock().unwrap() = snapshot.clone(); return snapshot; } } } async fn list_peers_from_interface_snapshot(&self) -> (u64, BTreeSet) { let snapshot = self.interface_peer_snapshot().await; (snapshot.generation, snapshot.peers.clone()) } async fn list_peers_from_interface>(&self) -> T { self.interface_peer_snapshot() .await .peers .iter() .copied() .collect() } async fn get_peer_identity_type_from_interface( &self, peer_id: PeerId, ) -> Option { let snapshot = self.interface_peer_snapshot().await; if let Some(identity_type) = snapshot.identity_types.get(&peer_id) { return *identity_type; } self.interface .lock() .await .as_ref() .unwrap() .get_peer_identity_type(peer_id) .await } async fn get_peer_public_key_from_interface(&self, peer_id: PeerId) -> Option> { self.interface .lock() .await .as_ref() .unwrap() .get_peer_public_key(peer_id) .await } fn update_my_peer_info(&self) -> bool { self.synced_route_info.update_my_peer_info( self.my_peer_id, self.my_peer_route_id, &self.global_ctx, *self.self_public_ipv6_addr_lease.lock().unwrap(), ) } async fn update_my_conn_info(&self) -> bool { let current_generation = self.interface_peers_generation.load(Ordering::Acquire); let generation_applied = self .applied_interface_peers_generation .load(Ordering::Acquire) == current_generation; if generation_applied { let need_periodic_requery = self .interface .lock() .await .as_ref() .map(|x| x.need_periodic_requery_peers()) .unwrap_or(false); if !need_periodic_requery { return false; } self.mark_interface_peers_dirty(); } let (generation, connected_peers) = self.list_peers_from_interface_snapshot().await; let updated = self .synced_route_info .update_my_conn_info(self.my_peer_id, connected_peers); self.applied_interface_peers_generation .store(generation, Ordering::Release); updated } async fn update_my_foreign_network(&self) -> bool { let last_time = self.last_update_my_foreign_network.load(); if last_time.is_some() && last_time.unwrap().elapsed().as_secs() < use_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC) { return false; } self.last_update_my_foreign_network .store(Some(std::time::Instant::now())); let foreign_networks = self .interface .lock() .await .as_ref() .unwrap() .list_foreign_networks() .await; // do not need update owner map because we always filter out my peer id. self.synced_route_info .update_my_foreign_network(self.my_peer_id, foreign_networks) } fn update_route_table(&self) { self.cost_calculator .write() .unwrap() .as_mut() .unwrap() .begin_update(); let calc_locked = self.cost_calculator.read().unwrap(); self.route_table.build_from_synced_info( self.my_peer_id, &self.synced_route_info, NextHopPolicy::LeastHop, calc_locked.as_ref().unwrap(), ); self.route_table_with_cost.build_from_synced_info( self.my_peer_id, &self.synced_route_info, NextHopPolicy::LeastCost, calc_locked.as_ref().unwrap(), ); drop(calc_locked); self.cost_calculator .write() .unwrap() .as_mut() .unwrap() .end_update(); } fn update_foreign_network_owner_map(&self) { self.foreign_network_my_peer_id_map.clear(); self.foreign_network_owner_map.clear(); for item in self.synced_route_info.foreign_network.iter() { let key = item.key(); let entry = item.value(); if key.peer_id == self.my_peer_id || !self.route_table.peer_reachable(key.peer_id) || entry.foreign_peer_ids.is_empty() { continue; } let network_identity = NetworkIdentity { network_name: key.network_name.clone(), network_secret: None, network_secret_digest: Some( entry .network_secret_digest .clone() .try_into() .unwrap_or_default(), ), }; self.foreign_network_owner_map .entry(network_identity) .or_default() .push(entry.my_peer_id_for_this_network); self.foreign_network_my_peer_id_map.insert( (key.network_name.clone(), entry.my_peer_id_for_this_network), key.peer_id, ); } } fn cost_calculator_need_update(&self) -> bool { self.cost_calculator .read() .unwrap() .as_ref() .map(|x| x.need_update()) .unwrap_or(false) } fn handle_global_ctx_event(&self, event: &GlobalCtxEvent) { if matches!( event, GlobalCtxEvent::PeerAdded(_) | GlobalCtxEvent::PeerRemoved(_) | GlobalCtxEvent::PeerConnAdded(_) | GlobalCtxEvent::PeerConnRemoved(_) ) { self.mark_interface_peers_dirty(); } } fn update_route_table_and_cached_local_conn_bitmap(&self) { self.update_peer_info_last_update(); // update route table first because we want to filter out unreachable peers. self.update_route_table(); let synced_version = self.synced_route_info.version.get(); // the conn_bitmap should contain complete list of directly connected peers. // use union of dst peers can preserve this property. let mut all_peer_ids: BTreeMap = BTreeMap::new(); let mut add_to_all_peer_ids = |peer_id: PeerId, version: Version| { all_peer_ids .entry(peer_id) .and_modify(|x| { if *x < version { *x = version; } }) .or_insert(version); }; for item in self.synced_route_info.conn_map.read().iter() { let src_peer_id = *item.0; if !self.route_table.peer_reachable(src_peer_id) { continue; } add_to_all_peer_ids(src_peer_id, item.1.version.get()); for dst_peer_id in item.1.connected_peers.iter() { add_to_all_peer_ids(*dst_peer_id, 0); } } let mut conn_bitmap = RouteConnBitmap { bitmap: vec![0; (all_peer_ids.len() * all_peer_ids.len()).div_ceil(8)], peer_ids: all_peer_ids .iter() .map(|x| PeerIdVersion { peer_id: *x.0, version: *x.1, }) .collect(), }; let locked_conn_map = self.synced_route_info.conn_map.read(); let all_peer_ids = &conn_bitmap.peer_ids; for (peer_idx, peer_id_version) in all_peer_ids.iter().enumerate() { let Some(connected) = locked_conn_map.get(&peer_id_version.peer_id) else { continue; }; for (idx, other_peer_id_version) in all_peer_ids.iter().enumerate() { if connected .connected_peers .contains(&other_peer_id_version.peer_id) { let bit_idx = peer_idx * all_peer_ids.len() + idx; conn_bitmap.bitmap[bit_idx / 8] |= 1 << (bit_idx % 8); } } } drop(locked_conn_map); let mut locked = self.cached_local_conn_map.lock().unwrap(); if self .cached_local_conn_map_version .set_if_larger(synced_version) { *locked = conn_bitmap; } } fn build_route_info(&self, session: &SyncRouteSession) -> Option> { let mut route_infos = Vec::new(); let peer_infos = self.synced_route_info.peer_infos.read(); let mut unreachable_peers_for_peer_info = session.unreachable_peers_for_peer_info.lock(); let last_sync_succ_timestamp = session.last_sync_succ_timestamp.load(); for (peer_id, peer_info) in peer_infos.iter().rev() { // stop iter if last_update of peer info is older than session.last_sync_succ_timestamp if let Some(last_update) = peer_info.last_update { let last_update = TryInto::::try_into(last_update).unwrap(); if last_sync_succ_timestamp.is_some_and(|t| last_update < t) { break; } } if session.check_saved_peer_info_update_to_date(peer_info.peer_id, peer_info.version) { continue; } // do not send unreachable peer info to dst peer. if !self.route_table.peer_reachable(*peer_id) { unreachable_peers_for_peer_info.insert(*peer_id, peer_info.version); continue; } route_infos.push(peer_info.clone()); } unreachable_peers_for_peer_info.retain(|peer_id, version| { if session.check_saved_peer_info_update_to_date(*peer_id, *version) { // if saved peer info is up-to-date, forget this peer id. return false; } let Some(peer_info) = peer_infos.get(peer_id) else { // if not found in peer info map, forget this peer id. return false; }; if self.route_table.peer_reachable(*peer_id) { route_infos.push(peer_info.clone()); } // this round rpc may fail, so keep it and remove the id only when it's in dst_saved_map true }); if route_infos.is_empty() { None } else { Some(route_infos) } } fn build_conn_peer_list( &self, session: &SyncRouteSession, estimated_size: &mut usize, ) -> Option { let last_sync_succ_timestamp = session.last_sync_succ_timestamp.load(); let mut peer_conn_infos = Vec::new(); *estimated_size = 0; let conn_map = self.synced_route_info.conn_map.read(); let mut unreachable_peers_for_conn_info = session.unreachable_peers_for_conn_info.lock(); let mut add_to_conn_peer_list = |peer_id: PeerId, conn_info: &RouteConnInfo| { peer_conn_infos.push(PeerConnInfo { peer_id: Some(PeerIdVersion { peer_id, version: conn_info.version.get(), }), connected_peer_ids: conn_info.connected_peers.iter().copied().collect(), }); *estimated_size += std::mem::size_of::() + conn_info.connected_peers.len() * std::mem::size_of::(); }; for (peer_id, conn_info) in conn_map.iter().rev() { // stop iter if last_update of conn info is older than session.last_sync_succ_timestamp let last_update = TryInto::::try_into(conn_info.last_update).unwrap(); if last_sync_succ_timestamp.is_some_and(|t| last_update < t) { break; } if session.check_saved_conn_version_update_to_date(*peer_id, conn_info.version.get()) { continue; } if !self.route_table.peer_reachable(*peer_id) { unreachable_peers_for_conn_info.insert(*peer_id, conn_info.version.get()); continue; } add_to_conn_peer_list(*peer_id, conn_info); } unreachable_peers_for_conn_info.retain(|peer_id, version| { if session.check_saved_conn_version_update_to_date(*peer_id, *version) { // if saved conn info is up-to-date, forget this peer id. return false; } let Some(conn_info) = conn_map.get(peer_id) else { // if not found in peer info map, forget this peer id. return false; }; if self.route_table.peer_reachable(*peer_id) { add_to_conn_peer_list(*peer_id, conn_info); } // this round rpc may fail, so keep it and remove the id only when it's in dst_saved_map true }); if peer_conn_infos.is_empty() { return None; } Some(RouteConnPeerList { peer_conn_infos }) } fn build_conn_bitmap(&self) -> RouteConnBitmap { self.cached_local_conn_map.lock().unwrap().clone() } fn estimate_conn_bitmap_size(&self) -> usize { let cached_conn_map = self.cached_local_conn_map.lock().unwrap(); cached_conn_map.bitmap.len() + (cached_conn_map.peer_ids.len() * std::mem::size_of::()) } fn build_foreign_network_info( &self, session: &SyncRouteSession, ) -> Option { let mut foreign_networks = RouteForeignNetworkInfos::default(); for item in self.synced_route_info.foreign_network.iter() { if session.check_saved_foreign_network_version_update_to_date( item.key(), item.value().version, ) { continue; } foreign_networks .infos .push(route_foreign_network_infos::Info { key: Some(item.key().clone()), value: Some(item.value().clone()), }); } if foreign_networks.infos.is_empty() { None } else { Some(foreign_networks) } } async fn update_my_infos(&self) -> bool { let my_peer_info_updated = self.update_my_peer_info(); let my_conn_info_updated = self.update_my_conn_info().await; let my_foreign_network_updated = self.update_my_foreign_network().await; let mut untrusted_changed = false; if my_peer_info_updated { untrusted_changed = self.refresh_credential_trusts_and_disconnect().await; } let mut public_ipv6_state_updated = false; if my_peer_info_updated || my_conn_info_updated || untrusted_changed { self.update_route_table_and_cached_local_conn_bitmap(); self.update_foreign_network_owner_map(); public_ipv6_state_updated = self.notify_public_ipv6_route_change(); } if my_peer_info_updated { self.update_peer_info_last_update(); } my_peer_info_updated || my_conn_info_updated || my_foreign_network_updated || public_ipv6_state_updated } async fn refresh_acl_groups(&self) -> bool { let my_peer_info_updated = self.update_my_peer_info(); let trust_admin_groups_without_proof = self .global_ctx .get_network_identity() .network_secret .is_none(); let peer_infos: Vec<_> = self .synced_route_info .peer_infos .read() .iter() .map(|(_, info)| info.clone()) .collect(); self.synced_route_info.verify_and_update_group_trusts( &peer_infos, &self.global_ctx.get_acl_group_declarations(), trust_admin_groups_without_proof, ); let untrusted = self.refresh_credential_trusts_with_current_topology(); self.disconnect_untrusted_peers(&untrusted).await; let mut public_ipv6_state_updated = false; if my_peer_info_updated || !untrusted.is_empty() { self.update_route_table_and_cached_local_conn_bitmap(); self.update_foreign_network_owner_map(); public_ipv6_state_updated = self.notify_public_ipv6_route_change(); } if my_peer_info_updated { self.update_peer_info_last_update(); } my_peer_info_updated || !untrusted.is_empty() || public_ipv6_state_updated } fn refresh_credential_trusts(&self) -> Vec { let network_identity = self.global_ctx.get_network_identity(); let (untrusted, global_trusted_keys) = self .synced_route_info .verify_and_update_credential_trusts_with_active_peers_protecting( network_identity.network_secret.as_deref(), |_| true, Some(self.my_peer_id), ); self.global_ctx .update_trusted_keys(global_trusted_keys, &network_identity.network_name); untrusted } fn refresh_credential_trusts_with_current_topology(&self) -> Vec { let network_identity = self.global_ctx.get_network_identity(); // Non-reusable credential owner election depends on reachability, so rebuild the // route table from the latest synced peer/conn state before checking active peers. self.update_route_table_and_cached_local_conn_bitmap(); let (untrusted, global_trusted_keys) = self .synced_route_info .verify_and_update_credential_trusts_with_active_peers_protecting( network_identity.network_secret.as_deref(), |peer_id| self.is_active_non_reusable_credential_peer(peer_id), Some(self.my_peer_id), ); self.global_ctx .update_trusted_keys(global_trusted_keys, &network_identity.network_name); if !untrusted.is_empty() { self.update_route_table_and_cached_local_conn_bitmap(); } untrusted } async fn refresh_credential_trusts_and_disconnect(&self) -> bool { let untrusted = self.refresh_credential_trusts_with_current_topology(); self.disconnect_untrusted_peers(&untrusted).await; !untrusted.is_empty() } async fn disconnect_untrusted_peers(&self, untrusted_peers: &[PeerId]) { if untrusted_peers.is_empty() { return; } let interface = self.interface.lock().await; let Some(interface) = interface.as_ref() else { return; }; for peer_id in untrusted_peers { tracing::warn!(?peer_id, "disconnecting untrusted peer"); interface.close_peer(*peer_id).await; } } fn build_sync_request( &self, session: &SyncRouteSession, dst_peer_id: PeerId, ) -> ( Option>, Option, Option, ) { let route_infos = self.build_route_info(session); let conn_info = self.build_conn_info(session, dst_peer_id); let foreign_network = self.build_foreign_network_info(session); (route_infos, conn_info, foreign_network) } fn build_conn_info( &self, session: &SyncRouteSession, dst_peer_id: PeerId, ) -> Option { // Check if destination peer supports selective peer list sync let dst_supports_peer_list = self .synced_route_info .peer_infos .read() .get(&dst_peer_id) .and_then(|p| p.feature_flag) .map(|x| x.support_conn_list_sync) .unwrap_or(false) || FORCE_USE_CONN_LIST.load(Ordering::Relaxed); // Both formats are supported, choose the more efficient one let mut conn_list_estimated_size = 0; let peer_list = self.build_conn_peer_list(session, &mut conn_list_estimated_size)?; let bitmap_size = self.estimate_conn_bitmap_size(); if conn_list_estimated_size < bitmap_size && dst_supports_peer_list { Some(peer_list.into()) } else { Some(self.build_conn_bitmap().into()) } } async fn clear_expired_peer(&self) { let now = SystemTime::now(); let mut to_remove = Vec::new(); for (peer_id, peer_info) in self.synced_route_info.peer_infos.read().iter() { if let Ok(d) = now.duration_since(peer_info.last_update.unwrap().try_into().unwrap()) && (d > REMOVE_DEAD_PEER_INFO_AFTER || (d > REMOVE_UNREACHABLE_PEER_INFO_AFTER && !self.route_table.peer_reachable(*peer_id))) { to_remove.push(*peer_id); } } self.synced_route_info .remove_peers(to_remove.iter().copied()); // clear expired foreign network info let mut to_remove = Vec::new(); for item in self.synced_route_info.foreign_network.iter() { let Some(since_last_update) = item .value() .last_update .and_then(|x| SystemTime::try_from(x).ok()) .and_then(|x| now.duration_since(x).ok()) else { to_remove.push(item.key().clone()); continue; }; if since_last_update > REMOVE_DEAD_PEER_INFO_AFTER { to_remove.push(item.key().clone()); } } for p in to_remove.iter() { self.synced_route_info.foreign_network.remove(p); } self.refresh_credential_trusts_and_disconnect().await; self.route_table.clean_expired_route_info(); self.route_table_with_cost.clean_expired_route_info(); } fn build_sync_route_raw_req( req: &SyncRouteInfoRequest, raw_peer_infos: &DashMap, ) -> DynamicMessage { use prost_reflect::Value; let mut req_dynamic_msg = DynamicMessage::new(SyncRouteInfoRequest::default().descriptor()); req_dynamic_msg.transcode_from(req).unwrap(); let peer_infos = req.peer_infos.as_ref().map(|x| &x.items); if let Some(peer_infos) = peer_infos { let mut peer_info_raws = Vec::new(); for peer_info in peer_infos.iter() { if let Some(info) = raw_peer_infos.get(&peer_info.peer_id) { peer_info_raws.push(Value::Message(info.clone())); } else { let mut p = DynamicMessage::new(RoutePeerInfo::default().descriptor()); p.transcode_from(peer_info).unwrap(); peer_info_raws.push(Value::Message(p)); } } let mut peer_infos = DynamicMessage::new(RoutePeerInfos::default().descriptor()); peer_infos.set_field_by_name("items", Value::List(peer_info_raws)); req_dynamic_msg.set_field_by_name("peer_infos", Value::Message(peer_infos)); } req_dynamic_msg } async fn sync_route_with_peer( &self, dst_peer_id: PeerId, peer_rpc: Arc, sync_as_initiator: bool, ) -> bool { let Some(session) = self.get_session(dst_peer_id) else { // if session not exist, exit the sync loop. return true; }; let _session_lock = session.lock.lock(); let my_peer_id = self.my_peer_id; let next_last_sync_succ_timestamp = self.synced_route_info.get_next_last_sync_succ_timestamp(); let (peer_infos, conn_info, foreign_network) = self.build_sync_request(&session, dst_peer_id); if peer_infos.is_none() && conn_info.is_none() && foreign_network.is_none() && !session.need_sync_initiator_info.load(Ordering::Relaxed) && !(sync_as_initiator && session.we_are_initiator.load(Ordering::Relaxed)) { return true; } tracing::debug!( ?foreign_network, "sync_route request need send to peer. my_id {:?}, dst_peer_id: {:?}, peer_infos: {:?}, conn_info: {:?}, synced_route_info: {:?} session: {:?}", my_peer_id, dst_peer_id, peer_infos, conn_info, self.synced_route_info, session ); session .need_sync_initiator_info .store(false, Ordering::Relaxed); let rpc_stub = peer_rpc .rpc_client() .scoped_client::>( self.my_peer_id, dst_peer_id, self.global_ctx.get_network_name(), ); let sync_route_info_req = SyncRouteInfoRequest { my_peer_id, my_session_id: session.my_session_id.load(Ordering::Relaxed), is_initiator: session.we_are_initiator.load(Ordering::Relaxed), peer_infos: peer_infos.clone().map(|x| RoutePeerInfos { items: x }), conn_info: conn_info.clone(), foreign_network_infos: foreign_network.clone(), }; let mut ctrl = BaseController::default(); ctrl.set_timeout_ms(3000); ctrl.set_raw_input( Self::build_sync_route_raw_req( &sync_route_info_req, &self.synced_route_info.raw_peer_infos, ) .encode_to_vec() .into(), ); drop(_session_lock); let ret = rpc_stub .sync_route_info(ctrl, SyncRouteInfoRequest::default()) .await; let _session_lock = session.lock.lock(); tracing::debug!( "sync_route_info resp: {:?}, req: {:?}, session: {:?}, my_info: {:?}, next_last_sync_succ_timestamp: {:?}", ret, sync_route_info_req, session, self.global_ctx.network, next_last_sync_succ_timestamp ); match ret.as_ref() { Err(e) => { tracing::error!( ?ret, ?my_peer_id, ?dst_peer_id, ?e, "sync_route_info failed" ); session .need_sync_initiator_info .store(true, Ordering::Relaxed); } Ok(resp) => { if let Some(err) = resp.error { if err == Error::DuplicatePeerId as i32 { if !self.global_ctx.get_feature_flags().is_public_server { panic!("duplicate peer id"); } } else { tracing::error!(?ret, ?my_peer_id, ?dst_peer_id, "sync_route_info failed"); session .need_sync_initiator_info .store(true, Ordering::Relaxed); } } else { session.rpc_tx_count.fetch_add(1, Ordering::Relaxed); session .dst_is_initiator .store(resp.is_initiator, Ordering::Relaxed); session.update_dst_session_id(resp.session_id); if let Some(peer_infos) = &peer_infos { session.update_dst_saved_peer_info_version(peer_infos, dst_peer_id); } if let Some(conn_info) = &conn_info { session.update_dst_saved_conn_info_version(conn_info, dst_peer_id); } if let Some(foreign_network) = &foreign_network { session .update_dst_saved_foreign_network_version(foreign_network, dst_peer_id); } session.update_last_sync_succ_timestamp(next_last_sync_succ_timestamp); } } } false } fn update_peer_info_last_update(&self) { tracing::debug!( "update_peer_info_last_update, my_peer_id: {:?}, prev: {:?}, new: {:?}", self.my_peer_id, self.peer_info_last_update.load(), std::time::Instant::now() ); self.peer_info_last_update.store(std::time::Instant::now()); } fn get_peer_info_last_update(&self) -> std::time::Instant { self.peer_info_last_update.load() } fn get_peer_groups(&self, peer_id: PeerId) -> Arc> { self.synced_route_info .group_trust_map_cache .get(&peer_id) .map(|groups| groups.value().clone()) .unwrap_or_default() } fn clean_dst_saved_map(&self, dst_peer_id: PeerId) { let Some(session) = self.get_session(dst_peer_id) else { return; }; session.clean_dst_saved_map(); } } impl Drop for PeerRouteServiceImpl { fn drop(&mut self) { tracing::debug!(?self, "drop PeerRouteServiceImpl"); } } #[derive(Clone)] struct RouteSessionManager { service_impl: Weak, peer_rpc: Weak, sync_now_broadcast: tokio::sync::broadcast::Sender<()>, } impl Debug for RouteSessionManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RouteSessionManager") .field("dump_sessions", &self.dump_sessions()) .finish() } } fn get_raw_peer_infos(req_raw_input: &mut bytes::Bytes) -> Option> { let sync_req_dynamic_msg = DynamicMessage::decode(SyncRouteInfoRequest::default().descriptor(), req_raw_input) .unwrap(); let peer_infos = sync_req_dynamic_msg.get_field_by_name("peer_infos")?; let infos = peer_infos .as_message()? .get_field_by_name("items")? .as_list()? .iter() .map(|x| x.as_message().unwrap().clone()) .collect(); Some(infos) } #[async_trait::async_trait] impl OspfRouteRpc for RouteSessionManager { type Controller = BaseController; async fn sync_route_info( &self, ctrl: BaseController, request: SyncRouteInfoRequest, ) -> Result { let from_peer_id = request.my_peer_id; let from_session_id = request.my_session_id; let is_initiator = request.is_initiator; let peer_infos = request.peer_infos.map(|x| x.items); let conn_info = request.conn_info; let foreign_network = request.foreign_network_infos; let raw_peer_infos = if let Some(peer_infos_ref) = &peer_infos { let r = get_raw_peer_infos(&mut ctrl.get_raw_input().unwrap()).unwrap(); assert_eq!(r.len(), peer_infos_ref.len()); Some(r) } else { None }; let ret = self .do_sync_route_info( from_peer_id, from_session_id, is_initiator, peer_infos, raw_peer_infos, conn_info, foreign_network, ) .await; Ok(match ret { Ok(v) => v, Err(e) => SyncRouteInfoResponse { error: Some(e as i32), ..Default::default() }, }) } } impl RouteSessionManager { fn new(service_impl: Arc, peer_rpc: Arc) -> Self { RouteSessionManager { service_impl: Arc::downgrade(&service_impl), peer_rpc: Arc::downgrade(&peer_rpc), sync_now_broadcast: tokio::sync::broadcast::channel(100).0, } } async fn session_task( peer_rpc: Weak, service_impl: Weak, dst_peer_id: PeerId, mut sync_now: tokio::sync::broadcast::Receiver<()>, ) { const RETRY_BASE_MS: u64 = 50; const RETRY_MAX_MS: u64 = 5000; let mut last_sync = Instant::now(); let mut last_clean_dst_saved_map = Instant::now(); // Keep retry_delay_ms across outer iterations so that rapid // connect/disconnect flaps don't fully reset the backoff. let mut retry_delay_ms = RETRY_BASE_MS; loop { loop { let Some(service_impl) = service_impl.clone().upgrade() else { return; }; let Some(peer_rpc) = peer_rpc.clone().upgrade() else { return; }; // if we are initiator, we should ensure the dst has the session. let sync_as_initiator = if last_sync.elapsed().as_secs() > 10 { last_sync = Instant::now(); true } else { false }; if service_impl .sync_route_with_peer(dst_peer_id, peer_rpc.clone(), sync_as_initiator) .await { if last_clean_dst_saved_map.elapsed().as_secs() > 60 { last_clean_dst_saved_map = Instant::now(); service_impl.clean_dst_saved_map(dst_peer_id); } // Successful sync: decay backoff towards base so the next // real failure still starts at a reasonable level, but // don't fully reset to avoid 50ms bursts during flapping. retry_delay_ms = (retry_delay_ms / 2).max(RETRY_BASE_MS); break; } drop(service_impl); drop(peer_rpc); tokio::time::sleep(Duration::from_millis(retry_delay_ms)).await; retry_delay_ms = (retry_delay_ms * 2).min(RETRY_MAX_MS); } sync_now = sync_now.resubscribe(); select! { _ = tokio::time::sleep(Duration::from_secs(1)) => {} ret = sync_now.recv() => if let Err(e) = ret { tracing::debug!(?e, "session_task sync_now recv failed, ospf route may exit"); break; } } } } fn stop_session(&self, peer_id: PeerId) -> Result<(), Error> { tracing::warn!(?peer_id, "stop ospf sync session"); let Some(service_impl) = self.service_impl.upgrade() else { return Err(Error::Stopped); }; service_impl.remove_session(peer_id); Ok(()) } fn start_session_task(&self, session: &SyncRouteSession) { if !session.task.is_running() { session.task.set_task(tokio::spawn(Self::session_task( self.peer_rpc.clone(), self.service_impl.clone(), session.dst_peer_id, self.sync_now_broadcast.subscribe(), ))); } } fn get_or_start_session(&self, peer_id: PeerId) -> Result, Error> { let Some(service_impl) = self.service_impl.upgrade() else { return Err(Error::Stopped); }; tracing::info!(?service_impl.my_peer_id, ?peer_id, "start ospf sync session"); let session = service_impl.get_or_create_session(peer_id); self.start_session_task(&session); Ok(session) } async fn maintain_sessions(&self, service_impl: Arc) -> bool { let mut cur_dst_peer_id_to_initiate = None; let mut next_sleep_ms = 0; loop { let mut recv = self.sync_now_broadcast.subscribe(); select! { _ = tokio::time::sleep(Duration::from_millis(next_sleep_ms)) => {} _ = recv.recv() => {} } let interface_snapshot = service_impl.interface_peer_snapshot().await; let peers = &interface_snapshot.peers; let session_peers = self.list_session_peer_set(); for peer_id in session_peers.iter() { if !peers.contains(peer_id) { if Some(*peer_id) == cur_dst_peer_id_to_initiate { cur_dst_peer_id_to_initiate = None; } let _ = self.stop_session(*peer_id); } } // find peer_ids that are not initiators. let mut initiator_candidates = Vec::new(); for peer_id in peers.iter().copied() { // Step 9a: Filter OSPF session candidates based on direct auth level. // - Credential nodes only initiate sessions to admin nodes (not other credential nodes) // - Admin nodes don't initiate sessions to credential nodes let identity_type = interface_snapshot .identity_types .get(&peer_id) .copied() .flatten() .unwrap_or(PeerIdentityType::Admin); if matches!(identity_type, PeerIdentityType::Credential) { continue; } let Some(session) = service_impl.get_session(peer_id) else { initiator_candidates.push(peer_id); continue; }; if !session.dst_is_initiator.load(Ordering::Relaxed) { initiator_candidates.push(peer_id); } } if initiator_candidates.is_empty() { next_sleep_ms = 1000; continue; } let mut new_initiator_dst = None; // if any peer has NoPAT or OpenInternet stun type, we should use it. for peer_id in initiator_candidates.iter() { let Some(nat_type) = service_impl.route_table.get_udp_nat_type(*peer_id) else { continue; }; if nat_type == NatType::NoPat || nat_type == NatType::OpenInternet { new_initiator_dst = Some(*peer_id); break; } } if new_initiator_dst.is_none() { new_initiator_dst = Some(*initiator_candidates.first().unwrap()); } if new_initiator_dst != cur_dst_peer_id_to_initiate { tracing::warn!( "new_initiator: {:?}, prev: {:?}, my_id: {:?}", new_initiator_dst, cur_dst_peer_id_to_initiate, service_impl.my_peer_id ); // update initiator flag for previous session if let Some(cur_peer_id_to_initiate) = cur_dst_peer_id_to_initiate && let Some(session) = service_impl.get_session(cur_peer_id_to_initiate) { session.update_initiator_flag(false); } cur_dst_peer_id_to_initiate = new_initiator_dst; // update initiator flag for new session let Ok(session) = self.get_or_start_session(new_initiator_dst.unwrap()) else { tracing::warn!("get_or_start_session failed"); continue; }; session.update_initiator_flag(true); self.sync_now("update_initiator_flag"); } // clear sessions that are neither dst_initiator or we_are_initiator. for peer_id in session_peers.iter() { if let Some(session) = service_impl.get_session(*peer_id) { if (session.dst_is_initiator.load(Ordering::Relaxed) || session.we_are_initiator.load(Ordering::Relaxed) || session.need_sync_initiator_info.load(Ordering::Relaxed)) && session.task.is_running() { continue; } let _ = self.stop_session(*peer_id); } } next_sleep_ms = 1000; } } fn list_session_peers(&self) -> Vec { let Some(service_impl) = self.service_impl.upgrade() else { return vec![]; }; service_impl.list_session_peers() } fn list_session_peer_set(&self) -> BTreeSet { let Some(service_impl) = self.service_impl.upgrade() else { return BTreeSet::new(); }; service_impl.list_session_peers().into_iter().collect() } fn dump_sessions(&self) -> Result { let Some(service_impl) = self.service_impl.upgrade() else { return Err(Error::Stopped); }; let mut ret = format!("my_peer_id: {:?}\n", service_impl.my_peer_id); for item in service_impl.sessions.iter() { ret += format!( " session: {}, {}\n", item.key(), item.value().short_debug_string() ) .as_str(); } Ok(ret.to_string()) } fn sync_now(&self, reason: &str) { let ret = self.sync_now_broadcast.send(()); tracing::debug!(?ret, ?reason, "sync_now_broadcast.send"); } fn extract_credential_peer_info( &self, from_peer_id: PeerId, peer_infos: &[RoutePeerInfo], raw_peer_infos: &[DynamicMessage], credential: &TrustedCredentialPubkey, ) -> Option<(RoutePeerInfo, DynamicMessage)> { let info_idx = peer_infos.iter().position(|p| p.peer_id == from_peer_id)?; let mut info = peer_infos[info_idx].clone(); let mut raw_info = raw_peer_infos[info_idx].clone(); let allowed_cidrs = &credential.allowed_proxy_cidrs; // Filter proxy_cidrs to only those allowed by credential if !allowed_cidrs.is_empty() { info.proxy_cidrs.retain(|cidr| { allowed_cidrs .iter() .any(|allowed| cidr_is_subset_str(cidr, allowed)) }); } else { // No allowed_proxy_cidrs → no proxy_cidrs allowed info.proxy_cidrs.clear(); } SyncedRouteInfo::mark_credential_peer(&mut info, true); patch_raw_from_info(&mut raw_info, &info, &["proxy_cidrs", "feature_flag"]); Some((info, raw_info)) } #[allow(clippy::too_many_arguments)] async fn do_sync_route_info( &self, from_peer_id: PeerId, from_session_id: SessionId, is_initiator: bool, peer_infos: Option>, raw_peer_infos: Option>, conn_info: Option, foreign_network: Option, ) -> Result { let Some(service_impl) = self.service_impl.upgrade() else { return Err(Error::Stopped); }; let my_peer_id = service_impl.my_peer_id; let session = self.get_or_start_session(from_peer_id)?; let from_identity_type = service_impl .get_peer_identity_type_from_interface(from_peer_id) .await .unwrap_or(PeerIdentityType::Admin); let from_is_credential = matches!(from_identity_type, PeerIdentityType::Credential); let credential_info = if from_is_credential { service_impl .get_peer_public_key_from_interface(from_peer_id) .await .and_then(|pubkey| { service_impl .synced_route_info .get_credential_info_by_pubkey(&pubkey) }) } else { None }; if from_is_credential && credential_info.is_none() { // no credential found return Err(Error::Stopped); } let _session_lock = session.lock.lock(); session.rpc_rx_count.fetch_add(1, Ordering::Relaxed); session.update_dst_session_id(from_session_id); let mut need_update_route_table = false; let mut untrusted_peers = Vec::new(); if let Some(peer_infos) = &peer_infos { // Step 9b: credential peers can only propagate their own route info // patch_raw_from_info(&mut raw, info, &["proxy_cidrs", "feature_flag"]); let (pi, rpi) = if from_is_credential { if let Some(ret) = self.extract_credential_peer_info( from_peer_id, peer_infos, raw_peer_infos.as_deref().unwrap(), credential_info.as_ref().unwrap(), ) { (&vec![ret.0], &vec![ret.1]) } else { (&vec![], &vec![]) } } else { (peer_infos, raw_peer_infos.as_ref().unwrap()) }; if !pi.is_empty() { let trust_admin_groups_without_proof = service_impl .global_ctx .get_network_identity() .network_secret .is_none(); service_impl.synced_route_info.update_peer_infos( my_peer_id, service_impl.my_peer_route_id, from_peer_id, pi, rpi, )?; service_impl .synced_route_info .verify_and_update_group_trusts( pi, &service_impl.global_ctx.get_acl_group_declarations(), trust_admin_groups_without_proof, ); session.update_dst_saved_peer_info_version(pi, from_peer_id); need_update_route_table = true; } } // Step 9b: credential peers' conn_info depends on allow_relay flag if let Some(conn_info) = &conn_info { let accept_conn_info = !from_is_credential || credential_info.map(|tc| tc.allow_relay).unwrap_or(false); if accept_conn_info { service_impl.synced_route_info.update_conn_info(conn_info); session.update_dst_saved_conn_info_version(conn_info, from_peer_id); need_update_route_table = true; } } if need_update_route_table { untrusted_peers = service_impl.refresh_credential_trusts_with_current_topology(); } let mut foreign_network_changed = false; if let Some(foreign_network) = &foreign_network { // Step 9b: credential peers' foreign_network_infos are always ignored if !from_is_credential { foreign_network_changed = service_impl .synced_route_info .update_foreign_network(foreign_network); session.update_dst_saved_foreign_network_version(foreign_network, from_peer_id); } } if need_update_route_table || foreign_network_changed { service_impl.update_route_table_and_cached_local_conn_bitmap(); service_impl.update_foreign_network_owner_map(); if need_update_route_table && let Some(public_ipv6_service) = service_impl.public_ipv6_service() { public_ipv6_service.handle_route_change(); } } tracing::debug!( "handling sync_route_info rpc: from_peer_id: {:?}, is_initiator: {:?}, peer_infos: {:?}, conn_info: {:?}, synced_route_info: {:?} session: {:?}, new_route_table: {:?}", from_peer_id, is_initiator, peer_infos, conn_info, service_impl.synced_route_info, session, service_impl.route_table ); session .dst_is_initiator .store(is_initiator, Ordering::Relaxed); let is_initiator = session.we_are_initiator.load(Ordering::Relaxed); let session_id = session.my_session_id.load(Ordering::Relaxed); drop(_session_lock); service_impl .disconnect_untrusted_peers(&untrusted_peers) .await; // Only trigger reverse sync when we actually received new data that // needs to be propagated to other peers. Previously this was // unconditional, which created an A→B→A→B ping-pong storm even when // there was nothing new to propagate. if need_update_route_table || foreign_network_changed { self.sync_now("sync_route_info"); } Ok(SyncRouteInfoResponse { is_initiator, session_id, error: None, }) } } struct OspfPublicIpv6RouteHandle { service_impl: Weak, } impl PublicIpv6RouteControl for OspfPublicIpv6RouteHandle { fn my_peer_id(&self) -> PeerId { self.service_impl .upgrade() .map(|service_impl| service_impl.my_peer_id) .unwrap_or_default() } fn peer_route_snapshot(&self) -> Vec { let Some(service_impl) = self.service_impl.upgrade() else { return Vec::new(); }; service_impl .synced_route_info .peer_infos .read() .iter() .map(|(peer_id, info)| PublicIpv6PeerRouteInfo { peer_id: *peer_id, inst_id: route_peer_inst_id(info), is_provider: info .feature_flag .as_ref() .map(|flags| flags.ipv6_public_addr_provider) .unwrap_or(false), prefix: info .ipv6_public_addr_prefix .map(Into::into) .map(|prefix: Ipv6Inet| prefix.network()), lease: info.ipv6_public_addr_lease.map(Into::into), reachable: *peer_id == service_impl.my_peer_id || service_impl.route_table.peer_reachable(*peer_id), }) .collect() } fn publish_self_public_ipv6_lease(&self, lease: Option) -> bool { let Some(service_impl) = self.service_impl.upgrade() else { return false; }; let mut current = service_impl.self_public_ipv6_addr_lease.lock().unwrap(); if *current == lease { return false; } *current = lease; drop(current); let changed = service_impl.update_my_peer_info(); if changed { service_impl.update_route_table_and_cached_local_conn_bitmap(); service_impl.update_foreign_network_owner_map(); } changed } } #[derive(Clone)] struct OspfPublicIpv6SyncTrigger { session_mgr: RouteSessionManager, } impl PublicIpv6SyncTrigger for OspfPublicIpv6SyncTrigger { fn sync_now(&self, reason: &str) { self.session_mgr.sync_now(reason); } } pub struct PeerRoute { my_peer_id: PeerId, global_ctx: ArcGlobalCtx, peer_rpc: Weak, service_impl: Arc, public_ipv6_service: Arc, session_mgr: RouteSessionManager, tasks: std::sync::Mutex>, } impl Debug for PeerRoute { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PeerRoute") .field("my_peer_id", &self.my_peer_id) .field("service_impl", &self.service_impl) .field("session_mgr", &self.session_mgr) .finish() } } impl PeerRoute { pub fn new( my_peer_id: PeerId, global_ctx: ArcGlobalCtx, peer_rpc: Arc, ) -> Arc { let service_impl = Arc::new(PeerRouteServiceImpl::new(my_peer_id, global_ctx.clone())); let session_mgr = RouteSessionManager::new(service_impl.clone(), peer_rpc.clone()); let public_ipv6_service = Arc::new(PublicIpv6Service::new( global_ctx.clone(), Arc::downgrade(&peer_rpc), Arc::new(OspfPublicIpv6RouteHandle { service_impl: Arc::downgrade(&service_impl), }), Arc::new(OspfPublicIpv6SyncTrigger { session_mgr: session_mgr.clone(), }), )); service_impl.set_public_ipv6_service(Arc::downgrade(&public_ipv6_service)); Arc::new(PeerRoute { my_peer_id, global_ctx, peer_rpc: Arc::downgrade(&peer_rpc), service_impl, public_ipv6_service, session_mgr, tasks: std::sync::Mutex::new(JoinSet::new()), }) } async fn clear_expired_peer(service_impl: Arc) { loop { tokio::time::sleep(Duration::from_secs(60)).await; service_impl.clear_expired_peer().await; // TODO: use debug log level for this. tracing::debug!(?service_impl, "clear_expired_peer"); } } async fn maintain_session_tasks( session_mgr: RouteSessionManager, service_impl: Arc, ) { session_mgr.maintain_sessions(service_impl).await; } async fn update_my_peer_info_routine( service_impl: Arc, session_mgr: RouteSessionManager, ) { let mut global_event_receiver = service_impl.global_ctx.subscribe(); service_impl.mark_interface_peers_dirty(); loop { if service_impl.update_my_infos().await { session_mgr.sync_now("update_my_infos"); } if service_impl.cost_calculator_need_update() { tracing::debug!("cost_calculator_need_update"); service_impl.synced_route_info.version.inc(); service_impl.update_route_table(); if let Some(public_ipv6_service) = service_impl.public_ipv6_service() { public_ipv6_service.handle_route_change(); } } select! { ev = global_event_receiver.recv() => { if let Ok(ev_ref) = &ev { service_impl.handle_global_ctx_event(ev_ref); } else { service_impl.mark_interface_peers_dirty(); global_event_receiver = global_event_receiver.resubscribe(); } tracing::info!(?ev, "global event received in update_my_peer_info_routine"); } _ = tokio::time::sleep(Duration::from_secs(1)) => {} } } } async fn start(&self) { let Some(peer_rpc) = self.peer_rpc.upgrade() else { return; }; // make sure my_peer_id is in the peer_infos. self.service_impl.update_my_infos().await; self.public_ipv6_service.handle_route_change(); peer_rpc.rpc_server().registry().register( OspfRouteRpcServer::new(self.session_mgr.clone()), &self.global_ctx.get_network_name(), ); peer_rpc.rpc_server().registry().register( PublicIpv6AddrRpcServer::new(self.public_ipv6_service.rpc_server()), &self.global_ctx.get_network_name(), ); self.tasks .lock() .unwrap() .spawn(Self::update_my_peer_info_routine( self.service_impl.clone(), self.session_mgr.clone(), )); self.tasks .lock() .unwrap() .spawn(Self::maintain_session_tasks( self.session_mgr.clone(), self.service_impl.clone(), )); self.tasks .lock() .unwrap() .spawn(Self::clear_expired_peer(self.service_impl.clone())); self.tasks .lock() .unwrap() .spawn(self.public_ipv6_service.clone().provider_gc_routine()); self.tasks .lock() .unwrap() .spawn(self.public_ipv6_service.clone().client_routine()); } } impl Drop for PeerRoute { fn drop(&mut self) { tracing::debug!( self.my_peer_id, network = ?self.global_ctx.get_network_identity(), service = ?self.service_impl, "PeerRoute drop" ); let Some(peer_rpc) = self.peer_rpc.upgrade() else { return; }; peer_rpc.rpc_server().registry().unregister( OspfRouteRpcServer::new(self.session_mgr.clone()), &self.global_ctx.get_network_name(), ); peer_rpc.rpc_server().registry().unregister( PublicIpv6AddrRpcServer::new(self.public_ipv6_service.rpc_server()), &self.global_ctx.get_network_name(), ); } } #[async_trait::async_trait] impl Route for PeerRoute { async fn open(&self, interface: RouteInterfaceBox) -> Result { *self.service_impl.interface.lock().await = Some(interface); self.start().await; Ok(1) } async fn close(&self) {} async fn get_next_hop(&self, dst_peer_id: PeerId) -> Option { let route_table = &self.service_impl.route_table; route_table .get_next_hop(dst_peer_id) .map(|x| x.next_hop_peer_id) } async fn get_next_hop_with_policy( &self, dst_peer_id: PeerId, policy: NextHopPolicy, ) -> Option { let route_table = if matches!(policy, NextHopPolicy::LeastCost) { &self.service_impl.route_table_with_cost } else { &self.service_impl.route_table }; route_table .get_next_hop(dst_peer_id) .map(|x| x.next_hop_peer_id) } async fn list_routes(&self) -> Vec { let route_table = &self.service_impl.route_table; let route_table_with_cost = &self.service_impl.route_table_with_cost; let mut routes = Vec::new(); for item in route_table.peer_infos.iter() { if *item.key() == self.my_peer_id { continue; } let Some(next_hop_peer) = route_table.get_next_hop(*item.key()) else { continue; }; let next_hop_peer_latency_first = route_table_with_cost.get_next_hop(*item.key()); let mut route: crate::proto::api::instance::Route = item.value().clone().into(); route.next_hop_peer_id = next_hop_peer.next_hop_peer_id; route.cost = next_hop_peer.path_len as i32; route.path_latency = next_hop_peer.path_latency; route.next_hop_peer_id_latency_first = next_hop_peer_latency_first.map(|x| x.next_hop_peer_id); route.cost_latency_first = next_hop_peer_latency_first.map(|x| x.path_len as i32); route.path_latency_latency_first = next_hop_peer_latency_first.map(|x| x.path_latency); route.feature_flag = item.feature_flag; routes.push(route); } routes } async fn list_proxy_cidrs(&self) -> BTreeSet { let my_peer_id = self.my_peer_id; self.service_impl .route_table .cidr_peer_id_map .load() .iter() .filter(|(_, pv)| pv.peer_id != my_peer_id) .map(|(cidr, _)| *cidr) .collect() } async fn list_proxy_cidrs_v6(&self) -> BTreeSet { let my_peer_id = self.my_peer_id; self.service_impl .route_table .cidr_v6_peer_id_map .load() .iter() .filter(|(_, pv)| pv.peer_id != my_peer_id) .map(|(cidr, _)| *cidr) .collect() } async fn list_public_ipv6_routes(&self) -> BTreeSet { self.public_ipv6_service.list_routes() } async fn get_my_public_ipv6_addr(&self) -> Option { self.public_ipv6_service.my_addr() } async fn get_public_ipv6_gateway_peer_id(&self) -> Option { self.public_ipv6_service.provider_peer_id_for_client() } async fn get_local_public_ipv6_info( &self, ) -> crate::proto::api::instance::ListPublicIpv6InfoResponse { let Some((provider, leases)) = self.public_ipv6_service.local_provider_state() else { return crate::proto::api::instance::ListPublicIpv6InfoResponse::default(); }; crate::proto::api::instance::ListPublicIpv6InfoResponse { provider_prefix: Some( Ipv6Inet::new( provider.prefix.first_address(), provider.prefix.network_length(), ) .unwrap() .into(), ), provider_leases: leases .into_iter() .map(|lease| crate::proto::api::instance::PublicIpv6LeaseInfo { peer_id: lease.peer_id, inst_id: lease.inst_id.to_string(), leased_addr: Some(lease.addr.into()), valid_until_unix_seconds: lease .valid_until .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64, reused: lease.reused, }) .collect(), } } async fn get_peer_id_by_ipv4(&self, ipv4_addr: &Ipv4Addr) -> Option { let route_table = &self.service_impl.route_table; if let Some(p) = route_table.ipv4_peer_id_map.get(ipv4_addr) { return Some(p.peer_id); } // only get peer id for proxy when the dst ipv4 is not in same network with us if self .global_ctx .is_ip_in_same_network(&std::net::IpAddr::V4(*ipv4_addr)) { tracing::trace!(?ipv4_addr, "ipv4 addr is in same network with us"); return None; } if let Some(peer_id) = route_table.get_peer_id_for_proxy(&IpAddr::V4(*ipv4_addr)) { return Some(peer_id); } tracing::debug!(?ipv4_addr, "no peer id for ipv4"); None } async fn get_peer_id_by_ipv6(&self, ipv6_addr: &Ipv6Addr) -> Option { let route_table = &self.service_impl.route_table; if let Some(p) = route_table.ipv6_peer_id_map.get(ipv6_addr) { return Some(p.peer_id); } // only get peer id for proxy when the dst ipv4 is not in same network with us if self .global_ctx .is_ip_in_same_network(&std::net::IpAddr::V6(*ipv6_addr)) { tracing::trace!(?ipv6_addr, "ipv6 addr is in same network with us"); return None; } if let Some(peer_id) = route_table.get_peer_id_for_proxy(&IpAddr::V6(*ipv6_addr)) { return Some(peer_id); } tracing::debug!(?ipv6_addr, "no peer id for ipv6"); None } async fn set_route_cost_fn(&self, _cost_fn: RouteCostCalculator) { *self.service_impl.cost_calculator.write().unwrap() = Some(_cost_fn); self.service_impl.synced_route_info.version.inc(); self.service_impl.update_route_table(); } async fn dump(&self) -> String { format!("{:#?}", self) } async fn list_foreign_network_info(&self) -> RouteForeignNetworkInfos { let route_table = &self.service_impl.route_table; let mut foreign_networks = RouteForeignNetworkInfos::default(); for item in self .service_impl .synced_route_info .foreign_network .iter() .filter(|x| !x.value().foreign_peer_ids.is_empty()) .filter(|x| route_table.peer_reachable(x.key().peer_id)) { foreign_networks .infos .push(route_foreign_network_infos::Info { key: Some(item.key().clone()), value: Some(item.value().clone()), }); } foreign_networks } async fn get_foreign_network_summary(&self) -> RouteForeignNetworkSummary { let mut info_map: BTreeMap = BTreeMap::new(); for item in self.service_impl.synced_route_info.foreign_network.iter() { let entry = info_map.entry(item.key().peer_id).or_default(); entry.network_count += 1; entry.peer_count += item.value().foreign_peer_ids.len() as u32; } RouteForeignNetworkSummary { info_map } } async fn list_peers_own_foreign_network( &self, network_identity: &NetworkIdentity, ) -> Vec { self.service_impl .foreign_network_owner_map .get(network_identity) .map(|x| x.clone()) .unwrap_or_default() } async fn get_origin_my_peer_id( &self, network_name: &str, foreign_my_peer_id: PeerId, ) -> Option { self.service_impl .foreign_network_my_peer_id_map .get(&(network_name.to_string(), foreign_my_peer_id)) .map(|x| *x) } async fn get_peer_info(&self, peer_id: PeerId) -> Option { self.service_impl .route_table .peer_infos .get(&peer_id) .map(|x| x.clone()) } async fn get_peer_info_last_update_time(&self) -> Instant { self.service_impl.get_peer_info_last_update() } fn get_peer_groups(&self, peer_id: PeerId) -> Arc> { self.service_impl.get_peer_groups(peer_id) } async fn refresh_acl_groups(&self) { if self.service_impl.refresh_acl_groups().await { self.session_mgr.sync_now("refresh_acl_groups"); } } } impl PeerPacketFilter for Arc {} #[cfg(test)] mod tests { use cidr::{Ipv4Cidr, Ipv4Inet, Ipv6Inet}; use dashmap::DashMap; use parking_lot::Mutex; use prefix_trie::PrefixMap; use prost_reflect::{DynamicMessage, ReflectMessage}; use std::net::IpAddr; use std::{ collections::{BTreeSet, HashMap}, sync::{ Arc, atomic::{AtomicU32, Ordering}, }, time::{Duration, SystemTime}, }; use super::{NextHopInfo, PeerRoute, REMOVE_DEAD_PEER_INFO_AFTER, RouteConnInfo}; use crate::{ common::{ PeerId, config::NetworkIdentity, global_ctx::{ GlobalCtxEvent, TrustedKeySource, tests::{get_mock_global_ctx, get_mock_global_ctx_with_network}, }, }, connector::udp_hole_punch::tests::replace_stun_info_collector, peers::{ create_packet_recv_chan, peer_manager::{PeerManager, RouteAlgoType}, peer_ospf_route::{FORCE_USE_CONN_LIST, PeerIdVersion, PeerRouteServiceImpl}, route_trait::{NextHopPolicy, Route, RouteCostCalculatorInterface, RouteInterface}, tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear}, }, proto::{ acl::{Acl, AclV1, GroupIdentity, GroupInfo}, common::{NatType, PeerFeatureFlag}, peer_rpc::{ ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, PeerGroupInfo, PeerIdentityType, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoRequest, TrustedCredentialPubkey, TrustedCredentialPubkeyProof, }, }, tunnel::common::tests::wait_for_condition, }; use prost::Message; struct AuthOnlyInterface { my_peer_id: PeerId, identity_type: DashMap, peer_public_key: DashMap>, } #[async_trait::async_trait] impl RouteInterface for AuthOnlyInterface { async fn list_peers(&self) -> Vec { Vec::new() } fn my_peer_id(&self) -> PeerId { self.my_peer_id } async fn get_peer_public_key(&self, peer_id: PeerId) -> Option> { self.peer_public_key .get(&peer_id) .map(|x| x.value().clone()) } async fn get_peer_identity_type(&self, peer_id: PeerId) -> Option { self.identity_type.get(&peer_id).map(|x| *x.value()) } } struct TrackingInterface { my_peer_id: PeerId, closed_peers: Arc>>, } #[async_trait::async_trait] impl RouteInterface for TrackingInterface { async fn list_peers(&self) -> Vec { Vec::new() } fn my_peer_id(&self) -> PeerId { self.my_peer_id } async fn close_peer(&self, peer_id: PeerId) { self.closed_peers.lock().push(peer_id); } } struct CountingInterface { my_peer_id: PeerId, peers: Arc>>, peer_identity_types: Arc>>>, list_peers_calls: Arc, get_peer_identity_type_calls: Arc, } #[async_trait::async_trait] impl RouteInterface for CountingInterface { async fn list_peers(&self) -> Vec { self.list_peers_calls.fetch_add(1, Ordering::Relaxed); self.peers.lock().clone() } async fn get_peer_identity_type(&self, peer_id: PeerId) -> Option { self.get_peer_identity_type_calls .fetch_add(1, Ordering::Relaxed); self.peer_identity_types .lock() .get(&peer_id) .copied() .flatten() } fn my_peer_id(&self) -> PeerId { self.my_peer_id } } #[tokio::test] async fn interface_peer_cache_refreshes_only_when_marked_dirty() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let peers = Arc::new(Mutex::new(vec![2, 3])); let peer_identity_types = Arc::new(Mutex::new(HashMap::new())); let list_peers_calls = Arc::new(AtomicU32::new(0)); let get_peer_identity_type_calls = Arc::new(AtomicU32::new(0)); *service_impl.interface.lock().await = Some(Box::new(CountingInterface { my_peer_id: 1, peers: peers.clone(), peer_identity_types, list_peers_calls: list_peers_calls.clone(), get_peer_identity_type_calls, })); let first: BTreeSet<_> = service_impl.list_peers_from_interface().await; let second: BTreeSet<_> = service_impl.list_peers_from_interface().await; assert_eq!(first, BTreeSet::from([2, 3])); assert_eq!(second, BTreeSet::from([2, 3])); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1); *peers.lock() = vec![2, 4]; service_impl.handle_global_ctx_event(&GlobalCtxEvent::PeerConnAdded(Default::default())); let third: BTreeSet<_> = service_impl.list_peers_from_interface().await; assert_eq!(third, BTreeSet::from([2, 4])); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2); } #[tokio::test] async fn update_my_conn_info_skips_interface_scan_when_topology_is_unchanged() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let peers = Arc::new(Mutex::new(vec![2, 3])); let peer_identity_types = Arc::new(Mutex::new(HashMap::new())); let list_peers_calls = Arc::new(AtomicU32::new(0)); let get_peer_identity_type_calls = Arc::new(AtomicU32::new(0)); *service_impl.interface.lock().await = Some(Box::new(CountingInterface { my_peer_id: 1, peers: peers.clone(), peer_identity_types, list_peers_calls: list_peers_calls.clone(), get_peer_identity_type_calls: get_peer_identity_type_calls.clone(), })); assert!(service_impl.update_my_conn_info().await); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1); assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 2); assert!(!service_impl.update_my_conn_info().await); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1); assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 2); *peers.lock() = vec![2, 4]; service_impl.handle_global_ctx_event(&GlobalCtxEvent::PeerConnRemoved(Default::default())); assert!(service_impl.update_my_conn_info().await); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2); assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4); assert!(!service_impl.update_my_conn_info().await); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2); assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4); } #[tokio::test] async fn get_peer_identity_type_reuses_snapshot_until_topology_changes() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let peers = Arc::new(Mutex::new(vec![2, 3])); let peer_identity_types = Arc::new(Mutex::new(HashMap::from([ (2, Some(PeerIdentityType::Credential)), (3, Some(PeerIdentityType::Admin)), (4, Some(PeerIdentityType::Admin)), ]))); let list_peers_calls = Arc::new(AtomicU32::new(0)); let get_peer_identity_type_calls = Arc::new(AtomicU32::new(0)); *service_impl.interface.lock().await = Some(Box::new(CountingInterface { my_peer_id: 1, peers: peers.clone(), peer_identity_types: peer_identity_types.clone(), list_peers_calls: list_peers_calls.clone(), get_peer_identity_type_calls: get_peer_identity_type_calls.clone(), })); assert_eq!( service_impl.get_peer_identity_type_from_interface(2).await, Some(PeerIdentityType::Credential) ); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1); assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 2); assert_eq!( service_impl.get_peer_identity_type_from_interface(2).await, Some(PeerIdentityType::Credential) ); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 1); assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 2); *peers.lock() = vec![2, 4]; service_impl.handle_global_ctx_event(&GlobalCtxEvent::PeerConnRemoved(Default::default())); assert_eq!( service_impl.get_peer_identity_type_from_interface(4).await, Some(PeerIdentityType::Admin) ); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2); assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4); assert_eq!( service_impl.get_peer_identity_type_from_interface(4).await, Some(PeerIdentityType::Admin) ); assert_eq!(list_peers_calls.load(Ordering::Relaxed), 2); assert_eq!(get_peer_identity_type_calls.load(Ordering::Relaxed), 4); } async fn create_mock_route(peer_mgr: Arc) -> Arc { let peer_route = PeerRoute::new( peer_mgr.my_peer_id(), peer_mgr.get_global_ctx(), peer_mgr.get_peer_rpc_mgr(), ); peer_mgr.add_route(peer_route.clone()).await; peer_route } fn get_rpc_counter(route: &Arc, peer_id: PeerId) -> (u32, u32) { let session = route.service_impl.get_session(peer_id).unwrap(); ( session.rpc_tx_count.load(Ordering::Relaxed), session.rpc_rx_count.load(Ordering::Relaxed), ) } fn get_is_initiator(route: &Arc, peer_id: PeerId) -> (bool, bool) { let session = route.service_impl.get_session(peer_id).unwrap(); ( session.we_are_initiator.load(Ordering::Relaxed), session.dst_is_initiator.load(Ordering::Relaxed), ) } fn make_credential_route_peer_info( peer_id: PeerId, noise_static_pubkey: &[u8], ) -> RoutePeerInfo { let mut peer_info = RoutePeerInfo::new(); peer_info.peer_id = peer_id; peer_info.version = 1; peer_info.noise_static_pubkey = noise_static_pubkey.to_vec(); peer_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); peer_info } fn make_route_conn_info(connected_peers: I, last_update: SystemTime) -> RouteConnInfo where I: IntoIterator, { RouteConnInfo { connected_peers: connected_peers.into_iter().collect(), version: 1.into(), last_update, } } async fn create_mock_pmgr() -> Arc { let (s, _r) = create_packet_recv_chan(); let peer_mgr = Arc::new(PeerManager::new( RouteAlgoType::None, get_mock_global_ctx(), s, )); replace_stun_info_collector(peer_mgr.clone(), NatType::Unknown); peer_mgr.run().await.unwrap(); peer_mgr } fn check_rpc_counter(route: &Arc, peer_id: PeerId, max_tx: u32, max_rx: u32) { let (tx1, rx1) = get_rpc_counter(route, peer_id); assert!(tx1 <= max_tx); assert!(rx1 <= max_rx); } #[tokio::test] async fn credential_flag_controls_role_classification() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let mut admin_info = RoutePeerInfo::new(); admin_info.peer_id = 10; admin_info.version = 1; admin_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: false, ..Default::default() }); let mut credential_info = RoutePeerInfo::new(); credential_info.peer_id = 11; credential_info.version = 1; credential_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(admin_info.peer_id, admin_info.clone()); guard.insert(credential_info.peer_id, credential_info.clone()); } assert!(service_impl.synced_route_info.is_admin_peer(&admin_info)); assert!( !service_impl .synced_route_info .is_admin_peer(&credential_info) ); assert!( service_impl .synced_route_info .is_credential_peer(credential_info.peer_id) ); assert!( !service_impl .synced_route_info .is_credential_peer(admin_info.peer_id) ); } #[tokio::test] async fn trusted_credentials_only_from_admin_publishers() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let network_secret = "sec1"; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; let admin_key = vec![1; 32]; let credential_key = vec![2; 32]; let mut admin_info = RoutePeerInfo::new(); admin_info.peer_id = 20; admin_info.version = 1; admin_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: false, ..Default::default() }); admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof::new_signed( TrustedCredentialPubkey { pubkey: admin_key.clone(), expiry_unix: now + 600, ..Default::default() }, network_secret, )]; let mut credential_info = RoutePeerInfo::new(); credential_info.peer_id = 21; credential_info.version = 1; credential_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); credential_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof::new_signed( TrustedCredentialPubkey { pubkey: credential_key.clone(), expiry_unix: now + 600, ..Default::default() }, network_secret, )]; { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(admin_info.peer_id, admin_info); guard.insert(credential_info.peer_id, credential_info); } service_impl .synced_route_info .verify_and_update_credential_trusts(Some(network_secret)); assert!( service_impl .synced_route_info .trusted_credential_pubkeys .contains_key(&admin_key) ); assert!( !service_impl .synced_route_info .trusted_credential_pubkeys .contains_key(&credential_key) ); } #[tokio::test] async fn credential_groups_merge_with_proof_groups_and_recompute_cleanly() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let network_secret = "sec1"; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; let credential_peer_id = 31; let credential_pubkey = vec![7; 32]; let mut credential_info = RoutePeerInfo::new(); credential_info.peer_id = credential_peer_id; credential_info.version = 1; credential_info.noise_static_pubkey = credential_pubkey.clone(); credential_info.groups = vec![PeerGroupInfo::generate_with_proof( "proof-group".to_string(), "proof-secret".to_string(), credential_peer_id, )]; let mut admin_info = RoutePeerInfo::new(); admin_info.peer_id = 32; admin_info.version = 1; admin_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: false, ..Default::default() }); admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof::new_signed( TrustedCredentialPubkey { pubkey: credential_pubkey.clone(), groups: vec!["cred-group".to_string()], expiry_unix: now + 600, ..Default::default() }, network_secret, )]; { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(admin_info.peer_id, admin_info.clone()); guard.insert(credential_peer_id, credential_info.clone()); } service_impl .synced_route_info .verify_and_update_group_trusts( &[credential_info], &[GroupIdentity { group_name: "proof-group".to_string(), group_secret: "proof-secret".to_string(), }], false, ); service_impl .synced_route_info .verify_and_update_credential_trusts(Some(network_secret)); let groups = service_impl.get_peer_groups(credential_peer_id); assert!(groups.contains(&"proof-group".to_string())); assert!(groups.contains(&"cred-group".to_string())); let guard = service_impl.synced_route_info.peer_infos.write(); let admin_info = guard.get(&32).unwrap().clone(); drop(guard); let mut updated_admin = admin_info; updated_admin.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof::new_signed( TrustedCredentialPubkey { pubkey: credential_pubkey.clone(), groups: vec!["replacement-group".to_string()], expiry_unix: now + 600, ..Default::default() }, network_secret, )]; service_impl .synced_route_info .peer_infos .write() .insert(updated_admin.peer_id, updated_admin); service_impl .synced_route_info .verify_and_update_credential_trusts(Some(network_secret)); let groups = service_impl.get_peer_groups(credential_peer_id); assert!(groups.contains(&"proof-group".to_string())); assert!(groups.contains(&"replacement-group".to_string())); assert!(!groups.contains(&"cred-group".to_string())); } #[tokio::test] async fn remove_peers_batches_cleanup_and_version_increment() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let removed_peer_ids = [41, 42]; let retained_peer_id = 43; { let mut peer_infos = service_impl.synced_route_info.peer_infos.write(); let mut conn_map = service_impl.synced_route_info.conn_map.write(); for peer_id in removed_peer_ids { let mut info = RoutePeerInfo::new(); info.peer_id = peer_id; info.version = 1; peer_infos.insert(peer_id, info); conn_map.insert(peer_id, RouteConnInfo::default()); } let mut retained_info = RoutePeerInfo::new(); retained_info.peer_id = retained_peer_id; retained_info.version = 1; peer_infos.insert(retained_peer_id, retained_info); conn_map.insert(retained_peer_id, RouteConnInfo::default()); } for peer_id in removed_peer_ids { service_impl.synced_route_info.raw_peer_infos.insert( peer_id, DynamicMessage::new(RoutePeerInfo::default().descriptor()), ); service_impl.synced_route_info.group_trust_map.insert( peer_id, HashMap::from([("guest".to_string(), vec![1, 2, 3])]), ); service_impl .synced_route_info .group_trust_map_cache .insert(peer_id, Arc::new(vec!["guest".to_string()])); service_impl.synced_route_info.foreign_network.insert( ForeignNetworkRouteInfoKey { peer_id, ..Default::default() }, ForeignNetworkRouteInfoEntry::default(), ); } service_impl.synced_route_info.foreign_network.insert( ForeignNetworkRouteInfoKey { peer_id: retained_peer_id, ..Default::default() }, ForeignNetworkRouteInfoEntry::default(), ); let initial_version = service_impl.synced_route_info.version.get(); service_impl .synced_route_info .remove_peers(removed_peer_ids); assert_eq!( service_impl.synced_route_info.version.get(), initial_version + 1 ); for peer_id in removed_peer_ids { assert!( !service_impl .synced_route_info .peer_infos .read() .contains_key(&peer_id) ); assert!( !service_impl .synced_route_info .conn_map .read() .contains_key(&peer_id) ); assert!( !service_impl .synced_route_info .raw_peer_infos .contains_key(&peer_id) ); assert!( !service_impl .synced_route_info .group_trust_map .contains_key(&peer_id) ); assert!( !service_impl .synced_route_info .group_trust_map_cache .contains_key(&peer_id) ); assert!( !service_impl.synced_route_info.foreign_network.contains_key( &ForeignNetworkRouteInfoKey { peer_id, ..Default::default() } ) ); } assert!( service_impl .synced_route_info .peer_infos .read() .contains_key(&retained_peer_id) ); assert!(service_impl.synced_route_info.foreign_network.contains_key( &ForeignNetworkRouteInfoKey { peer_id: retained_peer_id, ..Default::default() } )); } #[tokio::test] async fn verify_trusted_credential_hmac_with_raw_payload_bytes() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let network_secret = "sec1"; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; let credential_key = vec![7; 32]; let mut admin_info = RoutePeerInfo::new(); admin_info.peer_id = 30; admin_info.version = 1; let credential = TrustedCredentialPubkey { pubkey: credential_key.clone(), expiry_unix: now + 600, reusable: Some(true), ..Default::default() }; let mut raw_credential_bytes = credential.encode_to_vec(); prost::encoding::encode_key( 9999, prost::encoding::WireType::Varint, &mut raw_credential_bytes, ); prost::encoding::encode_varint(42, &mut raw_credential_bytes); let (admin_info, raw_admin_info) = make_route_info_with_raw_trusted_credential_proof( &admin_info, &raw_credential_bytes, &TrustedCredentialPubkeyProof::generate_credential_hmac_from_bytes( &raw_credential_bytes, network_secret, ), ); assert_eq!(admin_info.trusted_credential_pubkeys.len(), 1); assert!( !admin_info.trusted_credential_pubkeys[0].verify_credential_hmac(network_secret), "typed verification should fail after nested unknown fields are dropped" ); let mut credential_info = RoutePeerInfo::new(); credential_info.peer_id = 41; credential_info.version = 1; credential_info.noise_static_pubkey = credential_key.clone(); credential_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); let mut raw_credential_info = DynamicMessage::new(RoutePeerInfo::default().descriptor()); raw_credential_info .transcode_from(&credential_info) .unwrap(); { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(admin_info.peer_id, admin_info); guard.insert(credential_info.peer_id, credential_info); } service_impl .synced_route_info .raw_peer_infos .insert(30, raw_admin_info); service_impl .synced_route_info .raw_peer_infos .insert(41, raw_credential_info); let (untrusted_peers, _) = service_impl .synced_route_info .verify_and_update_credential_trusts(Some(network_secret)); assert!(untrusted_peers.is_empty()); assert!( service_impl .synced_route_info .trusted_credential_pubkeys .contains_key(&credential_key) ); } #[tokio::test] async fn non_reusable_credential_elects_lowest_peer_id() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let network_secret = "sec1"; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; let credential_key = vec![7; 32]; let mut admin_info = RoutePeerInfo::new(); admin_info.peer_id = 30; admin_info.version = 1; admin_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: false, ..Default::default() }); admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof::new_signed( TrustedCredentialPubkey { pubkey: credential_key.clone(), expiry_unix: now + 600, reusable: Some(false), ..Default::default() }, network_secret, )]; let mut original_peer = RoutePeerInfo::new(); original_peer.peer_id = 41; original_peer.version = 1; original_peer.noise_static_pubkey = credential_key.clone(); original_peer.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(admin_info.peer_id, admin_info.clone()); guard.insert(original_peer.peer_id, original_peer); } let (first_untrusted, _) = service_impl .synced_route_info .verify_and_update_credential_trusts(Some(network_secret)); assert!(first_untrusted.is_empty()); assert_eq!( service_impl .synced_route_info .non_reusable_credential_owners .get(&credential_key) .map(|entry| *entry.value()), Some(41) ); let mut new_peer = RoutePeerInfo::new(); new_peer.peer_id = 39; new_peer.version = 1; new_peer.noise_static_pubkey = credential_key.clone(); new_peer.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); service_impl .synced_route_info .peer_infos .write() .insert(new_peer.peer_id, new_peer); service_impl .synced_route_info .non_reusable_credential_owners .insert(credential_key.clone(), 41); let (second_untrusted, _) = service_impl .synced_route_info .verify_and_update_credential_trusts(Some(network_secret)); assert_eq!(second_untrusted, vec![41]); assert!( !service_impl .synced_route_info .peer_infos .read() .contains_key(&41) ); assert!( service_impl .synced_route_info .peer_infos .read() .contains_key(&39) ); assert_eq!( service_impl .synced_route_info .non_reusable_credential_owners .get(&credential_key) .map(|entry| *entry.value()), Some(39) ); } #[tokio::test] async fn non_reusable_credential_ignores_unreachable_stale_owner() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let network_secret = "sec1"; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; let credential_key = vec![8; 32]; let stale_peer_id = 41; let replacement_peer_id = 39; let mut admin_info = RoutePeerInfo::new(); admin_info.peer_id = 30; admin_info.version = 1; admin_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: false, ..Default::default() }); admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof::new_signed( TrustedCredentialPubkey { pubkey: credential_key.clone(), expiry_unix: now + 600, reusable: Some(false), ..Default::default() }, network_secret, )]; let mut stale_peer = RoutePeerInfo::new(); stale_peer.peer_id = stale_peer_id; stale_peer.version = 1; stale_peer.noise_static_pubkey = credential_key.clone(); stale_peer.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); let mut replacement_peer = RoutePeerInfo::new(); replacement_peer.peer_id = replacement_peer_id; replacement_peer.version = 1; replacement_peer.noise_static_pubkey = credential_key.clone(); replacement_peer.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(admin_info.peer_id, admin_info); guard.insert(stale_peer.peer_id, stale_peer); guard.insert(replacement_peer.peer_id, replacement_peer); } service_impl .synced_route_info .non_reusable_credential_owners .insert(credential_key.clone(), stale_peer_id); service_impl.route_table.next_hop_map.insert( replacement_peer_id, NextHopInfo { next_hop_peer_id: replacement_peer_id, path_latency: 0, path_len: 1, version: 1, }, ); service_impl.route_table.next_hop_map_version.set(1); let (untrusted_peers, _) = service_impl .synced_route_info .verify_and_update_credential_trusts_with_active_peers( Some(network_secret), |peer_id| service_impl.is_active_non_reusable_credential_peer(peer_id), ); assert!(untrusted_peers.is_empty()); assert!( service_impl .synced_route_info .peer_infos .read() .contains_key(&stale_peer_id) ); assert!( service_impl .synced_route_info .peer_infos .read() .contains_key(&replacement_peer_id) ); assert_eq!( service_impl .synced_route_info .non_reusable_credential_owners .get(&credential_key) .map(|entry| *entry.value()), Some(replacement_peer_id) ); } #[tokio::test] async fn credential_trust_refresh_does_not_remove_self_peer() { let my_peer_id = 11; let remote_peer_id = 12; let credential_key = vec![8; 32]; let service_impl = PeerRouteServiceImpl::new(my_peer_id, get_mock_global_ctx()); let self_info = make_credential_route_peer_info(my_peer_id, &credential_key); let remote_info = make_credential_route_peer_info(remote_peer_id, &credential_key); { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(self_info.peer_id, self_info); guard.insert(remote_info.peer_id, remote_info); } service_impl .synced_route_info .trusted_credential_pubkeys .insert( credential_key.clone(), TrustedCredentialPubkey { pubkey: credential_key, expiry_unix: i64::MAX, ..Default::default() }, ); let (untrusted_peers, _) = service_impl .synced_route_info .verify_and_update_credential_trusts_with_active_peers_protecting( None, |_| true, Some(my_peer_id), ); assert_eq!(untrusted_peers, vec![remote_peer_id]); assert!( service_impl .synced_route_info .peer_infos .read() .contains_key(&my_peer_id) ); assert!( !service_impl .synced_route_info .peer_infos .read() .contains_key(&remote_peer_id) ); } #[tokio::test] async fn credential_refresh_rebuilds_reachability_before_owner_election() { const NETWORK_SECRET: &str = "sec1"; const SELF_PEER_ID: PeerId = 1; let service_impl = PeerRouteServiceImpl::new( SELF_PEER_ID, get_mock_global_ctx_with_network(Some(NetworkIdentity::new( "test-net".to_string(), NETWORK_SECRET.to_string(), ))), ); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; let credential_key = vec![9; 32]; let admin_peer_id = 30; let stale_peer_id = 41; let replacement_peer_id = 39; let mut self_info = RoutePeerInfo::new(); self_info.peer_id = SELF_PEER_ID; self_info.version = 1; let mut admin_info = RoutePeerInfo::new(); admin_info.peer_id = admin_peer_id; admin_info.version = 1; admin_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: false, ..Default::default() }); admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof::new_signed( TrustedCredentialPubkey { pubkey: credential_key.clone(), expiry_unix: now + 600, reusable: Some(false), ..Default::default() }, NETWORK_SECRET, )]; let stale_peer = make_credential_route_peer_info(stale_peer_id, &credential_key); let replacement_peer = make_credential_route_peer_info(replacement_peer_id, &credential_key); { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(self_info.peer_id, self_info); guard.insert(admin_info.peer_id, admin_info); guard.insert(stale_peer.peer_id, stale_peer); guard.insert(replacement_peer.peer_id, replacement_peer); } let now = std::time::SystemTime::now(); { let mut guard = service_impl.synced_route_info.conn_map.write(); guard.insert(SELF_PEER_ID, make_route_conn_info([admin_peer_id], now)); guard.insert( admin_peer_id, make_route_conn_info([SELF_PEER_ID, replacement_peer_id], now), ); guard.insert( replacement_peer_id, make_route_conn_info([admin_peer_id], now), ); guard.insert(stale_peer_id, make_route_conn_info([], now)); } service_impl.synced_route_info.version.set(2); service_impl.update_route_table_and_cached_local_conn_bitmap(); assert!(!service_impl.is_active_non_reusable_credential_peer(stale_peer_id)); assert!(service_impl.is_active_non_reusable_credential_peer(replacement_peer_id)); service_impl.route_table.next_hop_map.clear(); service_impl.route_table.next_hop_map.insert( stale_peer_id, NextHopInfo { next_hop_peer_id: stale_peer_id, path_latency: 0, path_len: 1, version: 1, }, ); service_impl.route_table.next_hop_map_version.set(1); let untrusted = service_impl.refresh_credential_trusts_with_current_topology(); assert!(untrusted.is_empty()); assert!(!service_impl.is_active_non_reusable_credential_peer(stale_peer_id)); assert!(service_impl.is_active_non_reusable_credential_peer(replacement_peer_id)); assert_eq!( service_impl .synced_route_info .non_reusable_credential_owners .get(&credential_key) .map(|entry| *entry.value()), Some(replacement_peer_id) ); } #[tokio::test] async fn sync_route_info_marks_credential_sender_and_filters_entries() { let peer_mgr = create_mock_pmgr().await; let route = create_mock_route(peer_mgr.clone()).await; let from_peer_id: PeerId = 10001; let forwarded_peer_id: PeerId = 10002; let credential_pubkey = vec![3u8; 32]; let identity_type = DashMap::new(); identity_type.insert(from_peer_id, PeerIdentityType::Credential); let peer_public_key = DashMap::new(); peer_public_key.insert(from_peer_id, credential_pubkey.clone()); *route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface { my_peer_id: peer_mgr.my_peer_id(), identity_type, peer_public_key, })); route .service_impl .synced_route_info .trusted_credential_pubkeys .insert( credential_pubkey.clone(), TrustedCredentialPubkey { pubkey: credential_pubkey, expiry_unix: i64::MAX, ..Default::default() }, ); let mut sender_info = RoutePeerInfo::new(); sender_info.peer_id = from_peer_id; sender_info.version = 1; sender_info.proxy_cidrs = vec!["10.10.0.0/24".to_string()]; let mut forwarded_info = RoutePeerInfo::new(); forwarded_info.peer_id = forwarded_peer_id; forwarded_info.version = 1; let make_raw = |info: &RoutePeerInfo| { let mut raw = DynamicMessage::new(RoutePeerInfo::default().descriptor()); raw.transcode_from(info).unwrap(); raw }; let raw_infos = vec![make_raw(&sender_info), make_raw(&forwarded_info)]; route .session_mgr .do_sync_route_info( from_peer_id, 1, true, Some(vec![sender_info, forwarded_info]), Some(raw_infos), None, None, ) .await .unwrap(); let guard = route.service_impl.synced_route_info.peer_infos.read(); let stored = guard.get(&from_peer_id).unwrap(); assert!( stored .feature_flag .as_ref() .map(|x| x.is_credential_peer) .unwrap_or(false) ); assert!(stored.proxy_cidrs.is_empty()); assert!(guard.get(&forwarded_peer_id).is_none()); } // shared node doesn't have hmac. #[tokio::test] async fn sync_route_info_shared_sender_cannot_publish_trusted_credentials() { let peer_mgr = create_mock_pmgr().await; let route = create_mock_route(peer_mgr.clone()).await; let from_peer_id: PeerId = 10021; let forwarded_peer_id: PeerId = 10022; let credential_key = vec![9u8; 32]; let identity_type = DashMap::new(); identity_type.insert(from_peer_id, PeerIdentityType::SharedNode); *route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface { my_peer_id: peer_mgr.my_peer_id(), identity_type, peer_public_key: DashMap::new(), })); let mut sender_info = RoutePeerInfo::new(); sender_info.peer_id = from_peer_id; sender_info.version = 1; let mut forwarded_info = RoutePeerInfo::new(); forwarded_info.peer_id = forwarded_peer_id; forwarded_info.version = 1; forwarded_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof { credential: Some(TrustedCredentialPubkey { pubkey: credential_key.clone(), expiry_unix: i64::MAX, ..Default::default() }), credential_hmac: vec![1; 32], }]; let make_raw = |info: &RoutePeerInfo| { let mut raw = DynamicMessage::new(RoutePeerInfo::default().descriptor()); raw.transcode_from(info).unwrap(); raw }; let raw_infos = vec![make_raw(&sender_info), make_raw(&forwarded_info)]; route .session_mgr .do_sync_route_info( from_peer_id, 1, true, Some(vec![sender_info, forwarded_info]), Some(raw_infos), None, None, ) .await .unwrap(); assert!( !route .service_impl .synced_route_info .trusted_credential_pubkeys .contains_key(&credential_key) ); } #[tokio::test] async fn clear_expired_peer_recomputes_trust_after_last_admin_disappears() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let admin_peer_id: PeerId = 10051; let credential_peer_id: PeerId = 10052; let admin_pubkey = vec![5u8; 32]; let credential_pubkey = vec![6u8; 32]; let network_name = service_impl .global_ctx .get_network_identity() .network_name .clone(); let now = SystemTime::now(); let closed_peers = Arc::new(Mutex::new(Vec::new())); *service_impl.interface.lock().await = Some(Box::new(TrackingInterface { my_peer_id: service_impl.my_peer_id, closed_peers: closed_peers.clone(), })); { let mut guard = service_impl.synced_route_info.peer_infos.write(); let mut admin_info = RoutePeerInfo::new(); admin_info.peer_id = admin_peer_id; admin_info.version = 1; admin_info.last_update = Some((now - REMOVE_DEAD_PEER_INFO_AFTER - Duration::from_secs(1)).into()); admin_info.noise_static_pubkey = admin_pubkey; admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof { credential: Some(TrustedCredentialPubkey { pubkey: credential_pubkey.clone(), groups: vec!["guest".to_string()], expiry_unix: i64::MAX, ..Default::default() }), credential_hmac: vec![1; 32], }]; let mut credential_info = RoutePeerInfo::new(); credential_info.peer_id = credential_peer_id; credential_info.version = 1; credential_info.last_update = Some(now.into()); credential_info.noise_static_pubkey = credential_pubkey.clone(); guard.insert(admin_peer_id, admin_info); guard.insert(credential_peer_id, credential_info); } let (_, global_trusted_keys) = service_impl .synced_route_info .verify_and_update_credential_trusts(None); service_impl .global_ctx .update_trusted_keys(global_trusted_keys, &network_name); assert!( service_impl .synced_route_info .trusted_credential_pubkeys .contains_key(&credential_pubkey) ); assert!( service_impl .get_peer_groups(credential_peer_id) .contains(&"guest".to_string()) ); service_impl.clear_expired_peer().await; assert!(!service_impl.global_ctx.is_pubkey_trusted_with_source( &credential_pubkey, &network_name, TrustedKeySource::OspfCredential, )); assert!(closed_peers.lock().contains(&credential_peer_id)); assert!( !service_impl .synced_route_info .peer_infos .read() .contains_key(&admin_peer_id) ); assert!( !service_impl .synced_route_info .peer_infos .read() .contains_key(&credential_peer_id) ); assert!( !service_impl .synced_route_info .group_trust_map_cache .contains_key(&credential_peer_id) ); } #[tokio::test] async fn refresh_acl_groups_returns_true_when_untrusted_peers_are_disconnected() { let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); let credential_peer_id: PeerId = 10061; let credential_pubkey = vec![8u8; 32]; let closed_peers = Arc::new(Mutex::new(Vec::new())); *service_impl.interface.lock().await = Some(Box::new(TrackingInterface { my_peer_id: service_impl.my_peer_id, closed_peers: closed_peers.clone(), })); let mut credential_info = RoutePeerInfo::new(); credential_info.peer_id = credential_peer_id; credential_info.version = 1; credential_info.noise_static_pubkey = credential_pubkey.clone(); credential_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); let self_info = RoutePeerInfo::new_updated_self( service_impl.my_peer_id, service_impl.my_peer_route_id, &service_impl.global_ctx, None, ); let mut self_info = self_info; self_info.version = 1; self_info.last_update = Some(SystemTime::now().into()); { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(service_impl.my_peer_id, self_info); guard.insert(credential_peer_id, credential_info); } service_impl .synced_route_info .trusted_credential_pubkeys .insert( credential_pubkey.clone(), TrustedCredentialPubkey { pubkey: credential_pubkey.clone(), expiry_unix: i64::MAX, ..Default::default() }, ); assert!(service_impl.refresh_acl_groups().await); assert!(closed_peers.lock().contains(&credential_peer_id)); assert!( !service_impl .synced_route_info .peer_infos .read() .contains_key(&credential_peer_id) ); assert!( !service_impl .synced_route_info .trusted_credential_pubkeys .contains_key(&credential_pubkey) ); } #[tokio::test] async fn refresh_acl_groups_updates_local_membership_immediately() { let peer_mgr = create_mock_pmgr().await; let route = create_mock_route(peer_mgr.clone()).await; let my_peer_id = peer_mgr.my_peer_id(); assert!(route.service_impl.get_peer_groups(my_peer_id).is_empty()); peer_mgr.get_global_ctx().config.set_acl(Some(Acl { acl_v1: Some(AclV1 { group: Some(GroupInfo { declares: vec![GroupIdentity { group_name: "admin".to_string(), group_secret: "admin-secret".to_string(), }], members: vec!["admin".to_string()], }), ..Default::default() }), })); route.refresh_acl_groups().await; let groups = route.service_impl.get_peer_groups(my_peer_id); assert!(groups.contains(&"admin".to_string())); assert_eq!(groups.len(), 1); } #[tokio::test] async fn refresh_acl_groups_revalidates_cached_remote_groups() { let peer_mgr = create_mock_pmgr().await; let route = create_mock_route(peer_mgr.clone()).await; let remote_peer_id = 200; let remote_group = PeerGroupInfo::generate_with_proof( "ops".to_string(), "secret-v1".to_string(), remote_peer_id, ); peer_mgr.get_global_ctx().config.set_acl(Some(Acl { acl_v1: Some(AclV1 { group: Some(GroupInfo { declares: vec![GroupIdentity { group_name: "ops".to_string(), group_secret: "secret-v1".to_string(), }], members: vec![], }), ..Default::default() }), })); let mut remote_info = RoutePeerInfo::new(); remote_info.peer_id = remote_peer_id; remote_info.version = 1; remote_info.groups = vec![remote_group]; route .service_impl .synced_route_info .peer_infos .write() .insert(remote_peer_id, remote_info.clone()); route .service_impl .synced_route_info .verify_and_update_group_trusts( &[remote_info], &peer_mgr.get_global_ctx().get_acl_group_declarations(), false, ); assert!( route .service_impl .get_peer_groups(remote_peer_id) .contains(&"ops".to_string()) ); peer_mgr.get_global_ctx().config.set_acl(Some(Acl { acl_v1: Some(AclV1 { group: Some(GroupInfo { declares: vec![GroupIdentity { group_name: "ops".to_string(), group_secret: "secret-v2".to_string(), }], members: vec![], }), ..Default::default() }), })); route.refresh_acl_groups().await; assert!( route .service_impl .get_peer_groups(remote_peer_id) .is_empty() ); } #[tokio::test] async fn credential_verifier_trusts_admin_self_groups_from_multiple_admins() { let service_impl = PeerRouteServiceImpl::new( 1, get_mock_global_ctx_with_network(Some( crate::common::config::NetworkIdentity::new_credential("net1".to_string()), )), ); let mut admin_a = RoutePeerInfo::new(); admin_a.peer_id = 501; admin_a.version = 1; admin_a.groups = vec![ PeerGroupInfo { group_name: "ops".to_string(), group_proof: vec![1; 32], }, PeerGroupInfo { group_name: "core-admin".to_string(), group_proof: vec![2; 32], }, ]; let mut admin_b = RoutePeerInfo::new(); admin_b.peer_id = 502; admin_b.version = 1; admin_b.groups = vec![PeerGroupInfo { group_name: "audit".to_string(), group_proof: vec![3; 32], }]; service_impl .synced_route_info .verify_and_update_group_trusts(&[admin_a.clone(), admin_b.clone()], &[], true); let admin_a_groups = service_impl.get_peer_groups(admin_a.peer_id); assert!(admin_a_groups.contains(&"ops".to_string())); assert!(admin_a_groups.contains(&"core-admin".to_string())); let admin_b_groups = service_impl.get_peer_groups(admin_b.peer_id); assert!(admin_b_groups.contains(&"audit".to_string())); } #[tokio::test] async fn credential_verifier_still_checks_credential_self_declared_groups() { let service_impl = PeerRouteServiceImpl::new( 1, get_mock_global_ctx_with_network(Some( crate::common::config::NetworkIdentity::new_credential("net1".to_string()), )), ); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; let credential_peer_id = 601; let credential_pubkey = vec![9; 32]; let mut admin_info = RoutePeerInfo::new(); admin_info.peer_id = 600; admin_info.version = 1; admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof { credential: Some(TrustedCredentialPubkey { pubkey: credential_pubkey.clone(), groups: vec!["cred-acl".to_string()], expiry_unix: now + 600, ..Default::default() }), credential_hmac: vec![7; 32], }]; let mut credential_info = RoutePeerInfo::new(); credential_info.peer_id = credential_peer_id; credential_info.version = 1; credential_info.noise_static_pubkey = credential_pubkey.clone(); credential_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); credential_info.groups = vec![ PeerGroupInfo::generate_with_proof( "proof-group".to_string(), "proof-secret".to_string(), credential_peer_id, ), PeerGroupInfo::generate_with_proof( "invalid-group".to_string(), "wrong-secret".to_string(), credential_peer_id, ), ]; { let mut guard = service_impl.synced_route_info.peer_infos.write(); guard.insert(admin_info.peer_id, admin_info.clone()); guard.insert(credential_info.peer_id, credential_info.clone()); } service_impl .synced_route_info .verify_and_update_group_trusts( &[admin_info, credential_info], &[ GroupIdentity { group_name: "proof-group".to_string(), group_secret: "proof-secret".to_string(), }, GroupIdentity { group_name: "invalid-group".to_string(), group_secret: "actual-secret".to_string(), }, ], true, ); service_impl .synced_route_info .verify_and_update_credential_trusts(None); let groups = service_impl.get_peer_groups(credential_peer_id); assert!(groups.contains(&"proof-group".to_string())); assert!(groups.contains(&"cred-acl".to_string())); assert!(!groups.contains(&"invalid-group".to_string())); } #[rstest::rstest] #[tokio::test] async fn ospf_route_2node(#[values(true, false)] enable_conn_list_sync: bool) { FORCE_USE_CONN_LIST.store(enable_conn_list_sync, Ordering::Relaxed); let p_a = create_mock_pmgr().await; let p_b = create_mock_pmgr().await; connect_peer_manager(p_a.clone(), p_b.clone()).await; let r_a = create_mock_route(p_a.clone()).await; let r_b = create_mock_route(p_b.clone()).await; for r in [r_a.clone(), r_b.clone()].iter() { wait_for_condition( || async { println!("route: {:?}", r.list_routes().await); r.list_routes().await.len() == 1 }, Duration::from_secs(5), ) .await; } tokio::time::sleep(Duration::from_secs(3)).await; assert_eq!( 2, r_a.service_impl.synced_route_info.peer_infos.read().len() ); assert_eq!( 2, r_b.service_impl.synced_route_info.peer_infos.read().len() ); for s in r_a.service_impl.sessions.iter() { assert!(s.value().task.is_running()); } assert_eq!( r_a.service_impl .synced_route_info .peer_infos .read() .get(&p_a.my_peer_id()) .unwrap() .version, r_a.service_impl .get_session(p_b.my_peer_id()) .unwrap() .dst_saved_peer_info_versions .get(&p_a.my_peer_id()) .unwrap() .value() .get() ); assert_eq!((1, 1), get_rpc_counter(&r_a, p_b.my_peer_id())); assert_eq!((1, 1), get_rpc_counter(&r_b, p_a.my_peer_id())); let i_a = get_is_initiator(&r_a, p_b.my_peer_id()); let i_b = get_is_initiator(&r_b, p_a.my_peer_id()); assert_eq!(i_a.0, i_b.1); assert_eq!(i_b.0, i_a.1); println!("after drop p_b, r_b"); drop(r_b); drop(p_b); wait_for_condition( || async { r_a.list_routes().await.is_empty() }, Duration::from_secs(5), ) .await; wait_for_condition( || async { r_a.service_impl.sessions.is_empty() }, Duration::from_secs(5), ) .await; } #[rstest::rstest] #[tokio::test] async fn ospf_route_multi_node(#[values(true, false)] enable_conn_list_sync: bool) { FORCE_USE_CONN_LIST.store(enable_conn_list_sync, Ordering::Relaxed); let p_a = create_mock_pmgr().await; let p_b = create_mock_pmgr().await; let p_c = create_mock_pmgr().await; connect_peer_manager(p_a.clone(), p_b.clone()).await; connect_peer_manager(p_c.clone(), p_b.clone()).await; let r_a = create_mock_route(p_a.clone()).await; let r_b = create_mock_route(p_b.clone()).await; let r_c = create_mock_route(p_c.clone()).await; for r in [r_a.clone(), r_b.clone(), r_c.clone()].iter() { wait_for_condition( || async { r.service_impl.synced_route_info.peer_infos.read().len() == 3 }, Duration::from_secs(5), ) .await; } connect_peer_manager(p_a.clone(), p_c.clone()).await; // for full-connected 3 nodes, the sessions between them may be a cycle or a line wait_for_condition( || async { let mut lens = vec![ r_a.service_impl.sessions.len(), r_b.service_impl.sessions.len(), r_c.service_impl.sessions.len(), ]; lens.sort(); lens == vec![1, 1, 2] || lens == vec![2, 2, 2] }, Duration::from_secs(3), ) .await; let p_d = create_mock_pmgr().await; let r_d = create_mock_route(p_d.clone()).await; connect_peer_manager(p_d.clone(), p_a.clone()).await; connect_peer_manager(p_d.clone(), p_b.clone()).await; connect_peer_manager(p_d.clone(), p_c.clone()).await; // find the smallest peer_id, which should be a center node let mut all_route = [r_a.clone(), r_b.clone(), r_c.clone(), r_d.clone()]; all_route.sort_by_key(|r| r.my_peer_id); let mut all_peer_mgr = [p_a.clone(), p_b.clone(), p_c.clone(), p_d.clone()]; all_peer_mgr.sort_by_key(|p| p.my_peer_id()); wait_for_condition( || async { all_route[0].service_impl.sessions.len() == 3 }, Duration::from_secs(3), ) .await; for r in all_route.iter() { println!("session: {}", r.session_mgr.dump_sessions().unwrap()); } let p_e = create_mock_pmgr().await; let r_e = create_mock_route(p_e.clone()).await; let last_p = all_peer_mgr.last().unwrap(); connect_peer_manager(p_e.clone(), last_p.clone()).await; wait_for_condition( || async { r_e.session_mgr.list_session_peers().len() == 1 }, Duration::from_secs(3), ) .await; for s in r_e.service_impl.sessions.iter() { assert!(s.value().task.is_running()); } tokio::time::sleep(Duration::from_secs(2)).await; check_rpc_counter(&r_e, last_p.my_peer_id(), 2, 2); for r in all_route.iter() { if r.my_peer_id != last_p.my_peer_id() { wait_for_condition( || async { r.get_next_hop(p_e.my_peer_id()).await == Some(last_p.my_peer_id()) }, Duration::from_secs(3), ) .await; } else { wait_for_condition( || async { r.get_next_hop(p_e.my_peer_id()).await == Some(p_e.my_peer_id()) }, Duration::from_secs(3), ) .await; } } } async fn check_route_sanity(p: &Arc, routable_peers: Vec>) { let synced_info = &p.service_impl.synced_route_info; for routable_peer in routable_peers.iter() { // check conn map let conns = { let guard = synced_info.conn_map.read(); guard.get(&routable_peer.my_peer_id()).cloned().unwrap() }; assert_eq!( conns.connected_peers, routable_peer .get_peer_map() .list_peers() .into_iter() .collect::>() ); // check peer infos let peer_info = synced_info .peer_infos .read() .get(&routable_peer.my_peer_id()) .cloned() .unwrap(); assert_eq!(peer_info.peer_id, routable_peer.my_peer_id()); } } async fn print_routes(peers: Vec>) { for p in peers.iter() { println!("p:{:?}, route: {:#?}", p.my_peer_id, p.list_routes().await); } } #[rstest::rstest] #[tokio::test] async fn ospf_route_3node_disconnect(#[values(true, false)] enable_conn_list_sync: bool) { FORCE_USE_CONN_LIST.store(enable_conn_list_sync, Ordering::Relaxed); let p_a = create_mock_pmgr().await; let p_b = create_mock_pmgr().await; let p_c = create_mock_pmgr().await; connect_peer_manager(p_a.clone(), p_b.clone()).await; connect_peer_manager(p_c.clone(), p_b.clone()).await; let mgrs = vec![p_a.clone(), p_b.clone(), p_c.clone()]; let r_a = create_mock_route(p_a.clone()).await; let r_b = create_mock_route(p_b.clone()).await; let r_c = create_mock_route(p_c.clone()).await; for r in [r_a.clone(), r_b.clone(), r_c.clone()].iter() { wait_for_condition( || async { r.service_impl.synced_route_info.peer_infos.read().len() == 3 }, Duration::from_secs(5), ) .await; } tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; print_routes(vec![r_a.clone(), r_b.clone(), r_c.clone()]).await; check_route_sanity(&r_a, mgrs.clone()).await; check_route_sanity(&r_b, mgrs.clone()).await; check_route_sanity(&r_c, mgrs.clone()).await; assert_eq!(2, r_a.list_routes().await.len()); drop(mgrs); drop(r_c); drop(p_c); for r in [r_a.clone(), r_b.clone()].iter() { wait_for_condition( || async { r.list_routes().await.len() == 1 }, Duration::from_secs(5), ) .await; } } #[rstest::rstest] #[tokio::test] async fn peer_reconnect(#[values(true, false)] enable_conn_list_sync: bool) { FORCE_USE_CONN_LIST.store(enable_conn_list_sync, Ordering::Relaxed); let p_a = create_mock_pmgr().await; let p_b = create_mock_pmgr().await; let r_a = create_mock_route(p_a.clone()).await; let r_b = create_mock_route(p_b.clone()).await; connect_peer_manager(p_a.clone(), p_b.clone()).await; wait_for_condition( || async { r_a.list_routes().await.len() == 1 }, Duration::from_secs(5), ) .await; assert_eq!(1, r_b.list_routes().await.len()); check_rpc_counter(&r_a, p_b.my_peer_id(), 2, 2); p_a.get_peer_map() .close_peer(p_b.my_peer_id()) .await .unwrap(); wait_for_condition( || async { r_a.list_routes().await.is_empty() }, Duration::from_secs(5), ) .await; // reconnect connect_peer_manager(p_a.clone(), p_b.clone()).await; wait_for_condition( || async { r_a.list_routes().await.len() == 1 }, Duration::from_secs(5), ) .await; // wait session init tokio::time::sleep(Duration::from_secs(1)).await; println!("session: {:?}", r_a.session_mgr.dump_sessions()); check_rpc_counter(&r_a, p_b.my_peer_id(), 2, 2); } #[rstest::rstest] #[tokio::test] async fn test_cost_calculator(#[values(true, false)] enable_conn_list_sync: bool) { FORCE_USE_CONN_LIST.store(enable_conn_list_sync, Ordering::Relaxed); let p_a = create_mock_pmgr().await; let p_b = create_mock_pmgr().await; let p_c = create_mock_pmgr().await; let p_d = create_mock_pmgr().await; connect_peer_manager(p_a.clone(), p_b.clone()).await; connect_peer_manager(p_a.clone(), p_c.clone()).await; connect_peer_manager(p_d.clone(), p_b.clone()).await; connect_peer_manager(p_d.clone(), p_c.clone()).await; connect_peer_manager(p_b.clone(), p_c.clone()).await; let _r_a = create_mock_route(p_a.clone()).await; let _r_b = create_mock_route(p_b.clone()).await; let _r_c = create_mock_route(p_c.clone()).await; let r_d = create_mock_route(p_d.clone()).await; // in normal mode, packet from p_c should directly forward to p_a wait_for_condition( || async { (r_d.get_next_hop(p_a.my_peer_id()).await).is_some() }, Duration::from_secs(5), ) .await; struct TestCostCalculator { p_a_peer_id: PeerId, p_b_peer_id: PeerId, p_c_peer_id: PeerId, p_d_peer_id: PeerId, } impl RouteCostCalculatorInterface for TestCostCalculator { fn calculate_cost(&self, src: PeerId, dst: PeerId) -> i32 { if src == self.p_d_peer_id && dst == self.p_b_peer_id { return 100; } if src == self.p_d_peer_id && dst == self.p_c_peer_id { return 1; } if src == self.p_c_peer_id && dst == self.p_a_peer_id { return 101; } if src == self.p_b_peer_id && dst == self.p_a_peer_id { return 1; } if src == self.p_c_peer_id && dst == self.p_b_peer_id { return 2; } 1 } } r_d.set_route_cost_fn(Box::new(TestCostCalculator { p_a_peer_id: p_a.my_peer_id(), p_b_peer_id: p_b.my_peer_id(), p_c_peer_id: p_c.my_peer_id(), p_d_peer_id: p_d.my_peer_id(), })) .await; // after set cost, packet from p_c should forward to p_b first wait_for_condition( || async { r_d.get_next_hop_with_policy(p_a.my_peer_id(), NextHopPolicy::LeastCost) .await == Some(p_c.my_peer_id()) }, Duration::from_secs(5), ) .await; wait_for_condition( || async { r_d.get_next_hop_with_policy(p_a.my_peer_id(), NextHopPolicy::LeastHop) .await == Some(p_b.my_peer_id()) }, Duration::from_secs(5), ) .await; } #[rstest::rstest] #[tokio::test] async fn test_raw_peer_info(#[values(true, false)] enable_conn_list_sync: bool) { FORCE_USE_CONN_LIST.store(enable_conn_list_sync, Ordering::Relaxed); let mut req = SyncRouteInfoRequest::default(); let raw_info_map: DashMap = DashMap::new(); req.peer_infos = Some(RoutePeerInfos { items: vec![RoutePeerInfo { peer_id: 1, ..Default::default() }], }); let mut raw_req = DynamicMessage::new(RoutePeerInfo::default().descriptor()); raw_req .transcode_from(&req.peer_infos.as_ref().unwrap().items[0]) .unwrap(); raw_info_map.insert(1, raw_req); let out = PeerRouteServiceImpl::build_sync_route_raw_req(&req, &raw_info_map); let out_bytes = out.encode_to_vec(); let req2 = SyncRouteInfoRequest::decode(out_bytes.as_slice()).unwrap(); assert_eq!(req, req2); } #[rstest::rstest] #[tokio::test] async fn test_peer_id_map_override(#[values(true, false)] enable_conn_list_sync: bool) { FORCE_USE_CONN_LIST.store(enable_conn_list_sync, Ordering::Relaxed); let p_a = create_mock_peer_manager().await; let p_b = create_mock_peer_manager().await; let p_c = create_mock_peer_manager().await; connect_peer_manager(p_a.clone(), p_b.clone()).await; connect_peer_manager(p_b.clone(), p_c.clone()).await; let ip: Ipv4Inet = "10.0.0.1/24".parse().unwrap(); let ipv6: Ipv6Inet = "2001:db8::1/64".parse().unwrap(); let proxy: Ipv4Cidr = "10.3.0.0/24".parse().unwrap(); let check_route_peer_id = async |p: Arc| { let p = p.clone(); wait_for_condition( || async { p_a.get_route().get_peer_id_by_ipv4(&ip.address()).await == Some(p.my_peer_id()) && p_a.get_route().get_peer_id_by_ipv6(&ipv6.address()).await == Some(p.my_peer_id()) && p_a .get_route() .get_peer_id_by_ipv4(&proxy.first_address()) .await == Some(p.my_peer_id()) }, Duration::from_secs(5), ) .await; }; p_c.get_global_ctx().set_ipv4(Some(ip)); p_c.get_global_ctx().set_ipv6(Some(ipv6)); p_c.get_global_ctx() .config .add_proxy_cidr(proxy, None) .unwrap(); check_route_peer_id(p_c.clone()).await; p_b.get_global_ctx().set_ipv4(Some(ip)); p_b.get_global_ctx().set_ipv6(Some(ipv6)); p_b.get_global_ctx() .config .add_proxy_cidr(proxy, None) .unwrap(); check_route_peer_id(p_b.clone()).await; p_b.get_global_ctx() .set_ipv4(Some("10.0.0.2/24".parse().unwrap())); p_b.get_global_ctx() .set_ipv6(Some("2001:db8::2/64".parse().unwrap())); p_b.get_global_ctx().config.remove_proxy_cidr(proxy); check_route_peer_id(p_c.clone()).await; } #[rstest::rstest] #[tokio::test] async fn test_subnet_proxy_conflict(#[values(true, false)] enable_conn_list_sync: bool) { FORCE_USE_CONN_LIST.store(enable_conn_list_sync, Ordering::Relaxed); // Create three peer managers: A, B, C let p_a = create_mock_peer_manager().await; let p_b = create_mock_peer_manager().await; let p_c = create_mock_peer_manager().await; // Connect A-B-C in a line topology connect_peer_manager(p_a.clone(), p_b.clone()).await; connect_peer_manager(p_b.clone(), p_c.clone()).await; // Create routes for testing let route_a = p_a.get_route(); let route_b = p_b.get_route(); // Define the proxy CIDR that will be used by both A and B let proxy_cidr: Ipv4Cidr = "192.168.100.0/24".parse().unwrap(); let test_ip = proxy_cidr.first_address(); let mut cidr_peer_id_map: PrefixMap = PrefixMap::new(); cidr_peer_id_map.insert( proxy_cidr, PeerIdVersion { peer_id: p_c.my_peer_id(), version: 0, }, ); assert_eq!( cidr_peer_id_map .get_lpm(&Ipv4Cidr::new(test_ip, 32).unwrap()) .map(|v| v.1.peer_id) .unwrap_or(0), p_c.my_peer_id(), ); // First, add proxy CIDR to node C to establish a baseline route p_c.get_global_ctx() .config .add_proxy_cidr(proxy_cidr, None) .unwrap(); // Wait for route convergence - A should route to C for the proxy CIDR wait_for_condition( || async { let peer_id_for_proxy = route_a.get_peer_id_by_ipv4(&test_ip).await; peer_id_for_proxy == Some(p_c.my_peer_id()) }, Duration::from_secs(10), ) .await; // Now add the same proxy CIDR to node A (creating a conflict) p_a.get_global_ctx() .config .add_proxy_cidr(proxy_cidr, None) .unwrap(); // Wait for route convergence - A should now route to itself for the proxy CIDR wait_for_condition( || async { route_a.get_peer_id_by_ipv4(&test_ip).await == Some(p_a.my_peer_id()) }, Duration::from_secs(10), ) .await; // Also add the same proxy CIDR to node B (creating another conflict) p_b.get_global_ctx() .config .add_proxy_cidr(proxy_cidr, None) .unwrap(); // Wait for route convergence - B should route to itself for the proxy CIDR wait_for_condition( || async { route_b.get_peer_id_by_ipv4(&test_ip).await == Some(p_b.my_peer_id()) }, Duration::from_secs(5), ) .await; // Final verification: A should still route to itself even with multiple conflicts assert_eq!( route_a.get_peer_id_by_ipv4(&test_ip).await, Some(p_a.my_peer_id()) ); // remove proxy on A, a should route to B p_a.get_global_ctx().config.remove_proxy_cidr(proxy_cidr); wait_for_condition( || async { let peer_id_for_proxy = route_a.get_peer_id_by_ipv4(&test_ip).await; peer_id_for_proxy == Some(p_b.my_peer_id()) }, Duration::from_secs(10), ) .await; } #[rstest::rstest] #[tokio::test] async fn test_connect_at_different_time(#[values(true, false)] enable_conn_list_sync: bool) { FORCE_USE_CONN_LIST.store(enable_conn_list_sync, Ordering::Relaxed); // Create three peer managers: A, B, C let p_a = create_mock_peer_manager().await; let p_b = create_mock_peer_manager().await; let p_c = create_mock_peer_manager().await; // Connect A-B-C in a line topology connect_peer_manager(p_a.clone(), p_b.clone()).await; wait_route_appear(p_a.clone(), p_b.clone()).await.unwrap(); connect_peer_manager(p_b.clone(), p_c.clone()).await; wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); } /// Helper: create a raw DynamicMessage from a RoutePeerInfo with an extra /// unknown field appended (field number 9999, varint value 42). /// Returns the raw DynamicMessage and the encoded unknown field bytes. fn make_raw_with_unknown_field(info: &RoutePeerInfo) -> (DynamicMessage, Vec) { // Encode the info to bytes let mut bytes = info.encode_to_vec(); // Append an unknown field: field 9999, wire type 0 (varint), value 42 // Tag = (9999 << 3) | 0 = 79992, encoded as varint prost::encoding::encode_key(9999, prost::encoding::WireType::Varint, &mut bytes); prost::encoding::encode_varint(42, &mut bytes); let unknown_field_bytes = bytes[info.encoded_len()..].to_vec(); // Decode as DynamicMessage — unknown fields are preserved let raw = DynamicMessage::decode(RoutePeerInfo::default().descriptor(), bytes.as_slice()) .unwrap(); (raw, unknown_field_bytes) } /// Check that a raw DynamicMessage still contains the unknown field bytes /// by re-encoding and checking the suffix. fn raw_has_unknown_bytes(raw: &DynamicMessage, unknown_bytes: &[u8]) -> bool { let encoded = raw.encode_to_vec(); // The unknown field bytes should appear somewhere in the encoded output encoded .windows(unknown_bytes.len()) .any(|w| w == unknown_bytes) } fn encode_length_delimited_field(field_number: u32, payload: &[u8], dst: &mut Vec) { prost::encoding::encode_key( field_number, prost::encoding::WireType::LengthDelimited, dst, ); prost::encoding::encode_varint(payload.len() as u64, dst); dst.extend_from_slice(payload); } fn make_route_info_with_raw_trusted_credential_proof( info: &RoutePeerInfo, raw_credential_bytes: &[u8], credential_hmac: &[u8], ) -> (RoutePeerInfo, DynamicMessage) { let mut proof_bytes = Vec::new(); encode_length_delimited_field(1, raw_credential_bytes, &mut proof_bytes); encode_length_delimited_field(2, credential_hmac, &mut proof_bytes); let mut route_info_bytes = info.encode_to_vec(); encode_length_delimited_field(19, &proof_bytes, &mut route_info_bytes); let typed_info = RoutePeerInfo::decode(route_info_bytes.as_slice()).unwrap(); let raw_info = DynamicMessage::decode( RoutePeerInfo::default().descriptor(), route_info_bytes.as_slice(), ) .unwrap(); (typed_info, raw_info) } #[tokio::test] async fn sync_route_preserves_unknown_fields_for_credential_sender() { let peer_mgr = create_mock_pmgr().await; let route = create_mock_route(peer_mgr.clone()).await; let from_peer_id: PeerId = 20001; let credential_pubkey = vec![4u8; 32]; let identity_type = DashMap::new(); identity_type.insert(from_peer_id, PeerIdentityType::Credential); let peer_public_key = DashMap::new(); peer_public_key.insert(from_peer_id, credential_pubkey.clone()); *route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface { my_peer_id: peer_mgr.my_peer_id(), identity_type, peer_public_key, })); route .service_impl .synced_route_info .trusted_credential_pubkeys .insert( credential_pubkey.clone(), TrustedCredentialPubkey { pubkey: credential_pubkey, expiry_unix: i64::MAX, ..Default::default() }, ); let mut sender_info = RoutePeerInfo::new(); sender_info.peer_id = from_peer_id; sender_info.version = 1; let (raw, unknown_bytes) = make_raw_with_unknown_field(&sender_info); route .session_mgr .do_sync_route_info( from_peer_id, 1, true, Some(vec![sender_info]), Some(vec![raw]), None, None, ) .await .unwrap(); let stored_raw = route .service_impl .synced_route_info .raw_peer_infos .get(&from_peer_id) .expect("raw peer info should be stored"); assert!( raw_has_unknown_bytes(stored_raw.value(), &unknown_bytes), "unknown fields should be preserved for credential sender" ); } #[tokio::test] async fn sync_route_preserves_unknown_fields_for_shared_sender() { let peer_mgr = create_mock_pmgr().await; let route = create_mock_route(peer_mgr.clone()).await; let from_peer_id: PeerId = 20011; let forwarded_peer_id: PeerId = 20012; let identity_type = DashMap::new(); identity_type.insert(from_peer_id, PeerIdentityType::SharedNode); *route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface { my_peer_id: peer_mgr.my_peer_id(), identity_type, peer_public_key: DashMap::new(), })); let mut sender_info = RoutePeerInfo::new(); sender_info.peer_id = from_peer_id; sender_info.version = 1; let mut forwarded_info = RoutePeerInfo::new(); forwarded_info.peer_id = forwarded_peer_id; forwarded_info.version = 1; forwarded_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof { credential: Some(TrustedCredentialPubkey { pubkey: vec![9u8; 32], expiry_unix: i64::MAX, ..Default::default() }), credential_hmac: vec![1; 32], }]; let (raw_sender, unknown_sender) = make_raw_with_unknown_field(&sender_info); let (raw_forwarded, unknown_forwarded) = make_raw_with_unknown_field(&forwarded_info); route .session_mgr .do_sync_route_info( from_peer_id, 1, true, Some(vec![sender_info, forwarded_info]), Some(vec![raw_sender, raw_forwarded]), None, None, ) .await .unwrap(); // Shared node: trusted_credential_pubkeys cleared but unknown fields preserved let stored_sender = route .service_impl .synced_route_info .raw_peer_infos .get(&from_peer_id) .expect("sender raw should be stored"); assert!( raw_has_unknown_bytes(stored_sender.value(), &unknown_sender), "unknown fields should be preserved for shared sender's own info" ); let stored_forwarded = route .service_impl .synced_route_info .raw_peer_infos .get(&forwarded_peer_id) .expect("forwarded raw should be stored"); assert!( raw_has_unknown_bytes(stored_forwarded.value(), &unknown_forwarded), "unknown fields should be preserved for shared sender's forwarded info" ); } #[tokio::test] async fn sync_route_preserves_unknown_fields_for_admin_sender() { let peer_mgr = create_mock_pmgr().await; let route = create_mock_route(peer_mgr.clone()).await; let from_peer_id: PeerId = 20021; let identity_type = DashMap::new(); identity_type.insert(from_peer_id, PeerIdentityType::Admin); *route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface { my_peer_id: peer_mgr.my_peer_id(), identity_type, peer_public_key: DashMap::new(), })); let mut sender_info = RoutePeerInfo::new(); sender_info.peer_id = from_peer_id; sender_info.version = 1; // Set is_credential_peer=true so the mark_credential_peer(false) path triggers sender_info.feature_flag = Some(PeerFeatureFlag { is_credential_peer: true, ..Default::default() }); let (raw, unknown_bytes) = make_raw_with_unknown_field(&sender_info); route .session_mgr .do_sync_route_info( from_peer_id, 1, true, Some(vec![sender_info]), Some(vec![raw]), None, None, ) .await .unwrap(); let stored_raw = route .service_impl .synced_route_info .raw_peer_infos .get(&from_peer_id) .expect("raw peer info should be stored"); assert!( raw_has_unknown_bytes(stored_raw.value(), &unknown_bytes), "unknown fields should be preserved for admin sender (mark non-credential path)" ); } #[tokio::test] async fn sync_route_info_prioritizes_local_over_remote_for_overlapped_proxy_cidrs() { let peer_mgr = create_mock_pmgr().await; let route = create_mock_route(peer_mgr.clone()).await; let from_peer_id: PeerId = 11001; let peers = Arc::new(Mutex::new(vec![from_peer_id])); let peer_identity_types = Arc::new(Mutex::new(HashMap::from([( from_peer_id, Some(PeerIdentityType::Admin), )]))); *route.service_impl.interface.lock().await = Some(Box::new(CountingInterface { my_peer_id: peer_mgr.my_peer_id(), peers, peer_identity_types, list_peers_calls: Arc::new(AtomicU32::new(0)), get_peer_identity_type_calls: Arc::new(AtomicU32::new(0)), })); route.service_impl.mark_interface_peers_dirty(); assert!(route.service_impl.update_my_conn_info().await); route .service_impl .global_ctx .config .add_proxy_cidr("10.10.0.0/16".parse().unwrap(), None) .unwrap(); assert!(route.service_impl.update_my_peer_info()); let mut sender_info = RoutePeerInfo::new(); sender_info.peer_id = from_peer_id; sender_info.version = 1; sender_info.proxy_cidrs = vec![ "10.10.0.0/16".to_string(), "10.10.1.0/24".to_string(), "10.11.0.0/16".to_string(), ]; let make_raw = |info: &RoutePeerInfo| { let mut raw = DynamicMessage::new(RoutePeerInfo::default().descriptor()); raw.transcode_from(info).unwrap(); raw }; route .session_mgr .do_sync_route_info( from_peer_id, 1, true, Some(vec![sender_info.clone()]), Some(vec![make_raw(&sender_info)]), None, None, ) .await .unwrap(); // Keep route table in sync with interface-derived adjacency during assertion window. route .service_impl .update_route_table_and_cached_local_conn_bitmap(); // Control plane: keep what remote announced. let guard = route.service_impl.synced_route_info.peer_infos.read(); let stored = guard.get(&from_peer_id).unwrap(); assert_eq!(stored.proxy_cidrs, sender_info.proxy_cidrs); drop(guard); // Route-table filtering: local announced /16 should dominate remote equal/subset. assert_eq!( route .service_impl .route_table .get_peer_id_for_proxy(&"10.10.1.1".parse::().unwrap()), Some(peer_mgr.my_peer_id()) ); // Non-overlapped remote prefix should still route to remote. assert_eq!( route .service_impl .route_table .get_peer_id_for_proxy(&"10.11.0.1".parse::().unwrap()), Some(from_peer_id) ); } }