Skip to content

IPv6 support #1896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/debugpy/adapter/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ def main():
else:
endpoints["client"] = {"host": client_host, "port": client_port}

localhost = sockets.get_default_localhost()
if args.for_server is not None:
try:
server_host, server_port = servers.serve()
server_host, server_port = servers.serve(localhost)
except Exception as exc:
endpoints = {"error": "Can't listen for server connections: " + str(exc)}
else:
Expand All @@ -80,10 +81,11 @@ def main():
)

try:
sock = sockets.create_client()
ipv6 = localhost.count(":") > 1
sock = sockets.create_client(ipv6)
try:
sock.settimeout(None)
sock.connect(("127.0.0.1", args.for_server))
sock.connect((localhost, args.for_server))
sock_io = sock.makefile("wb", 0)
try:
sock_io.write(json.dumps(endpoints).encode("utf-8"))
Expand Down Expand Up @@ -137,6 +139,10 @@ def delete_listener_file():


def _parse_argv(argv):
from debugpy.common import sockets

host = sockets.get_default_localhost()

parser = argparse.ArgumentParser()

parser.add_argument(
Expand All @@ -154,7 +160,7 @@ def _parse_argv(argv):
parser.add_argument(
"--host",
type=str,
default="127.0.0.1",
default=host,
metavar="HOST",
help="start the adapter in debugServer mode on the specified host",
)
Expand Down
23 changes: 13 additions & 10 deletions src/debugpy/adapter/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ def property_or_debug_option(prop_name, flag_name):
self._forward_terminate_request = on_terminate == "KeyboardInterrupt"

launcher_path = request("debugLauncherPath", os.path.dirname(launcher.__file__))
adapter_host = request("debugAdapterHost", "127.0.0.1")
localhost = sockets.get_default_localhost()
adapter_host = request("debugAdapterHost", localhost)

try:
servers.serve(adapter_host)
Expand Down Expand Up @@ -472,20 +473,21 @@ def attach_request(self, request):
'"processId" and "subProcessId" are mutually exclusive'
)

localhost = sockets.get_default_localhost()
if listen != ():
if servers.is_serving():
raise request.isnt_valid(
'Multiple concurrent "listen" sessions are not supported'
)
host = listen("host", "127.0.0.1")
host = listen("host", localhost)
port = listen("port", int)
adapter.access_token = None
self.restart_requested = request("restart", False)
host, port = servers.serve(host, port)
else:
if not servers.is_serving():
servers.serve()
host, port = servers.listener.getsockname()
servers.serve(localhost)
host, port = sockets.get_address(servers.listener)

# There are four distinct possibilities here.
#
Expand Down Expand Up @@ -710,20 +712,20 @@ def disconnect(self):
super().disconnect()

