From 815c9df613888dafad20b2d34e91ab06dce2ec26 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Thu, 14 May 2026 20:11:12 +0200 Subject: [PATCH] feat(server): add support of IPv6 (no dual stack) --- docs/config/env.md | 2 +- example.env | 1 + src/main.rs | 82 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 64 insertions(+), 21 deletions(-) diff --git a/docs/config/env.md b/docs/config/env.md index 158d55c5..5888f511 100644 --- a/docs/config/env.md +++ b/docs/config/env.md @@ -9,7 +9,7 @@ Most runtime variables use the `OXICLOUD_` prefix. A few build-time or allocator | `OXICLOUD_STORAGE_PATH` | `./storage` | Root storage directory | | `OXICLOUD_STATIC_PATH` | `./static` | Static files directory | | `OXICLOUD_SERVER_PORT` | `8086` | Server port | -| `OXICLOUD_SERVER_HOST` | `127.0.0.1` | Server bind address | +| `OXICLOUD_SERVER_HOST` | `127.0.0.1` | Server bind addressi (IPv4 or IPv6 allowed) | | `OXICLOUD_BASE_URL` | (auto) | Public base URL for share links; defaults to `http://{host}:{port}` | | `OXICLOUD_MAX_UPLOAD_SIZE` | `10737418240` | Maximum upload size in bytes (10 GB on 64-bit, 1 GB on 32-bit) | diff --git a/example.env b/example.env index f330abf8..7a83a0e8 100644 --- a/example.env +++ b/example.env @@ -22,6 +22,7 @@ OXICLOUD_SERVER_PORT=8086 # Server bind address (default: 127.0.0.1) # Use 0.0.0.0 to bind to all interfaces in Docker +# IPv6 format allowed, either ::1 or [::1] OXICLOUD_SERVER_HOST=127.0.0.1 # Public base URL for generating share links and external URLs diff --git a/src/main.rs b/src/main.rs index bfef6ae8..7a68011a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,65 @@ use interfaces::{ create_api_routes, create_health_routes, create_public_api_routes, web::create_web_routes, }; +fn parse_addr(host: &str, port: u16) -> Result { + // Strip surrounding brackets from IPv6: [::1] -> ::1 + let host = host.trim(); + let host = host + .strip_prefix('[') + .and_then(|h| h.strip_suffix(']')) + .unwrap_or(host); + + // Try parsing as IPv6 first, then IPv4 + // and format the address string accordingly + // - IPv6: "[::1]:8080" + // - IPv4: "127.0.0.1:8080" + let addr_str = if host.contains(':') { + format!("[{host}]:{port}") // IPv6 + } else { + format!("{host}:{port}") // IPv4 + }; + + addr_str + .parse::() + .map_err(|e| format!("Invalid address '{}': {}", addr_str, e)) +} + +fn make_socket(addr: &SocketAddr) -> std::io::Result { + let domain = if addr.is_ipv6() { + Domain::IPV6 + } else { + Domain::IPV4 + }; + let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?; + socket.set_reuse_address(true)?; + // Allow multiple workers on the same port (future-ready) + #[cfg(not(windows))] + socket.set_reuse_port(true)?; + // Disable Nagle's algorithm — send small responses (JSON, PROPFIND) + // immediately instead of waiting up to 40ms for coalescing. + socket.set_tcp_nodelay(true)?; + // Detect dead connections within 60s instead of hours + socket.set_keepalive(true)?; + socket.set_tcp_keepalive( + &TcpKeepalive::new() + .with_time(Duration::from_secs(60)) + .with_interval(Duration::from_secs(10)), + )?; + socket.set_nonblocking(true)?; + + // For IPv6: disable dual-stack to be explicit about what you're binding + // (set true to restrict to IPv6-only, false to also accept IPv4-mapped) + if addr.is_ipv6() { + socket.set_only_v6(true)?; // explicit: one socket = one protocol + } + + socket.bind(&(*addr).into())?; + // High backlog for connection bursts (WebDAV clients open many parallel connections) + socket.listen(2048)?; + + Ok(socket) +} + #[tokio::main] async fn main() -> Result<(), Box> { // Load .env file if present (for local development) @@ -503,28 +562,11 @@ async fn main() -> Result<(), Box> { } // Start server — tuned socket for low-latency responses - let addr = SocketAddr::from(([0, 0, 0, 0], config.server_port)); + // TODO: suport multiple addresses ? + let addr = parse_addr(&config.server_host, config.server_port)?; tracing::info!("Starting OxiCloud server on http://{}", addr); - let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?; - socket.set_reuse_address(true)?; - // Allow multiple workers on the same port (future-ready) - #[cfg(not(windows))] - socket.set_reuse_port(true)?; - // Disable Nagle's algorithm — send small responses (JSON, PROPFIND) - // immediately instead of waiting up to 40ms for coalescing. - socket.set_tcp_nodelay(true)?; - // Detect dead connections within 60s instead of hours - socket.set_keepalive(true)?; - socket.set_tcp_keepalive( - &TcpKeepalive::new() - .with_time(Duration::from_secs(60)) - .with_interval(Duration::from_secs(10)), - )?; - socket.set_nonblocking(true)?; - socket.bind(&addr.into())?; - // High backlog for connection bursts (WebDAV clients open many parallel connections) - socket.listen(2048)?; + let socket = make_socket(&addr)?; let listener = tokio::net::TcpListener::from_std(socket.into())?;