Skip to content

Commit 34d5de9

Browse files
IPv6 support (#1896)
* Add initial support for IPv6 * address comments (part 1) * quick clean up of missed fix suggestion * Fix misassigned default serving server address in adapter client * add wrapper method to get host and port from`getsockname`
1 parent 4bc7343 commit 34d5de9

File tree

13 files changed

+123
-50
lines changed

13 files changed

+123
-50
lines changed

src/debugpy/adapter/__main__.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ def main():
6565
else:
6666
endpoints["client"] = {"host": client_host, "port": client_port}
6767

68+
localhost = sockets.get_default_localhost()
6869
if args.for_server is not None:
6970
try:
70-
server_host, server_port = servers.serve()
71+
server_host, server_port = servers.serve(localhost)
7172
except Exception as exc:
7273
endpoints = {"error": "Can't listen for server connections: " + str(exc)}
7374
else:
@@ -80,10 +81,11 @@ def main():
8081
)
8182

8283
try:
83-
sock = sockets.create_client()
84+
ipv6 = localhost.count(":") > 1
85+
sock = sockets.create_client(ipv6)
8486
try:
8587
sock.settimeout(None)
86-
sock.connect(("127.0.0.1", args.for_server))
88+
sock.connect((localhost, args.for_server))
8789
sock_io = sock.makefile("wb", 0)
8890
try:
8991
sock_io.write(json.dumps(endpoints).encode("utf-8"))
@@ -137,6 +139,10 @@ def delete_listener_file():
137139

138140

139141
def _parse_argv(argv):
142+
from debugpy.common import sockets
143+
144+
host = sockets.get_default_localhost()
145+
140146
parser = argparse.ArgumentParser()
141147

142148
parser.add_argument(
@@ -154,7 +160,7 @@ def _parse_argv(argv):
154160
parser.add_argument(
155161
"--host",
156162
type=str,
157-
default="127.0.0.1",
163+
default=host,
158164
metavar="HOST",
159165
help="start the adapter in debugServer mode on the specified host",
160166
)

src/debugpy/adapter/clients.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,8 @@ def property_or_debug_option(prop_name, flag_name):
404404
self._forward_terminate_request = on_terminate == "KeyboardInterrupt"
405405

406406
launcher_path = request("debugLauncherPath", os.path.dirname(launcher.__file__))
407-
adapter_host = request("debugAdapterHost", "127.0.0.1")
407+
localhost = sockets.get_default_localhost()
408+
adapter_host = request("debugAdapterHost", localhost)
408409

409410
try:
410411
servers.serve(adapter_host)
@@ -472,20 +473,21 @@ def attach_request(self, request):
472473
'"processId" and "subProcessId" are mutually exclusive'
473474
)
474475

476+
localhost = sockets.get_default_localhost()
475477
if listen != ():
476478
if servers.is_serving():
477479
raise request.isnt_valid(
478480
'Multiple concurrent "listen" sessions are not supported'
479481
)
480-
host = listen("host", "127.0.0.1")
482+
host = listen("host", localhost)
481483
port = listen("port", int)
482484
adapter.access_token = None
483485
self.restart_requested = request("restart", False)
484486
host, port = servers.serve(host, port)
485487
else:
486488
if not servers.is_serving():
487-
servers.serve()
488-
host, port = servers.listener.getsockname()
489+
servers.serve(localhost)
490+
host, port = sockets.get_address(servers.listener)
489491

490492
# There are four distinct possibilities here.
491493
#
@@ -710,20 +712,20 @@ def disconnect(self):
710712
super().disconnect()
711713

