Skip to content

Commit e8ee0eb

Browse files
hyperpolymathclaude
andcommitted
fix(verisim-api): bracket IPv6 hosts in socket addresses
The HTTP/gRPC/TLS servers each did `format!("{}:{}", host, port)` to build a socket address string, which produces invalid output when host is an IPv6 literal without brackets. With VERISIM_HOST=:: and VERISIM_GRPC_PORT=50051 the gRPC server emitted "::50051" and SocketAddr::parse rejected it with "invalid socket address syntax", crashing the process. This is a real problem on fly.io where apps default to ::-binding to register with the 6PN network. Fix: add fmt_socket_addr(host, port) helper that wraps the host in brackets per RFC 3986 §3.2.2 when it contains ':' and isn't already bracketed. Use it for all three bind sites (HTTP, gRPC, TLS). Tests: 4 new unit tests covering IPv4 passthrough, IPv6 bracket-insertion, pre-bracketed passthrough, and a SocketAddr::parse roundtrip against "::", "::1", "0.0.0.0", "127.0.0.1". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 16ed608 commit e8ee0eb

1 file changed

Lines changed: 45 additions & 3 deletions

File tree

  • rust-core/verisim-api/src

rust-core/verisim-api/src/lib.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,18 @@ fn validate_limit(limit: usize) -> usize {
213213
limit.min(MAX_RESULT_LIMIT)
214214
}
215215

216+
/// Format a host+port into a socket address string that `SocketAddr::parse`
217+
/// accepts. IPv6 hosts (containing `:` and not already bracketed) are
218+
/// wrapped in brackets per RFC 3986 §3.2.2, so that `"::"` + `8080`
219+
/// becomes `"[::]:8080"` rather than the ambiguous `"::8080"`.
220+
fn fmt_socket_addr(host: &str, port: u16) -> String {
221+
if host.contains(':') && !host.starts_with('[') {
222+
format!("[{host}]:{port}")
223+
} else {
224+
format!("{host}:{port}")
225+
}
226+
}
227+
216228
/// Validate a octad ID: max 128 chars, alphanumeric + dash + underscore only.
217229
fn validate_octad_id(id: &str) -> Result<(), ApiError> {
218230
if id.is_empty() {
@@ -2043,15 +2055,15 @@ pub async fn serve(config: ApiConfig) -> Result<(), std::io::Error> {
20432055
.layer(axum::extract::DefaultBodyLimit::max(config.max_body_size));
20442056

20452057
// Start HTTP server
2046-
let http_addr = format!("{}:{}", config.host, config.port);
2058+
let http_addr = fmt_socket_addr(&config.host, config.port);
20472059
info!(addr = %http_addr, "Starting VeriSimDB HTTP server");
20482060
let listener = TcpListener::bind(&http_addr).await?;
20492061

20502062
let http_server = axum::serve(listener, app).with_graceful_shutdown(shutdown_signal());
20512063

20522064
// Start gRPC server (if enabled)
20532065
if config.grpc_port > 0 {
2054-
let grpc_addr = format!("{}:{}", config.host, config.grpc_port);
2066+
let grpc_addr = fmt_socket_addr(&config.host, config.grpc_port);
20552067
info!(addr = %grpc_addr, "Starting VeriSimDB gRPC server");
20562068

20572069
let grpc_router = grpc::build_grpc_router(state);
@@ -2126,7 +2138,7 @@ pub async fn serve_tls(
21262138
let octad_store = state.octad_store.clone();
21272139
let app = build_router(state);
21282140

2129-
let addr = format!("{}:{}", config.host, config.port);
2141+
let addr = fmt_socket_addr(&config.host, config.port);
21302142
info!(addr = %addr, cert = %cert_path, "Starting VeriSimDB API server with TLS");
21312143

21322144
let tls_config = RustlsConfig::from_pem_file(cert_path, key_path)
@@ -2638,6 +2650,36 @@ mod tests {
26382650
// Additional unit tests — API endpoints and validation
26392651
// -----------------------------------------------------------------------
26402652

2653+
#[test]
2654+
fn test_fmt_socket_addr_ipv4() {
2655+
assert_eq!(fmt_socket_addr("0.0.0.0", 8080), "0.0.0.0:8080");
2656+
assert_eq!(fmt_socket_addr("127.0.0.1", 80), "127.0.0.1:80");
2657+
}
2658+
2659+
#[test]
2660+
fn test_fmt_socket_addr_ipv6_unbracketed_gets_brackets() {
2661+
assert_eq!(fmt_socket_addr("::", 8080), "[::]:8080");
2662+
assert_eq!(fmt_socket_addr("::1", 22), "[::1]:22");
2663+
assert_eq!(fmt_socket_addr("2001:db8::1", 443), "[2001:db8::1]:443");
2664+
}
2665+
2666+
#[test]
2667+
fn test_fmt_socket_addr_ipv6_prebracketed_passes_through() {
2668+
assert_eq!(fmt_socket_addr("[::]", 8080), "[::]:8080");
2669+
assert_eq!(fmt_socket_addr("[::1]", 22), "[::1]:22");
2670+
}
2671+
2672+
#[test]
2673+
fn test_fmt_socket_addr_parses_roundtrip() {
2674+
use std::net::SocketAddr;
2675+
let cases = [("::", 8080u16), ("::1", 22u16), ("0.0.0.0", 80u16), ("127.0.0.1", 3000u16)];
2676+
for (host, port) in cases {
2677+
let s = fmt_socket_addr(host, port);
2678+
let parsed: SocketAddr = s.parse().unwrap_or_else(|e| panic!("parse {s}: {e}"));
2679+
assert_eq!(parsed.port(), port, "port mismatch for host={host}");
2680+
}
2681+
}
2682+
26412683
#[test]
26422684
fn test_validate_limit_caps_at_max() {
26432685
assert_eq!(validate_limit(5000), MAX_RESULT_LIMIT);

0 commit comments

Comments
 (0)