From bc87935b91607195c7c6b5e281a78e8b55b7cbc8 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 21 Jul 2026 23:11:03 -0500 Subject: [PATCH] fix(core): bracket IPv6 bind addresses in forward arg and access URL ForwardSpec::parse supports IPv6 binds (e.g. '::1:8080'), but ssh_forward_arg emitted '::1:8080:127.0.0.1:8080', which OpenSSH misparses, and access_url printed the invalid URL 'http://::1:8080/'. Bracket IPv6 literals as OpenSSH and RFC 3986 require, matching the bracketing logic already used by format_gateway_url, and extend the existing forward spec tests with IPv6 cases. Signed-off-by: Andrew White --- crates/openshell-core/src/forward.rs | 32 +++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/openshell-core/src/forward.rs b/crates/openshell-core/src/forward.rs index 70ab74edd0..1b76cb1dc0 100644 --- a/crates/openshell-core/src/forward.rs +++ b/crates/openshell-core/src/forward.rs @@ -580,21 +580,41 @@ impl ForwardSpec { } /// The SSH `-L` local-forward argument: `bind_addr:port:127.0.0.1:port`. + /// IPv6 bind addresses are bracketed as OpenSSH requires (`[::1]:port:...`). pub fn ssh_forward_arg(&self) -> String { - format!("{}:{}:127.0.0.1:{}", self.bind_addr, self.port, self.port) + format!( + "{}:{}:127.0.0.1:{}", + bracket_ipv6(&self.bind_addr), + self.port, + self.port + ) } /// A human-readable URL for the forwarded port. pub fn access_url(&self) -> String { let host = if self.bind_addr == "0.0.0.0" || self.bind_addr == "::" { - "localhost" + "localhost".to_string() } else { - &self.bind_addr + bracket_ipv6(&self.bind_addr) }; format!("http://{host}:{}/", self.port) } } +/// Bracket an IPv6 literal for use in `host:port` contexts (SSH `-L` +/// arguments and URLs); leave other addresses unchanged. +fn bracket_ipv6(addr: &str) -> String { + if addr + .parse::() + .is_ok_and(|ip| ip.is_ipv6()) + && !addr.starts_with('[') + { + format!("[{addr}]") + } else { + addr.to_string() + } +} + impl std::fmt::Display for ForwardSpec { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if self.bind_addr == Self::DEFAULT_BIND_ADDR { @@ -1411,6 +1431,9 @@ mod tests { let spec = ForwardSpec::parse("8080").unwrap(); assert_eq!(spec.ssh_forward_arg(), "127.0.0.1:8080:127.0.0.1:8080"); + + let spec = ForwardSpec::parse("::1:8080").unwrap(); + assert_eq!(spec.ssh_forward_arg(), "[::1]:8080:127.0.0.1:8080"); } #[test] @@ -1661,6 +1684,9 @@ mod tests { let spec = ForwardSpec::parse("0.0.0.0:8080").unwrap(); assert_eq!(spec.access_url(), "http://localhost:8080/"); + + let spec = ForwardSpec::parse("::1:8080").unwrap(); + assert_eq!(spec.access_url(), "http://[::1]:8080/"); } #[test]