Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
112 changes: 79 additions & 33 deletions src/windows/common/VirtioNetworking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int
return S_OK;
}

int result = 0;
const auto ipAddress = (addr.si_family == AF_INET) ? reinterpret_cast<const void*>(&addr.Ipv4.sin_addr)
: reinterpret_cast<const void*>(&addr.Ipv6.sin6_addr);
const bool loopback = INET_IS_ADDR_LOOPBACK(addr.si_family, ipAddress);
Expand All @@ -111,10 +110,12 @@ HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int
// Only intercepting 127.0.0.1; any other loopback address will remain on 'lo'.
if (addr.Ipv4.sin_addr.s_addr != htonl(INADDR_LOOPBACK))
{
return result;
return S_OK;
}
}

const auto guestPort = INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&addr));

if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::LocalhostRelay) && (unspecified || loopback))
{
SOCKADDR_INET localAddr = addr;
Expand All @@ -130,57 +131,102 @@ HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int
localAddr.Ipv6.sin6_port = addr.Ipv6.sin6_port;
}
}
result = ModifyOpenPorts(c_loopbackDeviceName, localAddr, protocol, allocate);
LOG_HR_IF_MSG(
E_FAIL, result != S_OK, "Failure adding localhost relay port %d", INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&localAddr)));

try
{
const auto addrStr = wsl::windows::common::string::SockAddrInetToString(localAddr);
ModifyOpenPorts(c_loopbackDeviceName, addrStr.c_str(), guestPort, guestPort, protocol, allocate);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION_MSG("Failure adding localhost relay port %d", guestPort);
}
}

if (!loopback)
{
const int localResult = ModifyOpenPorts(c_eth0DeviceName, addr, protocol, allocate);
LOG_HR_IF_MSG(E_FAIL, localResult != S_OK, "Failure adding relay port %d", INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&addr)));
if (result == 0)
try
{
const auto addrStr = wsl::windows::common::string::SockAddrInetToString(addr);
ModifyOpenPorts(c_eth0DeviceName, addrStr.c_str(), guestPort, guestPort, protocol, allocate);
}
catch (...)
{
result = localResult;
LOG_CAUGHT_EXCEPTION_MSG("Failure adding relay port %d", guestPort);
}
}

return result;
return S_OK;
}

int VirtioNetworking::ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& addr, _In_ int protocol, _In_ bool isOpen) const
uint16_t VirtioNetworking::ModifyOpenPorts(
_In_ PCWSTR tag, _In_opt_ PCSTR hostAddress, _In_ uint16_t HostPort, _In_ uint16_t GuestPort, _In_ int protocol, _In_ bool isOpen) const
{
if (protocol != IPPROTO_TCP && protocol != IPPROTO_UDP)
{
LOG_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported bind protocol %d", protocol);
return 0;
}
THROW_HR_IF_MSG(
HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
protocol != IPPROTO_TCP && protocol != IPPROTO_UDP,
"Unsupported bind protocol %d",
protocol);

auto lock = m_lock.lock_exclusive();
const auto server = m_guestDeviceManager->GetRemoteFileSystem(VIRTIO_NET_CLASS_ID, c_defaultDeviceTag);
if (server)
THROW_HR_IF(E_NOT_SET, !server);

// format: tag={tag}[;host_port={port}];guest_port={port}[;listen_addr={addr}|;allocate=false][;udp]
std::wstring portString = std::format(L"tag={};guest_port={};listen_addr={}", tag, GuestPort, hostAddress);

Comment on lines +175 to +177
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

listen_addr is interpolated into a semicolon-delimited options string that gets parsed on the guest side. Since MapVirtioNetPort’s ListenAddress ultimately comes from a caller-supplied string, it would be safer to validate/normalize it to a real IPv4/IPv6 address (e.g., via inet_pton/InetPton) and reject values containing ';' or other delimiters so a malformed address can’t perturb option parsing.

Copilot uses AI. Check for mistakes.
if (HostPort != WSLC_EPHEMERAL_PORT)
{
std::wstring portString = std::format(L"tag={};port_number={}", tag, INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&addr)));
if (protocol == IPPROTO_UDP)
{
portString += L";udp";
}
portString += std::format(L";host_port={}", HostPort);
}

if (!isOpen)
{
portString += L";allocate=false";
}
else
{
const auto addrStr = wsl::windows::common::string::SockAddrInetToWstring(addr);
portString += std::format(L";listen_addr={}", addrStr);
}
if (!isOpen)
{
portString += L";allocate=false";
}
Comment on lines +175 to +186
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

ModifyOpenPorts always includes a listen_addr field in the options string ("...;listen_addr={}") even when isOpen==false (allocate=false). The comment above indicates listen_addr and allocate=false are mutually exclusive; emitting both (or emitting listen_addr with an empty value) risks breaking close/unmap behavior. Build the options string so listen_addr is only included for open mappings, and use allocate=false without listen_addr for closes if that’s what the guest expects.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems legit


