Implemented AutoTLS via RustTLS

This commit is contained in:
2026-03-10 16:20:19 +00:00
parent 588b610e08
commit bf16ff40f9
7 changed files with 411 additions and 35 deletions

261
Cargo.lock generated
View File

@@ -67,6 +67,45 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 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]] [[package]]
name = "async-lock" name = "async-lock"
version = "3.4.2" version = "3.4.2"
@@ -321,6 +360,22 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.15" version = "0.5.15"
@@ -356,6 +411,26 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.8" version = "0.5.8"
@@ -377,6 +452,17 @@ dependencies = [
"objc2", "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]] [[package]]
name = "dunce" name = "dunce"
version = "1.0.5" version = "1.0.5"
@@ -478,13 +564,19 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"furumi-common", "furumi-common",
"hyper",
"hyper-util",
"moka", "moka",
"prost", "prost",
"rustls",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-rustls",
"tokio-stream", "tokio-stream",
"tonic", "tonic",
"tower 0.5.3",
"tracing", "tracing",
"webpki-roots",
] ]
[[package]] [[package]]
@@ -533,6 +625,7 @@ dependencies = [
"once_cell", "once_cell",
"prometheus", "prometheus",
"prost", "prost",
"rcgen",
"rustls", "rustls",
"tempfile", "tempfile",
"thiserror 2.0.18", "thiserror 2.0.18",
@@ -993,6 +1086,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.1.1" version = "1.1.1"
@@ -1054,6 +1153,16 @@ dependencies = [
"libc", "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]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@@ -1112,6 +1221,15 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" 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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@@ -1124,6 +1242,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]] [[package]]
name = "page_size" name = "page_size"
version = "0.6.0" version = "0.6.0"
@@ -1438,6 +1562,20 @@ dependencies = [
"getrandom 0.2.17", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -1490,6 +1628,15 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.44" version = "0.38.44"
@@ -1525,12 +1672,34 @@ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"log", "log",
"once_cell", "once_cell",
"ring",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki", "rustls-webpki",
"subtle", "subtle",
"zeroize", "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]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.14.0" version = "1.14.0"
@@ -1564,12 +1733,44 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 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]] [[package]]
name = "semver" name = "semver"
version = "1.0.27" version = "1.0.27"
@@ -1749,6 +1950,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 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]] [[package]]
name = "tagptr" name = "tagptr"
version = "0.2.0" version = "0.2.0"
@@ -1876,6 +2088,16 @@ dependencies = [
"syn", "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]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.18" version = "0.1.18"
@@ -1921,8 +2143,11 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project", "pin-project",
"prost", "prost",
"rustls-native-certs",
"rustls-pemfile",
"socket2 0.5.10", "socket2 0.5.10",
"tokio", "tokio",
"tokio-rustls",
"tokio-stream", "tokio-stream",
"tower 0.4.13", "tower 0.4.13",
"tower-layer", "tower-layer",
@@ -2213,6 +2438,15 @@ dependencies = [
"semver", "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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -2411,6 +2645,33 @@ dependencies = [
"wasmparser", "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]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.42" version = "0.8.42"

View File

@@ -1,16 +1,16 @@
# Furumi-ng # 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. Designed for streaming media (video, music) over the network.
## Architecture ## 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-client-core** — cross-platform gRPC client library with attribute caching
- **furumi-mount-linux** — mounts the remote directory locally via FUSE (read-only) - **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 # Build
cargo build --release --workspace cargo build --release --workspace
# Server # Server — auto-generates TLS certificate, saves it for client
./target/release/furumi-server \ ./target/release/furumi-server \
--root /path/to/media \ --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 \ --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 --mount /mnt/remote
# Use it # Use it
@@ -38,6 +38,16 @@ ls /mnt/remote
mpv /mnt/remote/video.mkv 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 ## Configuration
All options can be set via CLI flags or environment variables. 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 | | 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 | | `--root` | `FURUMI_ROOT` | `.` | Directory to expose |
| `--token` | `FURUMI_TOKEN` | *(empty, auth off)* | Bearer token | | `--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 ### Client
| Flag | Env | Default | Description | | 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 | | `--token` | `FURUMI_TOKEN` | *(empty)* | Bearer token |
| `--mount` | `FURUMI_MOUNT` | — | Mount point directory | | `--mount` | `FURUMI_MOUNT` | — | Mount point directory |
| `--tls-ca` | `FURUMI_TLS_CA` | — | Server CA cert PEM file |
## Prometheus Metrics
Available at `http://<metrics-bind>/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 ## Requirements
- Linux with `libfuse3-dev` (for client) - Linux with `libfuse3-dev` and `pkg-config` (for client)
- Rust 2024 edition - Rust 2024 edition
## License ## License

View File

@@ -9,8 +9,14 @@ anyhow = "1.0.102"
prost = "0.13.5" prost = "0.13.5"
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", "tls-native-roots"] }
thiserror = "2.0.18" thiserror = "2.0.18"
moka = { version = "0.12.10", features = ["sync", "future"] } moka = { version = "0.12.10", features = ["sync", "future"] }
async-trait = "0.1.89" async-trait = "0.1.89"
tracing = "0.1.44" 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"] }

View File

@@ -38,15 +38,40 @@ pub struct FurumiClient {
} }
impl FurumiClient { impl FurumiClient {
/// Connects to the Furumi-ng server with an optional bearer token. /// Connects to the Furumi-ng server.
pub async fn connect(addr: &str, token: &str) -> Result<Self> { ///
let endpoint = Endpoint::from_shared(addr.to_string()) /// - `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> {
// 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)))? .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://
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); info!("Connecting to {}", addr);
let channel = endpoint.connect().await?; let channel = endpoint.connect().await?;
@@ -58,7 +83,7 @@ impl FurumiClient {
let attr_cache = Cache::builder() let attr_cache = Cache::builder()
.max_capacity(100_000) .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(); .build();
Ok(Self { client, attr_cache }) Ok(Self { client, attr_cache })

View File

@@ -8,10 +8,10 @@ use std::sync::Arc;
use furumi_client_core::FurumiClient; use furumi_client_core::FurumiClient;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about = "Furumi-ng: mount remote filesystem via encrypted gRPC + FUSE")]
struct Args { struct Args {
/// Server address to connect to /// Server address to connect to (use https:// for encrypted connection)
#[arg(short, long, env = "FURUMI_SERVER", default_value = "http://[::1]:50051")] #[arg(short, long, env = "FURUMI_SERVER", default_value = "https://0.0.0.0:50051")]
server: String, server: String,
/// Authentication Bearer token (leave empty if auth is disabled on server) /// Authentication Bearer token (leave empty if auth is disabled on server)
@@ -21,6 +21,10 @@ 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>> {
@@ -32,6 +36,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Error: Mount point {:?} does not exist or is not a directory", args.mount); eprintln!("Error: Mount point {:?} does not exist or is not a directory", args.mount);
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()
@@ -39,7 +54,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.build()?; .build()?;
let client = rt.block_on(async { 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()); let fuse_fs = fs::FurumiFuse::new(client, rt.handle().clone());

View File

@@ -14,7 +14,7 @@ futures-util = "0.3.32"
jsonwebtoken = "10.3.0" jsonwebtoken = "10.3.0"
libc = "0.2.183" libc = "0.2.183"
prost = "0.13.5" prost = "0.13.5"
rustls = "0.23.37" 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"
@@ -26,6 +26,7 @@ async-trait = "0.1.89"
prometheus = { version = "0.14.0", features = ["process"] } prometheus = { version = "0.14.0", features = ["process"] }
axum = { version = "0.7", features = ["tokio"] } axum = { version = "0.7", features = ["tokio"] }
once_cell = "1.21.3" once_cell = "1.21.3"
rcgen = { version = "0.14.7", features = ["pem"] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.26.0" tempfile = "3.26.0"

View File

@@ -8,17 +8,17 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use axum::{Router, routing::get}; use axum::{Router, routing::get};
use clap::Parser; use clap::Parser;
use tonic::transport::Server; use tonic::transport::{Identity, Server, ServerTlsConfig};
use vfs::local::LocalVfs; use vfs::local::LocalVfs;
use furumi_common::proto::remote_file_system_server::RemoteFileSystemServer; use furumi_common::proto::remote_file_system_server::RemoteFileSystemServer;
use server::RemoteFileSystemImpl; use server::RemoteFileSystemImpl;
use security::AuthInterceptor; use security::AuthInterceptor;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about = "Furumi-ng: remote filesystem server over encrypted gRPC")]
struct Args { struct Args {
/// IP address and port to bind the gRPC server to /// 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, bind: String,
/// Document root directory to expose via VFS /// Document root directory to expose via VFS
@@ -32,6 +32,14 @@ struct Args {
/// IP address and port for the Prometheus metrics HTTP endpoint /// IP address and port for the Prometheus metrics HTTP endpoint
#[arg(long, env = "FURUMI_METRICS_BIND", default_value = "0.0.0.0:9090")] #[arg(long, env = "FURUMI_METRICS_BIND", default_value = "0.0.0.0:9090")]
metrics_bind: String, 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<PathBuf>,
} }
async fn metrics_handler() -> String { async fn metrics_handler() -> String {
@@ -40,12 +48,17 @@ async fn metrics_handler() -> String {
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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(); tracing_subscriber::fmt::init();
let args = Args::parse(); let args = Args::parse();
let addr: SocketAddr = args.bind.parse().unwrap_or_else(|e| { let addr: SocketAddr = args.bind.parse().unwrap_or_else(|e| {
eprintln!("Error: Invalid bind address '{}': {}", args.bind, 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); std::process::exit(1);
}); });
@@ -69,14 +82,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let auth = AuthInterceptor::new(args.token.clone()); let auth = AuthInterceptor::new(args.token.clone());
let svc = RemoteFileSystemServer::with_interceptor(remote_fs, auth.clone()); let svc = RemoteFileSystemServer::with_interceptor(remote_fs, auth.clone());
// Print startup info
println!("Furumi-ng Server listening on {}", addr); 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() { if args.token.is_empty() {
println!("WARNING: Authentication is DISABLED"); println!("WARNING: Authentication is DISABLED");
} else { } else {
println!("Authentication is enabled (Bearer token required)"); println!("Authentication: enabled (Bearer token)");
} }
println!("Document Root: {:?}", root_path); 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 // Spawn the Prometheus metrics HTTP server on a separate port
let metrics_app = Router::new().route("/metrics", get(metrics_handler)); let metrics_app = Router::new().route("/metrics", get(metrics_handler));
@@ -85,14 +104,39 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
axum::serve(metrics_listener, metrics_app).await.unwrap(); axum::serve(metrics_listener, metrics_app).await.unwrap();
}); });
Server::builder() let mut builder = Server::builder()
// Enable TCP Keep-Alive and HTTP2 Ping to keep connections alive for long media streams
.tcp_keepalive(Some(std::time::Duration::from_secs(60))) .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) .add_service(svc)
.serve(addr) .serve(addr)
.await?; .await?;
Ok(()) Ok(())
} }