Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fe043b9
Portmapping: add windowsAddress support
richfr Mar 30, 2026
04a4541
Portmapping: add windowsAddress support
richfr Mar 30, 2026
cf34fb8
Addressing PR feedback: update wslcsdk.cpp
richfr Apr 1, 2026
954d1a0
Addressing clang formatting error
richfr Apr 1, 2026
31e28b4
Providing default values for port bindings: wslcsdk.cpp
richfr Apr 1, 2026
7ca8e51
Merge branch 'feature/wsl-for-apps' into richfr/portmapping
1wizkid Apr 1, 2026
7f1eca4
Merge branch 'feature/wsl-for-apps' into richfr/portmapping
1wizkid Apr 1, 2026
eff50d2
Fixed merge conflicts. Set default BindingAddress to 127.0.0.1. Valid…
richfr Apr 7, 2026
41a39eb
Added new function InetNtopToHresult() to map inet result error value…
richfr Apr 7, 2026
895a604
return family name if socket address is invalid
richfr Apr 7, 2026
a788da8
Removed unnecessasry variable bindingAddressStrings. Also, commented …
richfr Apr 7, 2026
644ebab
Fixed clang formatting errors
richfr Apr 7, 2026
47c28f2
Added WSLC Sdk API tests for WslcContainerPortMapping.windowsAddress …
richfr Apr 7, 2026
9ed0f18
Add unique name to containerSettings var to make test debugging from …
richfr Apr 7, 2026
fb9c466
Adjust AF_UNIX portmapping test so that it fails if value accepted
richfr Apr 8, 2026
13ba738
Return more accurate strncpy_s error result
richfr Apr 8, 2026
e482e48
Improved error return value of IP address verification
richfr Apr 8, 2026
1e7656a
Merge branch 'feature/wsl-for-apps' into richfr/portmapping
1wizkid Apr 8, 2026
f4fd0ba
Merge branch 'feature/wsl-for-apps' into richfr/portmapping
1wizkid Apr 8, 2026
45b6841
Merge branch 'feature/wsl-for-apps' into richfr/portmapping
1wizkid Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added diagnostics/trace01.etl
Binary file not shown.
Binary file added diagnostics/trace02.etl
Binary file not shown.
82 changes: 75 additions & 7 deletions src/windows/WslcSDK/wslcsdk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,25 @@ void EnsureAbsolutePath(const std::filesystem::path& path, bool containerPath)
}
}

HRESULT InetNtopToHresult(int af, const void* src, char* dst, size_t dstCount)
{
const char* result = inet_ntop(af, src, dst, dstCount);
if (result)
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.

I recommend throwing the win32 error directly:

THROW_WIN32_IF(WSAGetLastError(), inet_ntop(af, src, dst, dstCount) == nullptr);

{
return S_OK;
}

switch (errno)
{
case EAFNOSUPPORT:
return E_INVALIDARG;
case ENOSPC:
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
default:
return E_FAIL;
}
}

bool CopyProcessSettingsToRuntime(WSLCProcessOptions& runtimeOptions, const WslcContainerProcessOptionsInternal* initProcessOptions)
{
if (initProcessOptions)
Expand Down Expand Up @@ -617,6 +636,8 @@ try
auto result = std::make_unique<WslcContainerImpl>();

WSLCContainerOptions containerOptions{};
std::unique_ptr<WSLCPortMapping[]> convertedPorts; // this must stay in same scope as containerOptions since containerOptions.Ports is getting a raw pointer to the array owned by convertedPorts.

containerOptions.Image = internalContainerSettings->image;
containerOptions.Name = internalContainerSettings->runtimeName;
containerOptions.HostName = internalContainerSettings->HostName;
Expand Down Expand Up @@ -659,7 +680,6 @@ try
containerOptions.NamedVolumesCount = static_cast<ULONG>(internalContainerSettings->namedVolumesCount);
}

std::unique_ptr<WSLCPortMapping[]> convertedPorts;
if (internalContainerSettings->ports && internalContainerSettings->portsCount)
{
convertedPorts = std::make_unique<WSLCPortMapping[]>(internalContainerSettings->portsCount);
Expand All @@ -671,14 +691,58 @@ try
convertedPort.HostPort = internalPort.windowsPort;
convertedPort.ContainerPort = internalPort.containerPort;

// TODO: Ipv6 & custom binding address support.
convertedPort.Family = AF_INET;

// TODO: Consider using standard protocol numbers instead of our own enum.
convertedPort.Protocol = internalPort.protocol == WSLC_PORT_PROTOCOL_TCP ? IPPROTO_TCP : IPPROTO_UDP;
strcpy_s(convertedPort.BindingAddress, "127.0.0.1");
// Validate IP address if provided and if valid, copy to runtime structure.
if (internalPort.windowsAddress != nullptr)
{
char addrBuf[INET6_ADDRSTRLEN]{};
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.

Instead of having a temporary buffer, I recommend copying directly to convertedPort.BindingAddress

switch (internalPort.windowsAddress->ss_family)
{
case AF_INET:
{
const auto* addr4 = reinterpret_cast<const sockaddr_in*>(internalPort.windowsAddress);

HRESULT hr = InetNtopToHresult(AF_INET, &addr4->sin_addr, addrBuf, sizeof(addrBuf));
if (FAILED(hr))
{
THROW_HR_MSG(hr, "inet_ntop() failed for address family AF_INET");
}
convertedPort.Family = AF_INET;
break;
}

case AF_INET6:
{
const auto* addr6 = reinterpret_cast<const sockaddr_in6*>(internalPort.windowsAddress);
HRESULT hr = InetNtopToHresult(AF_INET6, &addr6->sin6_addr, addrBuf, sizeof(addrBuf));
if (FAILED(hr))
{
THROW_HR_MSG(hr, "inet_ntop() failed for address family AF_INET6");
}
convertedPort.Family = AF_INET6;
break;
}

default:
// Reject unsupported or malformed address families
THROW_HR(E_INVALIDARG);
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.

nit: I recommend logging the address family to make debugging easier

}
HRESULT hr = strncpy_s(convertedPort.BindingAddress, sizeof(convertedPort.BindingAddress), addrBuf, _TRUNCATE);

if (hr == STRUNCATE)
{
// Log this as a warning since the address is valid but was truncated to fit in the buffer, which means the port mapping may not work as expected.
}
}
else
{
// If no binding address is provided, default to local host.
convertedPort.Family = AF_INET;
strcpy_s(convertedPort.BindingAddress, "127.0.0.1");
}
}
containerOptions.Ports = convertedPorts.get();
containerOptions.Ports = convertedPorts.get(); // Make sure convertedPorts stays in scope for life of containerOptions
containerOptions.PortsCount = static_cast<ULONG>(internalContainerSettings->portsCount);
}

Expand Down Expand Up @@ -809,7 +873,11 @@ try

for (uint32_t i = 0; i < portMappingCount; ++i)
{
RETURN_HR_IF(E_NOTIMPL, portMappings[i].windowsAddress != nullptr);
if (portMappings[i].windowsAddress != nullptr)
{
const auto family = portMappings[i].windowsAddress->ss_family;
RETURN_HR_IF(E_INVALIDARG, family != AF_INET && family != AF_INET6);
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.

nit: I recommend logging the invalid address family here

}
RETURN_HR_IF(E_NOTIMPL, portMappings[i].protocol != 0);
}

Expand Down
125 changes: 114 additions & 11 deletions test/windows/WslcSdkTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -877,18 +877,18 @@ class WslcSdkTests

// Negative: port mappings with NONE networking must fail at container creation.
{
WslcContainerSettings containerSettings;
VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings, WSLC_CONTAINER_NETWORKING_MODE_NONE));
WslcContainerSettings containerSettings1;
VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings1));
VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings1, WSLC_CONTAINER_NETWORKING_MODE_NONE));

WslcContainerPortMapping mapping{};
mapping.windowsPort = 12342;
mapping.containerPort = 8000;
mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings, &mapping, 1));
VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings1, &mapping, 1));

WslcContainer rawContainer = nullptr;
VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &rawContainer, nullptr), E_INVALIDARG);
VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings1, &rawContainer, nullptr), E_INVALIDARG);
VERIFY_IS_NULL(rawContainer);
}

Expand All @@ -902,19 +902,19 @@ class WslcSdkTests
const char* env[] = {"PYTHONUNBUFFERED=1"};
VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env)));

WslcContainerSettings containerSettings;
VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings));
VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));
WslcContainerSettings containerSettings2;
VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings2));
VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings2, &procSettings));
VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings2, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));

WslcContainerPortMapping mapping{};
mapping.windowsPort = 12341;
mapping.containerPort = 8000;
mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings, &mapping, 1));
VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings2, &mapping, 1));

UniqueContainer container;
VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings2, &container, nullptr));
VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));

UniqueProcess process;
Expand All @@ -927,6 +927,109 @@ class WslcSdkTests

ExpectHttpResponse(L"http://127.0.0.1:12341", 200);
}

// Functional: port mapping with explicit IPv4 windowsAddress (127.0.0.1).
{
WslcProcessSettings procSettings;
VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
const char* argv[] = {"python3", "-m", "http.server", "8000"};
VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
const char* env[] = {"PYTHONUNBUFFERED=1"};
VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env)));

WslcContainerSettings containerSettings3;
VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings3));
VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings3, &procSettings));
VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings3, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));

sockaddr_storage addr4{};
auto* sin4 = reinterpret_cast<sockaddr_in*>(&addr4);
sin4->sin_family = AF_INET;
VERIFY_ARE_EQUAL(inet_pton(AF_INET, "127.0.0.1", &sin4->sin_addr), 1);

WslcContainerPortMapping mapping{};
mapping.windowsPort = 12343;
mapping.containerPort = 8000;
mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
mapping.windowsAddress = &addr4;
VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings3, &mapping, 1));

UniqueContainer container;
VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings3, &container, nullptr));
VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));

UniqueProcess process;
VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));

wil::unique_handle ownedStdout;
VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));

WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s);

ExpectHttpResponse(L"http://127.0.0.1:12343", 200);
}

// Functional: port mapping with explicit IPv6 windowsAddress (::1).
{
WslcProcessSettings procSettings;
VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
const char* argv[] = {"python3", "-m", "http.server", "8000", "--bind", "::"};
VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
const char* env[] = {"PYTHONUNBUFFERED=1"};
VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env)));

WslcContainerSettings containerSettings4;
VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings4));
VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings4, &procSettings));
VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings4, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));

sockaddr_storage addr6{};
auto* sin6 = reinterpret_cast<sockaddr_in6*>(&addr6);
sin6->sin6_family = AF_INET6;
VERIFY_ARE_EQUAL(inet_pton(AF_INET6, "::1", &sin6->sin6_addr), 1);

WslcContainerPortMapping mapping{};
mapping.windowsPort = 12344;
mapping.containerPort = 8000;
mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
mapping.windowsAddress = &addr6;
VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings4, &mapping, 1));

UniqueContainer container;
VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings4, &container, nullptr));
VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));

UniqueProcess process;
VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));

wil::unique_handle ownedStdout;
VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));

WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s);

ExpectHttpResponse(L"http://[::1]:12344", 200);
}

// Negative: unsupported address family must fail at container creation.
{
WslcContainerSettings containerSettings5;
VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings5));
VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings5, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));

sockaddr_storage badAddr{};
badAddr.ss_family = AF_UNIX; // unsupported for port mapping

WslcContainerPortMapping mapping{};
mapping.windowsPort = 12345;
mapping.containerPort = 8000;
mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
mapping.windowsAddress = &badAddr;
VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings5, &mapping, 1));

WslcContainer rawContainer = nullptr;
VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings5, &rawContainer, nullptr), E_INVALIDARG);
VERIFY_IS_NULL(rawContainer);
}
}

TEST_METHOD(ContainerVolumeUnit)
Expand Down
Loading