Honor credential reusable flag (#2157)

- propagate reusable through credential storage, CLI, RPC, routing, and tests
- enforce reusable=false owner election with current topology
- preserve proof-backed groups when refreshing credential ACL groups
This commit is contained in:
KKRainbow
2026-04-25 00:22:40 +08:00
committed by GitHub
parent f7ea78d4f0
commit 4688ad74ad
8 changed files with 1461 additions and 205 deletions
+729 -106
View File
@@ -48,8 +48,8 @@ use crate::{
OspfRouteRpcClientFactory, OspfRouteRpcServer, PeerGroupInfo, PeerIdVersion,
PeerIdentityType, RouteForeignNetworkInfos, RouteForeignNetworkSummary, RoutePeerInfo,
RoutePeerInfos, SyncRouteInfoError, SyncRouteInfoRequest, SyncRouteInfoResponse,
TrustedCredentialPubkey, route_foreign_network_infos, route_foreign_network_summary,
sync_route_info_request::ConnInfo,
TrustedCredentialPubkey, TrustedCredentialPubkeyProof, route_foreign_network_infos,
route_foreign_network_summary, sync_route_info_request::ConnInfo,
},
rpc_types::{
self,
@@ -123,6 +123,20 @@ fn patch_raw_from_info(raw: &mut DynamicMessage, info: &RoutePeerInfo, fields: &
}
}
fn raw_credential_bytes_from_route_info(
raw_route_info: &DynamicMessage,
proof_idx: usize,
) -> Option<Vec<u8>> {
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())
}
#[derive(Debug, Clone)]
struct AtomicVersion(Arc<AtomicU32>);
@@ -401,6 +415,9 @@ struct SyncedRouteInfo {
// Aggregated trusted credential pubkeys from all admin nodes
// Maps pubkey bytes -> TrustedCredentialPubkey
trusted_credential_pubkeys: DashMap<Vec<u8>, TrustedCredentialPubkey>,
// Tracks the currently accepted peer for non-reusable credentials.
// Maps credential pubkey bytes -> peer_id.
non_reusable_credential_owners: DashMap<Vec<u8>, PeerId>,
version: AtomicVersion,
}
@@ -457,6 +474,212 @@ impl SyncedRouteInfo {
.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<PeerId, RoutePeerInfo>,
network_secret: Option<&str>,
now: i64,
) -> (
HashMap<Vec<u8>, TrustedCredentialPubkey>,
HashMap<Vec<u8>, 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<Vec<u8>, TrustedCredentialPubkey>,
) -> HashSet<Vec<u8>> {
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<F>(
&self,
peer_infos: &OrderedHashMap<PeerId, RoutePeerInfo>,
all_trusted: &HashMap<Vec<u8>, TrustedCredentialPubkey>,
mut is_peer_active: F,
) -> (HashMap<Vec<u8>, PeerId>, BTreeSet<PeerId>)
where
F: FnMut(PeerId) -> bool,
{
let mut candidates: BTreeMap<Vec<u8>, BTreeSet<PeerId>> = 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<Vec<u8>, 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<PeerId, RoutePeerInfo>,
all_trusted: &HashMap<Vec<u8>, 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<PeerId, RoutePeerInfo>,
prev_trusted: &HashSet<Vec<u8>>,
all_trusted: &HashMap<Vec<u8>, TrustedCredentialPubkey>,
) -> BTreeSet<PeerId> {
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<T: FromIterator<PeerId>>(&self, peer_id: PeerId) -> Option<T> {
self.conn_map
.read()
@@ -967,110 +1190,37 @@ impl SyncedRouteInfo {
Vec<PeerId>,
HashMap<Vec<u8>, crate::common::global_ctx::TrustedKeyMetadata>,
) {
use crate::common::global_ctx::{TrustedKeyMetadata, TrustedKeySource};
self.verify_and_update_credential_trusts_with_active_peers(network_secret, |_| true)
}
fn verify_and_update_credential_trusts_with_active_peers<F>(
&self,
network_secret: Option<&str>,
is_peer_active: F,
) -> (
Vec<PeerId>,
HashMap<Vec<u8>, 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;
// 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();
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);
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 proof in &info.trusted_credential_pubkeys {
// If we have a network_secret, verify the HMAC as before.
// If we don't (e.g. credential nodes), accept proofs from admin peers
// based on the authenticated channel instead of local HMAC verification.
let hmac_valid = network_secret
.map(|secret| proof.verify_credential_hmac(secret))
.unwrap_or(true);
if !hmac_valid {
continue;
}
let Some(tc) = proof.credential.as_ref() else {
continue;
};
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) {
// Start from proof-backed groups so credential-declared groups can coexist
// without leaving stale credential-only entries behind after refreshes.
let mut group_map = self.get_proof_groups(info.peer_id);
for g in &tc.groups {
group_map.entry(g.clone()).or_default();
}
self.set_peer_groups(info.peer_id, group_map);
}
}
// 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);
}
}
let mut untrusted_peers =
Self::collect_revoked_credential_peers(&peer_infos, &prev_trusted, &all_trusted);
untrusted_peers.extend(duplicate_untrusted_peers);
// Remove untrusted peers from peer_infos so they won't appear in route graph
if !untrusted_peers.is_empty() {
@@ -1081,7 +1231,7 @@ impl SyncedRouteInfo {
self.remove_peers(untrusted_peers.iter().copied());
}
(untrusted_peers, global_trusted_keys)
(untrusted_peers.into_iter().collect(), global_trusted_keys)
}
fn is_admin_peer(&self, info: &RoutePeerInfo) -> bool {
@@ -1928,6 +2078,7 @@ impl PeerRouteServiceImpl {
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(),
},
cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::default()),
@@ -1949,6 +2100,12 @@ impl PeerRouteServiceImpl {
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()
@@ -2492,7 +2649,7 @@ impl PeerRouteServiceImpl {
trust_admin_groups_without_proof,
);
let untrusted = self.refresh_credential_trusts();
let untrusted = self.refresh_credential_trusts_with_current_topology();
self.disconnect_untrusted_peers(&untrusted).await;
if my_peer_info_updated || !untrusted.is_empty() {
@@ -2513,11 +2670,34 @@ impl PeerRouteServiceImpl {
.verify_and_update_credential_trusts(network_identity.network_secret.as_deref());
self.global_ctx
.update_trusted_keys(global_trusted_keys, &network_identity.network_name);
untrusted
}
fn refresh_credential_trusts_with_current_topology(&self) -> Vec<PeerId> {
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(
network_identity.network_secret.as_deref(),
|peer_id| self.is_active_non_reusable_credential_peer(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();
let untrusted = self.refresh_credential_trusts_with_current_topology();
self.disconnect_untrusted_peers(&untrusted).await;
!untrusted.is_empty()
}
@@ -3298,9 +3478,7 @@ impl RouteSessionManager {
}
if need_update_route_table {
// Run credential verification and update route table
untrusted_peers = service_impl.refresh_credential_trusts();
service_impl.update_route_table_and_cached_local_conn_bitmap();
untrusted_peers = service_impl.refresh_credential_trusts_with_current_topology();
}
let mut foreign_network_changed = false;
@@ -3738,10 +3916,11 @@ mod tests {
time::{Duration, SystemTime},
};
use super::{PeerRoute, REMOVE_DEAD_PEER_INFO_AFTER, RouteConnInfo};
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},
@@ -3767,7 +3946,6 @@ mod tests {
tunnel::common::tests::wait_for_condition,
};
use prost::Message;
struct AuthOnlyInterface {
my_peer_id: PeerId,
identity_type: DashMap<PeerId, PeerIdentityType>,
@@ -3987,6 +4165,32 @@ mod tests {
)
}
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<I>(connected_peers: I, last_update: SystemTime) -> RouteConnInfo
where
I: IntoIterator<Item = PeerId>,
{
RouteConnInfo {
connected_peers: connected_peers.into_iter().collect(),
version: 1.into(),
last_update,
}
}
async fn create_mock_pmgr() -> Arc<PeerManager> {
let (s, _r) = create_packet_recv_chan();
let peer_mgr = Arc::new(PeerManager::new(
@@ -4331,6 +4535,393 @@ mod tests {
));
}
#[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_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;
@@ -5474,6 +6065,38 @@ mod tests {
.any(|w| w == unknown_bytes)
}
fn encode_length_delimited_field(field_number: u32, payload: &[u8], dst: &mut Vec<u8>) {
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;