FIX: AutoTLS via RustTLS
This commit is contained in:
@@ -4,14 +4,21 @@ use furumi_common::proto::{
|
|||||||
PathRequest, ReadRequest,
|
PathRequest, ReadRequest,
|
||||||
};
|
};
|
||||||
use moka::future::Cache;
|
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 std::time::Duration;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tonic::codegen::InterceptedService;
|
use tonic::codegen::InterceptedService;
|
||||||
use tonic::metadata::MetadataValue;
|
use tonic::metadata::MetadataValue;
|
||||||
use tonic::transport::{Channel, Endpoint};
|
use tonic::transport::{Channel, Endpoint, Uri};
|
||||||
use tonic::{Request, Status};
|
use tonic::{Request, Status};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
// ── Auth interceptor ───────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AuthInterceptor {
|
pub struct AuthInterceptor {
|
||||||
token: String,
|
token: String,
|
||||||
@@ -31,6 +38,91 @@ impl tonic::service::Interceptor for AuthInterceptor {
|
|||||||
|
|
||||||
pub type GrpcClient = RemoteFileSystemClient<InterceptedService<Channel, 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)]
|
#[derive(Clone)]
|
||||||
pub struct FurumiClient {
|
pub struct FurumiClient {
|
||||||
client: GrpcClient,
|
client: GrpcClient,
|
||||||
@@ -40,40 +132,48 @@ pub struct FurumiClient {
|
|||||||
impl FurumiClient {
|
impl FurumiClient {
|
||||||
/// Connects to the Furumi-ng server.
|
/// Connects to the Furumi-ng server.
|
||||||
///
|
///
|
||||||
/// - `addr`: Server URL. Use `https://` for TLS, `http://` for plaintext.
|
/// - Use `https://` for encrypted connection (accepts any server certificate)
|
||||||
/// - `token`: Bearer token for auth (empty = no auth).
|
/// - Use `http://` for plaintext (not recommended)
|
||||||
/// - `tls_ca_pem`: Optional CA certificate PEM bytes for verifying the server.
|
///
|
||||||
/// If using TLS without a CA cert, pass `None` (uses system roots).
|
/// No certificate files needed — TLS provides encryption only.
|
||||||
pub async fn connect(addr: &str, token: &str, tls_ca_pem: Option<Vec<u8>>) -> Result<Self> {
|
pub async fn connect(addr: &str, token: &str) -> Result<Self> {
|
||||||
// Install ring as the default crypto provider for rustls
|
// Install ring as the default crypto provider for rustls
|
||||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
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)))?
|
.map_err(|e| ClientError::Internal(format!("Invalid URI: {}", e)))?
|
||||||
.timeout(Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.concurrency_limit(256)
|
.concurrency_limit(256)
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60)))
|
.tcp_keepalive(Some(Duration::from_secs(60)))
|
||||||
.http2_keep_alive_interval(Duration::from_secs(60));
|
.http2_keep_alive_interval(Duration::from_secs(60));
|
||||||
|
|
||||||
// Configure TLS if using https://
|
let channel = if is_https {
|
||||||
if addr.starts_with("https://") {
|
info!("TLS enabled (encryption only, certificate verification disabled)");
|
||||||
let mut tls_config = tonic::transport::ClientTlsConfig::new()
|
|
||||||
.domain_name("furumi-ng");
|
|
||||||
|
|
||||||
if let Some(ca_pem) = tls_ca_pem {
|
let crypto = rustls::ClientConfig::builder()
|
||||||
info!("TLS enabled with server CA certificate");
|
.dangerous()
|
||||||
tls_config = tls_config
|
.with_custom_certificate_verifier(Arc::new(NoVerifier))
|
||||||
.ca_certificate(tonic::transport::Certificate::from_pem(ca_pem));
|
.with_no_client_auth();
|
||||||
} else {
|
|
||||||
info!("TLS enabled with system root certificates");
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint = endpoint.tls_config(tls_config)
|
let connector = InsecureTlsConnector {
|
||||||
.map_err(|e| ClientError::Internal(format!("TLS config error: {}", e)))?;
|
tls: tokio_rustls::TlsConnector::from(Arc::new(crypto)),
|
||||||
}
|
};
|
||||||
|
|
||||||
info!("Connecting to {}", addr);
|
endpoint.connect_with_connector(connector).await?
|
||||||
let channel = endpoint.connect().await?;
|
} else {
|
||||||
|
info!("Connecting without TLS (plaintext)");
|
||||||
|
endpoint.connect().await?
|
||||||
|
};
|
||||||
|
|
||||||
let interceptor = AuthInterceptor {
|
let interceptor = AuthInterceptor {
|
||||||
token: token.to_string(),
|
token: token.to_string(),
|
||||||
@@ -102,9 +202,7 @@ impl FurumiClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let response = client.get_attr(req).await?.into_inner();
|
let response = client.get_attr(req).await?.into_inner();
|
||||||
|
|
||||||
self.attr_cache.insert(path.to_string(), response.clone()).await;
|
self.attr_cache.insert(path.to_string(), response.clone()).await;
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +225,7 @@ impl FurumiClient {
|
|||||||
Ok(entries)
|
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(
|
pub async fn read_file(
|
||||||
&self,
|
&self,
|
||||||
path: &str,
|
path: &str,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use furumi_client_core::FurumiClient;
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about = "Furumi-ng: mount remote filesystem via encrypted gRPC + FUSE")]
|
#[command(version, about = "Furumi-ng: mount remote filesystem via encrypted gRPC + FUSE")]
|
||||||
struct Args {
|
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")]
|
#[arg(short, long, env = "FURUMI_SERVER", default_value = "https://0.0.0.0:50051")]
|
||||||
server: String,
|
server: String,
|
||||||
|
|
||||||
@@ -21,10 +21,6 @@ struct Args {
|
|||||||
/// Mount point directory
|
/// Mount point directory
|
||||||
#[arg(short, long, env = "FURUMI_MOUNT")]
|
#[arg(short, long, env = "FURUMI_MOUNT")]
|
||||||
mount: PathBuf,
|
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>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -37,24 +33,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
std::process::exit(1);
|
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
|
// Create a robust tokio runtime for the background gRPC work
|
||||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let client = rt.block_on(async {
|
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());
|
let fuse_fs = fs::FurumiFuse::new(client, rt.handle().clone());
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ rustls = { version = "0.23.37", features = ["ring"] }
|
|||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
tokio-stream = "0.1.18"
|
tokio-stream = "0.1.18"
|
||||||
tonic = "0.12.3"
|
tonic = { version = "0.12.3", features = ["tls"] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||||
async-stream = "0.3.6"
|
async-stream = "0.3.6"
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ struct Args {
|
|||||||
/// Disable TLS encryption (not recommended, use only for debugging)
|
/// Disable TLS encryption (not recommended, use only for debugging)
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_tls: bool,
|
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 {
|
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 cert_pem = cert_pair.cert.pem();
|
||||||
let key_pem = cert_pair.signing_key.serialize_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 identity = Identity::from_pem(cert_pem, key_pem);
|
||||||
let tls_config = ServerTlsConfig::new().identity(identity);
|
let tls_config = ServerTlsConfig::new().identity(identity);
|
||||||
builder = builder.tls_config(tls_config)?;
|
builder = builder.tls_config(tls_config)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user