feat: Implement SSH master connector and CLI for certificate management

This commit is contained in:
GW_MC
2026-03-21 03:07:58 +00:00
parent 2fcdc7d0df
commit 1a453a7e5c
8 changed files with 687 additions and 3 deletions

View File

@@ -0,0 +1,132 @@
use std::{fs::File, io::Read, sync::Arc};
use tokio::{fs::read, sync::Mutex};
use nxmesh_proto::agent_service_client::AgentServiceClient;
use tonic::transport::{Certificate, ClientTlsConfig, Identity};
use tracing::warn;
use crate::config::settings::{self, MAuthSettings, TLSSettings};
use super::{AgentClient, MasterConnectorTrait};
pub struct SshMasterConnector {
client: Arc<Mutex<AgentClient>>,
}
impl SshMasterConnector {
pub async fn new(
settings: crate::config::settings::GrpcSettings,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let tls_config = Self::generate_tls_config(&settings.m_auth).await?;
// Create a gRPC channel
let endpoint = tonic::transport::Channel::from_shared(settings.connection_string.clone())
.map_err(|e| format!("Failed to create gRPC endpoint: {}", e))?
.tls_config(tls_config)
.map_err(|e| {
format!(
"Failed to set TLS config: {}. Ensure TLS settings and certificates are correct.",
e
)
})?
.connect_timeout(std::time::Duration::from_secs(5))
.timeout(std::time::Duration::from_secs(10))
.connect_lazy();
// Create the gRPC client
let client = Arc::new(Mutex::new(AgentServiceClient::new(endpoint)));
Ok(Self { client })
}
async fn generate_tls_config(
settings: &MAuthSettings,
) -> Result<ClientTlsConfig, Box<dyn std::error::Error + Send + Sync>> {
let tls_config = match &settings {
MAuthSettings::Tls(tls_settings) => {
let (ca, cert, key) = match tls_settings {
TLSSettings::RawPath {
ca_path,
cert_path,
key_path,
} => {
// Read the certificate and key from the specified file paths
let ca = read(ca_path).await?;
let cert = read(cert_path).await?;
let key = read(key_path).await?;
(ca, cert, key)
}
TLSSettings::ZipPath { cert_zip_path } => {
// Extract the certificate and key from the zip file
Self::extract_certificate(cert_zip_path).await?
}
};
// TODO: allow skipping SANs validation if specified in the settings
ClientTlsConfig::new()
.ca_certificate(Certificate::from_pem(&ca))
.identity(Identity::from_pem(&cert, &key))
}
#[allow(unreachable_patterns)]
_ => {
return Err("TLS settings are required for SSH connection".into());
}
};
Ok(tls_config)
}
async fn extract_certificate(
cert_zip_path: &str,
) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), Box<dyn std::error::Error + Send + Sync>> {
// unzip the file and extract the cert, ca and key
let file = File::open(cert_zip_path)?;
let mut archive = zip::ZipArchive::new(file)?;
let mut cert = Vec::new();
let mut key = Vec::new();
let mut ca = Vec::new();
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = match file.enclosed_name() {
Some(path) => path.to_owned(),
None => continue,
};
let file_name = outpath
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
if file_name != "cert.pem" && file_name != "key.pem" && file_name != "ca.pem" {
warn!("Unexpected file in certificate zip: {}", file_name);
continue;
}
if file_name == "cert.pem" {
file.read_to_end(&mut cert)?;
} else if file_name == "key.pem" {
file.read_to_end(&mut key)?;
} else if file_name == "ca.pem" {
file.read_to_end(&mut ca)?;
}
}
if cert.is_empty() || key.is_empty() || ca.is_empty() {
return Err("Certificate zip must contain cert.pem, key.pem and ca.pem".into());
}
Ok((ca, cert, key))
}
}
#[async_trait::async_trait]
impl MasterConnectorTrait for SshMasterConnector {
async fn connect(
&mut self,
_settings: &crate::config::settings::Settings,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// ensure connection if required
Ok(())
}
fn get_client(&self) -> Arc<Mutex<AgentClient>> {
self.client.clone()
}
}