if (protocol == IPPROTO_UDP)
{
portString += L";udp";
}

const HRESULT addShareResult = server->AddShare(portString.c_str(), nullptr, 0);

LOG_IF_FAILED(server->AddShare(portString.c_str(), nullptr, 0));
if (HostPort == WSLC_EPHEMERAL_PORT && isOpen && SUCCEEDED(addShareResult))
{
Comment on lines +195 to +196
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

For ephemeral binds, this interprets IPlan9FileSystem::AddShare's HRESULT success code as S_OK + allocatedPort. This is a nonstandard encoding for HRESULT and could misbehave for success codes like S_FALSE or other non-zero successes. Consider returning the allocated port through an explicit out-param/response channel, or at least range-check that addShareResult is within [S_OK, S_OK + 65535] before decoding.

Suggested change
if (HostPort == WSLC_EPHEMERAL_PORT && isOpen && SUCCEEDED(addShareResult))
{
if (HostPort == WSLC_EPHEMERAL_PORT && isOpen)
{
THROW_IF_FAILED_MSG(addShareResult, "Failed to set virtionet port mapping: %ls", portString.c_str());
constexpr HRESULT c_maxEncodedEphemeralPortResult = S_OK + UINT16_MAX;
THROW_HR_IF_MSG(
E_UNEXPECTED,
addShareResult < S_OK || addShareResult > c_maxEncodedEphemeralPortResult,
"Unexpected AddShare success code for ephemeral port mapping: 0x%08x (%ls)",
static_cast<unsigned int>(addShareResult),
portString.c_str());

Copilot uses AI. Check for mistakes.
// For anonymous binds, the allocated host port is encoded in the return value.
return static_cast<uint16_t>(addShareResult - S_OK);
}

return 0;
THROW_IF_FAILED_MSG(addShareResult, "Failed to set virtionet port mapping: %ls", portString.c_str());
return HostPort;
}

HRESULT VirtioNetworking::MapPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ PCSTR ListenAddress, _Out_ USHORT* AllocatedHostPort) const
try
{
RETURN_HR_IF(E_POINTER, AllocatedHostPort == nullptr || ListenAddress == nullptr);
RETURN_HR_IF_MSG(E_INVALIDARG, Protocol != IPPROTO_TCP && Protocol != IPPROTO_UDP, "Invalid protocol: %i", Protocol);

*AllocatedHostPort = 0;

*AllocatedHostPort = ModifyOpenPorts(c_eth0DeviceName, ListenAddress, HostPort, GuestPort, Protocol, true);
return S_OK;
}
CATCH_RETURN()

HRESULT VirtioNetworking::UnmapPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ PCSTR ListenAddress) const
try
{
RETURN_HR_IF(E_POINTER, ListenAddress == nullptr);
RETURN_HR_IF(E_INVALIDARG, Protocol != IPPROTO_TCP && Protocol != IPPROTO_UDP);

const auto listenAddrW = wsl::shared::string::MultiByteToWide(ListenAddress);

ModifyOpenPorts(c_eth0DeviceName, nullptr, HostPort, GuestPort, Protocol, false);
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

VirtioNetworking::UnmapPort ignores the provided ListenAddress (it converts it to wide but then never uses it) and calls ModifyOpenPorts with hostAddress=nullptr. This produces a portString with an empty listen_addr, which is unlikely to match the mapping created by MapPort and may cause unmap to fail or leak the mapping. Pass the same ListenAddress through to ModifyOpenPorts (or, if listen_addr should not be present for allocate=false, update ModifyOpenPorts to omit listen_addr when isOpen==false and ensure unmap uses the expected format).

Suggested change
ModifyOpenPorts(c_eth0DeviceName, nullptr, HostPort, GuestPort, Protocol, false);
ModifyOpenPorts(c_eth0DeviceName, listenAddrW.c_str(), HostPort, GuestPort, Protocol, false);

Copilot uses AI. Check for mistakes.
return S_OK;
}
CATCH_RETURN()

void VirtioNetworking::RefreshGuestConnection()
{
Expand Down
6 changes: 5 additions & 1 deletion src/windows/common/VirtioNetworking.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,15 @@ class VirtioNetworking : public INetworkingEngine
void FillInitialConfiguration(LX_MINI_INIT_NETWORKING_CONFIGURATION& message) override;
void StartPortTracker(wil::unique_socket&& socket) override;

HRESULT MapPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ PCSTR ListenAddress, _Out_ USHORT* AllocatedHostPort) const;

HRESULT UnmapPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ PCSTR ListenAddress) const;

private:
static void NETIOAPI_API_ OnNetworkConnectivityChange(PVOID context, NL_NETWORK_CONNECTIVITY_HINT hint);

HRESULT HandlePortNotification(const SOCKADDR_INET& addr, int protocol, bool allocate) const noexcept;
int ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& addr, _In_ int protocol, _In_ bool isOpen) const;
uint16_t ModifyOpenPorts(_In_ PCWSTR tag, _In_opt_ PCSTR hostAddress, _In_ uint16_t HostPort, _In_ uint16_t GuestPort, _In_ int protocol, _In_ bool isOpen) const;
void RefreshGuestConnection();
void SetupLoopbackDevice();
void SendDefaultRoute(const std::wstring& gateway, wsl::shared::hns::ModifyRequestType requestType);
Expand Down
30 changes: 30 additions & 0 deletions src/windows/service/exe/HcsVirtualMachine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,36 @@ try
}
CATCH_RETURN()

HRESULT HcsVirtualMachine::MapVirtioNetPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress, _Out_ USHORT* AllocatedHostPort)
try
{
RETURN_HR_IF(E_POINTER, AllocatedHostPort == nullptr || ListenAddress == nullptr);

*AllocatedHostPort = 0;

std::lock_guard lock(m_lock);

auto* virtioNet = dynamic_cast<wsl::core::VirtioNetworking*>(m_networkEngine.get());
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), virtioNet == nullptr);

return virtioNet->MapPort(HostPort, GuestPort, Protocol, ListenAddress, AllocatedHostPort);
}
CATCH_RETURN()

HRESULT HcsVirtualMachine::UnmapVirtioNetPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress)
try
{
RETURN_HR_IF(E_POINTER, ListenAddress == nullptr);

std::lock_guard lock(m_lock);

auto* virtioNet = dynamic_cast<wsl::core::VirtioNetworking*>(m_networkEngine.get());
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), virtioNet == nullptr);

return virtioNet->UnmapPort(HostPort, GuestPort, Protocol, ListenAddress);
}
CATCH_RETURN()

void CALLBACK HcsVirtualMachine::OnVmExitCallback(HCS_EVENT* Event, void* Context)
try
{
Expand Down
4 changes: 4 additions & 0 deletions src/windows/service/exe/HcsVirtualMachine.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ class HcsVirtualMachine
IFACEMETHOD(DetachDisk)(_In_ ULONG Lun) override;
IFACEMETHOD(AddShare)(_In_ LPCWSTR WindowsPath, _In_ BOOL ReadOnly, _Out_ GUID* ShareId) override;
IFACEMETHOD(RemoveShare)(_In_ REFGUID ShareId) override;
IFACEMETHOD(MapVirtioNetPort)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think for naming just MapPort and UnmapPort would be better.

(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress, _Out_ USHORT* AllocatedHostPort) override;
IFACEMETHOD(UnmapVirtioNetPort)
(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress) override;

private:
struct DiskInfo
Expand Down
21 changes: 21 additions & 0 deletions src/windows/service/inc/wslc.idl
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ cpp_quote("#endif")
#define WSLC_MAX_VOLUME_NAME_LENGTH 255
#define WSLC_CONTAINER_ID_LENGTH 64
#define WSLC_MAX_BINDING_ADDRESS_LENGTH 45
#define WSLC_EPHEMERAL_PORT 0

cpp_quote("#define WSLC_MAX_CONTAINER_NAME_LENGTH 255")
cpp_quote("#define WSLC_MAX_IMAGE_NAME_LENGTH 255")
cpp_quote("#define WSLC_MAX_VOLUME_NAME_LENGTH 255")
cpp_quote("#define WSLC_CONTAINER_ID_LENGTH 64")
cpp_quote("#define WSLC_MAX_BINDING_ADDRESS_LENGTH 45")
cpp_quote("#define WSLC_EPHEMERAL_PORT 0")

typedef
struct _WSLCVersion {
Expand Down Expand Up @@ -425,6 +427,25 @@ interface IWSLCVirtualMachine : IUnknown

// Removes a previously added filesystem share.
HRESULT RemoveShare([in] REFGUID ShareId);

// Maps a port via VirtioNetworking.
// For anonymous binds (HostPort == WSLC_EPHEMERAL_PORT), the networking engine allocates a host port
// and returns it in AllocatedHostPort.
// Protocol must be IPPROTO_TCP or IPPROTO_UDP.
// ListenAddress is the IP address to bind on (e.g. "127.0.0.1", "0.0.0.0", "::1").
HRESULT MapVirtioNetPort(
[in] USHORT HostPort,
[in] USHORT GuestPort,
[in] int Protocol,
[in] LPCSTR ListenAddress,
[out, retval] USHORT* AllocatedHostPort);

// Unmaps a port previously mapped via MapVirtioNetPort.
HRESULT UnmapVirtioNetPort(
[in] USHORT HostPort,
[in] USHORT GuestPort,
[in] int Protocol,
[in] LPCSTR ListenAddress);
}

typedef enum _WSLCSessionStorageFlags
Expand Down
22 changes: 8 additions & 14 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,27 +54,21 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Session& sessio
{
auto portMapping = PublishPort::Parse(port);

const int protocol = portMapping.PortProtocol() == PublishPort::Protocol::UDP ? IPPROTO_UDP : IPPROTO_TCP;
const int family = (portMapping.HostIP().has_value() && portMapping.HostIP()->IsIPv6()) ? AF_INET6 : AF_INET;
std::optional<std::string> bindAddress;
if (portMapping.HostIP().has_value())
{
// https://github.com/microsoft/WSL/issues/14433
// The following scenarios are currently not implemented:
// - Ephemeral host port mappings
// - Host port mappings with a specific host IP
// - Host port mappings with UDP protocol
if (portMapping.HostPort().IsEphemeral() || portMapping.HostIP().has_value() ||
portMapping.PortProtocol() == PublishPort::Protocol::UDP)
{
THROW_HR_WITH_USER_ERROR(
HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
"Port mappings with ephemeral host ports, specific host IPs, or UDP protocol are not currently supported");
}
bindAddress = portMapping.HostIP()->IP();
}

auto containerPort = portMapping.ContainerPort();
for (uint16_t i = 0; i < containerPort.Count(); ++i)
{
auto currentContainerPort = static_cast<uint16_t>(containerPort.Start() + i);
auto currentHostPort = static_cast<uint16_t>(portMapping.HostPort().Start() + i);
containerLauncher.AddPort(currentHostPort, currentContainerPort, AF_INET);
auto currentHostPort = portMapping.HostPort().IsEphemeral() ? static_cast<uint16_t>(WSLC_EPHEMERAL_PORT)
: static_cast<uint16_t>(portMapping.HostPort().Start() + i);
containerLauncher.AddPort(currentHostPort, currentContainerPort, family, protocol, bindAddress);
}
}

Expand Down
33 changes: 23 additions & 10 deletions src/windows/wslcsession/WSLCVirtualMachine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,19 @@ uint16_t VMPortMapping::HostPort() const
}
}

void VMPortMapping::SetHostPort(uint16_t port)
{
if (BindAddress.si_family == AF_INET6)
{
BindAddress.Ipv6.sin6_port = htons(port);
}
else
{
WI_ASSERT(BindAddress.si_family == AF_INET);
BindAddress.Ipv4.sin_port = htons(port);
}
}

std::string VMPortMapping::BindingAddressString() const
{
char buffer[INET6_ADDRSTRLEN]{};
Expand Down Expand Up @@ -890,15 +903,15 @@ void WSLCVirtualMachine::MapPort(VMPortMapping& Mapping)
}
else if (m_networkingMode == WSLCNetworkingModeVirtioProxy)
{
// TODO: Switch to using the native virtionet relay.
THROW_HR_IF_MSG(
HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
!Mapping.IsLocalhost() || Mapping.Protocol != IPPROTO_TCP,
"Unsupported port mapping for virtionet mode: %hs, protocol: %i",
Mapping.BindingAddressString().c_str(),
Mapping.Protocol);
USHORT allocatedHostPort = 0;
THROW_IF_FAILED(m_vm->MapVirtioNetPort(
Mapping.HostPort(), Mapping.VmPort->Port(), Mapping.Protocol, Mapping.BindingAddressString().c_str(), &allocatedHostPort));

MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), false);
// For anonymous binds, write back the allocated host port.
if (Mapping.HostPort() == WSLC_EPHEMERAL_PORT && allocatedHostPort != 0)
{
Mapping.SetHostPort(allocatedHostPort);
}
}
else
{
Expand All @@ -922,8 +935,8 @@ void WSLCVirtualMachine::UnmapPort(VMPortMapping& Mapping)
}
else if (m_networkingMode == WSLCNetworkingModeVirtioProxy)
{
// TODO: Switch to using the native virtionet relay.
MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), true);
THROW_IF_FAILED(m_vm->UnmapVirtioNetPort(
Mapping.HostPort(), Mapping.VmPort->Port(), Mapping.Protocol, Mapping.BindingAddressString().c_str()));
}
else
{
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslcsession/WSLCVirtualMachine.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ struct VMPortMapping
void Attach(WSLCVirtualMachine& Vm);
void Detach();
uint16_t HostPort() const;
void SetHostPort(uint16_t port);

static VMPortMapping LocalhostTcpMapping(int Family, uint16_t WindowsPort);
static VMPortMapping FromWSLCPortMapping(const ::WSLCPortMapping& Mapping);
Expand Down
Loading