Skip to content

WSLC: Add UDP and IPv6 support for exposed image ports - #41124

Merged
David Bennett (dkbennett) merged 6 commits into
masterfrom
user/dkbennett/ipv6udp_ports
Jul 23, 2026
Merged

David Bennett (dkbennett) merged 6 commits into
masterfrom
user/dkbennett/ipv6udp_ports

Conversation

@dkbennett

@dkbennett David Bennett (dkbennett) commented Jul 20, 2026 •

Copy link
Copy Markdown
Member

Summary of the Pull Request

Support IPv6 and UDP in image-declared exposed ports. The wslc -P/--publish-all path now publishes every port from an image's ExposedPorts config on both IPv4 and IPv6 loopback, for both TCP and UDP — previously it silently published only TCP on IPv4 loopback. UDP publishing is conditional on networking relay (which is TCP only) and emits a warning and gracefully skips.

PR Checklist

  • Closes: Link to issue #xxx
  • Communication: I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected
  • Tests: Added/updated if needed and all pass
  • Localization: All end user facing strings can be localized
  • Dev docs: Added/updated if needed
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

When a container is created with -P/--publish-all, wslc reads the image's ExposedPorts config and publishes each declared port to an ephemeral host port. The prior implementation had two limitations:

  • UDP was dropped. The loop skipped any non-TCP protocol (if (protocol != IPPROTO_TCP) continue;), so a EXPOSE 53/udp port was never published.
  • IPv6 was unsupported. Each mapping hardcoded AF_INET and 127.0.0.1, so exposed ports were reachable only over IPv4 loopback.

An image's ExposedPorts keys carry only a port and protocol (port/tcp or port/udp) and never an address family, so the code has to choose what to bind. This change mirrors Docker's documented -P default, which publishes to all host addresses on both IPv4 and IPv6, while preserving wslc's convention of binding loopback by default. Each exposed port is now published dual-stack: one mapping on 127.0.0.1 (IPv4) and one on ::1 (IPv6), preserving the port's declared protocol (tcp/udp).

Networking-mode handling for UDP

wslc has two port-forwarding backends. The virtioNet path (MapVirtioNetPort, Consomme mode) maps ports at the netstack level and natively supports UDP and IPv6. The userspace wslrelay path (NAT mode, or Consomme with the wslrelay feature flag) is a TCP-only stream relay — WSLCVirtualMachine::MapPort() rejects any non-TCP mapping with ERROR_NOT_SUPPORTED.

Because publish-all previously skipped UDP unconditionally, an image exposing a UDP port worked (the port was just dropped) regardless of mode. Now that UDP mappings are generated, they would fail the entire container create/start under the wslrelay path. To avoid that regression, the exposed-ports loop now skips UDP ports when UseWslRelayPortForwarding() is active and emits a one-time user warning (MessageWslcPublishAllUdpNotSupported). TCP (IPv4 + IPv6) publishes normally in all modes; UDP publishes fully on the virtioNet path. This guard is self-retiring — it becomes a no-op automatically once the relay path is no longer used. Adding UDP support to the wslrelay stream relay is potential follow-up work; it is a substantial cross-boundary feature (datagram framing over hvsocket, per-source session tracking) and unnecessary given virtioNet already covers UDP.

Testing

Added end-to-end test WSLCE2E_Container_Create_PublishAll_DualStack (test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp). It builds an image with EXPOSE 8080/tcp and EXPOSE 9090/udp, runs -P, and asserts that each port is published with exactly two bindings — one on 127.0.0.1 and one on ::1 — each on a non-zero ephemeral host port. This covers the full {tcp, udp} × {ipv4, ipv6} matrix in a single test. Verified passing against a locally built and deployed x64 Debug build.

Validation Steps Performed

  • Newly added E2E test passes locally.

Copilot AI review requested due to automatic review settings July 20, 2026 21:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates WSLC’s --publish-all (-P) behavior to expand image-declared ExposedPorts into published port mappings for both UDP and IPv6 (dual-stack loopback), aligning the publish-all behavior more closely with Docker-style expectations while retaining WSLC’s loopback-only binding convention.

