mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-16 02:45:41 +00:00
feat(credential): implement credential peer auth and trust propagation (#1968)
- add credential manager and RPC/CLI for generate/list/revoke - support credential-based Noise authentication and revocation handling - propagate trusted credential metadata through OSPF route sync - classify direct peers by auth level in session maintenance - normalize sender credential flag for legacy non-secure compatibility - add unit/integration tests for credential join, relay and revocation
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
sync::{
|
||||
@@ -43,9 +43,10 @@ use crate::{
|
||||
route_foreign_network_infos, route_foreign_network_summary,
|
||||
sync_route_info_request::ConnInfo, ForeignNetworkRouteInfoEntry,
|
||||
ForeignNetworkRouteInfoKey, OspfRouteRpc, OspfRouteRpcClientFactory,
|
||||
OspfRouteRpcServer, PeerGroupInfo, PeerIdVersion, RouteForeignNetworkInfos,
|
||||
RouteForeignNetworkSummary, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoError,
|
||||
SyncRouteInfoRequest, SyncRouteInfoResponse,
|
||||
OspfRouteRpcServer, PeerGroupInfo, PeerIdVersion, PeerIdentityType,
|
||||
RouteForeignNetworkInfos, RouteForeignNetworkSummary, RoutePeerInfo, RoutePeerInfos,
|
||||
SyncRouteInfoError, SyncRouteInfoRequest, SyncRouteInfoResponse,
|
||||
TrustedCredentialPubkey,
|
||||
},
|
||||
rpc_types::{
|
||||
self,
|
||||
@@ -80,6 +81,26 @@ static REMOVE_UNREACHABLE_PEER_INFO_AFTER: Duration = Duration::from_secs(90);
|
||||
|
||||
type Version = u32;
|
||||
|
||||
/// Check if `child` CIDR is a subset of `parent` CIDR (both as string representations).
|
||||
/// Returns true if child is contained within parent, or if they are equal.
|
||||
fn cidr_is_subset_str(child: &str, parent: &str) -> bool {
|
||||
let Ok(child_cidr) = child.parse::<IpCidr>() else {
|
||||
return false;
|
||||
};
|
||||
let Ok(parent_cidr) = parent.parse::<IpCidr>() else {
|
||||
return false;
|
||||
};
|
||||
match (child_cidr, parent_cidr) {
|
||||
(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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AtomicVersion(Arc<AtomicU32>);
|
||||
|
||||
@@ -147,6 +168,7 @@ impl RoutePeerInfo {
|
||||
|
||||
quic_port: None,
|
||||
noise_static_pubkey: Vec::new(),
|
||||
trusted_credential_pubkeys: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +228,17 @@ impl RoutePeerInfo {
|
||||
|
||||
noise_static_pubkey,
|
||||
|
||||
// Only admin nodes (holding network_secret) publish trusted credential pubkeys
|
||||
trusted_credential_pubkeys: if global_ctx
|
||||
.get_network_identity()
|
||||
.network_secret
|
||||
.is_some()
|
||||
{
|
||||
global_ctx.get_credential_manager().get_trusted_pubkeys()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -336,6 +369,10 @@ struct SyncedRouteInfo {
|
||||
group_trust_map: DashMap<PeerId, HashMap<String, Vec<u8>>>,
|
||||
group_trust_map_cache: DashMap<PeerId, Arc<Vec<String>>>, // 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<Vec<u8>, TrustedCredentialPubkey>,
|
||||
|
||||
version: AtomicVersion,
|
||||
}
|
||||
|
||||
@@ -352,6 +389,19 @@ impl Debug for SyncedRouteInfo {
|
||||
}
|
||||
|
||||
impl SyncedRouteInfo {
|
||||
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 get_connected_peers<T: FromIterator<PeerId>>(&self, peer_id: PeerId) -> Option<T> {
|
||||
self.conn_map
|
||||
.read()
|
||||
@@ -830,6 +880,160 @@ impl SyncedRouteInfo {
|
||||
self.group_trust_map_cache
|
||||
.insert(my_peer_id, Arc::new(my_group_names));
|
||||
}
|
||||
|
||||
/// 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,
|
||||
) -> (
|
||||
Vec<PeerId>,
|
||||
HashMap<Vec<u8>, crate::common::global_ctx::TrustedKeyMetadata>,
|
||||
) {
|
||||
use crate::common::global_ctx::{TrustedKeyMetadata, TrustedKeySource};
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
// Step 1: Collect trusted credential pubkeys from admin nodes (take union)
|
||||
// Only trust nodes whose secret_digest matches ours (i.e. they hold network_secret)
|
||||
let mut all_trusted: HashMap<Vec<u8>, TrustedCredentialPubkey> = HashMap::new();
|
||||
// Also collect all peer pubkeys for GlobalCtx synchronization
|
||||
let mut global_trusted_keys: HashMap<Vec<u8>, TrustedKeyMetadata> = HashMap::new();
|
||||
|
||||
let peer_infos = self.peer_infos.read();
|
||||
|
||||
for (_, info) in peer_infos.iter() {
|
||||
if !self.is_admin_peer(info) {
|
||||
continue;
|
||||
}
|
||||
// Collect all peer noise_static_pubkeys as trusted keys
|
||||
if !info.noise_static_pubkey.is_empty() {
|
||||
global_trusted_keys.insert(
|
||||
info.noise_static_pubkey.clone(),
|
||||
TrustedKeyMetadata {
|
||||
source: TrustedKeySource::OspfNode,
|
||||
expiry_unix: None, // Peer pubkeys never expire
|
||||
},
|
||||
);
|
||||
}
|
||||
for tc in &info.trusted_credential_pubkeys {
|
||||
if tc.expiry_unix > now {
|
||||
all_trusted
|
||||
.entry(tc.pubkey.clone())
|
||||
.or_insert_with(|| tc.clone());
|
||||
// Also add to global trusted keys
|
||||
global_trusted_keys.insert(
|
||||
tc.pubkey.clone(),
|
||||
TrustedKeyMetadata {
|
||||
source: TrustedKeySource::OspfCredential,
|
||||
expiry_unix: Some(tc.expiry_unix),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the previous trusted set to detect revoked credentials
|
||||
let prev_trusted: HashSet<Vec<u8>> = self
|
||||
.trusted_credential_pubkeys
|
||||
.iter()
|
||||
.map(|r| r.key().clone())
|
||||
.collect();
|
||||
|
||||
// Update the trusted_credential_pubkeys map
|
||||
self.trusted_credential_pubkeys.clear();
|
||||
for (k, v) in &all_trusted {
|
||||
self.trusted_credential_pubkeys.insert(k.clone(), v.clone());
|
||||
}
|
||||
|
||||
// Step 2: Update group trust map for credential peers
|
||||
// Credential peers get their groups from the TrustedCredentialPubkey declaration
|
||||
for (_, info) in peer_infos.iter() {
|
||||
if info.noise_static_pubkey.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(tc) = all_trusted.get(&info.noise_static_pubkey) {
|
||||
// This peer is a credential peer, assign groups from credential declaration
|
||||
if !tc.groups.is_empty() {
|
||||
let mut group_map = HashMap::new();
|
||||
let mut group_names = Vec::new();
|
||||
for g in &tc.groups {
|
||||
group_map.insert(g.clone(), Vec::new()); // no proof needed, admin-declared
|
||||
group_names.push(g.clone());
|
||||
}
|
||||
self.group_trust_map.insert(info.peer_id, group_map);
|
||||
self.group_trust_map_cache
|
||||
.insert(info.peer_id, Arc::new(group_names));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Find and remove peers with revoked/expired credentials.
|
||||
// A peer is untrusted if:
|
||||
// - Its noise_static_pubkey was in the PREVIOUS trusted set (it was a credential peer)
|
||||
// - Its noise_static_pubkey is NOT in the CURRENT trusted set (credential revoked/expired)
|
||||
let mut untrusted_peers = Vec::new();
|
||||
for (peer_id, info) in peer_infos.iter() {
|
||||
if info.noise_static_pubkey.is_empty() || info.version == 0 {
|
||||
continue;
|
||||
}
|
||||
// Only remove peers whose pubkey was previously trusted but no longer is
|
||||
if prev_trusted.contains(&info.noise_static_pubkey)
|
||||
&& !all_trusted.contains_key(&info.noise_static_pubkey)
|
||||
{
|
||||
untrusted_peers.push(*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
|
||||
let mut peer_infos_write = self.peer_infos.write();
|
||||
for peer_id in &untrusted_peers {
|
||||
tracing::warn!(?peer_id, "removing untrusted peer from route info");
|
||||
peer_infos_write.remove(peer_id);
|
||||
self.raw_peer_infos.remove(peer_id);
|
||||
}
|
||||
drop(peer_infos_write);
|
||||
// Also remove from conn_map
|
||||
let mut conn_map = self.conn_map.write();
|
||||
for peer_id in &untrusted_peers {
|
||||
conn_map.remove(peer_id);
|
||||
}
|
||||
self.version.inc();
|
||||
}
|
||||
|
||||
(untrusted_peers, 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(&self, peer_id: PeerId) -> Option<TrustedCredentialPubkey> {
|
||||
let peer_infos = self.peer_infos.read();
|
||||
let info = peer_infos.get(&peer_id)?;
|
||||
if info.noise_static_pubkey.is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.trusted_credential_pubkeys
|
||||
.get(&info.noise_static_pubkey)
|
||||
.map(|r| r.value().clone())
|
||||
}
|
||||
}
|
||||
|
||||
type PeerGraph = Graph<PeerId, usize, Directed>;
|
||||
@@ -977,6 +1181,14 @@ impl RouteTable {
|
||||
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<usize>| {
|
||||
if *e.weight() >= AVOID_RELAY_COST {
|
||||
AVOID_RELAY_COST + 1
|
||||
@@ -1020,6 +1232,14 @@ impl RouteTable {
|
||||
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() {
|
||||
@@ -1058,6 +1278,18 @@ impl RouteTable {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1596,6 +1828,7 @@ impl PeerRouteServiceImpl {
|
||||
foreign_network: DashMap::new(),
|
||||
group_trust_map: DashMap::new(),
|
||||
group_trust_map_cache: DashMap::new(),
|
||||
trusted_credential_pubkeys: DashMap::new(),
|
||||
version: AtomicVersion::new(),
|
||||
},
|
||||
cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::default()),
|
||||
@@ -1607,6 +1840,24 @@ impl PeerRouteServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_my_secret_digest(&self) -> Option<Vec<u8>> {
|
||||
let ni = self.global_ctx.get_network_identity();
|
||||
ni.network_secret_digest.map(|d| d.to_vec())
|
||||
}
|
||||
|
||||
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 get_or_create_session(&self, dst_peer_id: PeerId) -> Arc<SyncRouteSession> {
|
||||
self.sessions
|
||||
.entry(dst_peer_id)
|
||||
@@ -1640,29 +1891,31 @@ impl PeerRouteServiceImpl {
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn get_peer_identity_type_from_interface(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
) -> Option<PeerIdentityType> {
|
||||
self.interface
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_peer_identity_type(peer_id)
|
||||
.await
|
||||
}
|
||||
|
||||
fn update_my_peer_info(&self) -> bool {
|
||||
if self.synced_route_info.update_my_peer_info(
|
||||
self.synced_route_info.update_my_peer_info(
|
||||
self.my_peer_id,
|
||||
self.my_peer_route_id,
|
||||
&self.global_ctx,
|
||||
) {
|
||||
self.update_route_table_and_cached_local_conn_bitmap();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
async fn update_my_conn_info(&self) -> bool {
|
||||
let connected_peers: BTreeSet<PeerId> = self.list_peers_from_interface().await;
|
||||
let updated = self
|
||||
.synced_route_info
|
||||
.update_my_conn_info(self.my_peer_id, connected_peers);
|
||||
|
||||
if updated {
|
||||
self.update_route_table_and_cached_local_conn_bitmap();
|
||||
}
|
||||
|
||||
updated
|
||||
self.synced_route_info
|
||||
.update_my_conn_info(self.my_peer_id, connected_peers)
|
||||
}
|
||||
|
||||
async fn update_my_foreign_network(&self) -> bool {
|
||||
@@ -1921,15 +2174,6 @@ impl PeerRouteServiceImpl {
|
||||
// stop iter if last_update of conn info is older than session.last_sync_succ_timestamp
|
||||
let last_update = TryInto::<SystemTime>::try_into(conn_info.last_update).unwrap();
|
||||
if last_sync_succ_timestamp.is_some_and(|t| last_update < t) {
|
||||
tracing::debug!(
|
||||
"ignore conn info {:?} because last_update: {:?} is older than last_sync_succ_timestamp: {:?}, conn_map count: {}, my_peer_id: {:?}, session: {:?}",
|
||||
conn_info,
|
||||
last_update,
|
||||
last_sync_succ_timestamp,
|
||||
conn_map.len(),
|
||||
self.my_peer_id,
|
||||
session
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2012,7 +2256,16 @@ impl PeerRouteServiceImpl {
|
||||
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;
|
||||
if my_conn_info_updated || my_peer_info_updated {
|
||||
let mut untrusted_changed = false;
|
||||
if my_peer_info_updated {
|
||||
let (untrusted, global_trusted_keys) =
|
||||
self.synced_route_info.verify_and_update_credential_trusts();
|
||||
self.global_ctx.update_trusted_keys(global_trusted_keys);
|
||||
untrusted_changed = !untrusted.is_empty();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
if my_peer_info_updated {
|
||||
@@ -2168,7 +2421,7 @@ impl PeerRouteServiceImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
tracing::debug!(?foreign_network, "sync_route request need send to peer. my_id {:?}, pper_id: {:?}, peer_infos: {:?}, conn_info: {:?}, synced_route_info: {:?} session: {:?}",
|
||||
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
|
||||
@@ -2504,16 +2757,28 @@ impl RouteSessionManager {
|
||||
}
|
||||
|
||||
// find peer_ids that are not initiators.
|
||||
let initiator_candidates = peers
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
let Some(session) = service_impl.get_session(**x) else {
|
||||
return true;
|
||||
};
|
||||
!session.dst_is_initiator.load(Ordering::Relaxed)
|
||||
})
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
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 = service_impl
|
||||
.get_peer_identity_type_from_interface(peer_id)
|
||||
.await
|
||||
.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;
|
||||
@@ -2626,6 +2891,12 @@ impl RouteSessionManager {
|
||||
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 _session_lock = session.lock.lock();
|
||||
|
||||
session.rpc_rx_count.fetch_add(1, Ordering::Relaxed);
|
||||
@@ -2635,38 +2906,119 @@ impl RouteSessionManager {
|
||||
let mut need_update_route_table = false;
|
||||
|
||||
if let Some(peer_infos) = &peer_infos {
|
||||
// Step 9b: credential peers can only propagate their own route info
|
||||
let normalize_raw = |info: &RoutePeerInfo| {
|
||||
let mut raw = DynamicMessage::new(RoutePeerInfo::default().descriptor());
|
||||
raw.transcode_from(info).unwrap();
|
||||
raw
|
||||
};
|
||||
let normalized_peer_infos: Vec<RoutePeerInfo>;
|
||||
let normalized_raw_peer_infos: Vec<DynamicMessage>;
|
||||
let (pi, rpi) = if from_is_credential {
|
||||
let allowed_cidrs = service_impl
|
||||
.synced_route_info
|
||||
.get_credential_info(from_peer_id)
|
||||
.map(|tc| tc.allowed_proxy_cidrs.clone())
|
||||
.unwrap_or_default();
|
||||
normalized_peer_infos = peer_infos
|
||||
.iter()
|
||||
.filter(|info| info.peer_id == from_peer_id)
|
||||
.cloned()
|
||||
.map(|mut info| {
|
||||
// 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);
|
||||
info
|
||||
})
|
||||
.collect();
|
||||
normalized_raw_peer_infos =
|
||||
normalized_peer_infos.iter().map(normalize_raw).collect();
|
||||
(&normalized_peer_infos, &normalized_raw_peer_infos)
|
||||
} else {
|
||||
let mut peer_infos_mut = peer_infos.clone();
|
||||
let mut raw_peer_infos_mut = raw_peer_infos
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| peer_infos_mut.iter().map(normalize_raw).collect());
|
||||
if let Some((idx, info)) = peer_infos_mut
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, info)| info.peer_id == from_peer_id)
|
||||
{
|
||||
let mut info = info.clone();
|
||||
SyncedRouteInfo::mark_credential_peer(&mut info, false);
|
||||
peer_infos_mut[idx] = info.clone();
|
||||
raw_peer_infos_mut[idx] = normalize_raw(&info);
|
||||
}
|
||||
normalized_peer_infos = peer_infos_mut;
|
||||
normalized_raw_peer_infos = raw_peer_infos_mut;
|
||||
(&normalized_peer_infos, &normalized_raw_peer_infos)
|
||||
};
|
||||
|
||||
service_impl.synced_route_info.update_peer_infos(
|
||||
my_peer_id,
|
||||
service_impl.my_peer_route_id,
|
||||
from_peer_id,
|
||||
peer_infos,
|
||||
raw_peer_infos.as_ref().unwrap(),
|
||||
pi,
|
||||
rpi,
|
||||
)?;
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.verify_and_update_group_trusts(
|
||||
peer_infos,
|
||||
pi,
|
||||
&service_impl.global_ctx.get_acl_group_declarations(),
|
||||
);
|
||||
session.update_dst_saved_peer_info_version(peer_infos, from_peer_id);
|
||||
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 {
|
||||
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;
|
||||
let accept_conn_info = if from_is_credential {
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.get_credential_info(from_peer_id)
|
||||
.map(|tc| tc.allow_relay)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
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 {
|
||||
// Run credential verification and update route table
|
||||
let (_untrusted, global_trusted_keys) = service_impl
|
||||
.synced_route_info
|
||||
.verify_and_update_credential_trusts();
|
||||
// Sync trusted keys to GlobalCtx for handshake verification
|
||||
service_impl
|
||||
.global_ctx
|
||||
.update_trusted_keys(global_trusted_keys);
|
||||
service_impl.update_route_table_and_cached_local_conn_bitmap();
|
||||
}
|
||||
|
||||
if let Some(foreign_network) = &foreign_network {
|
||||
service_impl
|
||||
.synced_route_info
|
||||
.update_foreign_network(foreign_network);
|
||||
session.update_dst_saved_foreign_network_version(foreign_network, from_peer_id);
|
||||
// Step 9b: credential peers' foreign_network_infos are always ignored
|
||||
if !from_is_credential {
|
||||
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.is_some() {
|
||||
@@ -3041,12 +3393,15 @@ mod tests {
|
||||
create_packet_recv_chan,
|
||||
peer_manager::{PeerManager, RouteAlgoType},
|
||||
peer_ospf_route::{PeerIdVersion, PeerRouteServiceImpl, FORCE_USE_CONN_LIST},
|
||||
route_trait::{NextHopPolicy, Route, RouteCostCalculatorInterface},
|
||||
route_trait::{NextHopPolicy, Route, RouteCostCalculatorInterface, RouteInterface},
|
||||
tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear},
|
||||
},
|
||||
proto::{
|
||||
common::NatType,
|
||||
peer_rpc::{RoutePeerInfo, RoutePeerInfos, SyncRouteInfoRequest},
|
||||
common::{NatType, PeerFeatureFlag},
|
||||
peer_rpc::{
|
||||
PeerIdentityType, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoRequest,
|
||||
TrustedCredentialPubkey,
|
||||
},
|
||||
},
|
||||
tunnel::common::tests::wait_for_condition,
|
||||
};
|
||||
@@ -3054,6 +3409,26 @@ mod tests {
|
||||
|
||||
use super::PeerRoute;
|
||||
|
||||
struct AuthOnlyInterface {
|
||||
my_peer_id: PeerId,
|
||||
identity_type: DashMap<PeerId, PeerIdentityType>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RouteInterface for AuthOnlyInterface {
|
||||
async fn list_peers(&self) -> Vec<PeerId> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn my_peer_id(&self) -> PeerId {
|
||||
self.my_peer_id
|
||||
}
|
||||
|
||||
async fn get_peer_identity_type(&self, peer_id: PeerId) -> Option<PeerIdentityType> {
|
||||
self.identity_type.get(&peer_id).map(|x| *x.value())
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_mock_route(peer_mgr: Arc<PeerManager>) -> Arc<PeerRoute> {
|
||||
let peer_route = PeerRoute::new(
|
||||
peer_mgr.my_peer_id(),
|
||||
@@ -3098,6 +3473,213 @@ mod tests {
|
||||
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 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![TrustedCredentialPubkey {
|
||||
pubkey: admin_key.clone(),
|
||||
expiry_unix: now + 600,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
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![TrustedCredentialPubkey {
|
||||
pubkey: credential_key.clone(),
|
||||
expiry_unix: now + 600,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
{
|
||||
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();
|
||||
|
||||
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 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 identity_type = DashMap::new();
|
||||
identity_type.insert(from_peer_id, PeerIdentityType::Credential);
|
||||
*route.service_impl.interface.lock().await = Some(Box::new(AuthOnlyInterface {
|
||||
my_peer_id: peer_mgr.my_peer_id(),
|
||||
identity_type,
|
||||
}));
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sync_route_info_forces_non_credential_for_legacy_admin_sender() {
|
||||
let peer_mgr = create_mock_pmgr().await;
|
||||
let route = create_mock_route(peer_mgr.clone()).await;
|
||||
let from_peer_id: PeerId = 10011;
|
||||
let other_peer_id: PeerId = 10012;
|
||||
|
||||
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,
|
||||
}));
|
||||
|
||||
let mut sender_info = RoutePeerInfo::new();
|
||||
sender_info.peer_id = from_peer_id;
|
||||
sender_info.version = 1;
|
||||
sender_info.feature_flag = Some(PeerFeatureFlag {
|
||||
is_credential_peer: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut other_info = RoutePeerInfo::new();
|
||||
other_info.peer_id = other_peer_id;
|
||||
other_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(&other_info)];
|
||||
|
||||
route
|
||||
.session_mgr
|
||||
.do_sync_route_info(
|
||||
from_peer_id,
|
||||
1,
|
||||
true,
|
||||
Some(vec![sender_info, other_info]),
|
||||
Some(raw_infos),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let guard = route.service_impl.synced_route_info.peer_infos.read();
|
||||
let sender = guard.get(&from_peer_id).unwrap();
|
||||
assert!(!sender
|
||||
.feature_flag
|
||||
.as_ref()
|
||||
.map(|x| x.is_credential_peer)
|
||||
.unwrap_or(false));
|
||||
assert!(guard.get(&other_peer_id).is_some());
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[tokio::test]
|
||||
async fn ospf_route_2node(#[values(true, false)] enable_conn_list_sync: bool) {
|
||||
|
||||
Reference in New Issue
Block a user