Use the virtionet port mapper for container port bindings (#40145)

* virtiofs port mapping poc * Format * Save state * save state * save state * save state * Save state * Format * Save state * Save state * Save state * Save state * Save state * Save state * Save state * Format * Restore workaround * Save state * Save state * merge * Save state * Disable ipv6 DAD in virtionet mode * Apply PR feedback * Cleanup diff * Save state * Don't set the localhost flag is the legacy relay is requested * Cleanup diff * Format * Update tests * Format * Remove GH comment * Apply PR feedback * Apply PR feedback * Restore netmask * Fix winrt test * Save state * Use client_ip=127.0.0.1 for WSL * Update comment

Blue committed Jun 22, 2026 at 15:30 UTC 9b8beedfb5b5d092c6fa13288d7cec218ae6076c
34 files changed +893 -140
localization/strings/en-US/Resources.resw
+4
@@ -2062,6 +2062,10 @@ Usage:
2062 <value>Invalid IP address '{}'</value>
2063 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2064 </data>
2065 + <data name="MessageFailedToMapPort" xml:space="preserve">
2066 + <value>Failed to map port '{}', {}</value>
2067 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2068 + </data>
2069 <data name="MessageWslcOpenSessionFailed" xml:space="preserve">
2070 <value>OpenSessionByName('{}') failed</value>
2071 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
packages.config
+1 -1
@@ -19,7 +19,7 @@
19 <package id="Microsoft.WSL.bsdtar" version="0.0.2-2" />
20 <package id="Microsoft.WSL.Dependencies.amd64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
21 <package id="Microsoft.WSL.Dependencies.arm64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
22 - <package id="Microsoft.WSL.DeviceHost" version="1.2.34-0" />
22 + <package id="Microsoft.WSL.DeviceHost" version="1.2.36-0" />
23 <package id="Microsoft.WSL.Kernel" version="6.18.35.1-1" targetFramework="native" />
24 <package id="Microsoft.WSL.LinuxSdk" version="1.20.0" targetFramework="native" />
25 <package id="Microsoft.WSL.TestData" version="0.4.0" />
src/linux/init/GnsEngine.cpp
+2 -1
@@ -488,7 +488,8 @@ std::tuple<bool, int> GnsEngine::ProcessNextMessage(wsl::shared::Transaction& tr
488 "LxGnsMessageCreateDeviceRequest [Loopback]: InitializeLoopbackConfiguration deviceName {}, interfaceName {}",
489 wsl::shared::string::GuidToString<char>(createDeviceRequest.lowerEdgeAdapterId.value_or(emptyGuid)).c_str(),
490 gelnic.Name().c_str());
491 - manager.InitializeLoopbackConfiguration(gelnic);
491 + manager.InitializeLoopbackConfiguration(gelnic, createDeviceRequest.flags);
492 +
493 break;
494 }
495 default:
src/linux/init/NetworkManager.cpp
+12 -2
@@ -281,8 +281,18 @@ void NetworkManager::EnableLoopbackRouting(Interface& interface)
281 loopback interface. Every packet that arrives in the guest having a loopback destination address will
282 arrive on the GELNIC.
283 */
284 -void NetworkManager::InitializeLoopbackConfiguration(Interface& gelnic)
284 +void NetworkManager::InitializeLoopbackConfiguration(Interface& gelnic, wsl::shared::hns::CreateDeviceFlags flags)
285 {
286 + if (WI_IsFlagSet(flags, wsl::shared::hns::CreateDeviceFlags::DisableDAD))
287 + {
288 + gelnic.DisableNetworkSetting("accept_dad", AF_INET6);
289 + gelnic.DisableNetworkSetting("dad_transmits", AF_INET6);
290 +
291 + // Toggle ipv6 to reset our temporary address.
292 + gelnic.EnableNetworkSetting("disable_ipv6", AF_INET6);
293 + gelnic.DisableNetworkSetting("disable_ipv6", AF_INET6);
294 + }
295 +
296 // Enable routing of IPv4 loopback on the GELNIC.
297 GNS_LOG_INFO("Enabling IPv4 loopback routing on GELNIC adapter {}", gelnic.Name().c_str());
298 EnableLoopbackRouting(gelnic);
@@ -535,4 +545,4 @@ void NetworkManager::EnableIpv4ArpFilter()
545 wsl::shared::conncheck::ConnCheckResult NetworkManager::SendConnectRequest(const char* remoteAddress)
546 {
547 return wsl::shared::conncheck::CheckConnection(remoteAddress, nullptr, "80");
538 -}
\ No newline at end of file
548 +}
src/linux/init/NetworkManager.h
+1 -1
@@ -57,7 +57,7 @@ public:
57
58 void EnableLoopbackRouting(Interface& interface);
59
60 - void InitializeLoopbackConfiguration(Interface& gelnic);
60 + void InitializeLoopbackConfiguration(Interface& gelnic, wsl::shared::hns::CreateDeviceFlags flags);
61
62 void AddMirroredLoopbackRoutingRules(Interface& gelnic, int addressFamily);
63
src/shared/inc/hns_schema.h
+12 -3
@@ -300,19 +300,28 @@ NLOHMANN_JSON_SERIALIZE_ENUM(
300 {DeviceType::VirtualCellular, "VirtualCellular"},
301 })
302
303 +enum class CreateDeviceFlags
304 +{
305 + None = 0,
306 + DisableDAD = 1
307 +};
308 +
309 +DEFINE_ENUM_FLAG_OPERATORS(CreateDeviceFlags);
310 +
311 struct CreateDeviceRequest
312 {
313 DeviceType type{};
314 std::wstring deviceName;
315 std::optional<GUID> lowerEdgeAdapterId;
316 std::optional<std::wstring> lowerEdgeDeviceName;
317 + CreateDeviceFlags flags{CreateDeviceFlags::None};
318 };
319
311 -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_FROM_ONLY(CreateDeviceRequest, type, deviceName, lowerEdgeAdapterId, lowerEdgeDeviceName);
320 +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT_FROM_ONLY(CreateDeviceRequest, type, deviceName, lowerEdgeAdapterId, lowerEdgeDeviceName, flags);
321
322 inline void to_json(nlohmann::json& j, const CreateDeviceRequest& request)
323 {
315 - j = nlohmann::json{{"type", request.type}, {"deviceName", request.deviceName}};
324 + j = nlohmann::json{{"type", request.type}, {"deviceName", request.deviceName}, {"flags", request.flags}};
325
326 if (request.lowerEdgeAdapterId.has_value())
327 {
@@ -542,4 +551,4 @@ struct ModifyGuestNetworkServiceSettingRequest
551 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ModifyGuestNetworkServiceSettingRequest, ResourceType, RequestType, Settings);
552 };
553
545 -} // namespace wsl::shared::hns
\ No newline at end of file
554 +} // namespace wsl::shared::hns
src/shared/inc/lxinitshared.h
+1 -1
@@ -78,7 +78,7 @@ Abstract:
78 //
79 // The hard-coded link-local addresses used for communicating over the loopback to the host
80 //
81 -#define LX_INIT_IPV4_LOOPBACK_GATEWAY_ADDRESS "169.254.73.152"
81 +#define LX_INIT_IPV4_LOOPBACK_GATEWAY_ADDRESS "169.254.73.249"
82 #define LX_INIT_IPV6_LOOPBACK_GATEWAY_ADDRESS "fe80::500:4aef:feef:2aa2"
83
84 //
src/windows/common/HandleIO.cpp
+1 -1
@@ -1269,7 +1269,7 @@ void WriteHandle::Schedule()
1269 else
1270 {
1271 auto error = GetLastError();
1272 - THROW_LAST_ERROR_IF_MSG(error != ERROR_IO_PENDING, "Handle: 0x%p", (void*)Handle.Get());
1272 + THROW_LAST_ERROR_IF_MSG(error != ERROR_IO_PENDING, "Handle: 0x%p, size: %zu", (void*)Handle.Get(), buffer.size());
1273
1274 // The write is pending, update to 'Pending'
1275 State = IOHandleStatus::Pending;
src/windows/common/VirtioNetworking.cpp
+108 -37
@@ -73,7 +73,11 @@ void VirtioNetworking::StartPortTracker(wil::unique_socket&& socket)
73
74 m_gnsPortTrackerChannel.emplace(
75 std::move(socket),
76 - [&](const SOCKADDR_INET& addr, int protocol, bool allocate) { return HandlePortNotification(addr, protocol, allocate); },
76 + [&](const SOCKADDR_INET& addr, int protocol, bool allocate) {
77 + return wil::ResultFromException([&]() {
78 + HandlePortNotification(addr, protocol, INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&addr)), allocate);
79 + });
80 + },
81 [](const std::string&, bool) {}); // TODO: reconsider if InterfaceStateCallback is needed.
82 }
83
@@ -84,14 +88,13 @@ try
88 }
89 CATCH_LOG()
90
87 -HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int protocol, bool allocate) const noexcept
91 +uint16_t VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int protocol, uint16_t guestPort, bool allocate) const
92 {
93 if (addr.si_family == AF_INET6 && WI_IsFlagClear(m_flags, VirtioNetworkingFlags::Ipv6))
94 {
91 - return S_OK;
95 + return 0;
96 }
97
94 - int result = 0;
98 const auto ipAddress = (addr.si_family == AF_INET) ? reinterpret_cast<const void*>(&addr.Ipv4.sin_addr)
99 : reinterpret_cast<const void*>(&addr.Ipv6.sin6_addr);
100 const bool loopback = INET_IS_ADDR_LOOPBACK(addr.si_family, ipAddress);
@@ -101,13 +104,22 @@ HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int
104 // Only intercepting 127.0.0.1; any other loopback address will remain on 'lo'.
105 if (addr.Ipv4.sin_addr.s_addr != htonl(INADDR_LOOPBACK))
106 {
104 - return result;
107 + return 0;
108 }
109 }
110 + SOCKADDR_INET localAddr = addr;
111 + std::function<void()> removePort;
112 +
113 + auto hostPort = INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&addr));
114 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&removePort]() {
115 + if (removePort)
116 + {
117 + removePort();
118 + }
119 + });
120
121 if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::LocalhostRelay) && (unspecified || loopback))
122 {
110 - SOCKADDR_INET localAddr = addr;
123 if (!loopback)
124 {
125 INETADDR_SETLOOPBACK(reinterpret_cast<PSOCKADDR>(&localAddr));
@@ -120,50 +132,92 @@ HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int
132 localAddr.Ipv6.sin6_port = addr.Ipv6.sin6_port;
133 }
134 }
123 - result = ModifyOpenPorts(c_loopbackDeviceName, localAddr, protocol, allocate);
124 - LOG_HR_IF_MSG(
125 - E_FAIL, result != S_OK, "Failure adding localhost relay port %d", INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&localAddr)));
135 +
136 + hostPort = ModifyOpenPorts(c_loopbackDeviceName, localAddr, hostPort, guestPort, protocol, allocate);
137 +
138 + // Revert the change on failure.
139 + removePort = [&]() { ModifyOpenPorts(c_loopbackDeviceName, localAddr, hostPort, guestPort, protocol, !allocate); };
140 }
141
142 if (!loopback)
143 {
130 - const int localResult = ModifyOpenPorts(c_eth0DeviceName, addr, protocol, allocate);
131 - LOG_HR_IF_MSG(E_FAIL, localResult != S_OK, "Failure adding relay port %d", INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&addr)));
132 - if (result == 0)
133 - {
134 - result = localResult;
135 - }
144 + hostPort = ModifyOpenPorts(c_eth0DeviceName, addr, hostPort, guestPort, protocol, allocate);
145 }
146
138 - return result;
147 + cleanup.release();
148 +
149 + return hostPort;
150 }
151
141 -int VirtioNetworking::ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& addr, _In_ int protocol, _In_ bool isOpen) const
152 +uint16_t VirtioNetworking::ModifyOpenPorts(
153 + _In_ PCWSTR tag, _In_ const SOCKADDR_INET& hostAddress, _In_ uint16_t HostPort, _In_ uint16_t GuestPort, _In_ int protocol, _In_ bool isOpen) const
154 {
143 - if (protocol != IPPROTO_TCP && protocol != IPPROTO_UDP)
144 - {
145 - LOG_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported bind protocol %d", protocol);
146 - return 0;
147 - }
155 + THROW_HR_IF_MSG(
156 + HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
157 + protocol != IPPROTO_TCP && protocol != IPPROTO_UDP,
158 + "Unsupported bind protocol %d",
159 + protocol);
160
161 auto lock = m_lock.lock_exclusive();
162 const auto server = m_guestDeviceManager->GetRemoteFileSystem(VIRTIO_NET_CLASS_ID, c_defaultDeviceTag);
151 - if (server)
163 + THROW_HR_IF(E_UNEXPECTED, !server);
164 +
165 + const auto hostAddressStr = wsl::windows::common::string::SockAddrInetToString(hostAddress);
166 +
167 + std::wstring portString = std::format(L"tag={};guest_port={};listen_addr={}", tag, GuestPort, hostAddressStr.c_str());
168 +
169 + if (HostPort != WSLC_EPHEMERAL_PORT)
170 {
153 - std::wstring portString = std::format(L"tag={};port_number={}", tag, INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&addr)));
154 - if (protocol == IPPROTO_UDP)
155 - {
156 - portString += L";udp";
157 - }
171 + portString += std::format(L";host_port={}", HostPort);
172 + }
173 +
174 + if (!isOpen)
175 + {
176 + portString += L";allocate=false";
177 + }
178 +
179 + if (protocol == IPPROTO_UDP)
180 + {
181 + portString += L";udp";
182 + }
183
159 - const auto addrStr = wsl::windows::common::string::SockAddrInetToWstring(addr);
160 - portString += std::format(L";listen_addr={};allocate={}", addrStr, isOpen ? L"true" : L"false");
184 + const HRESULT addShareResult = server->AddShare(portString.c_str(), nullptr, 0);
185 + WSL_LOG("MapVirtioPort", TraceLoggingValue(portString.c_str(), "PortString"), TraceLoggingValue(addShareResult, "Result"));
186
162 - LOG_IF_FAILED(server->AddShare(portString.c_str(), nullptr, 0));
187 + if (HostPort == WSLC_EPHEMERAL_PORT && isOpen && SUCCEEDED(addShareResult))
188 + {
189 + // For anonymous binds, the allocated host port is encoded in the return value.
190 + return static_cast<uint16_t>(addShareResult - S_OK);
191 }
192
165 - return 0;
193 + THROW_IF_FAILED_MSG(addShareResult, "Failed to set virtionet port mapping: %ls", portString.c_str());
194 + return HostPort;
195 +}
196 +
197 +HRESULT VirtioNetworking::MapPort(_In_ const SOCKADDR_INET& ListenAddress, _In_ USHORT GuestPort, _In_ int Protocol, _Out_ USHORT* AllocatedHostPort) const
198 +try
199 +{
200 + RETURN_HR_IF(E_POINTER, AllocatedHostPort == nullptr);
201 + RETURN_HR_IF_MSG(E_INVALIDARG, Protocol != IPPROTO_TCP && Protocol != IPPROTO_UDP, "Invalid protocol: %i", Protocol);
202 +
203 + *AllocatedHostPort = 0;
204 +
205 + *AllocatedHostPort = HandlePortNotification(ListenAddress, Protocol, GuestPort, true);
206 +
207 + return S_OK;
208 +}
209 +CATCH_RETURN()
210 +
211 +HRESULT VirtioNetworking::UnmapPort(_In_ const SOCKADDR_INET& ListenAddress, _In_ USHORT GuestPort, _In_ int Protocol) const
212 +try
213 +{
214 + RETURN_HR_IF(E_INVALIDARG, Protocol != IPPROTO_TCP && Protocol != IPPROTO_UDP);
215 +
216 + HandlePortNotification(ListenAddress, Protocol, GuestPort, false);
217 +
218 + return S_OK;
219 }
220 +CATCH_RETURN()
221
222 void VirtioNetworking::RefreshGuestConnection()
223 {
@@ -178,6 +232,14 @@ void VirtioNetworking::RefreshGuestConnection()
232 }
233 };
234
235 + ULONG net_mask{};
236 + if (ConvertLengthToIpv4Mask(networkSettings->PreferredIpAddress.PrefixLength, &net_mask) == 0)
237 + {
238 + auto net_mask_string =
239 + std::format(L"{}.{}.{}.{}", net_mask & 0xFF, (net_mask >> 8) & 0xFF, (net_mask >> 16) & 0xFF, (net_mask >> 24) & 0xFF);
240 + appendOption(L"netmask", net_mask_string);
241 + }
242 +
243 appendOption(L"client_ip", networkSettings->PreferredIpAddress.AddressString);
244 std::wstring default_route = networkSettings->GetBestGatewayAddressString();
245 appendOption(L"gateway_ip", default_route);
@@ -206,6 +268,8 @@ void VirtioNetworking::RefreshGuestConnection()
268 // Add virtio net adapter to guest. If the adapter already exists update adapter state.
269 if (device_options != m_trackedDeviceOptions)
270 {
271 +
272 + WSL_LOG("RefreshVirtioNetConnection", TraceLoggingValue(device_options.c_str(), "DeviceOptions"));
273 if (!m_adapterId.has_value())
274 {
275 m_adapterId = m_guestDeviceManager->AddGuestDevice(
@@ -245,23 +309,27 @@ void VirtioNetworking::RefreshGuestConnection()
309
310 void VirtioNetworking::SetupLoopbackDevice()
311 {
312 + const auto* clientIp = WI_IsFlagSet(m_flags, VirtioNetworkingFlags::LoopbackClientIp) ? L"127.0.0.1" : L"169.254.73.250";
313 + const auto deviceOptions =
314 + std::format(L"client_ip={};client_mac=00:11:22:33:44:55;gateway_ip=169.254.73.249;netmask=255.255.255.248", clientIp);
315 +
316 m_localhostAdapterId = m_guestDeviceManager->AddGuestDevice(
317 VIRTIO_NET_DEVICE_ID,
318 VIRTIO_NET_CLASS_ID,
319 c_loopbackDeviceName,
320 m_swiotlbOption.c_str(),
253 - L"client_ip=127.0.0.1;client_mac=00:11:22:33:44:55",
321 + deviceOptions.c_str(),
322 0,
323 m_userToken.get());
324
257 - // The loopback gateway (see LX_INIT_IPV4_LOOPBACK_GATEWAY_ADDRESS) is 169.254.73.152, so assign loopback0 an
258 - // address of 169.254.73.153 with a netmask of 30 so that the only addresses associated with this adapter are
325 + // The loopback gateway (see LX_INIT_IPV4_LOOPBACK_GATEWAY_ADDRESS) is 169.254.73.249, so assign loopback0 an
326 + // address of 169.254.73.153 with a netmask of 29 so that the only addresses associated with this adapter are
327 // itself and the gateway.
328 // N.B. The MAC address is advertised with the virtio device so doesn't need to be explicitly set.
329 hns::HNSEndpoint endpointProperties;
330 endpointProperties.ID = m_localhostAdapterId.value();
263 - endpointProperties.IPAddress = L"169.254.73.153";
264 - endpointProperties.PrefixLength = 30;
331 + endpointProperties.IPAddress = L"169.254.73.250";
332 + endpointProperties.PrefixLength = 29;
333 endpointProperties.PortFriendlyName = c_loopbackDeviceName;
334 m_gnsChannel.SendEndpointState(endpointProperties);
335
@@ -269,6 +337,9 @@ void VirtioNetworking::SetupLoopbackDevice()
337 createLoopbackDevice.deviceName = c_loopbackDeviceName;
338 createLoopbackDevice.type = hns::DeviceType::Loopback;
339 createLoopbackDevice.lowerEdgeAdapterId = m_localhostAdapterId.value();
340 +
341 + // ipv6 duplicate address detection (DAD) breaks the ipv6 localhost relay since we can't predict the address before the guest tells us about it.
342 + createLoopbackDevice.flags = hns::CreateDeviceFlags::DisableDAD;
343 constexpr auto loopbackType = GnsMessageType(createLoopbackDevice);
344 m_gnsChannel.SendNetworkDeviceMessage(loopbackType, ToJsonW(createLoopbackDevice).c_str());
345 }
src/windows/common/VirtioNetworking.h
+8 -2
@@ -16,6 +16,7 @@ enum class VirtioNetworkingFlags
16 LocalhostRelay = 0x1,
17 DnsTunneling = 0x2,
18 Ipv6 = 0x4,
19 + LoopbackClientIp = 0x8,
20 };
21 DEFINE_ENUM_FLAG_OPERATORS(VirtioNetworkingFlags);
22
@@ -44,11 +45,16 @@ public:
45 void FillInitialConfiguration(LX_MINI_INIT_NETWORKING_CONFIGURATION& message) override;
46 void StartPortTracker(wil::unique_socket&& socket) override;
47
48 + HRESULT MapPort(_In_ const SOCKADDR_INET& ListenAddress, _In_ USHORT GuestPort, _In_ int Protocol, _Out_ USHORT* AllocatedHostPort) const;
49 +
50 + HRESULT UnmapPort(_In_ const SOCKADDR_INET& ListenAddress, _In_ USHORT GuestPort, _In_ int Protocol) const;
51 +
52 private:
53 static void NETIOAPI_API_ OnNetworkConnectivityChange(PVOID context, NL_NETWORK_CONNECTIVITY_HINT hint);
54
50 - HRESULT HandlePortNotification(const SOCKADDR_INET& addr, int protocol, bool allocate) const noexcept;
51 - int ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& addr, _In_ int protocol, _In_ bool isOpen) const;
55 + uint16_t HandlePortNotification(const SOCKADDR_INET& addr, int protocol, uint16_t guestPort, bool allocate) const;
56 + uint16_t ModifyOpenPorts(
57 + _In_ PCWSTR tag, _In_ const SOCKADDR_INET& hostAddress, _In_ uint16_t HostPort, _In_ uint16_t GuestPort, _In_ int protocol, _In_ bool isOpen) const;
58 void RefreshGuestConnection();
59 void SetupLoopbackDevice();
60 void SendDefaultRoute(const std::wstring& gateway, wsl::shared::hns::ModifyRequestType requestType);
src/windows/common/WSLCUserSettings.cpp
+14
@@ -116,6 +116,20 @@ namespace details {
116 return value;
117 }
118
119 + WSLC_VALIDATE_SETTING(SessionPortRelay)
120 + {
121 + if (value == "virtionet")
122 + {
123 + return PortRelayType::VirtioNet;
124 + }
125 + if (value == "wslrelay")
126 + {
127 + return PortRelayType::WslRelay;
128 + }
129 +
130 + return std::nullopt;
131 + }
132 +
133 WSLC_VALIDATE_SETTING(CredentialStore)
134 {
135 if (value == "wincred")
src/windows/common/WSLCUserSettings.h
+8
@@ -42,6 +42,7 @@ enum class Setting : size_t
42 SessionHostFileShareMode,
43 SessionDnsTunneling,
44 CredentialStore,
45 + SessionPortRelay,
46
47 Max
48 };
@@ -58,6 +59,12 @@ enum class CredentialStoreType
59 File
60 };
61
62 +enum class PortRelayType
63 +{
64 + VirtioNet,
65 + WslRelay
66 +};
67 +
68 namespace details {
69
70 template <Setting S>
@@ -89,6 +96,7 @@ namespace details {
96 DEFINE_SETTING_MAPPING(SessionHostFileShareMode, std::string, HostFileShareMode, HostFileShareMode::VirtioFs, "session.hostFileShareMode")
97 DEFINE_SETTING_MAPPING(SessionDnsTunneling, bool, bool, true, "session.dnsTunneling")
98 DEFINE_SETTING_MAPPING(CredentialStore, std::string, CredentialStoreType, CredentialStoreType::WinCred, "credentialStore")
99 + DEFINE_SETTING_MAPPING(SessionPortRelay, std::string, PortRelayType, PortRelayType::VirtioNet, "experimental.portRelay")
100
101 #undef DEFINE_SETTING_MAPPING
102 // clang-format on
src/windows/common/wslutil.cpp
+3 -1
@@ -163,7 +163,9 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
163 X(WSLC_E_SESSION_NOT_FOUND),
164 X(WSLC_E_WU_SEARCH_FAILED),
165 X_WIN32(RPC_S_SERVER_UNAVAILABLE),
166 - X_WIN32(ERROR_ELEVATION_REQUIRED)};
166 + X_WIN32(ERROR_ELEVATION_REQUIRED),
167 + X_WIN32(WSAEACCES),
168 + X_WIN32(WSAEADDRINUSE)};
169
170 #undef X
171
src/windows/service/exe/HcsVirtualMachine.cpp
+56
@@ -23,6 +23,7 @@ Abstract:
23 #include "wslutil.h"
24 #include "lxinitshared.h"
25 #include "DnsResolver.h"
26 +#include "string.hpp"
27
28 using namespace wsl::windows::common;
29 using helpers::WindowsBuildNumbers;
@@ -34,6 +35,26 @@ constexpr auto SAVED_STATE_FILE_PREFIX = L"saved-state-";
35
36 namespace {
37
38 +SOCKADDR_INET CreateListenAddress(LPCSTR Address, uint16_t HostPort)
39 +{
40 + auto listenAddr = wsl::windows::common::string::StringToSockAddrInet(wsl::shared::string::MultiByteToWide(Address));
41 +
42 + if (listenAddr.si_family == AF_INET)
43 + {
44 + listenAddr.Ipv4.sin_port = HostPort;
45 + }
46 + else if (listenAddr.si_family == AF_INET6)
47 + {
48 + listenAddr.Ipv6.sin6_port = HostPort;
49 + }
50 + else
51 + {
52 + THROW_HR_MSG(E_INVALIDARG, "Unsupported address family: %d", listenAddr.si_family);
53 + }
54 +
55 + return listenAddr;
56 +}
57 +
58 // Replace any character outside the conservative ASCII allowlist with '_' so the
59 // result is safe to use as the HCS HostingProcessNameSuffix (which becomes the
60 // vmmem-XXX process name visible in Task Manager and parsed by various tooling).
@@ -468,6 +489,11 @@ try
489 WI_SetFlag(flags, wsl::core::VirtioNetworkingFlags::DnsTunneling);
490 }
491
492 + if (!FeatureEnabled(WslcFeatureFlagsPortRelayWslRelay))
493 + {
494 + WI_SetFlag(flags, wsl::core::VirtioNetworkingFlags::LocalhostRelay);
495 + }
496 +
497 m_networkEngine = std::make_unique<wsl::core::VirtioNetworking>(
498 wsl::core::GnsChannel(std::move(gnsSocketHandle)), flags, nullptr, m_guestDeviceManager, m_userToken, m_swiotlbOption);
499 }
@@ -679,6 +705,36 @@ try
705 }
706 CATCH_RETURN()
707
708 +HRESULT HcsVirtualMachine::MapVirtioNetPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress, _Out_ USHORT* AllocatedHostPort)
709 +try
710 +{
711 + RETURN_HR_IF(E_POINTER, AllocatedHostPort == nullptr || ListenAddress == nullptr);
712 +
713 + *AllocatedHostPort = 0;
714 +
715 + std::lock_guard lock(m_lock);
716 +
717 + auto* virtioNet = dynamic_cast<wsl::core::VirtioNetworking*>(m_networkEngine.get());
718 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), virtioNet == nullptr);
719 +
720 + return virtioNet->MapPort(CreateListenAddress(ListenAddress, HostPort), GuestPort, Protocol, AllocatedHostPort);
721 +}
722 +CATCH_RETURN()
723 +
724 +HRESULT HcsVirtualMachine::UnmapVirtioNetPort(_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress)
725 +try
726 +{
727 + RETURN_HR_IF(E_POINTER, ListenAddress == nullptr);
728 +
729 + std::lock_guard lock(m_lock);
730 +
731 + auto* virtioNet = dynamic_cast<wsl::core::VirtioNetworking*>(m_networkEngine.get());
732 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), virtioNet == nullptr);
733 +
734 + return virtioNet->UnmapPort(CreateListenAddress(ListenAddress, HostPort), GuestPort, Protocol);
735 +}
736 +CATCH_RETURN()
737 +
738 void CALLBACK HcsVirtualMachine::OnVmExitCallback(HCS_EVENT* Event, void* Context)
739 try
740 {
src/windows/service/exe/HcsVirtualMachine.h
+4
@@ -48,6 +48,10 @@ public:
48 IFACEMETHOD(RemoveShare)(_In_ REFGUID ShareId) override;
49 IFACEMETHOD(ApplyGuestCapabilities)(_In_ const WSLCGuestCapabilities* Capabilities) override;
50 IFACEMETHOD(GetTerminationEvent)(_Out_ HANDLE* Event) override;
51 + IFACEMETHOD(MapVirtioNetPort)
52 + (_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress, _Out_ USHORT* AllocatedHostPort) override;
53 + IFACEMETHOD(UnmapVirtioNetPort)
54 + (_In_ USHORT HostPort, _In_ USHORT GuestPort, _In_ int Protocol, _In_ LPCSTR ListenAddress) override;
55 IFACEMETHOD(GetTerminationReason)(_Out_ WSLCVirtualMachineTerminationReason* Reason, _Out_ LPWSTR* Details) override;
56
57 private:
src/windows/service/exe/WSLCSessionManager.cpp
+4
@@ -122,6 +122,10 @@ private:
122 Settings.FeatureFlags,
123 WslcFeatureFlagsVirtioFs,
124 userSettings.Get<settings::Setting::SessionHostFileShareMode>() == settings::HostFileShareMode::VirtioFs);
125 + WI_SetFlagIf(
126 + Settings.FeatureFlags,
127 + WslcFeatureFlagsPortRelayWslRelay,
128 + userSettings.Get<settings::Setting::SessionPortRelay>() == settings::PortRelayType::WslRelay);
129 Settings.StorageFlags = storageFlags;
130 }
131 };
src/windows/service/exe/WslCoreVm.cpp
+2 -1
@@ -606,7 +606,8 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
606 }
607 else if (m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy)
608 {
609 - wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::Ipv6;
609 + wsl::core::VirtioNetworkingFlags flags =
610 + wsl::core::VirtioNetworkingFlags::Ipv6 | wsl::core::VirtioNetworkingFlags::LoopbackClientIp;
611 WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::LocalhostRelay, m_vmConfig.EnableLocalhostRelay);
612 WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::DnsTunneling, m_vmConfig.EnableDnsTunneling);
613 // NAT may have fallen back to virtio proxy after the early-config message; drop the unused DNS hvsocket.
src/windows/service/inc/WSLCShared.idl
+2 -1
@@ -154,9 +154,10 @@ typedef enum _WSLCFeatureFlags
154 WslcFeatureFlagsGPU = 4,
155 WslcFeatureFlagsVirtioFs = 8,
156 WslcFeatureFlagsDebug = 16,
157 + WslcFeatureFlagsPortRelayWslRelay = 32, // Use the wslrelay-based localhost port relay in VirtioProxy networking mode.
158 } WSLCFeatureFlags;
159
159 -cpp_quote("#define WSLCFeatureFlagsValid (WslcFeatureFlagsDnsTunneling | WslcFeatureFlagsEarlyBootDmesg | WslcFeatureFlagsGPU | WslcFeatureFlagsVirtioFs | WslcFeatureFlagsDebug)")
160 +cpp_quote("#define WSLCFeatureFlagsValid (WslcFeatureFlagsDnsTunneling | WslcFeatureFlagsEarlyBootDmesg | WslcFeatureFlagsGPU | WslcFeatureFlagsVirtioFs | WslcFeatureFlagsDebug | WslcFeatureFlagsPortRelayWslRelay)")
161
162 cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCFeatureFlags);")
163
src/windows/service/inc/wslc.idl
+15
@@ -32,6 +32,7 @@ cpp_quote("#endif")
32 #define WSLC_MAX_NETWORK_NAME_LENGTH 255
33 #define WSLC_CONTAINER_ID_LENGTH 64
34 #define WSLC_MAX_BINDING_ADDRESS_LENGTH 45
35 +#define WSLC_EPHEMERAL_PORT 0
36 #define WSLC_MAX_SAVE_IMAGES_COUNT 256
37
38 cpp_quote("#define WSLC_MAX_CONTAINER_NAME_LENGTH 255")
@@ -387,6 +388,20 @@ interface IWSLCVirtualMachine : IUnknown
388 // Returns an event that is signaled when the VM exits (graceful or forced).
389 HRESULT GetTerminationEvent([out, system_handle(sh_event)] HANDLE* Event);
390
391 + HRESULT MapVirtioNetPort(
392 + [in] USHORT HostPort,
393 + [in] USHORT GuestPort,
394 + [in] int Protocol,
395 + [in] LPCSTR ListenAddress,
396 + [out, retval] USHORT* AllocatedHostPort);
397 +
398 + // Unmaps a port previously mapped via MapVirtioNetPort.
399 + HRESULT UnmapVirtioNetPort(
400 + [in] USHORT HostPort,
401 + [in] USHORT GuestPort,
402 + [in] int Protocol,
403 + [in] LPCSTR ListenAddress);
404 +
405 // Returns the cached termination reason and details. These are only available after the
406 // termination event has been signaled; before that the call fails.
407 HRESULT GetTerminationReason([out] WSLCVirtualMachineTerminationReason* Reason, [out] LPWSTR* Details);
src/windows/wslc/services/ContainerService.cpp
+8 -12
@@ -86,25 +86,21 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(
86 {
87 auto portMapping = PublishPort::Parse(port);
88
89 + const int protocol = portMapping.PortProtocol() == PublishPort::Protocol::UDP ? IPPROTO_UDP : IPPROTO_TCP;
90 + const int family = (portMapping.HostIP().has_value() && portMapping.HostIP()->IsIPv6()) ? AF_INET6 : AF_INET;
91 + std::optional<std::string> bindAddress;
92 + if (portMapping.HostIP().has_value())
93 {
90 - // https://github.com/microsoft/WSL/issues/14433
91 - // The following scenarios are currently not implemented:
92 - // - Host port mappings with a specific host IP
93 - // - Host port mappings with UDP protocol
94 - if (portMapping.HostIP().has_value() || portMapping.PortProtocol() == PublishPort::Protocol::UDP)
95 - {
96 - THROW_HR_WITH_USER_ERROR(
97 - HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
98 - "Port mappings with specific host IPs or UDP protocol are not currently supported");
99 - }
94 + bindAddress = portMapping.HostIP()->IP();
95 }
96
97 auto containerPort = portMapping.ContainerPort();
98 for (uint16_t i = 0; i < containerPort.Count(); ++i)
99 {
100 auto currentContainerPort = static_cast<uint16_t>(containerPort.Start() + i);
106 - auto currentHostPort = static_cast<uint16_t>(portMapping.HostPort().Start() + i);
107 - containerLauncher.AddPort(currentHostPort, currentContainerPort, AF_INET);
101 + auto currentHostPort = portMapping.HostPort().IsEphemeral() ? static_cast<uint16_t>(WSLC_EPHEMERAL_PORT)
102 + : static_cast<uint16_t>(portMapping.HostPort().Start() + i);
103 + containerLauncher.AddPort(currentHostPort, currentContainerPort, family, protocol, bindAddress);
104 }
105 }
106
src/windows/wslcsession/WSLCContainer.cpp
+6 -7
@@ -302,7 +302,7 @@ std::vector<ContainerPortMapping> BuildPortMappings(std::vector<_WSLCPortMapping
302 const bool allocateVmPorts = NetworkModeAllocatesVmPorts(primary);
303 for (auto& e : requestedPorts)
304 {
305 - if (e.HostPort == WSLC_EPHEMERAL_PORT)
305 + if (e.HostPort == WSLC_EPHEMERAL_PORT && vm.NetworkingMode() == WSLCNetworkingModeNAT)
306 {
307 e.HostPort = AllocateEphemeralPort(e.Family, e.BindingAddress);
308 }
@@ -1678,8 +1678,9 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1678 // In that networking mode, the host port always matches the vm port.
1679 auto hostPort = e.VmMapping.VmPort ? e.VmMapping.VmPort->Port() : e.VmMapping.HostPort();
1680
1681 - portEntry.emplace_back(
1682 - common::docker_schema::PortMapping{.HostIp = e.VmMapping.BindingAddressString(), .HostPort = std::to_string(hostPort)});
1681 + // Use catch-all binding address based on the address family. :: binds all ipv6 interfaces, and 0:0:0:0 binds all ipv4 interfaces.
1682 + portEntry.emplace_back(common::docker_schema::PortMapping{
1683 + .HostIp = e.VmMapping.IsIPv6() ? "::" : "0.0.0.0", .HostPort = std::to_string(hostPort)});
1684 }
1685
1686 auto labels = ParseKeyValuePairs(containerOptions.Labels, containerOptions.LabelsCount, WSLCContainerMetadataLabel);
@@ -2041,9 +2042,7 @@ void WSLCContainerImpl::MapPorts()
2042 m_virtualMachine.TryAllocatePort(e.ContainerPort, e.VmMapping.BindAddress.si_family, e.VmMapping.Protocol);
2043
2044 THROW_HR_WITH_USER_ERROR_IF(
2044 - HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS),
2045 - wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id),
2046 - !allocatedPort);
2045 + HRESULT_FROM_WIN32(WSAEADDRINUSE), wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id), !allocatedPort);
2046
2047 e.VmMapping.AssignVmPort(allocatedPort);
2048
@@ -2061,7 +2060,7 @@ void WSLCContainerImpl::MapPorts()
2060 if (result == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) || result == HRESULT_FROM_WIN32(WSAEADDRINUSE))
2061 {
2062 THROW_HR_WITH_USER_ERROR(
2064 - HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id));
2063 + HRESULT_FROM_WIN32(WSAEADDRINUSE), wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id));
2064 }
2065 throw;
2066 }
src/windows/wslcsession/WSLCSession.cpp
+1 -1
@@ -3003,7 +3003,7 @@ try
3003 {
3004 // No existing port allocation, create a new one.
3005 auto allocated = std::make_pair(m_virtualMachine->TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast<size_t>(0));
3006 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), allocated.first == nullptr);
3006 + THROW_HR_IF(HRESULT_FROM_WIN32(WSAEADDRINUSE), allocated.first == nullptr);
3007
3008 it = m_allocatedPorts.emplace(LinuxPort, allocated).first;
3009 inserted = true;
src/windows/wslcsession/WSLCVirtualMachine.cpp
+52 -13
@@ -185,6 +185,19 @@ uint16_t VMPortMapping::HostPort() const
185 }
186 }
187
188 +void VMPortMapping::SetHostPort(uint16_t port)
189 +{
190 + if (BindAddress.si_family == AF_INET6)
191 + {
192 + BindAddress.Ipv6.sin6_port = htons(port);
193 + }
194 + else
195 + {
196 + WI_ASSERT(BindAddress.si_family == AF_INET);
197 + BindAddress.Ipv4.sin_port = htons(port);
198 + }
199 +}
200 +
201 std::string VMPortMapping::BindingAddressString() const
202 {
203 char buffer[INET6_ADDRSTRLEN]{};
@@ -434,6 +447,17 @@ bool WSLCVirtualMachine::FeatureEnabled(WSLCFeatureFlags Value) const
447 return static_cast<ULONG>(m_featureFlags) & static_cast<ULONG>(Value);
448 }
449
450 +WSLCNetworkingMode WSLCVirtualMachine::NetworkingMode() const
451 +{
452 + return m_networkingMode;
453 +}
454 +
455 +bool WSLCVirtualMachine::UseWslRelayPortForwarding() const
456 +{
457 + return m_networkingMode == WSLCNetworkingModeNAT ||
458 + (m_networkingMode == WSLCNetworkingModeVirtioProxy && FeatureEnabled(WslcFeatureFlagsPortRelayWslRelay));
459 +}
460 +
461 void WSLCVirtualMachine::WatchForExitedProcesses(wsl::shared::SocketChannel& Channel)
462 try
463 {
@@ -941,12 +965,12 @@ void WSLCVirtualMachine::MapPort(VMPortMapping& Mapping)
965 {
966 THROW_HR_MSG(E_ILLEGAL_STATE_CHANGE, "Port mapping is not supported with the current networking mode");
967 }
944 - else if (m_networkingMode == WSLCNetworkingModeNAT)
968 + else if (UseWslRelayPortForwarding())
969 {
970 THROW_HR_IF_MSG(
971 HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
972 !Mapping.IsLocalhost() || Mapping.Protocol != IPPROTO_TCP,
949 - "Unsupported port mapping for NAT mode: %hs, protocol: %i",
973 + "Unsupported port mapping for the wslrelay port relay: %hs, protocol: %i",
974 Mapping.BindingAddressString().c_str(),
975 Mapping.Protocol);
976
@@ -954,15 +978,30 @@ void WSLCVirtualMachine::MapPort(VMPortMapping& Mapping)
978 }
979 else if (m_networkingMode == WSLCNetworkingModeVirtioProxy)
980 {
957 - // TODO: Switch to using the native virtionet relay.
958 - THROW_HR_IF_MSG(
959 - HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
960 - !Mapping.IsLocalhost() || Mapping.Protocol != IPPROTO_TCP,
961 - "Unsupported port mapping for virtionet mode: %hs, protocol: %i",
962 - Mapping.BindingAddressString().c_str(),
963 - Mapping.Protocol);
981 + USHORT allocatedHostPort = 0;
982 + auto result = m_vm->MapVirtioNetPort(
983 + Mapping.HostPort(), Mapping.VmPort->Port(), Mapping.Protocol, Mapping.BindingAddressString().c_str(), &allocatedHostPort);
984
965 - MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), false);
985 + if (FAILED(result))
986 + {
987 + auto portString = std::format(
988 + "{}:{}/{}",
989 + Mapping.IsIPv6() ? std::format("[{}]", Mapping.BindingAddressString()) : Mapping.BindingAddressString(),
990 + Mapping.HostPort(),
991 + Mapping.Protocol == IPPROTO_TCP ? "tcp" : "udp");
992 +
993 + THROW_HR_WITH_USER_ERROR(result, shared::Localization::MessageFailedToMapPort(portString, common::wslutil::GetErrorString(result)));
994 + }
995 +
996 + // For anonymous binds, write back the allocated host port.
997 + if (Mapping.HostPort() == WSLC_EPHEMERAL_PORT)
998 + {
999 + WSL_LOG(
1000 + "AllocatedHostPort",
1001 + TraceLoggingValue(allocatedHostPort, "HostPort"),
1002 + TraceLoggingValue(Mapping.VmPort->Port(), "GuestPort"));
1003 + Mapping.SetHostPort(allocatedHostPort);
1004 + }
1005 }
1006 else
1007 {
@@ -980,14 +1019,14 @@ void WSLCVirtualMachine::UnmapPort(VMPortMapping& Mapping)
1019 {
1020 THROW_HR_MSG(E_ILLEGAL_STATE_CHANGE, "Port mapping is not supported with the current networking mode");
1021 }
983 - else if (m_networkingMode == WSLCNetworkingModeNAT)
1022 + else if (UseWslRelayPortForwarding())
1023 {
1024 MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), true);
1025 }
1026 else if (m_networkingMode == WSLCNetworkingModeVirtioProxy)
1027 {
989 - // TODO: Switch to using the native virtionet relay.
990 - MapRelayPort(Mapping.BindAddress.si_family, Mapping.HostPort(), Mapping.VmPort->Port(), true);
1028 + THROW_IF_FAILED(m_vm->UnmapVirtioNetPort(
1029 + Mapping.HostPort(), Mapping.VmPort->Port(), Mapping.Protocol, Mapping.BindingAddressString().c_str()));
1030 }
1031 else
1032 {
src/windows/wslcsession/WSLCVirtualMachine.h
+5
@@ -92,6 +92,7 @@ struct VMPortMapping
92 void Attach(WSLCVirtualMachine& Vm);
93 void Detach();
94 uint16_t HostPort() const;
95 + void SetHostPort(uint16_t port);
96
97 static VMPortMapping LocalhostTcpMapping(int Family, uint16_t WindowsPort);
98 static VMPortMapping FromWSLCPortMapping(const ::WSLCPortMapping& Mapping);
@@ -183,9 +184,13 @@ public:
184
185 bool FeatureEnabled(WSLCFeatureFlags Flag) const;
186
187 + WSLCNetworkingMode NetworkingMode() const;
188 +
189 private:
190 void MapRelayPort(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort, _In_ bool Remove);
191
192 + bool UseWslRelayPortForwarding() const;
193 +
194 // Initial setup during Connect()
195 void ConfigureNetworking();
196
src/windows/wslrelay/localhost.cpp
+8 -2
@@ -593,7 +593,7 @@ void wsl::windows::wslrelay::localhost::RunWSLCPortRelay(const GUID& VmId, uint3
593 {
594 if (it != ports.end())
595 {
596 - result = HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS);
596 + result = HRESULT_FROM_WIN32(WSAEADDRINUSE);
597 continue;
598 }
599 else
@@ -615,6 +615,12 @@ void wsl::windows::wslrelay::localhost::RunWSLCPortRelay(const GUID& VmId, uint3
615 catch (...)
616 {
617 result = wil::ResultFromCaughtException();
618 + if (result == HRESULT_FROM_WIN32(WSAEACCES))
619 + {
620 + // Translate WSAEACCES to WSAEADDRINUSE to match the virtionet behavior.
621 + result = HRESULT_FROM_WIN32(WSAEADDRINUSE);
622 + }
623 +
624 continue;
625 }
626 }
@@ -646,4 +652,4 @@ void wsl::windows::wslrelay::localhost::RunWSLCPortRelay(const GUID& VmId, uint3
652
653 result = S_OK;
654 }
649 -}
\ No newline at end of file
655 +}
test/windows/NetworkTests.cpp
+7 -7
@@ -1426,12 +1426,12 @@ class NetworkTests
1426
1427 // Verify that the static neighbor entry was added for the gateway
1428 const bool gatewayArpEntryExists =
1429 - LxsstuLaunchWsl(L"ip neigh show dev eth0 | grep \"169\\.254\\.73\\.152 lladdr 00:11:22:33:44:55 PERMANENT\"") == (DWORD)0;
1429 + LxsstuLaunchWsl(L"ip neigh show dev eth0 | grep \"169\\.254\\.73\\.249 lladdr 00:11:22:33:44:55 PERMANENT\"") == (DWORD)0;
1430
1431 // Verify route was added for destination 127.0.0.1, with preferred source 127.0.0.1
1432 const bool routeToLoopbackRangeExists =
1433 LxsstuLaunchWsl(
1434 - L"ip route show table 127 | grep \"127\\.0\\.0\\.1 via 169\\.254\\.73\\.152 dev eth0\" | grep "
1434 + L"ip route show table 127 | grep \"127\\.0\\.0\\.1 via 169\\.254\\.73\\.249 dev eth0\" | grep "
1435 L"\"src 127\\.0\\.0\\.1\" | grep onlink") == (DWORD)0;
1436
1437 const bool shutdownSuccessful = WslShutdown();
@@ -1464,16 +1464,16 @@ class NetworkTests
1464
1465 const bool firstRouteExists =
1466 LxsstuLaunchWsl(
1467 - L"ip route show table 128 | grep \"127\\.0\\.0\\.1 via 169\\.254\\.73\\.152 dev eth0\" | grep \"src "
1467 + L"ip route show table 128 | grep \"127\\.0\\.0\\.1 via 169\\.254\\.73\\.249 dev eth0\" | grep \"src "
1468 L"127\\.0\\.0\\.1\" | grep onlink") == (DWORD)0;
1469 const bool secondRouteExists =
1470 LxsstuLaunchWsl(
1471 - L"ip route show table 128 | grep \"127\\.0\\.0\\.2 via 169\\.254\\.73\\.152 dev eth0\" | grep \"src "
1471 + L"ip route show table 128 | grep \"127\\.0\\.0\\.2 via 169\\.254\\.73\\.249 dev eth0\" | grep \"src "
1472 L"127\\.0\\.0\\.2\" | grep onlink") == (DWORD)0;
1473
1474 // Verify that the static neighbor entry was added for the gateway
1475 const bool gatewayArpEntryExists =
1476 - LxsstuLaunchWsl(L"ip neigh show dev eth0 | grep \"169\\.254\\.73\\.152 lladdr 00:11:22:33:44:55 PERMANENT\"") == (DWORD)0;
1476 + LxsstuLaunchWsl(L"ip neigh show dev eth0 | grep \"169\\.254\\.73\\.249 lladdr 00:11:22:33:44:55 PERMANENT\"") == (DWORD)0;
1477
1478 // Verify that the routes are deleted
1479 for (const auto address : ipAddresses)
@@ -1516,9 +1516,9 @@ class NetworkTests
1516
1517 // Verify that after configurations are applied, the route chosen for 127.0.0.1 tcp/udp is the desired one
1518 const bool loopbackTcpUsesCustomTable =
1519 - LxsstuLaunchWsl(L"ip route get from 127.0.0.1 127.0.0.1 ipproto tcp | grep \"via 169\\.254\\.73\\.152 dev eth0\"") == (DWORD)0;
1519 + LxsstuLaunchWsl(L"ip route get from 127.0.0.1 127.0.0.1 ipproto tcp | grep \"via 169\\.254\\.73\\.249 dev eth0\"") == (DWORD)0;
1520 const bool loopbackUdpUsesCustomTable =
1521 - LxsstuLaunchWsl(L"ip route get from 127.0.0.1 127.0.0.1 ipproto udp | grep \"via 169\\.254\\.73\\.152 dev eth0\"") == (DWORD)0;
1521 + LxsstuLaunchWsl(L"ip route get from 127.0.0.1 127.0.0.1 ipproto udp | grep \"via 169\\.254\\.73\\.249 dev eth0\"") == (DWORD)0;
1522
1523 const bool shutdownSuccessful = WslShutdown();
1524
test/windows/WSLCTests.cpp
+283 -27
@@ -509,7 +509,7 @@ class WSLCTests
509 // Reject invalid feature flags.
510 {
511 auto settings = GetDefaultSessionSettings(L"invalid-feature-flags");
512 - settings.FeatureFlags = static_cast<WSLCFeatureFlags>(0x20);
512 + settings.FeatureFlags = static_cast<WSLCFeatureFlags>(0x40);
513 wil::com_ptr<IWSLCSession> session;
514 VERIFY_ARE_EQUAL(E_INVALIDARG, sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session));
515 }
@@ -3308,7 +3308,7 @@ class WSLCTests
3308 VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, 1234, 80));
3309
3310 // Validate that the same port can't be bound twice
3311 - VERIFY_ARE_EQUAL(session->MapVmPort(AF_INET, 1234, 80), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3311 + VERIFY_ARE_EQUAL(session->MapVmPort(AF_INET, 1234, 80), HRESULT_FROM_WIN32(WSAEADDRINUSE));
3312
3313 // Check simple case
3314 listen(80, "port80", false);
@@ -3332,7 +3332,9 @@ class WSLCTests
3332 VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, 1234, 80));
3333
3334 // Verify that a proper error is returned if the mapping doesn't exist
3335 - VERIFY_ARE_EQUAL(session->UnmapVmPort(AF_INET, 1234, 80), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3335 + // TODO: update once virtionet error code is fixed.
3336 + VERIFY_ARE_EQUAL(
3337 + session->UnmapVmPort(AF_INET, 1234, 80), networkingMode == WSLCNetworkingModeNAT ? HRESULT_FROM_WIN32(ERROR_NOT_FOUND) : E_INVALIDARG);
3338
3339 // Unmap the v6 port
3340 VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET6, 1234, 80));
@@ -3373,9 +3375,18 @@ class WSLCTests
3375 VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + i), static_cast<uint16_t>(80 + i)));
3376 }
3377
3376 - VERIFY_ARE_EQUAL(
3377 - session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + c_maxPorts), static_cast<uint16_t>(80 + c_maxPorts)),
3378 - HRESULT_FROM_WIN32(ERROR_TOO_MANY_OPEN_FILES));
3378 + if (networkingMode == WSLCNetworkingModeNAT)
3379 + {
3380 + // In NAT mode, the 64th port mapping should fail with ERROR_TOO_MANY_OPEN_FILES since the relay process uses a file handle for each mapping.
3381 + VERIFY_ARE_EQUAL(
3382 + session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + c_maxPorts), static_cast<uint16_t>(80 + c_maxPorts)),
3383 + HRESULT_FROM_WIN32(ERROR_TOO_MANY_OPEN_FILES));
3384 + }
3385 + else
3386 + {
3387 + VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + c_maxPorts), static_cast<uint16_t>(80 + c_maxPorts)));
3388 + VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, static_cast<uint16_t>(20000 + c_maxPorts), static_cast<uint16_t>(80 + c_maxPorts)));
3389 + }
3390
3391 for (int i = 0; i < c_maxPorts; i++)
3392 {
@@ -7657,7 +7668,7 @@ class WSLCTests
7668 VERIFY_ARE_EQUAL(process.GetExitCode(), 128 + WSLCSignalSIGKILL);
7669 }
7670
7660 - void RunPortMappingsTest(IWSLCSession& session, std::string containerNetworkType)
7671 + void RunPortMappingsTest(IWSLCSession& session, const std::string& containerNetworkType, bool virtionet)
7672 {
7673 WEX::Logging::Log::Comment(
7674 std::format(L"Container network type: {}", wsl::shared::string::MultiByteToWide(containerNetworkType)).c_str());
@@ -7771,7 +7782,7 @@ class WSLCTests
7782 subLauncher.AddPort(1234, 8000, AF_INET);
7783
7784 auto [hresult, newContainer] = subLauncher.LaunchNoThrow(session);
7774 - VERIFY_ARE_EQUAL(hresult, HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
7785 + VERIFY_ARE_EQUAL(hresult, HRESULT_FROM_WIN32(WSAEADDRINUSE));
7786
7787 // Verify that a stopped container returns no ports.
7788 VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
@@ -7821,7 +7832,7 @@ class WSLCTests
7832 launcher.AddPort(1234, 8000, AF_INET);
7833 launcher.AddPort(1234, 8000, AF_INET);
7834
7824 - VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
7835 + VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(WSAEADDRINUSE));
7836 }
7837
7838 auto bindSocket = [](auto port) {
@@ -7841,7 +7852,7 @@ class WSLCTests
7852 "python:3.12-alpine", "test-ports-fail", {"python3", "-m", "http.server"}, {"PYTHONUNBUFFERED=1"}, containerNetworkType);
7853
7854 launcher.AddPort(1235, 8000, AF_INET);
7844 - VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(WSAEACCES));
7855 + VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(WSAEADDRINUSE));
7856
7857 // Validate that Create() correctly cleans up bound ports after a port fails to map
7858 {
@@ -7850,7 +7861,7 @@ class WSLCTests
7861 launcher.AddPort(1236, 8000, AF_INET); // Should succeed
7862 launcher.AddPort(1235, 8000, AF_INET); // Should fail.
7863
7853 - VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(WSAEACCES));
7864 + VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(WSAEADDRINUSE));
7865
7866 // Validate that port 1236 is still available (was cleaned up after failure).
7867 VERIFY_IS_TRUE(!!bindSocket(1236));
@@ -7895,29 +7906,48 @@ class WSLCTests
7906 VERIFY_ARE_EQUAL(session.CreateContainer(&options, nullptr, &container), E_INVALIDARG);
7907 }
7908
7898 - // TODO: Update once UDP is supported.
7909 + if (virtionet)
7910 {
7900 - WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
7901 - launcher.AddPort(1234, 8000, AF_INET, IPPROTO_UDP);
7911 + {
7912 + WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
7913 + launcher.AddPort(1234, 8000, AF_INET, IPPROTO_UDP);
7914
7903 - VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
7904 - }
7915 + VERIFY_SUCCEEDED(launcher.LaunchNoThrow(session).first);
7916 + }
7917 +
7918 + {
7919 + WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
7920 + launcher.AddPort(1234, 8000, AF_INET, IPPROTO_TCP, "0.0.0.0");
7921
7906 - // TODO: Update once custom binding addresses are supported.
7922 + VERIFY_SUCCEEDED(launcher.LaunchNoThrow(session).first);
7923 + }
7924 + }
7925 + else
7926 {
7908 - WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
7909 - launcher.AddPort(1234, 8000, AF_INET, IPPROTO_TCP, "1.1.1.1");
7927 + {
7928 + WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
7929 + launcher.AddPort(1234, 8000, AF_INET, IPPROTO_UDP);
7930 +
7931 + VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
7932 + }
7933
7911 - VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
7934 + {
7935 + WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
7936 + launcher.AddPort(1234, 8000, AF_INET, IPPROTO_TCP, "0.0.0.0");
7937 +
7938 + VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
7939 + }
7940 }
7941 }
7942 }
7943
7916 - auto SetupPortMappingsTest(WSLCNetworkingMode networkingMode)
7944 + auto SetupPortMappingsTest(WSLCNetworkingMode networkingMode, WSLCFeatureFlags featureFlags = WslcFeatureFlagsNone)
7945 {
7946 auto settings = GetDefaultSessionSettings(L"networking-session", true, networkingMode);
7947 + settings.FeatureFlags = featureFlags;
7948
7920 - auto createNewSession = settings.NetworkingMode != m_defaultSessionSettings.NetworkingMode;
7949 + auto createNewSession = settings.NetworkingMode != m_defaultSessionSettings.NetworkingMode ||
7950 + settings.FeatureFlags != m_defaultSessionSettings.FeatureFlags;
7951 auto restore = createNewSession ? std::optional{ResetTestSession()} : std::nullopt;
7952 auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
7953
@@ -7928,16 +7958,242 @@ class WSLCTests
7958 {
7959 auto [restore, session] = SetupPortMappingsTest(WSLCNetworkingModeNAT);
7960
7931 - RunPortMappingsTest(*session, "bridge");
7932 - RunPortMappingsTest(*session, "host");
7961 + RunPortMappingsTest(*session, "bridge", false);
7962 + RunPortMappingsTest(*session, "host", false);
7963 }
7964
7965 WSLC_TEST_METHOD(PortMappingsVirtioProxy)
7966 {
7967 auto [restore, session] = SetupPortMappingsTest(WSLCNetworkingModeVirtioProxy);
7968
7939 - RunPortMappingsTest(*session, "bridge");
7940 - RunPortMappingsTest(*session, "host");
7969 + RunPortMappingsTest(*session, "bridge", true);
7970 + RunPortMappingsTest(*session, "host", true);
7971 + }
7972 +
7973 + WSLC_TEST_METHOD(PortMappingsVirtioProxyWslRelay)
7974 + {
7975 + auto [restore, session] = SetupPortMappingsTest(WSLCNetworkingModeVirtioProxy, WslcFeatureFlagsPortRelayWslRelay);
7976 +
7977 + RunPortMappingsTest(*session, "bridge", false);
7978 + RunPortMappingsTest(*session, "host", false);
7979 + }
7980 +
7981 + WSLC_TEST_METHOD(PortMappingsAdvanced)
7982 + {
7983 + auto [restore, session] = SetupPortMappingsTest(WSLCNetworkingModeVirtioProxy);
7984 +
7985 + // Helper to resolve the first non-loopback IPv4 address on an active host adapter.
7986 + auto getHostAdapterIpv4 = []() -> std::optional<std::string> {
7987 + ULONG bufferSize = 0;
7988 + constexpr ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER;
7989 + auto result = GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &bufferSize);
7990 + if (result != ERROR_BUFFER_OVERFLOW)
7991 + {
7992 + return std::nullopt;
7993 + }
7994 +
7995 + std::vector<BYTE> buffer(bufferSize);
7996 + auto* adapters = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data());
7997 + result = GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &bufferSize);
7998 + if (result != ERROR_SUCCESS)
7999 + {
8000 + return std::nullopt;
8001 + }
8002 +
8003 + for (auto* adapter = adapters; adapter != nullptr; adapter = adapter->Next)
8004 + {
8005 + if (adapter->OperStatus != IfOperStatusUp || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK || adapter->IfType == IF_TYPE_TUNNEL)
8006 + {
8007 + continue;
8008 + }
8009 +
8010 + for (auto* addr = adapter->FirstUnicastAddress; addr != nullptr; addr = addr->Next)
8011 + {
8012 + if (addr->Address.lpSockaddr->sa_family != AF_INET)
8013 + {
8014 + continue;
8015 + }
8016 +
8017 + auto& ipv4 = reinterpret_cast<sockaddr_in*>(addr->Address.lpSockaddr)->sin_addr;
8018 +
8019 + // Skip APIPA (169.254.x.x) addresses.
8020 + if ((ntohl(ipv4.s_addr) & 0xFFFF0000) == 0xA9FE0000)
8021 + {
8022 + continue;
8023 + }
8024 +
8025 + char buf[INET_ADDRSTRLEN];
8026 + inet_ntop(AF_INET, &ipv4, buf, sizeof(buf));
8027 + return std::string(buf);
8028 + }
8029 + }
8030 +
8031 + return std::nullopt;
8032 + };
8033 +
8034 + auto hostIp = getHostAdapterIpv4();
8035 +
8036 + struct PortMapping
8037 + {
8038 + uint16_t HostPort;
8039 + uint16_t ContainerPort;
8040 + int Family;
8041 + int Protocol = IPPROTO_TCP;
8042 + std::optional<std::string> BindingAddress;
8043 + };
8044 +
8045 + auto runCustomBindingTests = [&](const std::string& containerNetworkType) {
8046 + LogInfo("Container network type: %s", containerNetworkType.c_str());
8047 +
8048 + auto createTcpContainer = [&](const std::vector<PortMapping>& ports) {
8049 + static int containerIndex = 0;
8050 + WSLCContainerLauncher launcher(
8051 + "python:3.12-alpine",
8052 + std::format("test-ports-custom-{}", containerIndex++),
8053 + {"python3", "-m", "http.server", "--bind", "::"},
8054 + {"PYTHONUNBUFFERED=1"},
8055 + containerNetworkType);
8056 +
8057 + for (const auto& port : ports)
8058 + {
8059 + launcher.AddPort(port.HostPort, port.ContainerPort, port.Family, port.Protocol, port.BindingAddress);
8060 + }
8061 +
8062 + auto container = launcher.Launch(*session);
8063 + WaitForOutput(container.GetInitProcess().GetStdHandle(1), "Serving HTTP on");
8064 + return container;
8065 + };
8066 +
8067 + // Explicit localhost (127.0.0.1) binding.
8068 + {
8069 + auto container = createTcpContainer({{1260, 8000, AF_INET, IPPROTO_TCP, "127.0.0.1"}});
8070 + ExpectHttpResponse(L"http://127.0.0.1:1260", 200);
8071 + }
8072 +
8073 + // 0.0.0.0 (all interfaces) binding.
8074 + {
8075 + auto container = createTcpContainer({{1261, 8000, AF_INET, IPPROTO_TCP, "0.0.0.0"}});
8076 +
8077 + // Verify reachable via loopback.
8078 + ExpectHttpResponse(L"http://127.0.0.1:1261", 200);
8079 +
8080 + // Verify reachable via host adapter IP to confirm wildcard semantics.
8081 + if (hostIp.has_value())
8082 + {
8083 + auto url = std::format(L"http://{}:1261", wsl::shared::string::MultiByteToWide(hostIp.value()));
8084 + ExpectHttpResponse(url.c_str(), 200);
8085 + }
8086 + else
8087 + {
8088 + LogInfo("Skipping host adapter IP verification: no suitable IPv4 adapter found");
8089 + }
8090 + }
8091 +
8092 + // Main host adapter's IPv4 address binding.
8093 + {
8094 + if (hostIp.has_value())
8095 + {
8096 + auto container = createTcpContainer({{1262, 8000, AF_INET, IPPROTO_TCP, hostIp.value()}});
8097 +
8098 + auto url = std::format(L"http://{}:1262", wsl::shared::string::MultiByteToWide(hostIp.value()));
8099 + ExpectHttpResponse(url.c_str(), 200);
8100 + }
8101 + else
8102 + {
8103 + LogInfo("Skipping host adapter IP binding test: no suitable IPv4 adapter found");
8104 + }
8105 + }
8106 +
8107 + // Anonymous bind on localhost (ephemeral host port).
8108 + {
8109 + auto container = createTcpContainer({{WSLC_EPHEMERAL_PORT, 8000, AF_INET, IPPROTO_TCP, "127.0.0.1"}});
8110 +
8111 + auto inspectData = container.Inspect();
8112 + VERIFY_IS_TRUE(inspectData.Ports.contains("8000/tcp"));
8113 +
8114 + auto& bindings = inspectData.Ports["8000/tcp"];
8115 + VERIFY_ARE_EQUAL(1u, bindings.size());
8116 +
8117 + ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", bindings[0].HostPort).c_str(), 200);
8118 + }
8119 +
8120 + // Anonymous bind on host ip (ephemeral host port).
8121 + {
8122 + if (hostIp.has_value())
8123 + {
8124 + auto container = createTcpContainer({{WSLC_EPHEMERAL_PORT, 8000, AF_INET, IPPROTO_TCP, hostIp.value()}});
8125 +
8126 + auto inspectData = container.Inspect();
8127 + VERIFY_IS_TRUE(inspectData.Ports.contains("8000/tcp"));
8128 +
8129 + auto& bindings = inspectData.Ports["8000/tcp"];
8130 + VERIFY_ARE_EQUAL(1u, bindings.size());
8131 +
8132 + ExpectHttpResponse(std::format(L"http://{}:{}", hostIp.value(), bindings[0].HostPort).c_str(), 200);
8133 + }
8134 + else
8135 + {
8136 + LogInfo("Skipping host adapter IP binding test: no suitable IPv4 adapter found");
8137 + }
8138 + }
8139 +
8140 + // IPv6 loopback (::1) binding.
8141 + {
8142 + auto container = createTcpContainer({{1263, 8000, AF_INET6, IPPROTO_TCP, "::1"}});
8143 + ExpectHttpResponse(L"http://[::1]:1263", 200);
8144 + }
8145 +
8146 + // IPv6 wildcard (::) binding.
8147 + {
8148 + auto container = createTcpContainer({{1264, 8000, AF_INET6, IPPROTO_TCP, "::"}});
8149 + ExpectHttpResponse(L"http://[::1]:1264", 200);
8150 + }
8151 +
8152 + // UDP port mapping with a Python echo server.
8153 + {
8154 + // Inline Python UDP echo server: receives a datagram and sends it back uppercased.
8155 + static constexpr auto c_udpEchoScript =
8156 + "import socket,sys;"
8157 + "s=socket.socket(socket.AF_INET6,socket.SOCK_DGRAM);"
8158 + "s.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,0);"
8159 + "s.bind(('::',9000));"
8160 + "print('UDP listening',flush=True);"
8161 + "data,addr=s.recvfrom(1024);"
8162 + "s.sendto(data.upper(),addr)";
8163 +
8164 + static int udpContainerIndex = 0;
8165 + WSLCContainerLauncher launcher(
8166 + "python:3.12-alpine",
8167 + std::format("test-ports-custom-udp-{}", udpContainerIndex++),
8168 + {"python3", "-c", c_udpEchoScript},
8169 + {"PYTHONUNBUFFERED=1"},
8170 + containerNetworkType);
8171 +
8172 + launcher.AddPort(1265, 9000, AF_INET, IPPROTO_UDP, "127.0.0.1");
8173 +
8174 + auto container = launcher.Launch(*session);
8175 + WaitForOutput(container.GetInitProcess().GetStdHandle(1), "UDP listening");
8176 +
8177 + WSLCE2ETests::SendUdpAndReceive(1265, "hello", "HELLO");
8178 + }
8179 +
8180 + // Validate that trying to bind an address that the host doesn't have fails:
8181 + {
8182 + // Malformed address string.
8183 + {
8184 + WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
8185 + launcher.AddPort(1265, 8000, AF_INET, IPPROTO_TCP, "1.1.1.1");
8186 +
8187 + auto container = launcher.Create(*session);
8188 + VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr), HRESULT_FROM_WIN32(WSAEADDRNOTAVAIL));
8189 + ValidateCOMErrorMessage(
8190 + L"Failed to map port '1.1.1.1:1265/tcp', The requested address is not valid in its context. ");
8191 + }
8192 + }
8193 + };
8194 +
8195 + runCustomBindingTests("bridge");
8196 + runCustomBindingTests("host");
8197 }
8198
8199 TEST_METHOD(PortMappingsNone)
@@ -10148,7 +10404,7 @@ class WSLCTests
10404 VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
10405
10406 // Start container 2 — should fail because the host port is already reserved by container 1.
10151 - VERIFY_ARE_EQUAL(container2.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
10407 + VERIFY_ARE_EQUAL(container2.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr), HRESULT_FROM_WIN32(WSAEADDRINUSE));
10408 VERIFY_ARE_EQUAL(container2.State(), WslcContainerStateCreated);
10409 }
10410
test/windows/WslcSdkWinRTTests.cpp
+47
@@ -115,6 +115,41 @@ class WslcSdkWinRtTests
115 return output;
116 }
117
118 + void WaitForProcessOutput(WSLCSDK::Process const& process, std::string_view marker, std::chrono::seconds timeout = 60s)
119 + {
120 + auto stream = process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput);
121 + Buffer buffer{1024};
122 +
123 + std::string accumulated;
124 + const auto deadline = std::chrono::steady_clock::now() + timeout;
125 + for (auto now = std::chrono::steady_clock::now(); now < deadline; now = std::chrono::steady_clock::now())
126 + {
127 + const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now);
128 +
129 + // N.B. InputStreamOptions::Partial is not supported.
130 + auto read = stream.ReadAsync(buffer, buffer.Capacity(), InputStreamOptions::None);
131 + if (read.wait_for(remaining) != winrt::Windows::Foundation::AsyncStatus::Completed)
132 + {
133 + break;
134 + }
135 +
136 + const auto result = read.GetResults();
137 + if (result.Length() == 0)
138 + {
139 + break;
140 + }
141 +
142 + accumulated.append(reinterpret_cast<const char*>(result.data()), result.Length());
143 + if (accumulated.find(marker) != std::string::npos)
144 + {
145 + return;
146 + }
147 + }
148 +
149 + LogError("Timed out waiting for process output marker: '%hs'. Output: '%hs'", std::string(marker).c_str(), accumulated.c_str());
150 + VERIFY_FAIL();
151 + }
152 +
153 struct RunContainerOptions
154 {
155 std::vector<winrt::hstring> cmdLine = {};
@@ -697,6 +732,7 @@ class WslcSdkWinRtTests
732 procSettings.CmdLine(winrt::single_threaded_vector<winrt::hstring>({L"python3", L"-m", L"http.server", L"8000"}));
733 procSettings.EnvironmentVariables(
734 winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"PYTHONUNBUFFERED", L"1"}}));
735 + procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
736
737 auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine");
738 containerSettings.InitProcess(procSettings);
@@ -709,6 +745,9 @@ class WslcSdkWinRtTests
745
746 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
747
748 + // Wait for the in-container HTTP server to start listening before issuing a request.
749 + WaitForProcessOutput(container.InitProcess(), "Serving HTTP on");
750 +
751 ExpectHttpResponse(L"http://127.0.0.1:12341", 200, true);
752 }
753
@@ -718,6 +757,7 @@ class WslcSdkWinRtTests
757 procSettings.CmdLine(winrt::single_threaded_vector<winrt::hstring>({L"python3", L"-m", L"http.server", L"8000"}));
758 procSettings.EnvironmentVariables(
759 winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"PYTHONUNBUFFERED", L"1"}}));
760 + procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
761
762 auto portMapping = WSLCSDK::ContainerPortMapping(12343, 8000, WSLCSDK::PortProtocol::TCP);
763 portMapping.WindowsAddress(winrt::Windows::Networking::HostName(L"127.0.0.1"));
@@ -732,6 +772,9 @@ class WslcSdkWinRtTests
772
773 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
774
775 + // Wait for the in-container HTTP server to start listening before issuing a request.
776 + WaitForProcessOutput(container.InitProcess(), "Serving HTTP on");
777 +
778 ExpectHttpResponse(L"http://127.0.0.1:12343", 200, true);
779 }
780
@@ -742,6 +785,7 @@ class WslcSdkWinRtTests
785 winrt::single_threaded_vector<winrt::hstring>({L"python3", L"-m", L"http.server", L"--bind", L"::", L"8000"}));
786 procSettings.EnvironmentVariables(
787 winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"PYTHONUNBUFFERED", L"1"}}));
788 + procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
789
790 auto portMapping = WSLCSDK::ContainerPortMapping(12344, 8000, WSLCSDK::PortProtocol::TCP);
791 portMapping.WindowsAddress(winrt::Windows::Networking::HostName(L"::1"));
@@ -756,6 +800,9 @@ class WslcSdkWinRtTests
800
801 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
802
803 + // Wait for the in-container HTTP server to start listening before issuing a request.
804 + WaitForProcessOutput(container.InitProcess(), "Serving HTTP on");
805 +
806 ExpectHttpResponse(L"http://[::1]:12344", 200, true);
807 }
808 }
test/windows/wslc/WSLCCLISettingsUnitTests.cpp
+19
@@ -294,6 +294,8 @@ class WSLCCLISettingsUnitTests
294 " networkingMode: default\n"
295 " hostFileShareMode: default\n"
296 " dnsTunneling: default\n"
297 + "experimental:\n"
298 + " portRelay: default\n"
299 "credentialStore: default\n");
300
301 UserSettingsTest s{dir};
@@ -306,6 +308,7 @@ class WSLCCLISettingsUnitTests
308 VERIFY_ARE_EQUAL(static_cast<int>(WSLCNetworkingModeVirtioProxy), static_cast<int>(s.Get<Setting::SessionNetworkingMode>()));
309 VERIFY_ARE_EQUAL(static_cast<int>(HostFileShareMode::VirtioFs), static_cast<int>(s.Get<Setting::SessionHostFileShareMode>()));
310 VERIFY_IS_TRUE(s.Get<Setting::SessionDnsTunneling>());
311 + VERIFY_ARE_EQUAL(static_cast<int>(PortRelayType::VirtioNet), static_cast<int>(s.Get<Setting::SessionPortRelay>()));
312 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
313 }
314
@@ -521,12 +524,28 @@ class WSLCCLISettingsUnitTests
524 " networkingMode: nat\n"
525 " hostFileShareMode: virtiofs\n"
526 " dnsTunneling: true\n"
527 + "experimental:\n"
528 + " portRelay: wslrelay\n"
529 "credentialStore: wincred\n");
530
531 UserSettingsTest s{dir};
532
533 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
534 }
535 +
536 + TEST_METHOD(Validation_PortRelay_ExplicitValue)
537 + {
538 + auto dir = UniqueTempDir();
539 + WriteFile(
540 + dir / L"settings.yaml",
541 + "experimental:\n"
542 + " portRelay: wslrelay\n");
543 +
544 + UserSettingsTest s{dir};
545 +
546 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
547 + VERIFY_ARE_EQUAL(static_cast<int>(PortRelayType::WslRelay), static_cast<int>(s.Get<Setting::SessionPortRelay>()));
548 + }
549 };
550
551 } // namespace WSLCCLISettingsUnitTests
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+44 -9
@@ -1085,20 +1085,55 @@ class WSLCE2EContainerCreateTests
1085 }
1086
1087 // https://github.com/microsoft/WSL/issues/14433
1088 - WSLC_TEST_METHOD(WSLCE2E_Container_Create_PortUdp_NotSupported)
1088 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_UDP)
1089 {
1090 - auto result = RunWslc(std::format(L"container create --name {} -p 80:80/udp {}", WslcContainerName, DebianImage.NameAndTag()));
1091 - result.Verify({.Stderr = L"Port mappings with specific host IPs or UDP protocol are not currently supported\r\nError code: ERROR_NOT_SUPPORTED\r\n", .ExitCode = 1});
1092 - EnsureContainerDoesNotExist(WslcContainerName);
1090 + // Port bindings only show up in inspect after start, so create then start before inspecting.
1091 + auto result = RunWslc(std::format(
1092 + L"container create --name {} -p {}:{}/udp {} sleep 5",
1093 + WslcContainerName,
1094 + HostTestPort1,
1095 + ContainerTestPort,
1096 + DebianImage.NameAndTag()));
1097 + result.Verify({.Stderr = L"", .ExitCode = 0});
1098 +
1099 + result = RunWslc(std::format(L"container start {}", WslcContainerName));
1100 + result.Verify({.Stderr = L"", .ExitCode = 0});
1101 +
1102 + // Verify the UDP port mapping is correct in the container inspect data.
1103 + const auto inspect = InspectContainer(WslcContainerName);
1104 + const auto portKey = std::to_string(ContainerTestPort) + "/udp";
1105 + VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
1106 +
1107 + const auto& bindings = inspect.Ports.at(portKey);
1108 + VERIFY_ARE_EQUAL(1u, bindings.size());
1109 + VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), bindings[0].HostPort);
1110 + VERIFY_ARE_EQUAL("127.0.0.1", bindings[0].HostIp);
1111 }
1112
1113 // https://github.com/microsoft/WSL/issues/14433
1096 - WSLC_TEST_METHOD(WSLCE2E_Container_Create_PortHostIP_NotSupported)
1114 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_HostIP)
1115 {
1098 - auto result =
1099 - RunWslc(std::format(L"container create --name {} -p 127.0.0.1:80:80 {}", WslcContainerName, DebianImage.NameAndTag()));
1100 - result.Verify({.Stderr = L"Port mappings with specific host IPs or UDP protocol are not currently supported\r\nError code: ERROR_NOT_SUPPORTED\r\n", .ExitCode = 1});
1101 - EnsureContainerDoesNotExist(WslcContainerName);
1116 + // Port bindings only show up in inspect after start, so create then start before inspecting.
1117 + auto result = RunWslc(std::format(
1118 + L"container create --name {} -p 127.0.0.1:{}:{} {} sleep 5",
1119 + WslcContainerName,
1120 + HostTestPort1,
1121 + ContainerTestPort,
1122 + DebianImage.NameAndTag()));
1123 + result.Verify({.Stderr = L"", .ExitCode = 0});
1124 +
1125 + result = RunWslc(std::format(L"container start {}", WslcContainerName));
1126 + result.Verify({.Stderr = L"", .ExitCode = 0});
1127 +
1128 + // Verify the port mapping is bound to the requested host IP in the container inspect data.
1129 + const auto inspect = InspectContainer(WslcContainerName);
1130 + const auto portKey = std::to_string(ContainerTestPort) + "/tcp";
1131 + VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
1132 +
1133 + const auto& bindings = inspect.Ports.at(portKey);
1134 + VERIFY_ARE_EQUAL(1u, bindings.size());
1135 + VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), bindings[0].HostPort);
1136 + VERIFY_ARE_EQUAL("127.0.0.1", bindings[0].HostIp);
1137 }
1138
1139 private:
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+86 -9
@@ -416,6 +416,8 @@ class WSLCE2EContainerRunTests
416 GetPythonHttpServerScript(ContainerTestPort)));
417 result.Verify({.Stderr = L"", .ExitCode = 0});
418
419 + WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
420 +
421 // From the host side, verify we can connect to both ports
422 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
423 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort2).c_str(), HTTP_STATUS_OK, true);
@@ -443,7 +445,9 @@ class WSLCE2EContainerRunTests
445 auto startResult = RunWslc(std::format(L"container start {}", containerId));
446 startResult.Verify(
447 {.Stderr = std::format(
446 - L"Port 127.0.0.1:{}/tcp is already in use, cannot start container {}\r\nError code: ERROR_ALREADY_EXISTS\r\n", HostTestPort1, containerId),
448 + L"Failed to map port '127.0.0.1:{}/tcp', Only one usage of each socket address (protocol/network "
449 + L"address/port) is normally permitted. \r\nError code: WSAEADDRINUSE\r\n",
450 + HostTestPort1),
451 .ExitCode = 1});
452
453 // Clean up the created container
@@ -455,9 +459,37 @@ class WSLCE2EContainerRunTests
459 runResult.Verify({.ExitCode = 1});
460
461 VerifyContainerIsNotListed(WslcContainerName2);
462 +
463 + // Repeat the conflict scenario for an IPv6 loopback ([::1]) binding to validate the IPv6 error message.
464 + auto ipv6Server = RunWslc(std::format(
465 + L"container run -d --name {} -p [::1]:{}:{} {} {}",
466 + WslcContainerName2,
467 + HostTestPort2,
468 + ContainerTestPort,
469 + PythonImage.NameAndTag(),
470 + GetPythonHttpServerScript(ContainerTestPort)));
471 + ipv6Server.Verify({.Stderr = L"", .ExitCode = 0});
472 +
473 + // Create a second container mapping the same IPv6 address/port to validate the full error message.
474 + auto ipv6CreateResult =
475 + RunWslc(std::format(L"container create -p [::1]:{}:{} {}", HostTestPort2, ContainerTestPort, DebianImage.NameAndTag()));
476 + ipv6CreateResult.Verify({.Stderr = L"", .ExitCode = 0});
477 + auto ipv6ContainerId = ipv6CreateResult.GetStdoutOneLine();
478 +
479 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
480 + RunWslc(std::format(L"container rm {}", ipv6ContainerId)).Verify({.Stderr = L"", .ExitCode = 0});
481 + });
482 +
483 + // Attempt to start — should fail with a port conflict, with the IPv6 address bracketed in the message.
484 + auto ipv6StartResult = RunWslc(std::format(L"container start {}", ipv6ContainerId));
485 + ipv6StartResult.Verify(
486 + {.Stderr = std::format(
487 + L"Failed to map port '[::1]:{}/tcp', Only one usage of each socket address (protocol/network "
488 + L"address/port) is normally permitted. \r\nError code: WSAEADDRINUSE\r\n",
489 + HostTestPort2),
490 + .ExitCode = 1});
491 }
492
460 - // https://github.com/microsoft/WSL/issues/14433
493 WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortEphemeral)
494 {
495 // Start a container with an ephemeral host port mapping (-p 8080 means host picks a random port)
@@ -465,6 +497,9 @@ class WSLCE2EContainerRunTests
497 L"container run -d --name {} -p {} {} {}", WslcContainerName, ContainerTestPort, PythonImage.NameAndTag(), GetPythonHttpServerScript(ContainerTestPort)));
498 result.Verify({.Stderr = L"", .ExitCode = 0});
499
500 + // Wait for the in-container HTTP server to start listening before connecting.
501 + WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
502 +
503 // Inspect the container to find the allocated host port
504 auto inspectContainer = InspectContainer(WslcContainerName);
505 auto portKey = std::to_string(ContainerTestPort) + "/tcp";
@@ -480,18 +515,57 @@ class WSLCE2EContainerRunTests
515 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", hostPort).c_str(), HTTP_STATUS_OK, true);
516 }
517
483 - // https://github.com/microsoft/WSL/issues/14433
484 - WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortUdp_NotSupported)
518 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_UDP)
519 {
486 - auto result = RunWslc(std::format(L"container run -p 80:80/udp {}", DebianImage.NameAndTag()));
487 - result.Verify({.Stderr = L"Port mappings with specific host IPs or UDP protocol are not currently supported\r\nError code: ERROR_NOT_SUPPORTED\r\n", .ExitCode = 1});
520 + // Start a container with a UDP echo server listening on a port.
521 + auto result = RunWslc(std::format(
522 + L"container run -d --name {} -p {}:{}/udp {} {}",
523 + WslcContainerName,
524 + HostTestPort1,
525 + ContainerTestPort,
526 + PythonImage.NameAndTag(),
527 + GetPythonUdpEchoServerScript(ContainerTestPort)));
528 + result.Verify({.Stderr = L"", .ExitCode = 0});
529 +
530 + // Send a datagram from the host and verify the container echoes it back uppercased.
531 + SendUdpAndReceive(HostTestPort1, "hello", "HELLO");
532 +
533 + // Verify the UDP port mapping is reflected in the container inspect data.
534 + auto inspectContainer = InspectContainer(WslcContainerName);
535 + auto portKey = std::to_string(ContainerTestPort) + "/udp";
536 + VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
537 +
538 + auto portBindings = inspectContainer.Ports[portKey];
539 + VERIFY_ARE_EQUAL(1u, portBindings.size());
540 + VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
541 + VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
542 }
543
544 // https://github.com/microsoft/WSL/issues/14433
491 - WSLC_TEST_METHOD(WSLCE2E_Container_Run_PortHostIP_NotSupported)
545 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_HostIP)
546 {
493 - auto result = RunWslc(std::format(L"container run -p 127.0.0.1:80:80 {}", DebianImage.NameAndTag()));
494 - result.Verify({.Stderr = L"Port mappings with specific host IPs or UDP protocol are not currently supported\r\nError code: ERROR_NOT_SUPPORTED\r\n", .ExitCode = 1});
547 + // Start a container with a server listening on a port, bound to a specific host IP (127.0.0.1).
548 + auto result = RunWslc(std::format(
549 + L"container run -d --name {} -p 127.0.0.1:{}:{} {} {}",
550 + WslcContainerName,
551 + HostTestPort1,
552 + ContainerTestPort,
553 + PythonImage.NameAndTag(),
554 + GetPythonHttpServerScript(ContainerTestPort)));
555 + result.Verify({.Stderr = L"", .ExitCode = 0});
556 +
557 + WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
558 +
559 + ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
560 +
561 + auto inspectContainer = InspectContainer(WslcContainerName);
562 + auto portKey = std::to_string(ContainerTestPort) + "/tcp";
563 + VERIFY_IS_TRUE(inspectContainer.Ports.contains(portKey));
564 +
565 + auto portBindings = inspectContainer.Ports[portKey];
566 + VERIFY_ARE_EQUAL(1u, portBindings.size());
567 + VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), portBindings[0].HostPort);
568 + VERIFY_ARE_EQUAL("127.0.0.1", portBindings[0].HostIp);
569 }
570
571 WSLC_TEST_METHOD(WSLCE2E_Container_Run_Port_TCP)
@@ -506,6 +580,9 @@ class WSLCE2EContainerRunTests
580 GetPythonHttpServerScript(ContainerTestPort)));
581 result.Verify({.Stderr = L"", .ExitCode = 0});
582
583 + // Wait for the in-container HTTP server to start listening before connecting.
584 + WaitForContainerOutput(WslcContainerName, "Serving HTTP on");
585 +
586 // Verify we can connect to the server from the host side
587 ExpectHttpResponse(std::format(L"http://127.0.0.1:{}", HostTestPort1).c_str(), HTTP_STATUS_OK, true);
588
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+43 -1
@@ -617,7 +617,49 @@ void WriteTestFileContent(const std::filesystem::path& filePath, const std::stri
617
618 std::wstring GetPythonHttpServerScript(uint16_t port)
619 {
620 - return std::format(L"python3 -m http.server {}", port);
620 + return std::format(L"python3 -u -m http.server {}", port);
621 +}
622 +
623 +std::wstring GetPythonUdpEchoServerScript(uint16_t port)
624 +{
625 + // Inline Python UDP echo server: echoes each received datagram back uppercased, forever.
626 + return std::format(
627 + L"python3 -c \"import socket;"
628 + L"s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);"
629 + L"s.bind(('0.0.0.0',{}));"
630 + L"[s.sendto(d.upper(),a) for d,a in iter(lambda:s.recvfrom(1024),0)]\"",
631 + port);
632 +}
633 +
634 +std::string SendUdpAndReceive(uint16_t hostPort, const std::string& payload, const std::string& expectedReply, int family)
635 +{
636 + SOCKADDR_INET addr{};
637 + addr.si_family = static_cast<ADDRESS_FAMILY>(family);
638 + INETADDR_SETLOOPBACK(reinterpret_cast<PSOCKADDR>(&addr));
639 + SS_PORT(&addr) = htons(hostPort);
640 +
641 + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
642 + do
643 + {
644 + wil::unique_socket sock{::socket(family, SOCK_DGRAM, IPPROTO_UDP)};
645 + THROW_LAST_ERROR_IF(!sock);
646 +
647 + DWORD timeout = 1000;
648 + THROW_LAST_ERROR_IF(setsockopt(sock.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeout), sizeof(timeout)) == SOCKET_ERROR);
649 +
650 + if (sendto(sock.get(), payload.data(), static_cast<int>(payload.size()), 0, reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) != SOCKET_ERROR)
651 + {
652 + char buf[1024];
653 + const int received = recvfrom(sock.get(), buf, sizeof(buf), 0, nullptr, nullptr);
654 + if (received != SOCKET_ERROR && received > 0 && std::string(buf, received) == expectedReply)
655 + {
656 + return std::string(buf, received);
657 + }
658 + }
659 + } while (std::chrono::steady_clock::now() < deadline);
660 +
661 + VERIFY_FAIL(L"Timed out waiting for expected UDP echo reply from container");
662 + return {};
663 }
664
665 namespace {
test/windows/wslc/e2e/WSLCE2EHelpers.h
+5
@@ -153,6 +153,11 @@ inline auto SetupTestDirectory(const std::filesystem::path& directory)
153 }
154
155 std::wstring GetPythonHttpServerScript(uint16_t port);
156 +std::wstring GetPythonUdpEchoServerScript(uint16_t port);
157 +
158 +std::string SendUdpAndReceive(uint16_t hostPort, const std::string& payload, const std::string& expectedReply, int family = AF_INET);
159 +
160 +void WaitForContainerOutput(const std::wstring& containerName, std::string_view expected, std::chrono::milliseconds timeout = std::chrono::seconds(60));
161
162 // Default timeout of 0 will execute once.
163 template <typename IntervalRep, typename IntervalPeriod, typename TimeoutRep, typename TimeoutPeriod>
test/windows/wslc/e2e/WSLCExecutor.cpp
+21
@@ -231,6 +231,27 @@ WSLCExecutionResult RunWslcAndRedirectToFile(const std::wstring& commandLine, st
231 return {.CommandLine = std::move(effectiveCommandLine), .Stdout = L"", .Stderr = stdErrOutput, .ExitCode = exitCode};
232 }
233
234 +void WaitForContainerOutput(const std::wstring& containerName, std::string_view expected, std::chrono::milliseconds timeout)
235 +{
236 + auto cmd = std::format(L"\"{}\" container logs -f {}", GetWslcPath(), containerName);
237 +
238 + auto [parentStdoutRead, childStdoutWrite] = wslutil::OpenAnonymousPipe(65536, true, false);
239 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
240 +
241 + SubProcess process(nullptr, cmd.c_str());
242 + process.SetStdHandles(nullptr, childStdoutWrite.get(), nullptr);
243 +
244 + wil::unique_handle processHandle = process.Start();
245 + childStdoutWrite.reset();
246 +
247 + auto terminate = wil::scope_exit([&]() {
248 + LOG_IF_WIN32_BOOL_FALSE(TerminateProcess(processHandle.get(), 1));
249 + LOG_LAST_ERROR_IF(WaitForSingleObject(processHandle.get(), DefaultWaitTimeoutMs) != WAIT_OBJECT_0);
250 + });
251 +
252 + WaitForOutput(wil::unique_handle{parentStdoutRead.release()}, expected, timeout);
253 +}
254 +
255 std::wstring GetWslcHeader()
256 {
257 std::wstringstream header;