VirtioProxy networking: loopback connect() to a listening socket is refused unless accept() is already pending — breaks debugpy/VS Code Python debugging entirely
- Dominant language
- C++
- Stars
- 33.7k
- Forks
- 1.8k
- Avg merge
- 3d 17h
- Merged PRs (30d)
- 116
Description
### Windows Version
10.0.26200.8893
### WSL Version
2.7.10.0
### Are you using WSL 1 or WSL 2?
- [x] WSL 2
- [ ] WSL 1
### Kernel Version
6.18.33.2-2
### Distro Version
Ubuntu 26.04
### Other Software
- VS Code + ms-python.debugpy 2026.6.0 (bundled debugpy 1.8.20; also reproduced with debugpy 1.8.21)
- Python 3.13.14
- Environment: Azure Virtual Desktop VM. NAT mode fails to initialize on this VM
(falls back / only virtioproxy works), so `networkingMode=virtioproxy` is
effectively mandatory here.
### Repro Steps
Under `networkingMode=virtioproxy`, a TCP connect() to a loopback address where a
socket is in LISTEN state is REFUSED unless the listener has an accept() call
already pending at the instant the SYN arrives. Normal Linux kernel semantics
(connection queued in the listen backlog) do not apply.
Minimal repro, pure Python, no third-party software:
```python
import socket, threading, time
def case(accept_pending):
srv = socket.socket()
srv.bind(("127.0.0.1", 0)); srv.listen(1)
port = srv.getsockname()[1]
if accept_pending:
threading.Thread(target=lambda: srv.accept(), daemon=True).start()
time.sleep(0.3)
try:
c = socket.create_connection(("127.0.0.1", port), timeout=2)
c.close(); r = "CONNECTS"
except Exception as e:
r = type(e).__name__
srv.close(); return r
print("accept pending: ", case(True)) # CONNECTS
print("no accept pending:", case(False)) # ConnectionRefusedError <-- bug
```
Results on this machine (fully deterministic):
| scenario | result |
|--------------------------------------------|----------------------|
| listener + accept() pending | connects |
| listener in LISTEN, accept() not yet called | ConnectionRefusedError (ECONNREFUSED) |
| same tests against the eth0 IP (10.x) | connects (normal kernel semantics) |
| same tests against ::1 (IPv6) | connects (normal kernel semantics) |
Only IPv4 127.0.0.0/8 is affected — consistent with VirtioProxy redirecting
127.0.0.1 to the Windows host loopback (as documented by third parties, e.g.
ddev/ddev#8220; the mode itself is undocumented, see #12706). The pseudo-loopback
127.0.1.1 mentioned there does NOT work here (connection times out).
### Expected Behavior
connect() to a loopback socket in LISTEN state succeeds and is queued in the
listen backlog, regardless of whether accept() is already pending — standard
Linux TCP semantics, as in NAT/mirrored modes.
### Actual Behavior
connect() fails immediately with ECONNREFUSED whenever the listener has no
pending accept() at SYN time. `ss -ltnp` shows the socket in LISTEN the whole
time.
Practical impact: **VS Code Python debugging (debugpy) is completely broken**
under virtioproxy. debugpy's internal handshake (pydevd → adapter, launcher →
adapter, all on 127.0.0.1) loses this race deterministically:
```
File ".../debugpy/launcher/__init__.py", line 28, in connect
sock.connect((host, port))
ConnectionRefusedError: [Errno 111] Connection refused
```
`python -m debugpy --listen 127.0.0.1:0 --wait-for-client -c "pass"` dies the
same way (0/6 attempts). Retrying connect() for 2s does not help — the refusal
is persistent, not a startup race.
Workaround we use: a sitecustomize-style shim inside the venv that rewrites
AF_INET connect() targets 127.0.0.1/localhost/0.0.0.0 → the eth0 IP and bind()
127.0.0.1 → 0.0.0.0. With that, debugpy works 6/6. This confirms the issue is
purely the 127.0.0.0/8 interception layer.
Happy to provide `networking.log` / WSL logs if useful.
### Diagnostic Logs
_No response_
Contributor guide
Research direction
Start by running the minimal Python socket reproduction under networkingMode=virtioproxy, then compare IPv4 loopback behavior with eth0 and ::1. Use the debugpy/launcher/__init__.py connect() failure as the affected entry point and trace the VirtioProxy loopback handling. Done means a listening socket accepts queued connections without a pending accept(), while the supplied cases continue to pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, python
- Domain
- networking, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100