diff --git a/furumi-client-core/src/client.rs b/furumi-client-core/src/client.rs index c27a819..48b6c39 100644 --- a/furumi-client-core/src/client.rs +++ b/furumi-client-core/src/client.rs @@ -4,14 +4,21 @@ use furumi_common::proto::{ PathRequest, ReadRequest, }; use moka::future::Cache; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; use std::time::Duration; +use tokio::net::TcpStream; use tokio_stream::StreamExt; use tonic::codegen::InterceptedService; use tonic::metadata::MetadataValue; -use tonic::transport::{Channel, Endpoint}; +use tonic::transport::{Channel, Endpoint, Uri}; use tonic::{Request, Status}; use tracing::{debug, info}; +// ── Auth interceptor ─────────────────────────────────────────── + #[derive(Clone)] pub struct AuthInterceptor { token: String, @@ -31,6 +38,91 @@ impl tonic::service::Interceptor for AuthInterceptor { pub type GrpcClient = RemoteFileSystemClient>; +// ── TLS: accept any certificate (encryption only) ────────────── + +/// A rustls verifier that accepts any server certificate without validation. +/// Provides encryption without certificate trust chain verification. +#[derive(Debug)] +struct NoVerifier; + +impl rustls::client::danger::ServerCertVerifier for NoVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> std::result::Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + rustls::crypto::ring::default_provider() + .signature_verification_algorithms + .supported_schemes() + } +} + +/// Custom tower connector: TCP + TLS with NoVerifier, compatible with tonic's +/// `connect_with_connector` API. Bypasses tonic's built-in TLS which doesn't +/// support skip-verify in v0.12. +#[derive(Clone)] +struct InsecureTlsConnector { + tls: tokio_rustls::TlsConnector, +} + +impl tower::Service for InsecureTlsConnector { + type Response = hyper_util::rt::TokioIo>; + type Error = Box; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, uri: Uri) -> Self::Future { + let tls = self.tls.clone(); + Box::pin(async move { + let host = uri.host().unwrap_or("127.0.0.1").to_string(); + let port = uri.port_u16().unwrap_or(50051); + let addr = format!("{}:{}", host, port); + + let tcp = TcpStream::connect(&addr).await?; + + // Parse the server name — works with both hostnames and IP addresses + let server_name = rustls::pki_types::ServerName::try_from(host.as_str()) + .unwrap_or_else(|_| { + rustls::pki_types::ServerName::try_from("furumi-ng") + .expect("static string is valid") + }); + + let tls_stream = tls.connect(server_name.to_owned(), tcp).await?; + Ok(hyper_util::rt::TokioIo::new(tls_stream)) + }) + } +} + +// ── Client ────────────────────────────────────────────────────── + #[derive(Clone)] pub struct FurumiClient { client: GrpcClient, @@ -40,47 +132,55 @@ pub struct FurumiClient { impl FurumiClient { /// Connects to the Furumi-ng server. /// - /// - `addr`: Server URL. Use `https://` for TLS, `http://` for plaintext. - /// - `token`: Bearer token for auth (empty = no auth). - /// - `tls_ca_pem`: Optional CA certificate PEM bytes for verifying the server. - /// If using TLS without a CA cert, pass `None` (uses system roots). - pub async fn connect(addr: &str, token: &str, tls_ca_pem: Option>) -> Result { + /// - Use `https://` for encrypted connection (accepts any server certificate) + /// - Use `http://` for plaintext (not recommended) + /// + /// No certificate files needed — TLS provides encryption only. + pub async fn connect(addr: &str, token: &str) -> Result { // Install ring as the default crypto provider for rustls let _ = rustls::crypto::ring::default_provider().install_default(); - - let mut endpoint = Endpoint::from_shared(addr.to_string()) + + let is_https = addr.starts_with("https://"); + let tonic_addr = if is_https { + // Trick tonic into thinking this is a plaintext connection so it doesn't + // error out with HttpsUriWithoutTlsSupport. Our custom connector will + // handle the actual TLS wrapping. + addr.replacen("https://", "http://", 1) + } else { + addr.to_string() + }; + + let endpoint = Endpoint::from_shared(tonic_addr) .map_err(|e| ClientError::Internal(format!("Invalid URI: {}", e)))? .timeout(Duration::from_secs(30)) .concurrency_limit(256) .tcp_keepalive(Some(Duration::from_secs(60))) .http2_keep_alive_interval(Duration::from_secs(60)); - // Configure TLS if using https:// - if addr.starts_with("https://") { - let mut tls_config = tonic::transport::ClientTlsConfig::new() - .domain_name("furumi-ng"); - - if let Some(ca_pem) = tls_ca_pem { - info!("TLS enabled with server CA certificate"); - tls_config = tls_config - .ca_certificate(tonic::transport::Certificate::from_pem(ca_pem)); - } else { - info!("TLS enabled with system root certificates"); - } - - endpoint = endpoint.tls_config(tls_config) - .map_err(|e| ClientError::Internal(format!("TLS config error: {}", e)))?; - } + let channel = if is_https { + info!("TLS enabled (encryption only, certificate verification disabled)"); + + let crypto = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerifier)) + .with_no_client_auth(); + + let connector = InsecureTlsConnector { + tls: tokio_rustls::TlsConnector::from(Arc::new(crypto)), + }; + + endpoint.connect_with_connector(connector).await? + } else { + info!("Connecting without TLS (plaintext)"); + endpoint.connect().await? + }; - info!("Connecting to {}", addr); - let channel = endpoint.connect().await?; - let interceptor = AuthInterceptor { token: token.to_string(), }; let client = RemoteFileSystemClient::with_interceptor(channel, interceptor); - + let attr_cache = Cache::builder() .max_capacity(100_000) .time_to_live(Duration::from_secs(5)) @@ -102,9 +202,7 @@ impl FurumiClient { }); let response = client.get_attr(req).await?.into_inner(); - self.attr_cache.insert(path.to_string(), response.clone()).await; - Ok(response) } @@ -127,7 +225,7 @@ impl FurumiClient { Ok(entries) } - /// Fetches file chunk stream from the server. Returns the streaming receiver. + /// Fetches file chunk stream from the server. pub async fn read_file( &self, path: &str, diff --git a/furumi-mount-linux/src/main.rs b/furumi-mount-linux/src/main.rs index 6d2550c..8d4abbe 100644 --- a/furumi-mount-linux/src/main.rs +++ b/furumi-mount-linux/src/main.rs @@ -10,7 +10,7 @@ use furumi_client_core::FurumiClient; #[derive(Parser, Debug)] #[command(version, about = "Furumi-ng: mount remote filesystem via encrypted gRPC + FUSE")] struct Args { - /// Server address to connect to (use https:// for encrypted connection) + /// Server address (use https:// for encrypted, http:// for plaintext) #[arg(short, long, env = "FURUMI_SERVER", default_value = "https://0.0.0.0:50051")] server: String, @@ -21,10 +21,6 @@ struct Args { /// Mount point directory #[arg(short, long, env = "FURUMI_MOUNT")] mount: PathBuf, - - /// Path to server's TLS CA certificate PEM file (required for https:// connections) - #[arg(long, env = "FURUMI_TLS_CA")] - tls_ca: Option, } fn main() -> Result<(), Box> { @@ -36,17 +32,6 @@ fn main() -> Result<(), Box> { eprintln!("Error: Mount point {:?} does not exist or is not a directory", args.mount); std::process::exit(1); } - - // Load TLS CA certificate if provided - let tls_ca_pem = if let Some(ref ca_path) = args.tls_ca { - let pem = std::fs::read(ca_path).unwrap_or_else(|e| { - eprintln!("Error: Failed to read TLS CA cert {:?}: {}", ca_path, e); - std::process::exit(1); - }); - Some(pem) - } else { - None - }; // Create a robust tokio runtime for the background gRPC work let rt = tokio::runtime::Builder::new_multi_thread() @@ -54,7 +39,7 @@ fn main() -> Result<(), Box> { .build()?; let client = rt.block_on(async { - FurumiClient::connect(&args.server, &args.token, tls_ca_pem).await + FurumiClient::connect(&args.server, &args.token).await })?; let fuse_fs = fs::FurumiFuse::new(client, rt.handle().clone()); diff --git a/furumi-server/Cargo.toml b/furumi-server/Cargo.toml index 92808ec..8bbd61b 100644 --- a/furumi-server/Cargo.toml +++ b/furumi-server/Cargo.toml @@ -18,7 +18,7 @@ rustls = { version = "0.23.37", features = ["ring"] } thiserror = "2.0.18" tokio = { version = "1.50.0", features = ["full"] } tokio-stream = "0.1.18" -tonic = "0.12.3" +tonic = { version = "0.12.3", features = ["tls"] } tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } async-stream = "0.3.6" diff --git a/furumi-server/src/main.rs b/furumi-server/src/main.rs index 78fc422..d595757 100644 --- a/furumi-server/src/main.rs +++ b/furumi-server/src/main.rs @@ -36,10 +36,6 @@ struct Args { /// Disable TLS encryption (not recommended, use only for debugging) #[arg(long, default_value_t = false)] no_tls: bool, - - /// Save the auto-generated TLS certificate to this file (for client --tls-ca) - #[arg(long, env = "FURUMI_TLS_CERT_OUT")] - tls_cert_out: Option, } async fn metrics_handler() -> String { @@ -118,16 +114,6 @@ async fn main() -> Result<(), Box> { let cert_pem = cert_pair.cert.pem(); let key_pem = cert_pair.signing_key.serialize_pem(); - // Optionally save the certificate PEM for the client - if let Some(ref cert_path) = args.tls_cert_out { - std::fs::write(cert_path, &cert_pem) - .unwrap_or_else(|e| { - eprintln!("Error: Failed to write TLS cert to {:?}: {}", cert_path, e); - std::process::exit(1); - }); - println!("TLS certificate saved to {:?} (use with client --tls-ca)", cert_path); - } - let identity = Identity::from_pem(cert_pem, key_pem); let tls_config = ServerTlsConfig::new().identity(identity); builder = builder.tls_config(tls_config)?;