Skip to content

Comments

Initial support for port mapping in CLI#14225

Draft
AmelBawa-msft wants to merge 5 commits intofeature/wsl-for-appsfrom
user/amelbawa/port
Draft

Initial support for port mapping in CLI#14225
AmelBawa-msft wants to merge 5 commits intofeature/wsl-for-appsfrom
user/amelbawa/port

Conversation

@AmelBawa-msft
Copy link

Summary of the Pull Request

🦞 Port mapping parser

Parser support examples:

✅ Valid

 --- IP not specified ---
-p 8080:80
-p 443:443
-p 80
-p 80/tcp
-p 53/udp

# --- IPv4 specified ---
-p 127.0.0.1:8080:80
-p 0.0.0.0:8080:80
-p 192.168.1.50:8080:80
-p 127.0.0.1:5353:5353/udp
-p 127.0.0.1::80
-p 127.0.0.1::53/udp

# --- IPv6 specified ---
-p '[::1]:8080:80'
-p '[::]:8080:80'
-p '[2001:db8::10]:8080:80'
-p '[::1]:5353:5353/udp'
-p '[::1]::80'
-p '[::]::53/udp'

# --- Protocol not specified (defaults to tcp) ---
-p 9090:90
-p 127.0.0.1:9090:90
-p '[::1]:9090:90'

# --- Both tcp and udp (separate publishes) ---
-p 53:53/tcp
-p 53:53/udp
-p 5000:5000/tcp
-p 5000:5000/udp

# --- Port ranges ---
-p 8000-8005:8000-8005
-p 127.0.0.1:9000-9003:9000-9003
-p '[::1]:7000-7002:7000-7002'
-p 10000-10010:10000-10010/tcp
-p 20000-20002:20000-20002/udp

❌ Invalid

# Malformed IP/container combinations
-p 127.0.0.1:80
-p '[::1]:8080'

# Port range mismatches
-p 8000-8005:8000-8006
-p 8000-8005:8000
-p 8000:8000-8005

# Bad protocol
-p 8080:80/icmp
-p 8080:80/udpp

# IPv6 without brackets
-p ::1:8080:80

🦐 Full example

# Example 1: HOST_PORT:CONTAINER_PORT
# (host port) 8080 => (container port) 80
wslc container run -p 8080:80 -it --name demo nginx:alpine

# Example 2: CONTAINER_PORT
# (host port) ephemeral => (container port) 80 (e.g. 56956 => 80)
wslc container run -p 80 -it --name demo nginx:alpine

# Example 3: HOST_PORT_RANGE-CONTAINER_PORT_RANGE
wslc container run -p 8000-8080:8000-8080 -it --name demo nginx:alpine

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

Validation Steps Performed

@dkbennett
Copy link
Member

I have a meta comment before we go too far down the argument validation path. We have two types of arguments in the CLI:

  1. Arguments directly consumed by the CLI to change its behavior (progress settings, output, logging, settings specific to the CLI, etc)
  2. Arguments that are generally passed on to the service or another component, such as the operating system.

@OneBlue recommended that our general approach for argument validation for arguments to the service is that we should pass the argument along to the service with as little modification as possible, and if it's bad input the service will return error messages that we can surface to the user. I completely agree with this approach and believe it should apply to everything not just the service.

I would prefer not to have rigid validation of arguments in the CLI unless it's a type 1 argument specific to the CLI or some minimum parsing needs to be done in order to put the data into a form the service can consume (converting a string to two integers, for example).

The port validation is something I would expect the service to handle and I am generally concerned about the CLI parsing the port data argument differently from the service. I am strongly in favor of only validating the bare minimum needed to make it valid input to the service and let the service tell us if its inputs were invalid.

So the general guidance for argument validation should be: "Is something else already validating or going to validate this?" If the answer is yes, then the CLI should only validate it enough to get it to that validator and then rely on that validator to do its job correctly.

@AmelBawa-msft
Copy link
Author

@dkbennett great observation and feedback, thank you! I have a couple of questions before marking this PR as ready, one of which you touched on, and another related to what we currently support.

This PR contains the necessary pieces to light up the feature, but I’ve held off on marking it ready to avoid duplicating service logic and to clarify how certain parts of the syntax should be handled. Some aspects may require follow-up work with the runtime team. I will sync up today with the team for suggestions on the next step.

Excited to see this feature finally available 🚀🙌

Copy link
Contributor

Copilot AI left a comment

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 adds initial support for port mapping in the WSLC CLI, implementing the -p/--publish flag for the wslc container run and wslc container create commands. The implementation includes a comprehensive port mapping parser that supports various formats including IPv4/IPv6 addresses, port ranges, and protocol specifications (TCP/UDP).

Changes:

  • Adds PublishPort parser with support for formats like [host-ip:][host-port:]container-port[/protocol], including IPv6 addresses in brackets and port ranges
  • Integrates port mapping into CLI commands with -p/--publish argument
  • Implements ephemeral port allocation using socket binding when host port is not specified

Reviewed changes

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

Show a summary per file
File Description
src/windows/wslc/ContainerModel.h Adds PublishPort class with nested PortRange and IPAddress structs for parsing port mapping specifications
src/windows/wslc/ContainerModel.cpp Implements parsing logic for port mappings, IP addresses, and port ranges with validation
src/windows/wslc/ContainerService.cpp Adds ResolveOrAllocatePort function for ephemeral port allocation and integrates port mapping into container creation
src/windows/wslc/ContainerCommand.h Adds -p/--publish flag to container run and create commands with help text
src/windows/wslc/CMakeLists.txt Adds ContainerModel.cpp to build configuration

Comment on lines +46 to +49
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(loopback ? INADDR_LOOPBACK : INADDR_ANY);
addr.sin_port = htons(0);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The sockaddr_in structure is only suitable for IPv4 addresses. When handling IPv6 addresses (as determined by the host IP), a sockaddr_in6 structure should be used instead, with the appropriate IPv6 address fields (sin6_family, sin6_addr, sin6_port).

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +66
auto socket = ::socket(AF_INET, sockType, ipProto);
THROW_LAST_ERROR_IF(socket == INVALID_SOCKET);

// Bind to port 0 to ask Windows for an ephemeral port
auto loopback = port.HostIP().has_value() && port.HostIP()->IsLoopback();
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(loopback ? INADDR_LOOPBACK : INADDR_ANY);
addr.sin_port = htons(0);
if (::bind(socket, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) == SOCKET_ERROR)
{
::closesocket(socket);
THROW_LAST_ERROR();
}

// Read the chosen port back
sockaddr_in bound{};
int len = sizeof(bound);
if (::getsockname(socket, reinterpret_cast<sockaddr*>(&bound), &len) == SOCKET_ERROR)
{
::closesocket(socket);
THROW_LAST_ERROR();
}

auto chosen = ntohs(bound.sin_port);
::closesocket(socket);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

