mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 02:09:06 +00:00
feat(web): add webhook-managed machine access and multi-instance CLI support (#1989)
* feat: add webhook-managed access and multi-instance CLI support * fix(foreign): verify credential of foreign credential peer
This commit is contained in:
@@ -6,6 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use base64::{prelude::BASE64_STANDARD, Engine as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::AsyncReadExt as _;
|
||||
|
||||
@@ -405,6 +406,42 @@ impl From<PortForwardConfig> for PortForwardConfigPb {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_secure_mode_cfg(mut user_cfg: SecureModeConfig) -> anyhow::Result<SecureModeConfig> {
|
||||
if !user_cfg.enabled {
|
||||
return Ok(user_cfg);
|
||||
}
|
||||
|
||||
let private_key = if user_cfg.local_private_key.is_none() {
|
||||
// if no private key, generate random one
|
||||
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
||||
user_cfg.local_private_key = Some(BASE64_STANDARD.encode(private.clone().as_bytes()));
|
||||
private
|
||||
} else {
|
||||
// check if private key is valid
|
||||
user_cfg.private_key()?
|
||||
};
|
||||
|
||||
let public = x25519_dalek::PublicKey::from(&private_key);
|
||||
|
||||
match user_cfg.local_public_key {
|
||||
None => {
|
||||
user_cfg.local_public_key = Some(BASE64_STANDARD.encode(public.as_bytes()));
|
||||
}
|
||||
Some(ref user_pub) => {
|
||||
let public = user_cfg.public_key()?;
|
||||
if *user_pub != BASE64_STANDARD.encode(public.as_bytes()) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"local public key {} does not match generated public key {}",
|
||||
user_pub,
|
||||
BASE64_STANDARD.encode(public.as_bytes())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(user_cfg)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
struct Config {
|
||||
netns: Option<String>,
|
||||
|
||||
+30
-11
@@ -146,18 +146,37 @@ pub fn init(
|
||||
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(lf) = recver.recv() {
|
||||
let e = file_filter_reloader.modify(|f| {
|
||||
if let Ok(nf) = EnvFilter::builder()
|
||||
.with_default_directive(lf.parse::<LevelFilter>().unwrap().into())
|
||||
.from_env()
|
||||
.with_context(|| "failed to create file filter")
|
||||
{
|
||||
info!("Reload log filter succeed, new filter level: {:?}", lf);
|
||||
*f = nf;
|
||||
let parsed_level = match lf.parse::<LevelFilter>() {
|
||||
Ok(level) => level,
|
||||
Err(e) => {
|
||||
error!("Failed to parse new log level {:?}: {}", lf, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_filter = match EnvFilter::builder()
|
||||
.with_default_directive(parsed_level.into())
|
||||
.from_env()
|
||||
.with_context(|| "failed to create file filter")
|
||||
{
|
||||
Ok(filter) => Some(filter),
|
||||
Err(e) => {
|
||||
error!("Failed to build new log filter for {:?}: {:?}", lf, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match file_filter_reloader.modify(|f| {
|
||||
*f = new_filter
|
||||
.take()
|
||||
.expect("log filter reloader only applies one filter per reload");
|
||||
}) {
|
||||
Ok(()) => {
|
||||
info!("Reload log filter succeed, new filter level: {:?}", lf);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to reload log filter: {:?}", e);
|
||||
}
|
||||
});
|
||||
if e.is_err() {
|
||||
error!("Failed to reload log filter: {:?}", e);
|
||||
}
|
||||
}
|
||||
info!("Stop log filter reloader");
|
||||
|
||||
@@ -102,6 +102,9 @@ pub fn set_default_machine_id(mid: Option<String>) {
|
||||
|
||||
pub fn get_machine_id() -> uuid::Uuid {
|
||||
if let Some(default_mid) = use_global_var!(MACHINE_UID) {
|
||||
if let Ok(mid) = uuid::Uuid::parse_str(default_mid.trim()) {
|
||||
return mid;
|
||||
}
|
||||
let mut b = [0u8; 16];
|
||||
crate::tunnel::generate_digest_from_str("", &default_mid, &mut b);
|
||||
return uuid::Uuid::from_bytes(b);
|
||||
@@ -207,4 +210,12 @@ mod tests {
|
||||
assert_eq!(weak_js.weak_count(), 0);
|
||||
assert_eq!(weak_js.strong_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_machine_id_uses_uuid_seed_verbatim() {
|
||||
let raw = "33333333-3333-3333-3333-333333333333".to_string();
|
||||
set_default_machine_id(Some(raw.clone()));
|
||||
assert_eq!(get_machine_id(), uuid::Uuid::parse_str(&raw).unwrap());
|
||||
set_default_machine_id(None);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-43
@@ -10,9 +10,10 @@ use std::{
|
||||
use crate::{
|
||||
common::{
|
||||
config::{
|
||||
get_avaliable_encrypt_methods, load_config_from_file, ConfigFileControl, ConfigLoader,
|
||||
ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, NetworkIdentity,
|
||||
PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig,
|
||||
get_avaliable_encrypt_methods, load_config_from_file, process_secure_mode_cfg,
|
||||
ConfigFileControl, ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig,
|
||||
LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader,
|
||||
VpnPortalConfig,
|
||||
},
|
||||
constants::EASYTIER_VERSION,
|
||||
log,
|
||||
@@ -27,10 +28,8 @@ use crate::{
|
||||
web_client, ShellType,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use base64::{prelude::BASE64_STANDARD, Engine as _};
|
||||
use cidr::IpCidr;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use rand::rngs::OsRng;
|
||||
use rust_i18n::t;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
@@ -773,42 +772,6 @@ impl NetworkOptions {
|
||||
false
|
||||
}
|
||||
|
||||
fn process_secure_mode_cfg(mut user_cfg: SecureModeConfig) -> anyhow::Result<SecureModeConfig> {
|
||||
if !user_cfg.enabled {
|
||||
return Ok(user_cfg);
|
||||
}
|
||||
|
||||
let private_key = if user_cfg.local_private_key.is_none() {
|
||||
// if no private key, generate random one
|
||||
let private = x25519_dalek::StaticSecret::random_from_rng(OsRng);
|
||||
user_cfg.local_private_key = Some(BASE64_STANDARD.encode(private.clone().as_bytes()));
|
||||
private
|
||||
} else {
|
||||
// check if private key is valid
|
||||
user_cfg.private_key()?
|
||||
};
|
||||
|
||||
let public = x25519_dalek::PublicKey::from(&private_key);
|
||||
|
||||
match user_cfg.local_public_key {
|
||||
None => {
|
||||
user_cfg.local_public_key = Some(BASE64_STANDARD.encode(public.as_bytes()));
|
||||
}
|
||||
Some(ref user_pub) => {
|
||||
let public = user_cfg.public_key()?;
|
||||
if *user_pub != BASE64_STANDARD.encode(public.as_bytes()) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"local public key {} does not match generated public key {}",
|
||||
user_pub,
|
||||
BASE64_STANDARD.encode(public.as_bytes())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(user_cfg)
|
||||
}
|
||||
|
||||
fn merge_into(&self, cfg: &TomlConfigLoader) -> anyhow::Result<()> {
|
||||
if self.hostname.is_some() {
|
||||
cfg.set_hostname(self.hostname.clone());
|
||||
@@ -1006,7 +969,7 @@ impl NetworkOptions {
|
||||
local_private_key: Some(credential_secret.clone()),
|
||||
local_public_key: None,
|
||||
};
|
||||
cfg.set_secure_mode(Some(Self::process_secure_mode_cfg(c)?));
|
||||
cfg.set_secure_mode(Some(process_secure_mode_cfg(c)?));
|
||||
} else if let Some(secure_mode) = self.secure_mode {
|
||||
if secure_mode {
|
||||
let c = SecureModeConfig {
|
||||
@@ -1014,7 +977,7 @@ impl NetworkOptions {
|
||||
local_private_key: self.local_private_key.clone(),
|
||||
local_public_key: self.local_public_key.clone(),
|
||||
};
|
||||
cfg.set_secure_mode(Some(Self::process_secure_mode_cfg(c)?));
|
||||
cfg.set_secure_mode(Some(process_secure_mode_cfg(c)?));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1246
-786
File diff suppressed because it is too large
Load Diff
@@ -192,7 +192,13 @@ impl NetworkInstanceManager {
|
||||
self.instance_map.iter().map(|item| *item.key()).collect()
|
||||
}
|
||||
|
||||
pub fn get_network_instance_name(&self, instance_id: &uuid::Uuid) -> Option<String> {
|
||||
pub fn get_instance_name(&self, instance_id: &uuid::Uuid) -> Option<String> {
|
||||
self.instance_map
|
||||
.get(instance_id)
|
||||
.map(|instance| instance.value().get_inst_name())
|
||||
}
|
||||
|
||||
pub fn get_network_name(&self, instance_id: &uuid::Uuid) -> Option<String> {
|
||||
self.instance_map
|
||||
.get(instance_id)
|
||||
.map(|instance| instance.value().get_network_name())
|
||||
|
||||
+115
-8
@@ -1,4 +1,4 @@
|
||||
use crate::common::config::{ConfigFileControl, PortForwardConfig};
|
||||
use crate::common::config::{process_secure_mode_cfg, ConfigFileControl, PortForwardConfig};
|
||||
use crate::proto::api::{self, manage};
|
||||
use crate::proto::rpc_types::controller::BaseController;
|
||||
use crate::rpc_service::InstanceRpcService;
|
||||
@@ -509,10 +509,29 @@ impl NetworkConfig {
|
||||
cfg.set_hostname(self.hostname.clone());
|
||||
cfg.set_dhcp(self.dhcp.unwrap_or_default());
|
||||
cfg.set_inst_name(self.network_name.clone().unwrap_or_default());
|
||||
cfg.set_network_identity(NetworkIdentity::new(
|
||||
self.network_name.clone().unwrap_or_default(),
|
||||
self.network_secret.clone().unwrap_or_default(),
|
||||
));
|
||||
|
||||
// The web UI does not expose credential inputs directly, but imported/saved
|
||||
// NetworkConfig objects still need to preserve credential-mode instances via
|
||||
// secure_mode.local_private_key + empty network_secret.
|
||||
let credential_secret = if self.network_secret.is_some() {
|
||||
None
|
||||
} else {
|
||||
self.secure_mode
|
||||
.as_ref()
|
||||
.and_then(|mode| mode.local_private_key.clone())
|
||||
.filter(|s| !s.is_empty())
|
||||
};
|
||||
|
||||
if credential_secret.is_some() {
|
||||
cfg.set_network_identity(NetworkIdentity::new_credential(
|
||||
self.network_name.clone().unwrap_or_default(),
|
||||
));
|
||||
} else {
|
||||
cfg.set_network_identity(NetworkIdentity::new(
|
||||
self.network_name.clone().unwrap_or_default(),
|
||||
self.network_secret.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
|
||||
if !cfg.get_dhcp() {
|
||||
let virtual_ipv4 = self.virtual_ipv4.clone().unwrap_or_default();
|
||||
@@ -677,7 +696,30 @@ impl NetworkConfig {
|
||||
));
|
||||
}
|
||||
|
||||
cfg.set_secure_mode(self.secure_mode.clone());
|
||||
if let Some(credential_file) = self
|
||||
.credential_file
|
||||
.as_ref()
|
||||
.filter(|path| !path.is_empty())
|
||||
{
|
||||
cfg.set_credential_file(Some(credential_file.into()));
|
||||
}
|
||||
|
||||
if let Some(credential_secret) = credential_secret {
|
||||
cfg.set_secure_mode(Some(process_secure_mode_cfg(
|
||||
crate::proto::common::SecureModeConfig {
|
||||
enabled: true,
|
||||
local_private_key: Some(credential_secret),
|
||||
local_public_key: None,
|
||||
},
|
||||
)?));
|
||||
} else {
|
||||
cfg.set_secure_mode(
|
||||
self.secure_mode
|
||||
.clone()
|
||||
.map(process_secure_mode_cfg)
|
||||
.transpose()?,
|
||||
);
|
||||
}
|
||||
|
||||
let mut flags = gen_default_flags();
|
||||
if let Some(latency_first) = self.latency_first {
|
||||
@@ -900,7 +942,9 @@ impl NetworkConfig {
|
||||
}
|
||||
|
||||
result.secure_mode = config.get_secure_mode();
|
||||
|
||||
result.credential_file = config
|
||||
.get_credential_file()
|
||||
.map(|path| path.to_string_lossy().into_owned());
|
||||
let flags = config.get_flags();
|
||||
result.latency_first = Some(flags.latency_first);
|
||||
result.dev_name = Some(flags.dev_name.clone());
|
||||
@@ -947,7 +991,11 @@ impl NetworkConfig {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{common::config::ConfigLoader, proto::common::SecureModeConfig};
|
||||
use crate::{
|
||||
common::config::{process_secure_mode_cfg, ConfigLoader},
|
||||
proto::common::SecureModeConfig,
|
||||
};
|
||||
use base64::prelude::{Engine as _, BASE64_STANDARD};
|
||||
use rand::Rng;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
@@ -1195,6 +1243,10 @@ mod tests {
|
||||
config.set_flags(flags);
|
||||
}
|
||||
|
||||
if let Some(secure_mode) = config.get_secure_mode() {
|
||||
config.set_secure_mode(Some(process_secure_mode_cfg(secure_mode)?));
|
||||
}
|
||||
|
||||
let network_config = super::NetworkConfig::new_from_config(&config)?;
|
||||
let generated_config = network_config.gen_config()?;
|
||||
generated_config.set_peers(generated_config.get_peers()); // Ensure peers field is not None
|
||||
@@ -1211,4 +1263,59 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_conversion_credential_mode() -> Result<(), anyhow::Error> {
|
||||
let private_key = x25519_dalek::StaticSecret::from([7u8; 32]);
|
||||
let public_key = x25519_dalek::PublicKey::from(&private_key);
|
||||
let credential_secret = BASE64_STANDARD.encode(private_key.as_bytes());
|
||||
let credential_file = "/tmp/easytier-credentials.json".to_string();
|
||||
|
||||
let config = gen_default_config();
|
||||
config.set_network_identity(crate::common::config::NetworkIdentity::new_credential(
|
||||
"credential-net".to_string(),
|
||||
));
|
||||
config.set_inst_name("credential-net".to_string());
|
||||
config.set_credential_file(Some(credential_file.clone().into()));
|
||||
config.set_secure_mode(Some(SecureModeConfig {
|
||||
enabled: true,
|
||||
local_private_key: Some(credential_secret.clone()),
|
||||
local_public_key: Some(BASE64_STANDARD.encode(public_key.as_bytes())),
|
||||
}));
|
||||
|
||||
let network_config = super::NetworkConfig::new_from_config(&config)?;
|
||||
assert_eq!(
|
||||
network_config.credential_file.as_deref(),
|
||||
Some(credential_file.as_str())
|
||||
);
|
||||
assert_eq!(network_config.network_secret, None);
|
||||
assert_eq!(
|
||||
network_config
|
||||
.secure_mode
|
||||
.as_ref()
|
||||
.and_then(|mode| mode.local_private_key.as_deref()),
|
||||
Some(credential_secret.as_str())
|
||||
);
|
||||
|
||||
let generated_config = network_config.gen_config()?;
|
||||
assert_eq!(
|
||||
generated_config.get_network_identity().network_secret,
|
||||
None,
|
||||
"credential mode should not be converted back into network_secret mode"
|
||||
);
|
||||
assert_eq!(
|
||||
generated_config
|
||||
.get_credential_file()
|
||||
.map(|path| path.to_string_lossy().into_owned()),
|
||||
Some(credential_file)
|
||||
);
|
||||
assert_eq!(
|
||||
generated_config
|
||||
.get_secure_mode()
|
||||
.and_then(|mode| mode.local_private_key),
|
||||
Some(credential_secret)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,6 +617,16 @@ pub struct ForeignNetworkManager {
|
||||
}
|
||||
|
||||
impl ForeignNetworkManager {
|
||||
async fn is_shared_pubkey_trusted(
|
||||
entry: &ForeignNetworkEntry,
|
||||
remote_static_pubkey: &[u8],
|
||||
) -> bool {
|
||||
remote_static_pubkey.len() == 32
|
||||
&& entry
|
||||
.global_ctx
|
||||
.is_pubkey_trusted(remote_static_pubkey, &entry.network.network_name)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
my_peer_id: PeerId,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
@@ -655,12 +665,14 @@ impl ForeignNetworkManager {
|
||||
}
|
||||
|
||||
pub async fn add_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> {
|
||||
tracing::info!(peer_conn = ?peer_conn.get_conn_info(), network = ?peer_conn.get_network_identity(), "add new peer conn in foreign network manager");
|
||||
let conn_info = peer_conn.get_conn_info();
|
||||
let peer_network = peer_conn.get_network_identity();
|
||||
tracing::info!(peer_conn = ?conn_info, network = ?peer_network, "add new peer conn in foreign network manager");
|
||||
|
||||
let relay_peer_rpc = self.global_ctx.get_flags().relay_all_peer_rpc;
|
||||
let ret = self
|
||||
.global_ctx
|
||||
.check_network_in_whitelist(&peer_conn.get_network_identity().network_name)
|
||||
.check_network_in_whitelist(&peer_network.network_name)
|
||||
.map_err(Into::into);
|
||||
if ret.is_err() && !relay_peer_rpc {
|
||||
return ret;
|
||||
@@ -669,7 +681,7 @@ impl ForeignNetworkManager {
|
||||
let (entry, new_added) = self
|
||||
.data
|
||||
.get_or_insert_entry(
|
||||
&peer_conn.get_network_identity(),
|
||||
&peer_network,
|
||||
peer_conn.get_my_peer_id(),
|
||||
peer_conn.get_peer_id(),
|
||||
ret.is_ok(),
|
||||
@@ -679,10 +691,14 @@ impl ForeignNetworkManager {
|
||||
)
|
||||
.await;
|
||||
|
||||
let same_identity = entry.network == peer_network;
|
||||
let shared_peer = peer_conn.get_peer_identity_type() == PeerIdentityType::SharedNode;
|
||||
let shared_peer_trusted = shared_peer
|
||||
&& Self::is_shared_pubkey_trusted(&entry, &conn_info.noise_remote_static_pubkey).await;
|
||||
|
||||
let _g = entry.lock.lock().await;
|
||||
|
||||
if (entry.network != peer_conn.get_network_identity()
|
||||
&& peer_conn.get_peer_identity_type() != PeerIdentityType::SharedNode)
|
||||
if (!(same_identity || shared_peer_trusted))
|
||||
|| entry.my_peer_id != peer_conn.get_my_peer_id()
|
||||
{
|
||||
if new_added {
|
||||
@@ -697,9 +713,11 @@ impl ForeignNetworkManager {
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!(
|
||||
"network secret not match. exp: {:?} real: {:?}",
|
||||
"foreign peer identity not trusted. exp: {:?} real: {:?}, remote_pubkey_len: {}, shared_trusted: {}",
|
||||
entry.network,
|
||||
peer_conn.get_network_identity()
|
||||
peer_network,
|
||||
conn_info.noise_remote_static_pubkey.len(),
|
||||
shared_peer_trusted,
|
||||
)
|
||||
};
|
||||
tracing::error!(?err, "foreign network entry not match, disconnect peer");
|
||||
|
||||
@@ -1441,9 +1441,9 @@ impl PeerConn {
|
||||
let info = self.info.as_ref().unwrap();
|
||||
let mut ret = NetworkIdentity {
|
||||
network_name: info.network_name.clone(),
|
||||
..Default::default()
|
||||
network_secret: None,
|
||||
network_secret_digest: Some([0u8; 32]),
|
||||
};
|
||||
ret.network_secret_digest = Some([0u8; 32]);
|
||||
ret.network_secret_digest
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
@@ -1619,7 +1619,15 @@ pub mod tests {
|
||||
assert_eq!(c_peer.get_peer_id(), s_peer_id);
|
||||
assert_eq!(s_peer.get_peer_id(), c_peer_id);
|
||||
assert_eq!(c_peer.get_network_identity(), s_peer.get_network_identity());
|
||||
assert_eq!(c_peer.get_network_identity(), NetworkIdentity::default());
|
||||
assert_eq!(
|
||||
c_peer.get_network_identity().network_name,
|
||||
NetworkIdentity::default().network_name
|
||||
);
|
||||
assert_eq!(c_peer.get_network_identity().network_secret, None);
|
||||
assert_eq!(
|
||||
c_peer.get_network_identity().network_secret_digest,
|
||||
NetworkIdentity::default().network_secret_digest
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -301,6 +301,7 @@ message GenerateCredentialRequest {
|
||||
repeated string allowed_proxy_cidrs = 3; // optional: restrict proxy_cidrs
|
||||
int64 ttl_seconds = 4; // must be > 0: credential TTL in seconds (0 / omitted is invalid)
|
||||
optional string credential_id = 5; // optional: user-specified credential id, reused if already exists
|
||||
InstanceIdentifier instance = 6; // target network instance
|
||||
}
|
||||
|
||||
message GenerateCredentialResponse {
|
||||
@@ -310,13 +311,16 @@ message GenerateCredentialResponse {
|
||||
|
||||
message RevokeCredentialRequest {
|
||||
string credential_id = 1;
|
||||
InstanceIdentifier instance = 2; // target network instance
|
||||
}
|
||||
|
||||
message RevokeCredentialResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message ListCredentialsRequest {}
|
||||
message ListCredentialsRequest {
|
||||
InstanceIdentifier instance = 1; // target network instance
|
||||
}
|
||||
|
||||
message CredentialInfo {
|
||||
string credential_id = 1; // UUID
|
||||
|
||||
@@ -83,6 +83,8 @@ message NetworkConfig {
|
||||
optional bool disable_tcp_hole_punching = 54;
|
||||
|
||||
common.SecureModeConfig secure_mode = 55;
|
||||
reserved 56;
|
||||
optional string credential_file = 57;
|
||||
}
|
||||
|
||||
message PortForwardConfig {
|
||||
@@ -124,6 +126,7 @@ message NetworkMeta {
|
||||
common.UUID inst_id = 1;
|
||||
string network_name = 2;
|
||||
uint32 config_permission = 3;
|
||||
string instance_name = 4;
|
||||
}
|
||||
|
||||
message ValidateConfigRequest { NetworkConfig config = 1; }
|
||||
|
||||
@@ -32,7 +32,7 @@ impl CredentialManageRpc for CredentialManageRpcService {
|
||||
ctrl: Self::Controller,
|
||||
req: GenerateCredentialRequest,
|
||||
) -> crate::proto::rpc_types::error::Result<GenerateCredentialResponse> {
|
||||
super::get_instance_service(&self.instance_manager, &None)?
|
||||
super::get_instance_service(&self.instance_manager, &req.instance)?
|
||||
.get_credential_manage_service()
|
||||
.generate_credential(ctrl, req)
|
||||
.await
|
||||
@@ -43,7 +43,7 @@ impl CredentialManageRpc for CredentialManageRpcService {
|
||||
ctrl: Self::Controller,
|
||||
req: RevokeCredentialRequest,
|
||||
) -> crate::proto::rpc_types::error::Result<RevokeCredentialResponse> {
|
||||
super::get_instance_service(&self.instance_manager, &None)?
|
||||
super::get_instance_service(&self.instance_manager, &req.instance)?
|
||||
.get_credential_manage_service()
|
||||
.revoke_credential(ctrl, req)
|
||||
.await
|
||||
@@ -54,7 +54,7 @@ impl CredentialManageRpc for CredentialManageRpcService {
|
||||
ctrl: Self::Controller,
|
||||
req: ListCredentialsRequest,
|
||||
) -> crate::proto::rpc_types::error::Result<ListCredentialsResponse> {
|
||||
super::get_instance_service(&self.instance_manager, &None)?
|
||||
super::get_instance_service(&self.instance_manager, &req.instance)?
|
||||
.get_credential_manage_service()
|
||||
.list_credentials(ctrl, req)
|
||||
.await
|
||||
|
||||
@@ -276,13 +276,17 @@ impl WebClientService for InstanceManageRpcService {
|
||||
let Some(control) = self.manager.get_instance_config_control(&inst_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(name) = self.manager.get_network_instance_name(&inst_id) else {
|
||||
let Some(network_name) = self.manager.get_network_name(&inst_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(instance_name) = self.manager.get_instance_name(&inst_id) else {
|
||||
continue;
|
||||
};
|
||||
let meta = NetworkMeta {
|
||||
inst_id: Some(inst_id.into()),
|
||||
network_name: name,
|
||||
network_name,
|
||||
config_permission: control.permission.into(),
|
||||
instance_name,
|
||||
};
|
||||
metas.push(meta);
|
||||
}
|
||||
|
||||
@@ -50,13 +50,6 @@ impl LoggerRpc for LoggerRpcService {
|
||||
) -> Result<SetLoggerConfigResponse, rpc_types::error::Error> {
|
||||
let level_str = Self::log_level_to_string(request.level());
|
||||
|
||||
// 更新当前日志级别
|
||||
if let Some(current_level) = CURRENT_LOG_LEVEL.get() {
|
||||
if let Ok(mut level) = current_level.lock() {
|
||||
*level = level_str.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// 发送新的日志级别到 logger 重载器
|
||||
if let Some(sender) = LOGGER_LEVEL_SENDER.get() {
|
||||
if let Ok(sender) = sender.lock() {
|
||||
@@ -78,6 +71,13 @@ impl LoggerRpc for LoggerRpcService {
|
||||
)));
|
||||
}
|
||||
|
||||
// 更新当前日志级别
|
||||
if let Some(current_level) = CURRENT_LOG_LEVEL.get() {
|
||||
if let Ok(mut level) = current_level.lock() {
|
||||
*level = Self::log_level_to_string(request.level());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SetLoggerConfigResponse {})
|
||||
}
|
||||
|
||||
|
||||
@@ -234,12 +234,14 @@ where
|
||||
let config = self
|
||||
.handle_get_network_config(identify.clone(), instance_id)
|
||||
.await?;
|
||||
let network_name = config.network_name.unwrap_or_default();
|
||||
metas.insert(
|
||||
instance_id,
|
||||
NetworkMeta {
|
||||
inst_id: Some(instance_id.into()),
|
||||
network_name: config.network_name.unwrap_or_default(),
|
||||
network_name: network_name.clone(),
|
||||
config_permission: 0,
|
||||
instance_name: network_name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,6 +123,28 @@ async fn create_credential_config(
|
||||
config
|
||||
}
|
||||
|
||||
/// Helper: Create credential node config with a random, unknown key
|
||||
fn create_unknown_credential_config(
|
||||
network_name: String,
|
||||
inst_name: &str,
|
||||
ns: Option<&str>,
|
||||
ipv4: &str,
|
||||
ipv6: &str,
|
||||
) -> TomlConfigLoader {
|
||||
let random_private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
||||
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_inst_name(inst_name.to_owned());
|
||||
config.set_netns(ns.map(|s| s.to_owned()));
|
||||
config.set_ipv4(Some(ipv4.parse().unwrap()));
|
||||
config.set_ipv6(Some(ipv6.parse().unwrap()));
|
||||
config.set_listeners(vec![]);
|
||||
config.set_network_identity(NetworkIdentity::new_credential(network_name));
|
||||
config.set_secure_mode(Some(generate_secure_mode_config_with_key(&random_private)));
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Helper: Create admin node config
|
||||
fn create_admin_config(
|
||||
inst_name: &str,
|
||||
@@ -809,6 +831,113 @@ async fn credential_unknown_rejected() {
|
||||
drop_insts(vec![admin_inst, cred_inst]).await;
|
||||
}
|
||||
|
||||
/// Regression test: an unknown credential must still be rejected when it first connects via a
|
||||
/// shared node. If this fails, the shared path is incorrectly admitting the node into the target
|
||||
/// network's route domain.
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn credential_unknown_via_shared_rejected() {
|
||||
prepare_credential_network();
|
||||
|
||||
let admin_a_config =
|
||||
create_admin_config("admin_a", Some("ns_adm"), "10.144.144.1", "fd00::1/64");
|
||||
let mut admin_a_inst = Instance::new(admin_a_config);
|
||||
admin_a_inst.run().await.unwrap();
|
||||
|
||||
let shared_b_config =
|
||||
create_shared_config("shared_b", Some("ns_c1"), "10.144.144.2", "fd00::2/64");
|
||||
let mut shared_b_inst = Instance::new(shared_b_config);
|
||||
shared_b_inst.run().await.unwrap();
|
||||
|
||||
let admin_c_config =
|
||||
create_admin_config("admin_c", Some("ns_c3"), "10.144.144.4", "fd00::4/64");
|
||||
let mut admin_c_inst = Instance::new(admin_c_config);
|
||||
admin_c_inst.run().await.unwrap();
|
||||
|
||||
admin_a_inst
|
||||
.get_conn_manager()
|
||||
.add_connector(TcpTunnelConnector::new(
|
||||
"tcp://10.1.1.2:11010".parse().unwrap(),
|
||||
));
|
||||
admin_c_inst
|
||||
.get_conn_manager()
|
||||
.add_connector(TcpTunnelConnector::new(
|
||||
"tcp://10.1.1.2:11010".parse().unwrap(),
|
||||
));
|
||||
|
||||
let admin_c_peer_id = admin_c_inst.peer_id();
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
let a_routes = admin_a_inst.get_peer_manager().list_routes().await;
|
||||
let c_routes = admin_c_inst.get_peer_manager().list_routes().await;
|
||||
a_routes.iter().any(|r| r.peer_id == admin_c_peer_id)
|
||||
|| c_routes.iter().any(|r| r.peer_id == admin_a_inst.peer_id())
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
|
||||
let unknown_config = create_unknown_credential_config(
|
||||
admin_a_inst
|
||||
.get_global_ctx()
|
||||
.get_network_identity()
|
||||
.network_name
|
||||
.clone(),
|
||||
"unknown_d",
|
||||
Some("ns_c2"),
|
||||
"10.144.144.5",
|
||||
"fd00::5/64",
|
||||
);
|
||||
let mut unknown_inst = Instance::new(unknown_config);
|
||||
unknown_inst.run().await.unwrap();
|
||||
|
||||
unknown_inst
|
||||
.get_conn_manager()
|
||||
.add_connector(TcpTunnelConnector::new(
|
||||
"tcp://10.1.1.2:11010".parse().unwrap(),
|
||||
));
|
||||
|
||||
let unknown_peer_id = unknown_inst.peer_id();
|
||||
|
||||
println!("unknown_peer_id: {:?}", unknown_peer_id);
|
||||
|
||||
for _ in 0..5 {
|
||||
let admin_a_routes = admin_a_inst.get_peer_manager().list_routes().await;
|
||||
let admin_c_routes = admin_c_inst.get_peer_manager().list_routes().await;
|
||||
|
||||
assert!(
|
||||
!admin_a_routes.iter().any(|r| r.peer_id == unknown_peer_id),
|
||||
"unknown credential unexpectedly appeared in admin_a routes via shared path: {:?}",
|
||||
admin_a_routes.iter().map(|r| r.peer_id).collect::<Vec<_>>()
|
||||
);
|
||||
assert!(
|
||||
!admin_c_routes.iter().any(|r| r.peer_id == unknown_peer_id),
|
||||
"unknown credential unexpectedly appeared in admin_c routes via shared path: {:?}",
|
||||
admin_c_routes.iter().map(|r| r.peer_id).collect::<Vec<_>>()
|
||||
);
|
||||
assert!(
|
||||
!ping_test("ns_adm", "10.144.144.5", None).await,
|
||||
"admin_a unexpectedly reached unknown credential via shared path"
|
||||
);
|
||||
assert!(
|
||||
!ping_test("ns_c3", "10.144.144.5", None).await,
|
||||
"admin_c unexpectedly reached unknown credential via shared path"
|
||||
);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
println!("drop all");
|
||||
|
||||
drop_insts(vec![
|
||||
admin_a_inst,
|
||||
shared_b_inst,
|
||||
admin_c_inst,
|
||||
unknown_inst,
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
|
||||
Reference in New Issue
Block a user