fix(config): normalize [secure_mode] when loading TOML config files (#2562)

This commit is contained in:
KKRainbow
2026-09-10 08:56:57 +08:00
committed by GitHub
parent 0fba5769a3
commit 993640b1dd
3 changed files with 211 additions and 9 deletions
+21 -4
View File
@@ -119,13 +119,12 @@ pub fn normalize_secure_mode_config(
match config.local_public_key.as_ref() {
None => config.local_public_key = Some(generated_public_key),
Some(configured_public_key) => {
let public_key = config.public_key()?;
let canonical_public_key = BASE64_STANDARD.encode(public_key.as_bytes());
if configured_public_key != &canonical_public_key {
config.public_key()?;
if configured_public_key != &generated_public_key {
anyhow::bail!(
"local public key {} does not match generated public key {}",
configured_public_key,
canonical_public_key
generated_public_key
);
}
}
@@ -748,6 +747,24 @@ mod tests {
);
}
#[test]
fn secure_mode_normalization_rejects_mismatched_public_key() {
let private_key = StaticSecret::from([7; 32]);
let other_public_key = PublicKey::from(&StaticSecret::from([9; 32]));
let error = normalize_secure_mode_config(common_pb::SecureModeConfig {
enabled: true,
local_private_key: Some(BASE64_STANDARD.encode(private_key.as_bytes())),
local_public_key: Some(BASE64_STANDARD.encode(other_public_key.as_bytes())),
})
.unwrap_err()
.to_string();
assert!(
error.contains("does not match generated public key"),
"{error}"
);
}
#[test]
fn disabled_secure_mode_does_not_validate_keys() {
let config = common_pb::SecureModeConfig {
+117 -3
View File
@@ -6,6 +6,7 @@ use std::{
sync::{Arc, Mutex},
};
use super::normalize_secure_mode_config;
pub use super::{EncryptionAlgorithm, gateway::PortForwardConfig};
use anyhow::Context;
#[cfg(feature = "rich-config-errors")]
@@ -705,6 +706,12 @@ impl TomlConfig {
Self::gen_flags(config.flags.clone().unwrap_or_default())
.context("failed to parse flags")?,
);
config.secure_mode = config
.secure_mode
.take()
.map(normalize_secure_mode_config)
.transpose()
.context("failed to normalize [secure_mode] config")?;
let has_network_identity = config.network_identity.is_some();
let config = TomlConfig {
@@ -1271,7 +1278,7 @@ network_secret = "network-secret"
[secure_mode]
enabled = true
local_private_key = "noise-private-key"
local_private_key = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
[vpn_portal_config]
wireguard_listen = "0.0.0.0:51820"
@@ -1293,7 +1300,7 @@ group_secret = "group-secret"
let dumped = config.dump();
assert!(dumped.contains("network-secret"));
assert!(dumped.contains("noise-private-key"));
assert!(dumped.contains("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="));
assert!(dumped.contains("wireguard-private-key"));
assert!(dumped.contains("group-secret"));
assert_eq!(
@@ -1305,7 +1312,7 @@ group_secret = "group-secret"
let redacted = config.dump_redacted();
assert!(!redacted.contains("network-secret"));
assert!(!redacted.contains("noise-private-key"));
assert!(!redacted.contains("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="));
assert!(!redacted.contains("wireguard-private-key"));
assert!(!redacted.contains("group-secret"));
assert_eq!(redacted.matches("<redacted>").count(), 4);
@@ -1396,6 +1403,7 @@ source = "web"
#[cfg(test)]
mod compatibility_tests {
use super::*;
use base64::{Engine as _, prelude::BASE64_STANDARD};
#[cfg(feature = "config-write")]
#[test]
@@ -1649,6 +1657,112 @@ enabled = true
assert!(identity.network_secret_digest.is_some());
}
#[test]
fn test_toml_secure_mode_generates_keypair_when_keys_missing() {
let config = TomlConfigLoader::new_from_str(
r#"
[secure_mode]
enabled = true
"#,
)
.unwrap();
let secure_mode = config.get_secure_mode().unwrap();
let private_key = secure_mode.private_key().unwrap();
let public_key = secure_mode.public_key().unwrap();
assert_eq!(
x25519_dalek::PublicKey::from(&private_key).as_bytes(),
public_key.as_bytes()
);
}
#[test]
fn test_toml_secure_mode_derives_public_key_from_private_key() {
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
let config = TomlConfigLoader::new_from_str(&format!(
r#"
[secure_mode]
enabled = true
local_private_key = "{}"
"#,
BASE64_STANDARD.encode(private.as_bytes())
))
.unwrap();
let secure_mode = config.get_secure_mode().unwrap();
let private_key = secure_mode.private_key().unwrap();
assert_eq!(private_key.as_bytes(), private.as_bytes());
assert_eq!(
secure_mode.public_key().unwrap().as_bytes(),
x25519_dalek::PublicKey::from(&private).as_bytes()
);
}
#[test]
fn test_toml_secure_mode_rejects_mismatched_keypair() {
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
let other_public = x25519_dalek::PublicKey::from(
&x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng),
);
let error = TomlConfigLoader::new_from_str(&format!(
r#"
[secure_mode]
enabled = true
local_private_key = "{}"
local_public_key = "{}"
"#,
BASE64_STANDARD.encode(private.as_bytes()),
BASE64_STANDARD.encode(other_public.as_bytes())
))
.unwrap_err();
let error = format!("{error:#}");
assert!(
error.contains("failed to normalize [secure_mode] config"),
"{error}"
);
assert!(
error.contains("does not match generated public key"),
"{error}"
);
}
#[test]
fn test_toml_secure_mode_disabled_keeps_keys_unset() {
let config = TomlConfigLoader::new_from_str(
r#"
[secure_mode]
enabled = false
"#,
)
.unwrap();
let secure_mode = config.get_secure_mode().unwrap();
assert!(!secure_mode.enabled);
assert_eq!(secure_mode.local_private_key, None);
assert_eq!(secure_mode.local_public_key, None);
}
#[cfg(feature = "config-write")]
#[test]
fn test_toml_secure_mode_keypair_survives_roundtrip() {
let config = TomlConfigLoader::new_from_str(
r#"
[secure_mode]
enabled = true
"#,
)
.unwrap();
let dumped = config.dump();
let restored = TomlConfigLoader::new_from_str(&dumped).unwrap();
assert_eq!(
config.get_secure_mode().unwrap(),
restored.get_secure_mode().unwrap()
);
}
#[test]
fn test_acl_toml_rule_uses_defaults_for_omitted_fields() {
use crate::proto::acl::{Action, ChainType, Protocol};
+73 -2
View File
@@ -1204,10 +1204,21 @@ impl NetworkOptions {
} else if let Some(secure_mode) = self.secure_mode
&& secure_mode
{
// CLI key options replace the file's [secure_mode] keypair as a unit;
// without them the keys already loaded from the config file win.
let cli_private_key = self.local_private_key.clone().filter(|k| !k.is_empty());
let cli_public_key = self.local_public_key.clone().filter(|k| !k.is_empty());
let (local_private_key, local_public_key) =
if cli_private_key.is_some() || cli_public_key.is_some() {
(cli_private_key, cli_public_key)
} else {
cfg.get_secure_mode()
.map_or((None, None), |c| (c.local_private_key, c.local_public_key))
};
let c = SecureModeConfig {
enabled: secure_mode,
local_private_key: self.local_private_key.clone(),
local_public_key: self.local_public_key.clone(),
local_private_key,
local_public_key,
};
cfg.set_secure_mode(Some(normalize_secure_mode_config(c)?));
}
@@ -1919,6 +1930,66 @@ enabled = true
assert_eq!(cfg.get_hostname(), "override-host");
}
#[test]
fn secure_mode_cli_flag_preserves_config_file_keypair() {
use base64::{Engine as _, prelude::BASE64_STANDARD};
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
let cfg = TomlConfigLoader::new_from_str(&format!(
r#"
[secure_mode]
enabled = true
local_private_key = "{}"
"#,
BASE64_STANDARD.encode(private.as_bytes())
))
.unwrap();
let file_keypair = cfg.get_secure_mode().unwrap();
NetworkOptions {
secure_mode: Some(true),
..Default::default()
}
.merge_into(&cfg)
.unwrap();
let merged = cfg.get_secure_mode().unwrap();
assert!(merged.enabled);
assert_eq!(merged.local_private_key, file_keypair.local_private_key);
assert_eq!(merged.local_public_key, file_keypair.local_public_key);
assert_eq!(merged.private_key().unwrap().as_bytes(), private.as_bytes());
}
#[test]
fn secure_mode_cli_key_replaces_config_file_keypair() {
use base64::{Engine as _, prelude::BASE64_STANDARD};
let cfg = TomlConfigLoader::new_from_str(
r#"
[secure_mode]
enabled = true
"#,
)
.unwrap();
let cli_private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
NetworkOptions {
secure_mode: Some(true),
local_private_key: Some(BASE64_STANDARD.encode(cli_private.as_bytes())),
..Default::default()
}
.merge_into(&cfg)
.unwrap();
let merged = cfg.get_secure_mode().unwrap();
assert_eq!(
merged.private_key().unwrap().as_bytes(),
cli_private.as_bytes()
);
assert_eq!(
merged.public_key().unwrap().as_bytes(),
x25519_dalek::PublicKey::from(&cli_private).as_bytes()
);
}
#[test]
fn empty_stun_server_options_clear_existing_config() {
let cfg = TomlConfigLoader::new_from_str(