mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-02 09:09:17 +00:00
feat(credentials): manage declarative credentials through TOML (#2515)
* feat(credentials): manage declarative credentials through TOML Make managed credentials part of the canonical TOML configuration and load them before peers can authenticate. Reuse ConfigRpc hot patches to durably replace the configured credential set without restarting the instance. Serialize credential mutations so base, managed, and ephemeral keys cannot race into conflicts. Remove the managed overlay file format, digest protocol, capability negotiation, force reconciliation, and database CAS machinery. Redact credential secrets from debug output and management events. Write credential-bearing files atomically with private permissions. * fix(core): release JoinSet reapers with their owners Pass weak task-set references into background reapers so they cannot retain the JoinSet they are meant to collect. This lets stale smoltcp bridge tasks terminate when an IPv4 generation is replaced. Add ownership and TCP generation-replacement regressions covering the production port-forward failure.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use std::{io::Write, path::PathBuf, sync::Arc};
|
||||
|
||||
use atomic_write_file::AtomicWriteFile;
|
||||
use atomic_write_file::{AtomicWriteFile, OpenOptions};
|
||||
use easytier_core::peers::credential_manager::CredentialStorage;
|
||||
|
||||
struct FileCredentialStorage {
|
||||
@@ -9,21 +9,33 @@ struct FileCredentialStorage {
|
||||
|
||||
impl CredentialStorage for FileCredentialStorage {
|
||||
fn load(&self) -> anyhow::Result<Option<String>> {
|
||||
let Ok(serialized) = std::fs::read_to_string(&self.path) else {
|
||||
return Ok(None);
|
||||
let serialized = match std::fs::read_to_string(&self.path) {
|
||||
Ok(serialized) => serialized,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
tracing::info!(path = %self.path.display(), "loaded credentials");
|
||||
Ok(Some(serialized))
|
||||
}
|
||||
|
||||
fn store(&self, serialized_credentials: &str) -> anyhow::Result<()> {
|
||||
let mut file = AtomicWriteFile::open(&self.path)?;
|
||||
let mut file = restricted_atomic_file(&self.path)?;
|
||||
file.write_all(serialized_credentials.as_bytes())?;
|
||||
file.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn restricted_atomic_file(path: &std::path::Path) -> std::io::Result<AtomicWriteFile> {
|
||||
let mut options = OpenOptions::new();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
atomic_write_file::unix::OpenOptionsExt::preserve_mode(&mut options, false);
|
||||
std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
|
||||
}
|
||||
options.open(path)
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_credential_storage(
|
||||
path: Option<PathBuf>,
|
||||
) -> Option<Arc<dyn CredentialStorage>> {
|
||||
@@ -48,5 +60,28 @@ mod tests {
|
||||
storage.load().unwrap().as_deref(),
|
||||
Some("{\"credential\":false}")
|
||||
);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
|
||||
assert_eq!(
|
||||
std::fs::metadata(&storage.path)
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777,
|
||||
0o600
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_storage_reports_read_errors() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let storage = FileCredentialStorage {
|
||||
path: directory.path().to_path_buf(),
|
||||
};
|
||||
|
||||
assert!(storage.load().is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use std::path::Path;
|
||||
use std::{io::Write as _, path::Path};
|
||||
|
||||
use atomic_write_file::{AtomicWriteFile, OpenOptions};
|
||||
|
||||
use easytier_core::management::{ConfigFileControl, ConfigFilePermission, ConfigFileStorage};
|
||||
|
||||
@@ -31,7 +33,20 @@ impl ConfigFileStorage for NativeConfigFileStorage {
|
||||
}
|
||||
|
||||
async fn write(&self, path: &Path, contents: &[u8]) -> anyhow::Result<()> {
|
||||
tokio::fs::write(path, contents).await?;
|
||||
let path = path.to_owned();
|
||||
let contents = contents.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut options = OpenOptions::new();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
atomic_write_file::unix::OpenOptionsExt::preserve_mode(&mut options, false);
|
||||
std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
|
||||
}
|
||||
let mut file: AtomicWriteFile = options.open(path)?;
|
||||
file.write_all(&contents)?;
|
||||
file.commit()
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -40,3 +55,28 @@ impl ConfigFileStorage for NativeConfigFileStorage {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_write_is_atomic_and_private() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("instance.toml");
|
||||
let storage = NativeConfigFileStorage;
|
||||
|
||||
storage.write(&path, b"first").await.unwrap();
|
||||
storage.write(&path, b"second").await.unwrap();
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), "second");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
|
||||
assert_eq!(
|
||||
std::fs::metadata(path).unwrap().permissions().mode() & 0o777,
|
||||
0o600
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ impl TestConfigPatcher {
|
||||
&self,
|
||||
patch: crate::proto::api::config::InstanceConfigPatch,
|
||||
) -> anyhow::Result<()> {
|
||||
easytier_core::management::apply_config_patch(&self.core, patch).await
|
||||
easytier_core::management::apply_config_patch(&self.core, patch, None).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const PUBLIC_SERVER_NETWORK_NAME: &str = "__public_server__";
|
||||
const PUBLIC_SERVER_SHARED_SECRET: &str = "public-server-shared-secret";
|
||||
const NEED_P2P_ADMIN_NETWORK_NAME: &str = "need_p2p_credential_test_network";
|
||||
|
||||
fn generate_credential(
|
||||
async fn generate_credential(
|
||||
admin: &Instance,
|
||||
groups: Vec<String>,
|
||||
allow_relay: bool,
|
||||
@@ -46,9 +46,10 @@ fn generate_credential(
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn generate_credential_with_options(
|
||||
async fn generate_credential_with_options(
|
||||
admin: &Instance,
|
||||
groups: Vec<String>,
|
||||
allow_relay: bool,
|
||||
@@ -184,7 +185,7 @@ async fn create_credential_config(
|
||||
ipv6: &str,
|
||||
) -> TomlConfigLoader {
|
||||
let (_cred_id, cred_secret) =
|
||||
generate_credential(admin_inst, vec![], false, vec![], Duration::from_secs(3600));
|
||||
generate_credential(admin_inst, vec![], false, vec![], Duration::from_secs(3600)).await;
|
||||
|
||||
build_credential_config(
|
||||
admin_inst
|
||||
@@ -481,7 +482,8 @@ async fn credential_peers_p2p_to_need_p2p_admin_through_public_server(
|
||||
Duration::from_secs(3600),
|
||||
Some("credential-peer-a".to_string()),
|
||||
false,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
let (_credential_b_id, credential_b_secret) = generate_credential_with_options(
|
||||
&admin_inst,
|
||||
vec![],
|
||||
@@ -490,7 +492,8 @@ async fn credential_peers_p2p_to_need_p2p_admin_through_public_server(
|
||||
Duration::from_secs(3600),
|
||||
Some("credential-peer-b".to_string()),
|
||||
false,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
admin_inst
|
||||
.get_global_ctx()
|
||||
.issue_event(GlobalCtxEvent::CredentialChanged);
|
||||
@@ -593,7 +596,7 @@ async fn credential_peers_p2p_to_need_p2p_admin_through_public_server(
|
||||
.await;
|
||||
}
|
||||
|
||||
fn create_generated_credential_config(
|
||||
async fn create_generated_credential_config(
|
||||
admin_inst: &Instance,
|
||||
inst_name: &str,
|
||||
ns: Option<&str>,
|
||||
@@ -601,7 +604,7 @@ fn create_generated_credential_config(
|
||||
ipv6: &str,
|
||||
) -> (TomlConfigLoader, String) {
|
||||
let (cred_id, cred_secret) =
|
||||
generate_credential(admin_inst, vec![], false, vec![], Duration::from_secs(3600));
|
||||
generate_credential(admin_inst, vec![], false, vec![], Duration::from_secs(3600)).await;
|
||||
let config = build_credential_config(
|
||||
admin_inst
|
||||
.get_global_ctx()
|
||||
@@ -881,7 +884,8 @@ async fn credential_relay_capability(#[case] allow_relay: bool) {
|
||||
false,
|
||||
vec![],
|
||||
Duration::from_secs(3600),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_cred_b_id, cred_b_secret) = generate_credential(
|
||||
&admin_inst,
|
||||
@@ -889,7 +893,8 @@ async fn credential_relay_capability(#[case] allow_relay: bool) {
|
||||
false,
|
||||
vec![],
|
||||
Duration::from_secs(3600),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_cred_c_id, cred_c_secret) = generate_credential(
|
||||
&admin_inst,
|
||||
@@ -897,7 +902,8 @@ async fn credential_relay_capability(#[case] allow_relay: bool) {
|
||||
allow_relay,
|
||||
vec![],
|
||||
Duration::from_secs(3600),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create credential A on ns_c1
|
||||
let cred_a_config = {
|
||||
@@ -1215,7 +1221,8 @@ async fn credential_revocation_propagates() {
|
||||
false,
|
||||
vec![],
|
||||
Duration::from_secs(3600),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create credential node
|
||||
let cred_config = {
|
||||
@@ -1335,7 +1342,8 @@ async fn credential_non_reusable_allows_only_one_peer() {
|
||||
Duration::from_secs(3600),
|
||||
None,
|
||||
false,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
let network_name = admin_inst
|
||||
.get_global_ctx()
|
||||
@@ -1582,7 +1590,8 @@ async fn credential_unknown_via_shared_rejected(#[values(true, false)] test_revo
|
||||
Some("ns_c2"),
|
||||
"10.144.144.5",
|
||||
"fd00::5/64",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
(config, Some(cred_id))
|
||||
} else {
|
||||
(
|
||||
@@ -1841,7 +1850,8 @@ async fn credential_non_reusable_across_two_admins_allows_only_one_peer() {
|
||||
Duration::from_secs(3600),
|
||||
None,
|
||||
false,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
admin_a_inst
|
||||
.get_global_ctx()
|
||||
.issue_event(GlobalCtxEvent::CredentialChanged);
|
||||
|
||||
@@ -2210,6 +2210,7 @@ pub async fn wireguard_vpn_portal_dynamic_clients() {
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2239,6 +2240,7 @@ pub async fn wireguard_vpn_portal_dynamic_clients() {
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
@@ -2301,6 +2303,7 @@ pub async fn wireguard_vpn_portal_dynamic_clients() {
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user