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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ use smol::fs::File;
use smol::io::AsyncReadExt;
use smol::lock::OnceCell;
use std::ffi::OsString;
use std::net::Ipv4Addr;
use std::net::IpAddr;
use std::str::FromStr;
use std::{
ffi::OsStr,
@ -42,7 +42,7 @@ impl PythonDebugAdapter {
const LANGUAGE_NAME: &'static str = "Python";
async fn generate_debugpy_arguments<'a>(
host: &'a Ipv4Addr,
host: &'a IpAddr,
port: u16,
launch_mode: DebugpyLaunchMode<'a>,
user_installed_path: Option<&'a Path>,
@ -380,7 +380,7 @@ impl PythonDebugAdapter {
}
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;
DebugpyLaunchMode::AttachWithConnect { host: config_host }
@ -974,7 +974,7 @@ mod tests {
.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!({
"request": "attach",
"connect": {
@ -1018,7 +1018,7 @@ mod tests {
#[gpui::test]
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 args_without_host = PythonDebugAdapter::generate_debugpy_arguments(
@ -1071,7 +1071,7 @@ mod tests {
#[gpui::test]
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;
// Case 1: User-defined debugpy path (highest precedence)

View file

@ -24,7 +24,7 @@ use project::project_settings::ProjectSettings;
use semver::Version;
use std::{
env,
net::Ipv4Addr,
net::{IpAddr, Ipv4Addr},
path::{Path, PathBuf},
str::FromStr,
sync::{Arc, OnceLock},
@ -117,7 +117,7 @@ impl TryFrom<StartDebuggingRequestArguments> for extension::StartDebuggingReques
impl From<TcpArguments> for extension::TcpArguments {
fn from(value: TcpArguments) -> Self {
Self {
host: value.host.into(),
host: IpAddr::V4(Ipv4Addr::from_bits(value.host)),
port: value.port,
timeout: value.timeout,
}
@ -127,7 +127,10 @@ impl From<TcpArguments> for extension::TcpArguments {
impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
fn from(value: extension::TcpArgumentsTemplate) -> 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,
timeout: value.timeout,
}
@ -137,7 +140,7 @@ impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
impl From<TcpArgumentsTemplate> for extension::TcpArgumentsTemplate {
fn from(value: TcpArgumentsTemplate) -> Self {
Self {
host: value.host.map(Ipv4Addr::from_bits),
host: value.host.map(|bits| IpAddr::V4(Ipv4Addr::from_bits(bits))),
port: value.port,
timeout: value.timeout,
}
@ -904,13 +907,21 @@ impl dap::Host for WasmState {
let (host, port, timeout) =
::dap::configure_tcp_connection(task::TcpArgumentsTemplate {
port: template.port,
host: template.host.map(Ipv4Addr::from_bits),
host: template
.host
.map(|bits| IpAddr::V4(Ipv4Addr::from_bits(bits))),
timeout: template.timeout,
})
.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 {
port,
host: host.to_bits(),
host: host_bits,
timeout,
})
})

View file

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

View file

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

View file

@ -4,7 +4,7 @@ use gpui::SharedString;
use log as _;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr;
use std::net::IpAddr;
use std::path::PathBuf;
use util::{debug_panic, schemars::add_new_subschema};
@ -20,7 +20,7 @@ pub struct TcpArgumentsTemplate {
/// The host that the debug adapter is listening too
///
/// 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
///
/// Default: 2000ms
@ -29,8 +29,9 @@ pub struct TcpArgumentsTemplate {
impl TcpArgumentsTemplate {
/// Get the host or fallback to the default host
pub fn host(&self) -> Ipv4Addr {
self.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1))
pub fn host(&self) -> IpAddr {
self.host
.unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
}
pub fn from_proto(proto: proto::TcpHost) -> Result<Self> {
@ -389,8 +390,7 @@ impl DebugTaskFile {
},
"host": {
"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 (default: 127.0.0.1)"
"description": "The host that the debug adapter is listening to, as an IPv4 or IPv6 address (default: 127.0.0.1)"
},
"timeout": {
"type": "integer",