def report_sockets(self):
sockets = [
socks = [
{
"host": host,
"port": port,
"internal": listener is not clients.listener,
}
for listener in [clients.listener, launchers.listener, servers.listener]
if listener is not None
for (host, port) in [listener.getsockname()]
for (host, port) in [sockets.get_address(listener)]
]
self.channel.send_event(
"debugpySockets",
{
"sockets": sockets
"sockets": socks
},
)

Expand Down Expand Up @@ -759,10 +761,11 @@ def notify_of_subprocess(self, conn):
if "connect" not in body:
body["connect"] = {}
if "host" not in body["connect"]:
body["connect"]["host"] = host if host is not None else "127.0.0.1"
localhost = sockets.get_default_localhost()
body["connect"]["host"] = host or localhost
if "port" not in body["connect"]:
if port is None:
_, port = listener.getsockname()
_, port = sockets.get_address(listener)
body["connect"]["port"] = port

if self.capabilities["supportsStartDebuggingRequest"]:
Expand All @@ -779,7 +782,7 @@ def serve(host, port):
global listener
listener = sockets.serve("Client", Client, host, port)
sessions.report_sockets()
return listener.getsockname()
return sockets.get_address(listener)


def stop_serving():
Expand Down
7 changes: 4 additions & 3 deletions src/debugpy/adapter/launchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def spawn_debuggee(

arguments = dict(start_request.arguments)
if not session.no_debug:
_, arguments["port"] = servers.listener.getsockname()
_, arguments["port"] = sockets.get_address(servers.listener)
arguments["adapterAccessToken"] = adapter.access_token

def on_launcher_connected(sock):
Expand All @@ -108,10 +108,11 @@ def on_launcher_connected(sock):
sessions.report_sockets()

try:
launcher_host, launcher_port = listener.getsockname()
launcher_host, launcher_port = sockets.get_address(listener)
localhost = sockets.get_default_localhost()
launcher_addr = (
launcher_port
if launcher_host == "127.0.0.1"
if launcher_host == localhost
else f"{launcher_host}:{launcher_port}"
)
cmdline += [str(launcher_addr), "--"]
Expand Down
4 changes: 2 additions & 2 deletions src/debugpy/adapter/servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ def serve(host="127.0.0.1", port=0):
global listener
listener = sockets.serve("Server", Connection, host, port)
sessions.report_sockets()
return listener.getsockname()
return sockets.get_address(listener)


def is_serving():
Expand Down Expand Up @@ -475,7 +475,7 @@ def dont_wait_for_first_connection():


def inject(pid, debugpy_args, on_output):
host, port = listener.getsockname()
host, port = sockets.get_address(listener)

cmdline = [
sys.executable,
Expand Down
68 changes: 60 additions & 8 deletions src/debugpy/common/sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,68 @@
from debugpy.common import log
from debugpy.common.util import hide_thread_from_debugger

def can_bind_ipv4_localhost():
"""Check if we can bind to IPv4 localhost."""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Try to bind to IPv4 localhost on port 0 (any available port)
sock.bind(("127.0.0.1", 0))
sock.close()
return True
except (socket.error, OSError, AttributeError):
return False

def can_bind_ipv6_localhost():
"""Check if we can bind to IPv6 localhost."""
try:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Try to bind to IPv6 localhost on port 0 (any available port)
sock.bind(("::1", 0))
sock.close()
return True
except (socket.error, OSError, AttributeError):
return False

def get_default_localhost():
"""Get the default localhost address.
Defaults to IPv4 '127.0.0.1', but falls back to IPv6 '::1' if IPv4 is unavailable.
"""
# First try IPv4 (preferred default)
if can_bind_ipv4_localhost():
return "127.0.0.1"

# Fall back to IPv6 if IPv4 is not available
if can_bind_ipv6_localhost():
return "::1"

# If neither works, still return IPv4 as a last resort
# (this is a very unusual situation)
return "127.0.0.1"

def get_address(sock):
"""Gets the socket address host and port."""
try:
host, port = sock.getsockname()[:2]
except Exception as exc:
log.swallow_exception("Failed to get socket address:")
raise RuntimeError(f"Failed to get socket address: {exc}") from exc

return host, port

def create_server(host, port=0, backlog=socket.SOMAXCONN, timeout=None):
"""Return a local server socket listening on the given port."""

assert backlog > 0
if host is None:
host = "127.0.0.1"
host = get_default_localhost()
if port is None:
port = 0
ipv6 = host.count(":") > 1

try:
server = _new_sock()
server = _new_sock(ipv6)
if port != 0:
# If binding to a specific port, make sure that the user doesn't have
# to wait until the OS times out the socket to be able to use that port
Expand All @@ -42,13 +92,14 @@ def create_server(host, port=0, backlog=socket.SOMAXCONN, timeout=None):
return server


def create_client():
def create_client(ipv6=False):
"""Return a client socket that may be connected to a remote address."""
return _new_sock()
return _new_sock(ipv6)


def _new_sock():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
def _new_sock(ipv6=False):
address_family = socket.AF_INET6 if ipv6 else socket.AF_INET
sock = socket.socket(address_family, socket.SOCK_STREAM, socket.IPPROTO_TCP)

# Set TCP keepalive on an open socket.
# It activates after 1 second (TCP_KEEPIDLE,) of idleness,
Expand Down Expand Up @@ -102,13 +153,14 @@ def serve(name, handler, host, port=0, backlog=socket.SOMAXCONN, timeout=None):
log.reraise_exception(
"Error listening for incoming {0} connections on {1}:{2}:", name, host, port
)
host, port = listener.getsockname()
host, port = get_address(listener)
log.info("Listening for incoming {0} connections on {1}:{2}...", name, host, port)

def accept_worker():
while True:
try:
sock, (other_host, other_port) = listener.accept()
sock, address = listener.accept()
other_host, other_port = address[:2]
except (OSError, socket.error):
# Listener socket has been closed.
break
Expand Down
3 changes: 2 additions & 1 deletion src/debugpy/launcher/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def connect(host, port):

log.info("Connecting to adapter at {0}:{1}", host, port)

sock = sockets.create_client()
ipv6 = host.count(":") > 1
sock = sockets.create_client(ipv6)
sock.connect((host, port))
adapter_host = host

Expand Down
7 changes: 4 additions & 3 deletions src/debugpy/launcher/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

def main():
from debugpy import launcher
from debugpy.common import log
from debugpy.common import log, sockets
from debugpy.launcher import debuggee

log.to_file(prefix="debugpy.launcher")
Expand All @@ -38,9 +38,10 @@ def main():
# The first argument specifies the host/port on which the adapter is waiting
# for launcher to connect. It's either host:port, or just port.
adapter = launcher_argv[0]
host, sep, port = adapter.partition(":")
host, sep, port = adapter.rpartition(":")
host.strip("[]")
if not sep:
host = "127.0.0.1"
host = sockets.get_default_localhost()
port = adapter
port = int(port)

Expand Down
10 changes: 6 additions & 4 deletions src/debugpy/server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ def debug(address, **kwargs):
_, port = address
except Exception:
port = address
address = ("127.0.0.1", port)
localhost = sockets.get_default_localhost()
address = (localhost, port)
try:
port.__index__() # ensure it's int-like
except Exception:
Expand Down Expand Up @@ -143,8 +144,8 @@ def listen(address, settrace_kwargs, in_process_debug_adapter=False):
# Multiple calls to listen() cause the debuggee to hang
raise RuntimeError("debugpy.listen() has already been called on this process")

host, port = address
if in_process_debug_adapter:
host, port = address
log.info("Listening: pydevd without debugpy adapter: {0}:{1}", host, port)
settrace_kwargs["patch_multiprocessing"] = False
_settrace(
Expand All @@ -161,13 +162,14 @@ def listen(address, settrace_kwargs, in_process_debug_adapter=False):
server_access_token = codecs.encode(os.urandom(32), "hex").decode("ascii")

try:
endpoints_listener = sockets.create_server("127.0.0.1", 0, timeout=30)
localhost = sockets.get_default_localhost()
endpoints_listener = sockets.create_server(localhost, 0, timeout=30)
except Exception as exc:
log.swallow_exception("Can't listen for adapter endpoints:")
raise RuntimeError("can't listen for adapter endpoints: " + str(exc))

try:
endpoints_host, endpoints_port = endpoints_listener.getsockname()
endpoints_host, endpoints_port = sockets.get_address(endpoints_listener)
log.info(
"Waiting for adapter endpoints on {0}:{1}...",
endpoints_host,
Expand Down
7 changes: 4 additions & 3 deletions src/debugpy/server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

import debugpy
import debugpy.server
from debugpy.common import log
from debugpy.common import log, sockets
from debugpy.server import api


Expand Down Expand Up @@ -104,9 +104,10 @@ def do(arg, it):

# It's either host:port, or just port.
value = next(it)
host, sep, port = value.partition(":")
host, sep, port = value.rpartition(":")
host = host.strip("[]")
if not sep:
host = "127.0.0.1"
host = sockets.get_default_localhost()
port = value
try:
port = int(port)
Expand Down
2 changes: 1 addition & 1 deletion tests/debug/comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __str__(self):

def listen(self):
self._server_socket = sockets.create_server("127.0.0.1", 0, self.TIMEOUT)
_, self.port = self._server_socket.getsockname()
_, self.port = sockets.get_address(self._server_socket)
self._server_socket.listen(0)

def accept_worker():
Expand Down
3 changes: 2 additions & 1 deletion tests/debug/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,8 @@ def connect_to_adapter(self, address):

self.expected_adapter_sockets["client"]["port"] = port

sock = sockets.create_client()
ipv6 = host.count(":") > 1
sock = sockets.create_client(ipv6)
sock.connect(address)

stream = messaging.JsonIOStream.from_socket(sock, name=self.adapter_id)
Expand Down
Loading
Loading