diff --git a/Cargo.lock b/Cargo.lock index fb6cdef..7675767 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,45 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-lock" version = "3.4.2" @@ -321,6 +360,22 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -356,6 +411,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -377,6 +452,17 @@ dependencies = [ "objc2", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dunce" version = "1.0.5" @@ -478,13 +564,19 @@ dependencies = [ "anyhow", "async-trait", "furumi-common", + "hyper", + "hyper-util", "moka", "prost", + "rustls", "thiserror 2.0.18", "tokio", + "tokio-rustls", "tokio-stream", "tonic", + "tower 0.5.3", "tracing", + "webpki-roots", ] [[package]] @@ -533,6 +625,7 @@ dependencies = [ "once_cell", "prometheus", "prost", + "rcgen", "rustls", "tempfile", "thiserror 2.0.18", @@ -993,6 +1086,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.1.1" @@ -1054,6 +1153,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1112,6 +1221,15 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1124,6 +1242,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "page_size" version = "0.6.0" @@ -1438,6 +1562,20 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1490,6 +1628,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1525,12 +1672,34 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -1564,12 +1733,44 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -1749,6 +1950,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -1876,6 +2088,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -1921,8 +2143,11 @@ dependencies = [ "percent-encoding", "pin-project", "prost", + "rustls-native-certs", + "rustls-pemfile", "socket2 0.5.10", "tokio", + "tokio-rustls", "tokio-stream", "tower 0.4.13", "tower-layer", @@ -2213,6 +2438,15 @@ dependencies = [ "semver", ] +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2411,6 +2645,33 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "zerocopy" version = "0.8.42" diff --git a/README.md b/README.md index dd7fff1..9795639 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Furumi-ng -Remote filesystem over gRPC. Mount a directory from a remote server as a local folder via FUSE. +Remote filesystem over encrypted gRPC. Mount a directory from a remote server as a local folder via FUSE. Designed for streaming media (video, music) over the network. ## Architecture ``` -furumi-server (gRPC) ←→ furumi-client-core (library) ←→ furumi-mount-linux (FUSE) +furumi-server (gRPC + TLS) ←→ furumi-client-core ←→ furumi-mount-linux (FUSE) ``` -- **furumi-server** — exposes a directory over gRPC with optional Bearer token auth and Prometheus metrics +- **furumi-server** — exposes a directory over gRPC with auto-TLS, Bearer token auth, and Prometheus metrics - **furumi-client-core** — cross-platform gRPC client library with attribute caching - **furumi-mount-linux** — mounts the remote directory locally via FUSE (read-only) @@ -20,17 +20,17 @@ furumi-server (gRPC) ←→ furumi-client-core (library) ←→ furumi-mount # Build cargo build --release --workspace -# Server +# Server — auto-generates TLS certificate, saves it for client ./target/release/furumi-server \ --root /path/to/media \ - --bind 0.0.0.0:50051 \ - --token mysecrettoken - -# Client (on another machine) -mkdir -p /mnt/remote -./target/release/furumi-mount-linux \ - --server http://server-ip:50051 \ --token mysecrettoken \ + --tls-cert-out /tmp/furumi-ca.pem + +# Client — loads the server's certificate for encrypted connection +./target/release/furumi-mount-linux \ + --server https://server-ip:50051 \ + --token mysecrettoken \ + --tls-ca /tmp/furumi-ca.pem \ --mount /mnt/remote # Use it @@ -38,6 +38,16 @@ ls /mnt/remote mpv /mnt/remote/video.mkv ``` +## Encryption + +TLS is enabled by default. The server auto-generates a self-signed certificate on each start — no manual cert management required. The certificate is used **only for encryption**, not for server identity verification. + +To pass the certificate to the client: +1. Server: `--tls-cert-out /path/to/cert.pem` saves the generated cert +2. Client: `--tls-ca /path/to/cert.pem` loads it for the TLS handshake + +To disable TLS (not recommended): `--no-tls` on the server, and use `http://` on the client. + ## Configuration All options can be set via CLI flags or environment variables. @@ -46,22 +56,36 @@ All options can be set via CLI flags or environment variables. | Flag | Env | Default | Description | |------|-----|---------|-------------| -| `--bind` | `FURUMI_BIND` | `[::1]:50051` | gRPC listen address | +| `--bind` | `FURUMI_BIND` | `0.0.0.0:50051` | gRPC listen address | | `--root` | `FURUMI_ROOT` | `.` | Directory to expose | | `--token` | `FURUMI_TOKEN` | *(empty, auth off)* | Bearer token | -| `--metrics-bind` | `FURUMI_METRICS_BIND` | `0.0.0.0:9090` | Prometheus metrics endpoint | +| `--metrics-bind` | `FURUMI_METRICS_BIND` | `0.0.0.0:9090` | Prometheus endpoint | +| `--tls-cert-out` | `FURUMI_TLS_CERT_OUT` | — | Save auto-generated cert PEM | +| `--no-tls` | — | `false` | Disable TLS | ### Client | Flag | Env | Default | Description | |------|-----|---------|-------------| -| `--server` | `FURUMI_SERVER` | `http://[::1]:50051` | Server address | +| `--server` | `FURUMI_SERVER` | `https://0.0.0.0:50051` | Server address | | `--token` | `FURUMI_TOKEN` | *(empty)* | Bearer token | | `--mount` | `FURUMI_MOUNT` | — | Mount point directory | +| `--tls-ca` | `FURUMI_TLS_CA` | — | Server CA cert PEM file | + +## Prometheus Metrics + +Available at `http:///metrics`: + +- `furumi_grpc_requests_total` — request count by method and status +- `furumi_grpc_request_duration_seconds` — request latency histogram +- `furumi_bytes_read_total` — total bytes streamed +- `furumi_active_streams` — current streaming connections +- `furumi_file_open_errors_total` — file access errors +- `furumi_auth_failures_total` — authentication failures ## Requirements -- Linux with `libfuse3-dev` (for client) +- Linux with `libfuse3-dev` and `pkg-config` (for client) - Rust 2024 edition ## License diff --git a/furumi-client-core/Cargo.toml b/furumi-client-core/Cargo.toml index f6864a0..39c6509 100644 --- a/furumi-client-core/Cargo.toml +++ b/furumi-client-core/Cargo.toml @@ -9,8 +9,14 @@ anyhow = "1.0.102" prost = "0.13.5" tokio = { version = "1.50.0", features = ["full"] } tokio-stream = "0.1.18" -tonic = "0.12.3" +tonic = { version = "0.12.3", features = ["tls", "tls-native-roots"] } thiserror = "2.0.18" moka = { version = "0.12.10", features = ["sync", "future"] } async-trait = "0.1.89" tracing = "0.1.44" +rustls = { version = "0.23.37", features = ["ring"] } +tokio-rustls = "0.26.4" +webpki-roots = "1.0.6" +hyper-util = { version = "0.1.20", features = ["tokio"] } +hyper = { version = "1.8.1", features = ["client"] } +tower = { version = "0.5.3", features = ["util"] } diff --git a/furumi-client-core/src/client.rs b/furumi-client-core/src/client.rs index 27e3fc7..c27a819 100644 --- a/furumi-client-core/src/client.rs +++ b/furumi-client-core/src/client.rs @@ -38,15 +38,40 @@ pub struct FurumiClient { } impl FurumiClient { - /// Connects to the Furumi-ng server with an optional bearer token. - pub async fn connect(addr: &str, token: &str) -> Result { - let endpoint = Endpoint::from_shared(addr.to_string()) + /// 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 { + // 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()) .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)))?; + } + info!("Connecting to {}", addr); let channel = endpoint.connect().await?; @@ -58,7 +83,7 @@ impl FurumiClient { let attr_cache = Cache::builder() .max_capacity(100_000) - .time_to_live(Duration::from_secs(5)) // short TTL to catch up changes quickly + .time_to_live(Duration::from_secs(5)) .build(); Ok(Self { client, attr_cache }) diff --git a/furumi-mount-linux/src/main.rs b/furumi-mount-linux/src/main.rs index 7d14622..6d2550c 100644 --- a/furumi-mount-linux/src/main.rs +++ b/furumi-mount-linux/src/main.rs @@ -8,10 +8,10 @@ use std::sync::Arc; use furumi_client_core::FurumiClient; #[derive(Parser, Debug)] -#[command(version, about, long_about = None)] +#[command(version, about = "Furumi-ng: mount remote filesystem via encrypted gRPC + FUSE")] struct Args { - /// Server address to connect to - #[arg(short, long, env = "FURUMI_SERVER", default_value = "http://[::1]:50051")] + /// Server address to connect to (use https:// for encrypted connection) + #[arg(short, long, env = "FURUMI_SERVER", default_value = "https://0.0.0.0:50051")] server: String, /// Authentication Bearer token (leave empty if auth is disabled on server) @@ -21,6 +21,10 @@ 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> { @@ -32,6 +36,17 @@ 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() @@ -39,7 +54,7 @@ fn main() -> Result<(), Box> { .build()?; let client = rt.block_on(async { - FurumiClient::connect(&args.server, &args.token).await + FurumiClient::connect(&args.server, &args.token, tls_ca_pem).await })?; let fuse_fs = fs::FurumiFuse::new(client, rt.handle().clone()); diff --git a/furumi-server/Cargo.toml b/furumi-server/Cargo.toml index 3ad3cbd..92808ec 100644 --- a/furumi-server/Cargo.toml +++ b/furumi-server/Cargo.toml @@ -14,7 +14,7 @@ futures-util = "0.3.32" jsonwebtoken = "10.3.0" libc = "0.2.183" prost = "0.13.5" -rustls = "0.23.37" +rustls = { version = "0.23.37", features = ["ring"] } thiserror = "2.0.18" tokio = { version = "1.50.0", features = ["full"] } tokio-stream = "0.1.18" @@ -26,6 +26,7 @@ async-trait = "0.1.89" prometheus = { version = "0.14.0", features = ["process"] } axum = { version = "0.7", features = ["tokio"] } once_cell = "1.21.3" +rcgen = { version = "0.14.7", features = ["pem"] } [dev-dependencies] tempfile = "3.26.0" diff --git a/furumi-server/src/main.rs b/furumi-server/src/main.rs index 5525b88..78fc422 100644 --- a/furumi-server/src/main.rs +++ b/furumi-server/src/main.rs @@ -8,17 +8,17 @@ use std::path::PathBuf; use std::sync::Arc; use axum::{Router, routing::get}; use clap::Parser; -use tonic::transport::Server; +use tonic::transport::{Identity, Server, ServerTlsConfig}; use vfs::local::LocalVfs; use furumi_common::proto::remote_file_system_server::RemoteFileSystemServer; use server::RemoteFileSystemImpl; use security::AuthInterceptor; #[derive(Parser, Debug)] -#[command(version, about, long_about = None)] +#[command(version, about = "Furumi-ng: remote filesystem server over encrypted gRPC")] struct Args { /// IP address and port to bind the gRPC server to - #[arg(short, long, env = "FURUMI_BIND", default_value = "[::1]:50051")] + #[arg(short, long, env = "FURUMI_BIND", default_value = "0.0.0.0:50051")] bind: String, /// Document root directory to expose via VFS @@ -32,6 +32,14 @@ struct Args { /// IP address and port for the Prometheus metrics HTTP endpoint #[arg(long, env = "FURUMI_METRICS_BIND", default_value = "0.0.0.0:9090")] metrics_bind: String, + + /// 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 { @@ -40,12 +48,17 @@ async fn metrics_handler() -> String { #[tokio::main] async fn main() -> Result<(), Box> { + // Install ring as the default crypto provider for rustls (must be before any TLS usage) + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rustls crypto provider"); + tracing_subscriber::fmt::init(); let args = Args::parse(); let addr: SocketAddr = args.bind.parse().unwrap_or_else(|e| { eprintln!("Error: Invalid bind address '{}': {}", args.bind, e); - eprintln!(" Expected format: IP:PORT (e.g. 0.0.0.0:50051 or [::1]:50051)"); + eprintln!(" Expected format: IP:PORT (e.g. 0.0.0.0:50051)"); std::process::exit(1); }); @@ -69,14 +82,20 @@ async fn main() -> Result<(), Box> { let auth = AuthInterceptor::new(args.token.clone()); let svc = RemoteFileSystemServer::with_interceptor(remote_fs, auth.clone()); + // Print startup info println!("Furumi-ng Server listening on {}", addr); + if args.no_tls { + println!("WARNING: TLS is DISABLED — traffic is unencrypted"); + } else { + println!("TLS: enabled (auto-generated self-signed certificate)"); + } if args.token.is_empty() { println!("WARNING: Authentication is DISABLED"); } else { - println!("Authentication is enabled (Bearer token required)"); + println!("Authentication: enabled (Bearer token)"); } println!("Document Root: {:?}", root_path); - println!("Metrics endpoint: http://{}/metrics", metrics_addr); + println!("Metrics: http://{}/metrics", metrics_addr); // Spawn the Prometheus metrics HTTP server on a separate port let metrics_app = Router::new().route("/metrics", get(metrics_handler)); @@ -85,14 +104,39 @@ async fn main() -> Result<(), Box> { axum::serve(metrics_listener, metrics_app).await.unwrap(); }); - Server::builder() - // Enable TCP Keep-Alive and HTTP2 Ping to keep connections alive for long media streams + let mut builder = Server::builder() .tcp_keepalive(Some(std::time::Duration::from_secs(60))) - .http2_keepalive_interval(Some(std::time::Duration::from_secs(60))) + .http2_keepalive_interval(Some(std::time::Duration::from_secs(60))); + + if !args.no_tls { + let cert_pair = rcgen::generate_simple_self_signed(vec![ + "localhost".to_string(), + "furumi-ng".to_string(), + ]) + .expect("Failed to generate self-signed certificate"); + + 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)?; + } + + builder .add_service(svc) .serve(addr) .await?; Ok(()) } -