From b037ea9c3fe2ef3a56a4902968b58b23befb070c Mon Sep 17 00:00:00 2001 From: KKRainbow <443152178@qq.com> Date: Sat, 28 Mar 2026 22:19:23 +0800 Subject: [PATCH] Relax private mode foreign network secret checks (#2022) --- easytier/locales/app.yml | 4 +- easytier/src/peers/foreign_network_manager.rs | 47 +++- easytier/src/peers/peer_conn.rs | 34 +++ easytier/src/peers/peer_manager.rs | 30 ++- easytier/src/peers/tests.rs | 226 ++++++++++++++++++ 5 files changed, 323 insertions(+), 18 deletions(-) diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index 4a94979a..586e8afa 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -212,8 +212,8 @@ core_clap: en: "specify the top-level domain zone for magic DNS. if not provided, defaults to the value from dns_server module (et.net.). only used when accept_dns is true." zh-CN: "指定魔法DNS的顶级域名区域。如果未提供,默认使用dns_server模块中的值(et.net.)。仅在accept_dns为true时使用。" private_mode: - en: "if true, nodes with different network names or passwords from this network are not allowed to perform handshake or relay through this node." - zh-CN: "如果为true,则不允许使用了与本网络不相同的网络名称和密码的节点通过本节点进行握手或中转" + en: "if true, foreign networks are only allowed when this node can verify they use the same network secret, or when a foreign credential node is already trusted via admin-issued credential propagation; different or missing secrets are otherwise rejected." + zh-CN: "如果为true,则仅允许两类 foreign network 接入:本节点能验证其使用相同 network secret 的节点,或已通过 foreign network 管理节点传播而被信任的 credential 节点;否则 secret 不同或缺失时会被拒绝。" foreign_relay_bps_limit: en: "the maximum bps limit for foreign network relay, default is no limit. unit: BPS (bytes per second)" zh-CN: "作为共享节点时,限制非本地网络的流量转发速率,默认无限制,单位 BPS (字节每秒)" diff --git a/easytier/src/peers/foreign_network_manager.rs b/easytier/src/peers/foreign_network_manager.rs index 173a978c..cc06d4dd 100644 --- a/easytier/src/peers/foreign_network_manager.rs +++ b/easytier/src/peers/foreign_network_manager.rs @@ -730,18 +730,46 @@ impl ForeignNetworkManager { matches!(identity_type, PeerIdentityType::Admin) } - async fn is_credential_pubkey_trusted( - entry: &ForeignNetworkEntry, + fn credential_pubkey_is_trusted( + global_ctx: &ArcGlobalCtx, + network_name: &str, remote_static_pubkey: &[u8], ) -> bool { remote_static_pubkey.len() == 32 - && entry.global_ctx.is_pubkey_trusted_with_source( + && global_ctx.is_pubkey_trusted_with_source( remote_static_pubkey, - &entry.network.network_name, + network_name, TrustedKeySource::OspfCredential, ) } + fn is_credential_pubkey_trusted( + entry: &ForeignNetworkEntry, + remote_static_pubkey: &[u8], + ) -> bool { + Self::credential_pubkey_is_trusted( + &entry.global_ctx, + &entry.network.network_name, + remote_static_pubkey, + ) + } + + pub(crate) fn is_existing_credential_pubkey_trusted( + &self, + network_name: &str, + remote_static_pubkey: &[u8], + ) -> bool { + self.data + .get_network_entry(network_name) + .is_some_and(|entry| { + Self::credential_pubkey_is_trusted( + &entry.global_ctx, + &entry.network.network_name, + remote_static_pubkey, + ) + }) + } + fn build_trusted_key_items(entry: &ForeignNetworkEntry) -> Vec { entry .global_ctx @@ -839,8 +867,7 @@ impl ForeignNetworkManager { let same_identity = entry.network == peer_network; let peer_identity_type = peer_conn.get_peer_identity_type(); let credential_peer_trusted = peer_digest_empty - && Self::is_credential_pubkey_trusted(&entry, &conn_info.noise_remote_static_pubkey) - .await; + && Self::is_credential_pubkey_trusted(&entry, &conn_info.noise_remote_static_pubkey); let credential_identity_mismatch = credential_peer_trusted && Self::should_reject_credential_trust_path(peer_identity_type); @@ -1483,7 +1510,9 @@ pub mod tests { )]), &foreign_network.network_name, ); - assert!(!ForeignNetworkManager::is_credential_pubkey_trusted(&entry, &pubkey).await); + assert!(!ForeignNetworkManager::is_credential_pubkey_trusted( + &entry, &pubkey + )); entry.global_ctx.update_trusted_keys( HashMap::from([( @@ -1495,7 +1524,9 @@ pub mod tests { )]), &foreign_network.network_name, ); - assert!(ForeignNetworkManager::is_credential_pubkey_trusted(&entry, &pubkey).await); + assert!(ForeignNetworkManager::is_credential_pubkey_trusted( + &entry, &pubkey + )); } #[test] diff --git a/easytier/src/peers/peer_conn.rs b/easytier/src/peers/peer_conn.rs index 0aca7265..ff22b470 100644 --- a/easytier/src/peers/peer_conn.rs +++ b/easytier/src/peers/peer_conn.rs @@ -1472,6 +1472,40 @@ impl PeerConn { ret } + fn network_secret_digest_is_empty(network: &NetworkIdentity) -> bool { + network + .network_secret_digest + .as_ref() + .is_none_or(|digest| digest.iter().all(|byte| *byte == 0)) + } + + fn matches_local_secret_proof(&self) -> bool { + let Some(secret_proof) = self + .noise_handshake_result + .as_ref() + .and_then(|noise| noise.client_secret_proof.as_ref()) + else { + return false; + }; + + self.global_ctx + .get_secret_proof(&secret_proof.challenge) + .is_some_and(|mac| mac.verify_slice(&secret_proof.proof).is_ok()) + } + + pub(crate) fn matches_local_network_secret(&self) -> bool { + if self.matches_local_secret_proof() { + return true; + } + + let my_identity = self.global_ctx.get_network_identity(); + let peer_identity = self.get_network_identity(); + + !Self::network_secret_digest_is_empty(&my_identity) + && !Self::network_secret_digest_is_empty(&peer_identity) + && my_identity.network_secret_digest == peer_identity.network_secret_digest + } + pub fn get_close_notifier(&self) -> Arc { self.close_event_notifier.clone() } diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index 7a2c4b74..cd8d6126 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -697,12 +697,6 @@ impl PeerManager { return Ok(()); } - if self.global_ctx.config.get_flags().private_mode { - return Err(Error::SecretKeyError( - "private mode is turned on, network identity not match".to_string(), - )); - } - let mut peer_id = self .foreign_network_manager .get_network_peer_id(network_name); @@ -724,11 +718,31 @@ impl PeerManager { }) .await?; - let peer_network_name = conn.get_network_identity().network_name.clone(); + let peer_identity = conn.get_network_identity(); + let peer_network_name = peer_identity.network_name.clone(); + let my_identity = self.global_ctx.get_network_identity(); + let is_local_network = peer_network_name == my_identity.network_name; + let trusted_foreign_credential = + matches!(conn.get_peer_identity_type(), PeerIdentityType::Credential) + && self + .foreign_network_manager + .is_existing_credential_pubkey_trusted( + &peer_network_name, + &conn.get_conn_info().noise_remote_static_pubkey, + ); + let foreign_network_allowed = + conn.matches_local_network_secret() || trusted_foreign_credential; + + if !is_local_network && self.global_ctx.get_flags().private_mode && !foreign_network_allowed + { + return Err(Error::SecretKeyError( + "private mode is turned on, foreign network secret mismatch".to_string(), + )); + } conn.set_is_hole_punched(!is_directly_connected); - if peer_network_name == self.global_ctx.get_network_identity().network_name { + if is_local_network { self.add_new_peer_conn(conn).await?; } else { self.foreign_network_manager.add_peer_conn(conn).await?; diff --git a/easytier/src/peers/tests.rs b/easytier/src/peers/tests.rs index 628aebac..400c795b 100644 --- a/easytier/src/peers/tests.rs +++ b/easytier/src/peers/tests.rs @@ -13,6 +13,7 @@ use crate::{ stats_manager::{LabelSet, LabelType, MetricName}, PeerId, }, + proto::api::instance::TrustedKeySourcePb, tunnel::{ common::tests::wait_for_condition, packet_def::{PacketType, ZCPacket}, @@ -63,6 +64,92 @@ pub async fn create_mock_peer_manager_secure( peer_mgr } +fn set_private_mode(peer_mgr: &PeerManager, enabled: bool) { + let global_ctx = peer_mgr.get_global_ctx(); + let mut flags = global_ctx.get_flags(); + flags.private_mode = enabled; + global_ctx.set_flags(flags); +} + +async fn connect_client_and_server( + client: Arc, + server: Arc, +) -> (Result<(), Error>, Result<(), Error>) { + let (client_ring, server_ring) = create_ring_tunnel_pair(); + tokio::join!( + { + let client = client.clone(); + async move { + client.add_client_tunnel(client_ring, false).await?; + Ok(()) + } + }, + { + let server = server.clone(); + async move { server.add_tunnel_as_server(server_ring, true).await } + } + ) +} + +async fn wait_for_foreign_network(server: Arc, network_name: &'static str) { + wait_for_condition( + || { + let server = server.clone(); + async move { + server + .get_foreign_network_manager() + .list_foreign_networks() + .await + .foreign_networks + .contains_key(network_name) + } + }, + Duration::from_secs(10), + ) + .await; +} + +async fn wait_for_foreign_network_peer_count_at_least( + server: Arc, + network_name: &'static str, + min_peer_count: usize, +) { + wait_for_condition( + || { + let server = server.clone(); + async move { + server + .get_foreign_network_manager() + .list_foreign_networks() + .await + .foreign_networks + .get(network_name) + .map(|entry| entry.peers.len() >= min_peer_count) + .unwrap_or(false) + } + }, + Duration::from_secs(10), + ) + .await; +} + +async fn wait_for_public_peers_empty(client: Arc) { + wait_for_condition( + || { + let client = client.clone(); + async move { + client + .get_foreign_network_client() + .list_public_peers() + .await + .is_empty() + } + }, + Duration::from_secs(5), + ) + .await; +} + pub async fn connect_peer_manager(client: Arc, server: Arc) { let (a_ring, b_ring) = create_ring_tunnel_pair(); let a_mgr_copy = client; @@ -205,6 +292,145 @@ async fn relay_peer_map_secure_session_decrypt() { assert_eq!(packet.payload(), b"relay-hello"); } +#[tokio::test] +async fn private_mode_allows_foreign_network_with_same_secret() { + let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await; + let client = + create_mock_peer_manager_secure("tenant-a".to_string(), "shared".to_string()).await; + set_private_mode(&server, true); + + let (client_ret, server_ret) = connect_client_and_server(client, server.clone()).await; + + assert!(client_ret.is_ok(), "client should connect in private mode"); + assert!( + server_ret.is_ok(), + "server should accept foreign network with matching secret: {:?}", + server_ret + ); + wait_for_foreign_network(server, "tenant-a").await; +} + +#[tokio::test] +async fn private_mode_rejects_foreign_network_with_different_secret() { + let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await; + let client = create_mock_peer_manager_secure("tenant-a".to_string(), "other".to_string()).await; + set_private_mode(&server, true); + + let (client_ret, server_ret) = connect_client_and_server(client.clone(), server.clone()).await; + + assert!( + server_ret.is_err(), + "server should reject foreign network with mismatched secret in private mode" + ); + let _ = client_ret; + wait_for_public_peers_empty(client).await; + assert!(server + .get_foreign_network_manager() + .list_foreign_networks() + .await + .foreign_networks + .is_empty()); +} + +#[tokio::test] +async fn private_mode_allows_trusted_foreign_credential() { + let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await; + let admin = create_mock_peer_manager_secure("tenant-a".to_string(), "shared".to_string()).await; + set_private_mode(&server, true); + + let (_cred_id, cred_secret) = admin + .get_global_ctx() + .get_credential_manager() + .generate_credential(vec![], false, vec![], Duration::from_secs(3600)); + + let privkey_bytes: [u8; 32] = base64::engine::general_purpose::STANDARD + .decode(&cred_secret) + .unwrap() + .try_into() + .unwrap(); + let private = x25519_dalek::StaticSecret::from(privkey_bytes); + let public = x25519_dalek::PublicKey::from(&private); + let credential = create_mock_peer_manager_credential("tenant-a".to_string(), &private).await; + + connect_peer_manager(admin.clone(), server.clone()).await; + wait_for_condition( + || { + let server = server.clone(); + let pubkey = public.as_bytes().to_vec(); + async move { + server + .get_foreign_network_manager() + .list_foreign_networks_with_options(true) + .await + .foreign_networks + .get("tenant-a") + .map(|entry| { + entry.trusted_keys.iter().any(|trusted_key| { + trusted_key.pubkey == pubkey + && trusted_key.source == TrustedKeySourcePb::OspfCredential as i32 + }) + }) + .unwrap_or(false) + } + }, + Duration::from_secs(10), + ) + .await; + + let (client_ret, server_ret) = connect_client_and_server(credential, server.clone()).await; + + assert!( + client_ret.is_ok(), + "trusted foreign credential client should connect in private mode" + ); + assert!( + server_ret.is_ok(), + "server should allow trusted foreign credential in private mode: {:?}", + server_ret + ); + wait_for_foreign_network_peer_count_at_least(server, "tenant-a", 2).await; +} + +#[tokio::test] +async fn private_mode_rejects_untrusted_foreign_credential() { + let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await; + let admin = create_mock_peer_manager_secure("tenant-a".to_string(), "shared".to_string()).await; + set_private_mode(&server, true); + + let random_private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng); + let unknown_credential = + create_mock_peer_manager_credential("tenant-a".to_string(), &random_private).await; + + connect_peer_manager(admin.clone(), server.clone()).await; + wait_for_foreign_network(server.clone(), "tenant-a").await; + + let (client_ret, server_ret) = + connect_client_and_server(unknown_credential, server.clone()).await; + + let _ = client_ret; + assert!( + server_ret.is_err(), + "server should reject untrusted foreign credential in private mode" + ); + wait_for_condition( + || { + let server = server.clone(); + async move { + server + .get_foreign_network_manager() + .list_foreign_networks() + .await + .foreign_networks + .get("tenant-a") + .map(|entry| entry.peers.len() == 1) + .unwrap_or(false) + } + }, + Duration::from_secs(10), + ) + .await; +} + #[tokio::test] async fn relay_peer_map_retry_backoff_and_evict() { let (s, _r) = create_packet_recv_chan();