net_consomme: reply to forwarded UDP clients from the published port#3902
net_consomme: reply to forwarded UDP clients from the published port#3902benhillis wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes UDP port publishing behavior in net_consomme (consomme’s UDP forwarding path) so that replies from a guest service running on a published UDP port egress from the published host port, rather than from a per-connection ephemeral socket. This aligns behavior with connected UDP client expectations (e.g., QUIC/DTLS/game networking) and addresses microsoft/WSL#40999.
Changes:
- Track UDP GSO state on
UdpListenersockets (similar to existing per-connection tracking). - In
handle_udp, when sending a packet from a published guest port to a host-local destination, send via the listener’s host socket to preserve the published source port. - Add a unit test validating the reply source port and ensuring no per-connection UDP socket is created for this reply path.
When a guest replied to a UDP client on a published port, consomme sent the reply through a per-connection ephemeral socket rather than the listener's host socket. The reply's source port therefore did not match the published port. A connected UDP client only accepts datagrams from the exact address it sent to, so the reply was dropped and the client timed out. This broke ENet, QUIC, DTLS, and most game netcode running in wslc containers (microsoft/WSL#40999). handle_udp now detects datagrams whose source port is published to the host and destined for a local address, and sends them from the listener's host socket so the reply's source matches the published port. Adds a unit test covering the reply source port. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
25fe4e2 to
72a9466
Compare
| dst_sock_addr.set_port(listener.host_addr.port()); | ||
| } | ||
|
|
||
| // Guest reply from a published port to a local client: send from the listener's |
There was a problem hiding this comment.
I am not sure this should be local only; wouldn't we want the same behavior for a reply to a remote client?
We should be able to structure this a bit differently as well to get rid of all this duplicated code. Maybe move all the shared fields from UdpListener/UdpConnection into a common structure that they both include so we can return a &mut to that and then run the same code
Summary
Fixes microsoft/WSL#40999. UDP replies from a wslc container on a published port came back to the host client from a random ephemeral source port instead of the published port, so connected UDP clients (ENet, QUIC, DTLS, most game netcode) rejected them and timed out. TCP publishing was unaffected.
Root cause
wslc UDP port publishing routes through consomme (the wslrelay path is TCP only). When the container replies to a forwarded client,
handle_udpresolves the destination back to the host client (a local address) but then falls through toget_or_insert, which binds a fresh per-connection ephemeral socket and sends the reply from it. The client, using a connected UDP socket, only accepts datagrams from the exact address it sent to (the published port), so the reply is dropped.Inbound forwarding was fine because the listener socket is bound to the published port; only the return path picked the wrong socket.
Fix
In
handle_udp, when a datagram's source port is published to the host and its destination is a local address, send it from the listener's host socket. The reply's source then matches the published port, matching Docker's behavior. GSO size is tracked on the listener the same way it is on per-connection sockets. Non-local (genuine guest-initiated outbound) flows are unaffected and continue to use per-connection sockets.Testing
test_udp_reply_source_port: binds a listener, injects a guest datagram whose source is the published port and destination is a host client on loopback, and verifies the client receives the reply from the published host port and that no per-connection socket is created.cargo test -p consomme: 100/100 pass.cargo clippy -p consomme --all-targetsandcargo fmt -p consomme -- --checkclean.