Changes:

  • Expand image ExposedPorts into two bindings per exposed port: 127.0.0.1 (IPv4) and ::1 (IPv6), preserving the port’s declared protocol (tcp/udp).
  • Add an end-to-end test that validates {tcp,udp} × {ipv4,ipv6} publish-all bindings appear in inspect after container start.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp Adds an E2E test to validate publish-all creates dual-stack bindings for both TCP and UDP exposed ports.
src/windows/wslcsession/WSLCContainer.cpp Changes publish-all expansion logic to create IPv4+IPv6 loopback mappings and to no longer drop UDP.

Comment thread src/windows/wslcsession/WSLCContainer.cpp
Copilot AI review requested due to automatic review settings July 20, 2026 22:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/windows/wslcsession/WSLCContainer.cpp:1827

  • The PR description says --publish-all now publishes every ExposedPorts entry for both TCP and UDP, but this implementation intentionally skips UDP when UseWslRelayPortForwarding() is true (NAT / Consomme+wslrelay) and only emits a warning once. Please update the PR summary/description (and any user-facing docs/release notes, if applicable) to reflect that UDP publish-all is conditional on networking mode until the wslrelay path supports UDP.
            // The userspace wslrelay port relay only forwards TCP. When it's active, adding a UDP
            // mapping would fail the whole container (MapPort throws ERROR_NOT_SUPPORTED), so skip
            // UDP exposed ports and warn once. UDP is published normally on the virtioNet path.
            const bool relayForwarding = virtualMachine.UseWslRelayPortForwarding();
            bool warnedUdpSkipped = false;

Copilot AI review requested due to automatic review settings July 20, 2026 23:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/windows/wslcsession/WSLCContainer.cpp
Comment thread test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 00:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread src/windows/wslcsession/WSLCContainer.cpp
Copilot AI review requested due to automatic review settings July 21, 2026 01:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/windows/wslcsession/WSLCContainer.cpp:1852

  • When UseWslRelayPortForwarding() is active, the new publish-all IPv6 mapping (AF_INET6, "::1") will go through WSLCVirtualMachine::MapRelayPort() via MapPort(). The wslrelay side treats the incoming family value as a Linux LX_AF_* constant and converts it with WindowsAddressFamily() (see src/windows/wslrelay/localhost.cpp:239-244), so passing Windows AF_INET6 (23) causes an E_INVALIDARG and breaks container start/create in NAT/relay mode. This was previously masked for IPv4 because AF_INET happens to match LX_AF_INET.

Fix by translating the Windows family to LX_AF_INET/LX_AF_INET6 before sending WSLC_MAP_PORT::AddressFamily over the relay channel (e.g., in MapPort()/MapRelayPort()).

                // Exposed ports carry only a port and protocol (tcp/udp), never an address family.
                // Mirror Docker's dual-stack default by publishing each exposed port on both the IPv4
                // and IPv6 loopback, while keeping wslc's loopback-only default binding convention.
                for (const auto& [family, address] : {std::pair{AF_INET, "127.0.0.1"}, std::pair{AF_INET6, "::1"}})
                {
                    auto& createdPort = ports.emplace_back();

Comment thread test/windows/WSLCTests.cpp Outdated
Comment thread test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 20:39
@dkbennett
David Bennett (dkbennett) marked this pull request as ready for review July 21, 2026 20:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp

@OneBlue Blue (OneBlue) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you !

createdPort.Protocol = protocol;
strcpy_s(createdPort.BindingAddress, "127.0.0.1");
// Exposed ports carry only a port and protocol (tcp/udp), never an address family.
// Mirror Docker's dual-stack default by publishing each exposed port on both the IPv4

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking through the tests, I'm assuming that docker is doing the ipv4 <-> ipv6 translation for us ? If so, that's pretty cool !

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it just works! I was surprised at how easy it was, and the E2E test verifies the functionality.

@dkbennett
David Bennett (dkbennett) merged commit da3969e into master Jul 23, 2026
12 checks passed
@dkbennett
David Bennett (dkbennett) deleted the user/dkbennett/ipv6udp_ports branch July 23, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants