From 2fcdc7d0dfbec4bc54b23f6f605086be63e512fd Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:06:56 +0000 Subject: [PATCH] feat: Add SSH authentication interceptor and update proto definitions --- crates/nxmesh-proto/Cargo.toml | 12 +++ crates/nxmesh-proto/proto/agent.proto | 102 ++++------------------- crates/nxmesh-proto/src/auth/mod.rs | 1 + crates/nxmesh-proto/src/auth/ssh_auth.rs | 49 +++++++++++ crates/nxmesh-proto/src/lib.rs | 4 + 5 files changed, 82 insertions(+), 86 deletions(-) create mode 100644 crates/nxmesh-proto/src/auth/mod.rs create mode 100644 crates/nxmesh-proto/src/auth/ssh_auth.rs diff --git a/crates/nxmesh-proto/Cargo.toml b/crates/nxmesh-proto/Cargo.toml index 77b586b..2503d86 100644 --- a/crates/nxmesh-proto/Cargo.toml +++ b/crates/nxmesh-proto/Cargo.toml @@ -10,6 +10,18 @@ rust-version.workspace = true [dependencies] tonic.workspace = true prost.workspace = true +tonic-prost.workspace = true +tonic-async-interceptor = { workspace = true, optional = true } + +# allow user to specify tonic server or client +[features] +default = ["server", "client"] +server = [ + "tonic/server", + "tonic/tls-native-roots", + "dep:tonic-async-interceptor", +] +client = [] [build-dependencies] tonic-prost-build.workspace = true diff --git a/crates/nxmesh-proto/proto/agent.proto b/crates/nxmesh-proto/proto/agent.proto index 891a227..ed42b96 100644 --- a/crates/nxmesh-proto/proto/agent.proto +++ b/crates/nxmesh-proto/proto/agent.proto @@ -3,6 +3,9 @@ package nxmesh.agent.v1; option go_package = "github.com/nxmesh/api/agent/v1"; +// For all file paths in this proto, we use forward slashes ("/") as the separator, even on Windows. This is because gRPC and protobuf are designed to be cross-platform and forward slashes are universally accepted as path separators in URLs and many programming languages. Using forward slashes ensures consistency and avoids issues with escaping backslashes on different platforms. +// All file paths MUST be relative paths from other config files, e.g. "site.conf", "private/example.com.conf". Absolute paths or path traversal above the config directory should be rejected by the agent for security reasons. The config files must live within the generated config directory, e.g. "/etc/nginx/conf-/site.conf". This allows the agent to manage the lifecycle of config files, e.g. cleanup old configs after successful apply. + // AgentService defines the bidirectional communication between master and agents service AgentService { // Stream establishes a persistent connection for real-time communication @@ -43,13 +46,12 @@ message MasterMessage { // Registration message RegistrationRequest { - string token = 1; - string hostname = 2; - string ip_address = 3; - string version = 4; - repeated string capabilities = 5; - map labels = 6; - DeploymentMode deployment_mode = 7; + string hostname = 1; + string ip_address = 2; + string version = 3; + repeated string capabilities = 4; + map labels = 5; + DeploymentMode deployment_mode = 6; } message RegistrationResponse { @@ -104,94 +106,22 @@ message Alert { message ConfigUpdate { string config_id = 1; int64 version = 2; - repeated VirtualHost virtual_hosts = 3; - repeated Upstream upstreams = 4; - map certificates = 5; - GlobalSettings global_settings = 6; + repeated ConfigContent configs = 3; + repeated CertificateContent certificates = 4; } -message VirtualHost { - string id = 1; - string name = 2; - string server_name = 3; - uint32 listen_port = 4; - bool ssl_enabled = 5; - string ssl_certificate_id = 6; - bool http2_enabled = 7; - bool http3_enabled = 8; - repeated Location locations = 9; - map custom_directives = 10; -} - -message Location { +message ConfigContent { + // relative path from other config files, e.g. "site.conf", "private/example.com.conf" string path = 1; - string proxy_pass = 2; - string upstream_id = 3; - string root = 4; - string index = 5; - repeated Header custom_headers = 6; - repeated RewriteRule rewrite_rules = 7; - map custom_directives = 8; + string content = 2; } -message Header { - string name = 1; - string value = 2; - bool always = 3; -} - -message RewriteRule { - string pattern = 1; - string replacement = 2; - string flag = 3; -} - -message Upstream { +message CertificateContent { string id = 1; - string name = 2; - LoadBalanceAlgorithm algorithm = 3; - repeated UpstreamServer servers = 4; - HealthCheckConfig health_check = 5; - uint32 keepalive_connections = 6; -} - -enum LoadBalanceAlgorithm { - LOAD_BALANCE_ALGORITHM_UNSPECIFIED = 0; - ROUND_ROBIN = 1; - LEAST_CONNECTIONS = 2; - IP_HASH = 3; - WEIGHTED_ROUND_ROBIN = 4; -} - -message UpstreamServer { - string address = 1; - uint32 weight = 2; - bool backup = 3; - bool down = 4; - uint32 max_fails = 5; - uint32 fail_timeout_seconds = 6; -} - -message HealthCheckConfig { - bool enabled = 1; + // relative path from other config files, e.g. "certs/example.com.pem" string path = 2; - uint32 interval_seconds = 3; - uint32 timeout_seconds = 4; - uint32 healthy_threshold = 5; - uint32 unhealthy_threshold = 6; -} - -message Certificate { - string id = 1; - string domain = 2; string certificate_pem = 3; string private_key_pem = 4; - int64 expires_at = 5; -} - -message GlobalSettings { - map nginx_directives = 1; - map env_vars = 2; } message ConfigStatus { diff --git a/crates/nxmesh-proto/src/auth/mod.rs b/crates/nxmesh-proto/src/auth/mod.rs new file mode 100644 index 0000000..88b2f38 --- /dev/null +++ b/crates/nxmesh-proto/src/auth/mod.rs @@ -0,0 +1 @@ +pub mod ssh_auth; diff --git a/crates/nxmesh-proto/src/auth/ssh_auth.rs b/crates/nxmesh-proto/src/auth/ssh_auth.rs new file mode 100644 index 0000000..a3c74ac --- /dev/null +++ b/crates/nxmesh-proto/src/auth/ssh_auth.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; + +use tonic::{Request, Status, async_trait, transport::CertificateDer}; +use tonic_async_interceptor::{AsyncInterceptor, AsyncInterceptorLayer, async_interceptor}; + +pub fn create_ssh_auth_interceptor( + certificate_provider: Arc, +) -> AsyncInterceptorLayer { + async_interceptor(SshAuthInterceptor::new(certificate_provider)) +} + +#[derive(Clone)] +pub struct SshAuthInterceptor { + certificate_provider: Arc, +} + +#[async_trait] +pub trait CertificateValidationProvider: Send + Sync { + async fn is_authorized(&self, certs: &Arc>>) -> Result; +} + +impl AsyncInterceptor for SshAuthInterceptor { + type Future = + std::pin::Pin, Status>> + Send>>; + fn call(&mut self, req: Request<()>) -> Self::Future { + let this = self.clone(); + Box::pin(async move { this.authenticate(req).await }) + } +} + +impl SshAuthInterceptor { + pub fn new(certificate_provider: Arc) -> Self { + SshAuthInterceptor { + certificate_provider, + } + } + + async fn authenticate(&self, req: Request<()>) -> Result, Status> { + let certs = req.peer_certs().ok_or(Status::unauthenticated("No cert"))?; + + let is_authorized = self.certificate_provider.is_authorized(&certs).await?; + + if is_authorized { + Ok(req) + } else { + Err(Status::permission_denied("Blocked")) + } + } +} diff --git a/crates/nxmesh-proto/src/lib.rs b/crates/nxmesh-proto/src/lib.rs index e8da2fb..37c6dd2 100644 --- a/crates/nxmesh-proto/src/lib.rs +++ b/crates/nxmesh-proto/src/lib.rs @@ -1,9 +1,13 @@ //! NxMesh Protocol Buffers //! //! This crate contains the gRPC protocol definitions for master-agent communication. +#![forbid(clippy::unwrap_used, clippy::panic, unsafe_code)] +#![deny(clippy::expect_used)] pub mod agent { tonic::include_proto!("nxmesh.agent.v1"); } pub use agent::*; +pub mod auth; +pub use tonic_async_interceptor::*;