From 15f39de869282a809ef3b10a9a9cc1ca1e2a6433 Mon Sep 17 00:00:00 2001 From: houseme Date: Sun, 26 Jul 2026 10:37:41 +0800 Subject: [PATCH] fix: reject overlapping console listener address Fail startup when the configured console listener resolves to the same listener port covered by the S3 address. This keeps SO_REUSEPORT from hiding an intra-process S3/console endpoint collision. Co-Authored-By: heihutu --- rustfs/src/startup_server.rs | 112 ++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/rustfs/src/startup_server.rs b/rustfs/src/startup_server.rs index d974318db..7bc6ee81d 100644 --- a/rustfs/src/startup_server.rs +++ b/rustfs/src/startup_server.rs @@ -93,6 +93,7 @@ pub(crate) async fn init_startup_listen_context( } let server_addr = parse_and_resolve_address(config.address.as_str()).map_err(Error::other)?; + validate_console_listener_address(config, server_addr)?; let server_port = server_addr.port(); let server_address = server_addr.to_string(); @@ -343,13 +344,43 @@ fn console_http_server_config(config: &Config) -> Option { } } +fn validate_console_listener_address(config: &Config, s3_addr: SocketAddr) -> Result<()> { + if !config.console_enable || config.console_address.trim().is_empty() { + return Ok(()); + } + + let console_addr = parse_and_resolve_address(config.console_address.as_str()) + .map_err(|err| Error::other(format!("console address '{}': {err}", config.console_address)))?; + + if listener_addresses_overlap(s3_addr, console_addr) { + return Err(Error::new( + ErrorKind::AddrInUse, + format!( + "console address '{}' resolves to {console_addr} and conflicts with S3 address '{}' ({s3_addr}); \ + set RUSTFS_CONSOLE_ADDRESS to a different port or disable the console with RUSTFS_CONSOLE_ENABLE=false", + config.console_address, config.address + ), + )); + } + + Ok(()) +} + +fn listener_addresses_overlap(left: SocketAddr, right: SocketAddr) -> bool { + left.port() == right.port() && (left.ip() == right.ip() || left.ip().is_unspecified() || right.ip().is_unspecified()) +} + #[cfg(test)] mod tests { use super::{ DEFAULT_CREDENTIALS_WARNING_MESSAGE, console_http_server_config, find_embedded_available_port, - prepare_embedded_startup_config, s3_http_server_config, + init_startup_listen_context, listener_addresses_overlap, prepare_embedded_startup_config, s3_http_server_config, + validate_console_listener_address, }; use crate::config::Config; + use crate::storage_api::startup::runtime_sources::InstanceContext; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::sync::Arc; #[test] fn s3_http_server_config_disables_console_without_changing_address() { @@ -391,6 +422,85 @@ mod tests { assert!(console_http_server_config(&config).is_none()); } + #[test] + fn validate_console_listener_address_rejects_same_endpoint() { + let mut config = Config::new("127.0.0.1:9001", vec!["/tmp/rustfs-data".to_string()]); + config.console_enable = true; + config.console_address = "127.0.0.1:9001".to_string(); + + let err = validate_console_listener_address(&config, "127.0.0.1:9001".parse().expect("valid socket address")) + .expect_err("console must not bind the S3 endpoint"); + + assert_eq!(err.kind(), std::io::ErrorKind::AddrInUse); + let message = err.to_string(); + assert!(message.contains("RUSTFS_CONSOLE_ADDRESS")); + assert!(message.contains("127.0.0.1:9001")); + } + + #[test] + fn validate_console_listener_address_rejects_wildcard_same_port() { + let mut config = Config::new("127.0.0.1:9001", vec!["/tmp/rustfs-data".to_string()]); + config.console_enable = true; + config.console_address = ":9001".to_string(); + + let err = validate_console_listener_address(&config, "127.0.0.1:9001".parse().expect("valid socket address")) + .expect_err("wildcard console listener covers the S3 endpoint port"); + + assert_eq!(err.kind(), std::io::ErrorKind::AddrInUse); + } + + #[test] + fn validate_console_listener_address_allows_different_ports() { + let mut config = Config::new("127.0.0.1:9000", vec!["/tmp/rustfs-data".to_string()]); + config.console_enable = true; + config.console_address = ":9001".to_string(); + + validate_console_listener_address(&config, "127.0.0.1:9000".parse().expect("valid socket address")) + .expect("different listener ports should be allowed"); + } + + #[test] + fn validate_console_listener_address_skips_disabled_or_empty_console() { + let mut config = Config::new("127.0.0.1:9001", vec!["/tmp/rustfs-data".to_string()]); + config.console_enable = false; + config.console_address = "127.0.0.1:9001".to_string(); + validate_console_listener_address(&config, "127.0.0.1:9001".parse().expect("valid socket address")) + .expect("disabled console should not reserve a listener"); + + config.console_enable = true; + config.console_address.clear(); + validate_console_listener_address(&config, "127.0.0.1:9001".parse().expect("valid socket address")) + .expect("empty console address should skip console listener"); + } + + #[test] + fn listener_addresses_overlap_treats_unspecified_as_covering_the_port() { + assert!(listener_addresses_overlap( + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 9001), + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9001), + )); + assert!(!listener_addresses_overlap( + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 9001), + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9002), + )); + } + + #[tokio::test] + async fn init_startup_listen_context_rejects_console_s3_port_conflict() { + let mut config = Config::new("127.0.0.1:9001", vec!["/tmp/rustfs-data".to_string()]); + config.console_enable = true; + config.console_address = ":9001".to_string(); + let instance_ctx = Arc::new(InstanceContext::new()); + + let err = match init_startup_listen_context(&config, &instance_ctx).await { + Ok(_) => panic!("startup should reject overlapping S3 and console listeners before binding sockets"), + Err(err) => err, + }; + + assert_eq!(err.kind(), std::io::ErrorKind::AddrInUse); + assert!(err.to_string().contains("RUSTFS_CONSOLE_ADDRESS")); + } + #[test] fn is_using_default_credentials_returns_true_for_default_keys() { let mut config = Config::new("127.0.0.1:9000", Vec::new());