Skip to content

wslc: UDP port publishing rewrites reply source port, breaks connected UDP sockets (ENet/QUIC/games) #40999

Description

@Megasware128

Environment

  • WSL 2.9.3.0, kernel 6.18.35.2-1, WSLg 1.0.79
  • wslc 2.9.3.0
  • Windows 10.0.26200.8737 (Win 11)
  • Tested with python:3.11-slim and ubuntu:22.04, also seen with a real game server binary (HappinessMP)

What's happening

wslc run -p <port>:<port>/udp forwards inbound UDP fine. The container sees the client's real source port on the way in. The problem is on the way back: replies leave the container through wslc's proxy and come out on a different, random source port instead of the port that was published. A client using a connected UDP socket (connect() + send()/recv()) only accepts packets from the exact address:port it connected to, so it just times out waiting. Nothing looks wrong on the server side either. The container logs show the packet arriving and the reply being sent normally.

This breaks basically anything using UDP with a fixed reply port: ENet (used by HappinessMP, https://github.com/HappinessMP/enet), QUIC, DTLS, most game netcode. TCP publishing is fine, this is UDP-only.

Repro

udpecho.py, no deps:

import socket
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.bind(("::", 18888))
print("echo ready", flush=True)
while True:
    d, a = s.recvfrom(2048)
    print("recv from", a, flush=True)
    s.sendto(b"ECHO:" + d, a)

Run it:

wslc pull python:3.11-slim
wslc run -d --name udpecho-test -p 18888:18888/udp -v "${PWD}\udpecho.py:/udpecho.py" python:3.11-slim python3 /udpecho.py

Client (connected socket, plus an unconnected one for comparison):

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(5)
s.connect(("127.0.0.1", 18888))
print("local port:", s.getsockname()[1])
s.send(b"hello")
try:
    print("received:", s.recv(2048))
except socket.timeout:
    print("TIMEOUT on connected socket")

s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s2.settimeout(5)
s2.bind(("0.0.0.0", 0))
s2.sendto(b"hello2", ("127.0.0.1", 18888))
try:
    data, addr = s2.recvfrom(2048)
    print("unconnected got reply from:", addr, data)
except socket.timeout:
    print("TIMEOUT on unconnected socket too")

Result

Client:

local port: 53582
TIMEOUT on connected socket
unconnected got reply from: ('127.0.0.1', 53583) b'ECHO:hello2'

Container (wslc logs udpecho-test):

echo ready
recv from ('::ffff:169.254.73.254', 53582, 0, 0)
recv from ('::ffff:169.254.73.254', 49665, 0, 0)

So the container gets the right source port on the way in: 53582 and 49665, both match what the client actually used. But the reply to the first request comes back from port 53583. That's not the published port (18888) and not the port the client sent from either. The connected socket rejects it and the read times out.

Expected

Replies should appear to come from 127.0.0.1:<published port>, same as any normal port forward. I ran the identical repro against docker run -p 18888:18888/udp on the same machine (Docker engine 29.6.1) and there the connected socket got its reply correctly, from the published port.

Why it matters

If you're hosting a UDP game or voice server in a wslc container, connections from real clients just fail, silently, with nothing wrong showing up on either side. Server logs look clean the whole time. I ran into this trying to host a HappinessMP (GTA IV multiplayer mod) server, which uses ENet.

Workaround

Run the same image under Docker Desktop instead of wslc. UDP forwarding works correctly there.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions