FIX: AutoTLS via RustTLS

This commit is contained in:
2026-03-10 16:30:45 +00:00
parent bf16ff40f9
commit 67547d677c
4 changed files with 132 additions and 63 deletions

View File

@@ -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<InterceptedService<Channel, AuthInterceptor>>;
// ── 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<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
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<Uri> for InsecureTlsConnector {
type Response = hyper_util::rt::TokioIo<tokio_rustls::client::TlsStream<TcpStream>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<Box<dyn Future<Output = std::result::Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
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<Vec<u8>>) -> Result<Self> {
/// - 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<Self> {
// 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,

View File

@@ -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<PathBuf>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -36,17 +32,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
.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());

View File

@@ -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"

View File

@@ -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<PathBuf>,
}
async fn metrics_handler() -> String {
@@ -118,16 +114,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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)?;