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 <heihutu@gmail.com>
This commit is contained in:
houseme
2026-07-26 10:37:41 +08:00
parent 92f72c3912
commit 15f39de869
+111 -1
View File
@@ -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<Config> {
}
}
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());