dap: Support IPv6 addresses in TCP transport (#52244)

The DAP TCP transport layer was hardcoded to `Ipv4Addr`, so IPv6
addresses like `fd00::a` in a debug config's `connect.host` always
failed with `hostname must be IPv4: invalid IPv4 address syntax`.

Replaced `Ipv4Addr` with `IpAddr` and `SocketAddrV4` with `SocketAddr`
across the `task`, `dap`, `dap_adapters`, and `project` crates. The WASM
extension API still uses `u32` for the host field to avoid a breaking
WIT interface change; IPv4 round-trips through extensions as before.

Fixes #52237

Release Notes:

- Fixed DAP TCP transport rejecting IPv6 addresses when connecting to
remote debug adapters.

---------

Co-authored-by: moktamd <moktamd@users.noreply.github.com>
This commit is contained in:
moktamd 2026-04-27 17:42:45 +09:00 committed by GitHub
parent d7d1b54044
commit 5aa7eaa508
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 42 additions and 31 deletions

View file

@ -18,7 +18,7 @@ use std::{
borrow::Borrow, borrow::Borrow,
ffi::OsStr, ffi::OsStr,
fmt::Debug, fmt::Debug,
net::Ipv4Addr, net::IpAddr,
ops::Deref, ops::Deref,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@ -106,7 +106,7 @@ impl<'a> From<&'a str> for DebugAdapterName {
#[derive(Debug, Clone, PartialEq, Serialize)] #[derive(Debug, Clone, PartialEq, Serialize)]
pub struct TcpArguments { pub struct TcpArguments {
pub host: Ipv4Addr, pub host: IpAddr,
pub port: u16, pub port: u16,
pub timeout: Option<u64>, pub timeout: Option<u64>,
} }

View file

@ -6,7 +6,7 @@ pub mod proto_conversions;
mod registry; mod registry;
pub mod transport; pub mod transport;
use std::net::Ipv4Addr; use std::net::IpAddr;
pub use dap_types::*; pub use dap_types::*;
use debugger_settings::DebuggerSettings; use debugger_settings::DebuggerSettings;
@ -26,7 +26,7 @@ use task::{DebugScenario, TcpArgumentsTemplate};
pub async fn configure_tcp_connection( pub async fn configure_tcp_connection(
tcp_connection: TcpArgumentsTemplate, tcp_connection: TcpArgumentsTemplate,
) -> anyhow::Result<(Ipv4Addr, u16, Option<u64>)> { ) -> anyhow::Result<(IpAddr, u16, Option<u64>)> {
let host = tcp_connection.host(); let host = tcp_connection.host();
let timeout = tcp_connection.timeout; let timeout = tcp_connection.timeout;

View file

@ -18,7 +18,7 @@ use smol::{
}; };
use std::{ use std::{
collections::HashMap, collections::HashMap,
net::{Ipv4Addr, SocketAddrV4}, net::{IpAddr, SocketAddr},
process::Stdio, process::Stdio,
sync::Arc, sync::Arc,
time::Duration, time::Duration,
@ -472,7 +472,7 @@ impl TransportDelegate {
pub struct TcpTransport { pub struct TcpTransport {
executor: BackgroundExecutor, executor: BackgroundExecutor,
pub port: u16, pub port: u16,
pub host: Ipv4Addr, pub host: IpAddr,
pub timeout: u64, pub timeout: u64,
process: Arc<Mutex<Option<Child>>>, process: Arc<Mutex<Option<Child>>>,
_stderr_task: Option<Task<()>>, _stderr_task: Option<Task<()>>,
@ -489,8 +489,8 @@ impl TcpTransport {
} }
} }
pub async fn unused_port(host: Ipv4Addr) -> Result<u16> { pub async fn unused_port(host: IpAddr) -> Result<u16> {
Ok(TcpListener::bind(SocketAddrV4::new(host, 0)) Ok(TcpListener::bind(SocketAddr::new(host, 0))
.await? .await?
.local_addr()? .local_addr()?
.port()) .port())
@ -598,7 +598,7 @@ impl Transport for TcpTransport {
> { > {
let executor = self.executor.clone(); let executor = self.executor.clone();
let timeout = self.timeout; let timeout = self.timeout;
let address = SocketAddrV4::new(self.host, self.port); let address = SocketAddr::new(self.host, self.port);
let process = self.process.clone(); let process = self.process.clone();
executor.clone().spawn(async move { executor.clone().spawn(async move {
select! { select! {

View file

@ -14,7 +14,7 @@ use smol::fs::File;
use smol::io::AsyncReadExt; use smol::io::AsyncReadExt;
use smol::lock::OnceCell; use smol::lock::OnceCell;
use std::ffi::OsString; use std::ffi::OsString;
use std::net::Ipv4Addr; use std::net::IpAddr;
use std::str::FromStr; use std::str::FromStr;
use std::{ use std::{
ffi::OsStr, ffi::OsStr,
@ -42,7 +42,7 @@ impl PythonDebugAdapter {
const LANGUAGE_NAME: &'static str = "Python"; const LANGUAGE_NAME: &'static str = "Python";
async fn generate_debugpy_arguments<'a>( async fn generate_debugpy_arguments<'a>(
host: &'a Ipv4Addr, host: &'a IpAddr,
port: u16, port: u16,
launch_mode: DebugpyLaunchMode<'a>, launch_mode: DebugpyLaunchMode<'a>,
user_installed_path: Option<&'a Path>, user_installed_path: Option<&'a Path>,
@ -380,7 +380,7 @@ impl PythonDebugAdapter {
} }
if let Some(hostname) = config_host { if let Some(hostname) = config_host {
tcp_connection.host = Some(hostname.parse().context("hostname must be IPv4")?); tcp_connection.host = Some(hostname.parse().context("invalid IP address")?);
} }
tcp_connection.port = config_port; tcp_connection.port = config_port;
DebugpyLaunchMode::AttachWithConnect { host: config_host } DebugpyLaunchMode::AttachWithConnect { host: config_host }
@ -974,7 +974,7 @@ mod tests {
.contains("Cannot have two different ports") .contains("Cannot have two different ports")
); );
let host = Ipv4Addr::new(127, 0, 0, 1); let host = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
let config_with_host_conflict = json!({ let config_with_host_conflict = json!({
"request": "attach", "request": "attach",
"connect": { "connect": {
@ -1018,7 +1018,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_attach_with_connect_mode_generates_correct_arguments() { async fn test_attach_with_connect_mode_generates_correct_arguments() {
let host = Ipv4Addr::new(127, 0, 0, 1); let host = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
let port = 5678; let port = 5678;
let args_without_host = PythonDebugAdapter::generate_debugpy_arguments( let args_without_host = PythonDebugAdapter::generate_debugpy_arguments(
@ -1071,7 +1071,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_debugpy_install_path_cases() { async fn test_debugpy_install_path_cases() {
let host = Ipv4Addr::new(127, 0, 0, 1); let host = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
let port = 5678; let port = 5678;
// Case 1: User-defined debugpy path (highest precedence) // Case 1: User-defined debugpy path (highest precedence)

View file

@ -24,7 +24,7 @@ use project::project_settings::ProjectSettings;
use semver::Version; use semver::Version;
use std::{ use std::{
env, env,
net::Ipv4Addr, net::{IpAddr, Ipv4Addr},
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
@ -117,7 +117,7 @@ impl TryFrom<StartDebuggingRequestArguments> for extension::StartDebuggingReques
impl From<TcpArguments> for extension::TcpArguments { impl From<TcpArguments> for extension::TcpArguments {
fn from(value: TcpArguments) -> Self { fn from(value: TcpArguments) -> Self {
Self { Self {
host: value.host.into(), host: IpAddr::V4(Ipv4Addr::from_bits(value.host)),
port: value.port, port: value.port,
timeout: value.timeout, timeout: value.timeout,
} }
@ -127,7 +127,10 @@ impl From<TcpArguments> for extension::TcpArguments {
impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate { impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
fn from(value: extension::TcpArgumentsTemplate) -> Self { fn from(value: extension::TcpArgumentsTemplate) -> Self {
Self { Self {
host: value.host.map(Ipv4Addr::to_bits), host: value.host.and_then(|addr| match addr {
IpAddr::V4(v4) => Some(v4.to_bits()),
IpAddr::V6(_) => None,
}),
port: value.port, port: value.port,
timeout: value.timeout, timeout: value.timeout,
} }
@ -137,7 +140,7 @@ impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
impl From<TcpArgumentsTemplate> for extension::TcpArgumentsTemplate { impl From<TcpArgumentsTemplate> for extension::TcpArgumentsTemplate {
fn from(value: TcpArgumentsTemplate) -> Self { fn from(value: TcpArgumentsTemplate) -> Self {
Self { Self {
host: value.host.map(Ipv4Addr::from_bits), host: value.host.map(|bits| IpAddr::V4(Ipv4Addr::from_bits(bits))),
port: value.port, port: value.port,
timeout: value.timeout, timeout: value.timeout,
} }
@ -904,13 +907,21 @@ impl dap::Host for WasmState {
let (host, port, timeout) = let (host, port, timeout) =
::dap::configure_tcp_connection(task::TcpArgumentsTemplate { ::dap::configure_tcp_connection(task::TcpArgumentsTemplate {
port: template.port, port: template.port,
host: template.host.map(Ipv4Addr::from_bits), host: template
.host
.map(|bits| IpAddr::V4(Ipv4Addr::from_bits(bits))),
timeout: template.timeout, timeout: template.timeout,
}) })
.await?; .await?;
let host_bits = match host {
IpAddr::V4(v4) => v4.to_bits(),
IpAddr::V6(_) => {
anyhow::bail!("IPv6 addresses are not supported in the extension API")
}
};
Ok(TcpArguments { Ok(TcpArguments {
port, port,
host: host.to_bits(), host: host_bits,
timeout, timeout,
}) })
}) })

View file

@ -47,7 +47,7 @@ use std::{
borrow::Borrow, borrow::Borrow,
collections::BTreeMap, collections::BTreeMap,
ffi::OsStr, ffi::OsStr,
net::Ipv4Addr, net::{IpAddr, Ipv4Addr},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Once}, sync::{Arc, Once},
}; };
@ -323,7 +323,7 @@ impl DapStore {
let port_forwarding; let port_forwarding;
let connection; let connection;
if let Some(c) = binary.connection { if let Some(c) = binary.connection {
let host = Ipv4Addr::LOCALHOST; let host = IpAddr::V4(Ipv4Addr::LOCALHOST);
let port; let port;
if remote.read_with(cx, |remote, _cx| remote.shares_network_interface()) { if remote.read_with(cx, |remote, _cx| remote.shares_network_interface()) {
port = c.port; port = c.port;

View file

@ -48,7 +48,7 @@ use serde_json::Value;
use smol::net::{TcpListener, TcpStream}; use smol::net::{TcpListener, TcpStream};
use std::any::TypeId; use std::any::TypeId;
use std::collections::{BTreeMap, VecDeque}; use std::collections::{BTreeMap, VecDeque};
use std::net::Ipv4Addr; use std::net::{IpAddr, Ipv4Addr};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
@ -2901,7 +2901,7 @@ impl Session {
); );
None None
} else { } else {
let port = TcpTransport::unused_port(Ipv4Addr::LOCALHOST) let port = TcpTransport::unused_port(IpAddr::V4(Ipv4Addr::LOCALHOST))
.await .await
.context("getting port for DAP")?; .context("getting port for DAP")?;
request request

View file

@ -4,7 +4,7 @@ use gpui::SharedString;
use log as _; use log as _;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr; use std::net::IpAddr;
use std::path::PathBuf; use std::path::PathBuf;
use util::{debug_panic, schemars::add_new_subschema}; use util::{debug_panic, schemars::add_new_subschema};
@ -20,7 +20,7 @@ pub struct TcpArgumentsTemplate {
/// The host that the debug adapter is listening too /// The host that the debug adapter is listening too
/// ///
/// Default: 127.0.0.1 /// Default: 127.0.0.1
pub host: Option<Ipv4Addr>, pub host: Option<IpAddr>,
/// The max amount of time in milliseconds to connect to a tcp DAP before returning an error /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error
/// ///
/// Default: 2000ms /// Default: 2000ms
@ -29,8 +29,9 @@ pub struct TcpArgumentsTemplate {
impl TcpArgumentsTemplate { impl TcpArgumentsTemplate {
/// Get the host or fallback to the default host /// Get the host or fallback to the default host
pub fn host(&self) -> Ipv4Addr { pub fn host(&self) -> IpAddr {
self.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)) self.host
.unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
} }
pub fn from_proto(proto: proto::TcpHost) -> Result<Self> { pub fn from_proto(proto: proto::TcpHost) -> Result<Self> {
@ -389,8 +390,7 @@ impl DebugTaskFile {
}, },
"host": { "host": {
"type": "string", "type": "string",
"pattern": "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$", "description": "The host that the debug adapter is listening to, as an IPv4 or IPv6 address (default: 127.0.0.1)"
"description": "The host that the debug adapter is listening to (default: 127.0.0.1)"
}, },
"timeout": { "timeout": {
"type": "integer", "type": "integer",