microsoft / microsoft/WSL

VirtioProxy networking: loopback connect() to a listening socket is refused unless accept() is already pending — breaks debugpy/VS Code Python debugging entirely

Open
#41,139 3 comments 0 reactions 0 assignees View on GitHub
bug network wsl2
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.