712714
def report_sockets(self):
713-
sockets = [
715+
socks = [
714716
{
715717
"host": host,
716718
"port": port,
717719
"internal": listener is not clients.listener,
718720
}
719721
for listener in [clients.listener, launchers.listener, servers.listener]
720722
if listener is not None
721-
for (host, port) in [listener.getsockname()]
723+
for (host, port) in [sockets.get_address(listener)]
722724
]
723725
self.channel.send_event(
724726
"debugpySockets",
725727
{
726-
"sockets": sockets
728+
"sockets": socks
727729
},
728730
)
729731

@@ -759,10 +761,11 @@ def notify_of_subprocess(self, conn):
759761
if "connect" not in body:
760762
body["connect"] = {}
761763
if "host" not in body["connect"]:
762-
body["connect"]["host"] = host if host is not None else "127.0.0.1"
764+
localhost = sockets.get_default_localhost()
765+
body["connect"]["host"] = host or localhost
763766
if "port" not in body["connect"]:
764767
if port is None:
765-
_, port = listener.getsockname()
768+
_, port = sockets.get_address(listener)
766769
body["connect"]["port"] = port
767770

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

784787

785788
def stop_serving():

src/debugpy/adapter/launchers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def spawn_debuggee(
8989

9090
arguments = dict(start_request.arguments)
9191
if not session.no_debug:
92-
_, arguments["port"] = servers.listener.getsockname()
92+
_, arguments["port"] = sockets.get_address(servers.listener)
9393
arguments["adapterAccessToken"] = adapter.access_token
9494

9595
def on_launcher_connected(sock):
@@ -108,10 +108,11 @@ def on_launcher_connected(sock):
108108
sessions.report_sockets()
109109

110110
try:
111-
launcher_host, launcher_port = listener.getsockname()
111+
launcher_host, launcher_port = sockets.get_address(listener)
112+
localhost = sockets.get_default_localhost()
112113
launcher_addr = (
113114
launcher_port
114-
if launcher_host == "127.0.0.1"
115+
if launcher_host == localhost
115116
else f"{launcher_host}:{launcher_port}"
116117
)
117118
cmdline += [str(launcher_addr), "--"]

src/debugpy/adapter/servers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def serve(host="127.0.0.1", port=0):
395395
global listener
396396
listener = sockets.serve("Server", Connection, host, port)
397397
sessions.report_sockets()
398-
return listener.getsockname()
398+
return sockets.get_address(listener)
399399

400400

401401
def is_serving():
@@ -475,7 +475,7 @@ def dont_wait_for_first_connection():
475475

476476

477477
def inject(pid, debugpy_args, on_output):
478-
host, port = listener.getsockname()
478+
host, port = sockets.get_address(listener)
479479

480480
cmdline = [
481481
sys.executable,

src/debugpy/common/sockets.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,68 @@
99
from debugpy.common import log
1010
from debugpy.common.util import hide_thread_from_debugger
1111

12+
def can_bind_ipv4_localhost():
13+
"""Check if we can bind to IPv4 localhost."""
14+
try:
15+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
16+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
17+
# Try to bind to IPv4 localhost on port 0 (any available port)
18+
sock.bind(("127.0.0.1", 0))
19+
sock.close()
20+
return True
21+
except (socket.error, OSError, AttributeError):
22+
return False
23+
24+
def can_bind_ipv6_localhost():
25+
"""Check if we can bind to IPv6 localhost."""
26+
try:
27+
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
28+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
29+
# Try to bind to IPv6 localhost on port 0 (any available port)
30+
sock.bind(("::1", 0))
31+
sock.close()
32+
return True
33+
except (socket.error, OSError, AttributeError):
34+
return False
35+
36+
def get_default_localhost():
37+
"""Get the default localhost address.
38+
Defaults to IPv4 '127.0.0.1', but falls back to IPv6 '::1' if IPv4 is unavailable.
39+
"""
40+
# First try IPv4 (preferred default)
41+
if can_bind_ipv4_localhost():
42+
return "127.0.0.1"
43+
44+
# Fall back to IPv6 if IPv4 is not available
45+
if can_bind_ipv6_localhost():
46+
return "::1"
47+
48+
# If neither works, still return IPv4 as a last resort
49+
# (this is a very unusual situation)
50+
return "127.0.0.1"
51+
52+
def get_address(sock):
53+
"""Gets the socket address host and port."""
54+
try:
55+
host, port = sock.getsockname()[:2]
56+
except Exception as exc:
57+
log.swallow_exception("Failed to get socket address:")
58+
raise RuntimeError(f"Failed to get socket address: {exc}") from exc
59+
60+
return host, port
1261

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

1665
assert backlog > 0
1766
if host is None:
18-
host = "127.0.0.1"
67+
host = get_default_localhost()
1968
if port is None:
2069
port = 0
70+
ipv6 = host.count(":") > 1
2171

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

4494

45-
def create_client():
95+
def create_client(ipv6=False):
4696
"""Return a client socket that may be connected to a remote address."""
47-
return _new_sock()
97+
return _new_sock(ipv6)
4898

4999

50-
def _new_sock():
51-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
100+
def _new_sock(ipv6=False):
101+
address_family = socket.AF_INET6 if ipv6 else socket.AF_INET
102+
sock = socket.socket(address_family, socket.SOCK_STREAM, socket.IPPROTO_TCP)
52103

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

108159
def accept_worker():
109160
while True:
110161
try:
111-
sock, (other_host, other_port) = listener.accept()
162+
sock, address = listener.accept()
163+
other_host, other_port = address[:2]
112164
except (OSError, socket.error):
113165
# Listener socket has been closed.
114166
break

src/debugpy/launcher/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ def connect(host, port):
2323

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

26-
sock = sockets.create_client()
26+
ipv6 = host.count(":") > 1
27+
sock = sockets.create_client(ipv6)
2728
sock.connect((host, port))
2829
adapter_host = host
2930

src/debugpy/launcher/__main__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
def main():
1616
from debugpy import launcher
17-
from debugpy.common import log
17+
from debugpy.common import log, sockets
1818
from debugpy.launcher import debuggee
1919

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

src/debugpy/server/api.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ def debug(address, **kwargs):
100100
_, port = address
101101
except Exception:
102102
port = address
103-
address = ("127.0.0.1", port)
103+
localhost = sockets.get_default_localhost()
104+
address = (localhost, port)
104105
try:
105106
port.__index__() # ensure it's int-like
106107
except Exception:
@@ -143,8 +144,8 @@ def listen(address, settrace_kwargs, in_process_debug_adapter=False):
143144
# Multiple calls to listen() cause the debuggee to hang
144145
raise RuntimeError("debugpy.listen() has already been called on this process")
145146

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

163164
try:
164-
endpoints_listener = sockets.create_server("127.0.0.1", 0, timeout=30)
165+
localhost = sockets.get_default_localhost()
166+
endpoints_listener = sockets.create_server(localhost, 0, timeout=30)
165167
except Exception as exc:
166168
log.swallow_exception("Can't listen for adapter endpoints:")
167169
raise RuntimeError("can't listen for adapter endpoints: " + str(exc))
168170

169171
try:
170-
endpoints_host, endpoints_port = endpoints_listener.getsockname()
172+
endpoints_host, endpoints_port = sockets.get_address(endpoints_listener)
171173
log.info(
172174
"Waiting for adapter endpoints on {0}:{1}...",
173175
endpoints_host,

src/debugpy/server/cli.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
import debugpy
2222
import debugpy.server
23-
from debugpy.common import log
23+
from debugpy.common import log, sockets
2424
from debugpy.server import api
2525

2626

@@ -104,9 +104,10 @@ def do(arg, it):
104104

105105
# It's either host:port, or just port.
106106
value = next(it)
107-
host, sep, port = value.partition(":")
107+
host, sep, port = value.rpartition(":")
108+
host = host.strip("[]")
108109
if not sep:
109-
host = "127.0.0.1"
110+
host = sockets.get_default_localhost()
110111
port = value
111112
try:
112113
port = int(port)

tests/debug/comms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __str__(self):
2525

2626
def listen(self):
2727
self._server_socket = sockets.create_server("127.0.0.1", 0, self.TIMEOUT)
28-
_, self.port = self._server_socket.getsockname()
28+
_, self.port = sockets.get_address(self._server_socket)
2929
self._server_socket.listen(0)
3030

3131
def accept_worker():

0 commit comments

Comments
 (0)