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;