This code uses a raw socket handle and manually calls closesocket. Following the established pattern in this codebase (as seen in files like hvsocket.cpp, NatNetworking.cpp, etc.), sockets should be managed using wil::unique_socket for automatic cleanup and to avoid resource leaks if exceptions are thrown before closesocket is called.

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +132
for (unsigned int i = 0; i < containerPort.Count(); ++i)
{
int family = portMapping.HostIP().has_value() && portMapping.HostIP()->IsIPv6() ? AF_INET6 : AF_INET;
auto currentContainerPort = containerPort.Start() + i;
auto currentHostPort = ResolveOrAllocatePort(portMapping, i);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The ephemeral port allocation allocates a port for each iteration in the loop, but immediately closes the socket after reading the port number. This creates a race condition where the allocated port could be reused by another process before AddPort is called. For port ranges, this could lead to port conflicts. Consider keeping the socket open until after AddPort is called, or using a different mechanism to ensure ports remain reserved.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +62
PublishPort::PortRange result{};
result.m_start = startPort;
result.m_end = endPort;
return result;
}
else
{
// Single port specified
int port = parsePort(portPart, "Invalid port specified in port mapping.");

PublishPort::PortRange result{};
result.m_start = port;
result.m_end = port;
return result;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The PortRange struct's m_start and m_end members are private, but ParsePortPart attempts to directly assign to them. This will cause a compilation error. Consider making these members public, or adding a private constructor that takes start and end values.

Suggested change
PublishPort::PortRange result{};
result.m_start = startPort;
result.m_end = endPort;
return result;
}
else
{
// Single port specified
int port = parsePort(portPart, "Invalid port specified in port mapping.");
PublishPort::PortRange result{};
result.m_start = port;
result.m_end = port;
return result;
return PublishPort::PortRange(startPort, endPort);
}
else
{
// Single port specified
int port = parsePort(portPart, "Invalid port specified in port mapping.");
return PublishPort::PortRange(port, port);

Copilot uses AI. Check for mistakes.
"arguments (pos. 1..): Arguments to pass to container's init process",
"-t, --tty: Open a TTY with the container process",
"-i, --interactive: Keep stdin open",
"-p, --publish: Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])",
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The help text format description '[host-ip:]host-port:container-port[/protocol]' is misleading because it suggests host-port is always required. However, the parser supports the format 'container-port' (without host-port) for ephemeral port allocation. The format should be '[host-ip:][host-port:]container-port[/protocol]' to indicate that host-port is also optional.

Copilot uses AI. Check for mistakes.
// Create a socket matching the protocol.
const int sockType = (port.PortProtocol() == PublishPort::Protocol::TCP) ? SOCK_STREAM : SOCK_DGRAM;
const int ipProto = (port.PortProtocol() == PublishPort::Protocol::TCP) ? IPPROTO_TCP : IPPROTO_UDP;
auto socket = ::socket(AF_INET, sockType, ipProto);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The ResolveOrAllocatePort function always creates an AF_INET (IPv4) socket regardless of whether the port mapping specifies an IPv6 address. When the user specifies an IPv6 host IP (e.g., '[::1]:8080:80'), this function should create an AF_INET6 socket instead. The socket family should be determined based on whether port.HostIP() is IPv6, similar to how the family is determined in line 130.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +51
PublishPort::PortRange result{};
result.m_start = startPort;
result.m_end = endPort;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The PortRange struct's m_start and m_end members are private, but ParsePortPart (a static member function) attempts to directly assign to them. This will cause a compilation error because static member functions cannot access private non-static members without an object, and these assignments are trying to access private members of 'result'. Consider making these members public, or adding a private constructor that takes start and end values.

Copilot uses AI. Check for mistakes.
"-t, --tty: Open a TTY with the container process",
"-i, --interactive: Keep stdin open",
"-d, --detach: Run container in background",
"-p, --publish: Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])",
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The help text format description '[host-ip:]host-port:container-port[/protocol]' is misleading because it suggests host-port is always required. However, the parser supports the format 'container-port' (without host-port) for ephemeral port allocation. The format should be '[host-ip:][host-port:]container-port[/protocol]' to indicate that host-port is also optional.

Suggested change
"-p, --publish: Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])",
"-p, --publish: Publish a port from container to host (format: [host-ip:][host-port:]container-port[/protocol])",

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +37
static auto parsePort = [](const std::string& value, const char* errorMessage) -> int {
try
{
size_t idx = 0;
int port = std::stoi(value, &idx, 10);
if (idx != value.size())
{
THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
}
return port;
}
catch (const std::exception&)
{
THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage);
}
};
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The static lambda 'parsePort' is defined inside ParsePortPart. While this is thread-safe in C++11 and later (static local variables are initialized once in a thread-safe manner), the function could potentially be called recursively or concurrently. Consider making this a file-level static function instead for better clarity and to avoid potential confusion about initialization overhead on every call.

Copilot uses AI. Check for mistakes.
int family = portMapping.HostIP().has_value() && portMapping.HostIP()->IsIPv6() ? AF_INET6 : AF_INET;
auto currentContainerPort = containerPort.Start() + i;
auto currentHostPort = ResolveOrAllocatePort(portMapping, i);
containerLauncher.AddPortW(currentHostPort, currentContainerPort, family);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The method call uses 'AddPortW' but the actual method name in WSLAContainerLauncher is 'AddPort'. This will cause a compilation error. The method should be called as 'AddPort' without the 'W' suffix.

Suggested change
containerLauncher.AddPortW(currentHostPort, currentContainerPort, family);
containerLauncher.AddPort(currentHostPort, currentContainerPort, family);

Copilot uses AI. Check for mistakes.
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.

2 participants