Deny guest binds to host ephemeral port range in mirrored mode (#40597)
* deny host range binds * code review --------- Co-authored-by: Catalin-Emil Fetoiu <cfetoiu@microsoft.com>
FetoiuCatalin committed
May 22, 2026 at 12:57 UTC
d1936613453fcfeda9408830b42be4a2ac31d652
3 files changed
+147
-2
src/windows/service/exe/WslCoreGuestNetworkService.cpp
+75
-1
@@ -8,6 +8,7 @@
8
#include <TraceLoggingProvider.h>
9
10
#include "Stringify.h"
11
+#include "WmiService.h"
12
#include "WslTelemetry.h"
13
#include "hns_schema.h"
14
@@ -16,6 +17,8 @@ static constexpr auto c_dnsPortNumber = 53;
17
static constexpr auto c_mdnsPortNumber = 5353;
18
static constexpr auto c_llmnrPortNumber = 5355;
19
20
+static constexpr std::pair<uint16_t, uint16_t> c_invalidEphemeralPortRange = {1, 0};
21
+
22
using namespace wsl::shared;
23
24
constexpr IN_ADDR c_ipv4LoopbackAddr = IN4ADDR_LOOPBACK_INIT;
@@ -52,6 +55,15 @@ void wsl::core::networking::GuestNetworkService::CreateGuestNetworkService(
55
// Always allow binds for 53. This is a workaround to unblock Docker Desktop and needs to be revisited in the future.
56
m_ignoredPorts.insert(c_dnsPortNumber);
57
58
+ m_hostTcpEphemeralPortRange = QueryHostEphemeralPortRange(L"MSFT_NetTCPSetting");
59
+ m_hostUdpEphemeralPortRange = QueryHostEphemeralPortRange(L"MSFT_NetUDPSetting");
60
+ WSL_LOG(
61
+ "GuestNetworkService::CreateGuestNetworkService - host ephemeral port ranges",
62
+ TraceLoggingValue(m_hostTcpEphemeralPortRange.first, "tcpStartPort"),
63
+ TraceLoggingValue(m_hostTcpEphemeralPortRange.second, "tcpEndPort"),
64
+ TraceLoggingValue(m_hostUdpEphemeralPortRange.first, "udpStartPort"),
65
+ TraceLoggingValue(m_hostUdpEphemeralPortRange.second, "udpEndPort"));
66
+
67
hns::GuestNetworkService request{};
68
request.VirtualMachineId = VmId;
69
request.MirrorHostNetworking = true;
@@ -166,6 +178,53 @@ bool wsl::core::networking::GuestNetworkService::IsPortAllocationMulticast(const
178
return false;
179
}
180
181
+std::pair<uint16_t, uint16_t> wsl::core::networking::GuestNetworkService::QueryHostEphemeralPortRange(LPCWSTR WmiClassName) noexcept
182
+try
183
+{
184
+ const auto com = wil::CoInitializeEx();
185
+ WmiService service(L"ROOT\\StandardCimv2");
186
+ WmiEnumerate enumSetting(service);
187
+
188
+ auto query = std::format(L"SELECT DynamicPortRangeStartPort, DynamicPortRangeNumberOfPorts FROM {}", WmiClassName);
189
+ for (const auto& instance : enumSetting.query(query.c_str()))
190
+ {
191
+ unsigned int startPort = 0;
192
+ unsigned int numberOfPorts = 0;
193
+
194
+ if (instance.get(L"DynamicPortRangeStartPort", &startPort) &&
195
+ instance.get(L"DynamicPortRangeNumberOfPorts", &numberOfPorts) && startPort > 0 && numberOfPorts > 0)
196
+ {
197
+ const auto endPort = static_cast<uint64_t>(startPort) + numberOfPorts - 1;
198
+ if (startPort > UINT16_MAX || endPort > UINT16_MAX)
199
+ {
200
+ LOG_HR_MSG(E_FAIL, "Ephemeral port range overflows uint16_t: start=%u, count=%u", startPort, numberOfPorts);
201
+ return c_invalidEphemeralPortRange;
202
+ }
203
+
204
+ return {static_cast<uint16_t>(startPort), static_cast<uint16_t>(endPort)};
205
+ }
206
+ }
207
+
208
+ LOG_HR_MSG(E_FAIL, "No valid ephemeral port range found in WMI class %ls", WmiClassName);
209
+ return c_invalidEphemeralPortRange;
210
+}
211
+catch (...)
212
+{
213
+ LOG_CAUGHT_EXCEPTION_MSG("Failed to query host ephemeral port range from WMI class %ls", WmiClassName);
214
+ return c_invalidEphemeralPortRange;
215
+}
216
+
217
+bool wsl::core::networking::GuestNetworkService::IsPortInHostEphemeralRange(uint16_t PortNumber, int Protocol) const noexcept
218
+{
219
+ const auto& range = (Protocol == IPPROTO_UDP) ? m_hostUdpEphemeralPortRange : m_hostTcpEphemeralPortRange;
220
+ return PortNumber >= range.first && PortNumber <= range.second;
221
+}
222
+
223
+bool wsl::core::networking::GuestNetworkService::IsPortInGuestEphemeralRange(uint16_t PortNumber) const noexcept
224
+{
225
+ return PortNumber >= m_reservedPortRange.startingPort && PortNumber <= m_reservedPortRange.endingPort;
226
+}
227
+
228
int wsl::core::networking::GuestNetworkService::OnPortAllocationRequest(const SOCKADDR_INET& Address, _In_ int Protocol, _In_ bool Allocate) noexcept
229
try
230
{
@@ -204,7 +263,22 @@ try
263
264
const auto lock = m_dataLock.lock_exclusive();
265
207
- if (PortNumber >= m_reservedPortRange.startingPort && PortNumber <= m_reservedPortRange.endingPort)
266
+ // The guest's reserved ephemeral range can overlap with the host range. Ports in
267
+ // the guest range are safe for the guest to use even if they fall within the host range.
268
+ if (IsPortInHostEphemeralRange(PortNumber, Protocol) && !IsPortInGuestEphemeralRange(PortNumber))
269
+ {
270
+ const auto& range = (Protocol == IPPROTO_UDP) ? m_hostUdpEphemeralPortRange : m_hostTcpEphemeralPortRange;
271
+ WSL_LOG(
272
+ "GuestNetworkService::OnPortAllocationRequest - denying port in host ephemeral range",
273
+ TraceLoggingValue(StringAddress.c_str(), "IP address"),
274
+ TraceLoggingValue(Protocol == IPPROTO_TCP ? "TCP" : "UDP", "protocol"),
275
+ TraceLoggingValue(PortNumber, "portNumber"),
276
+ TraceLoggingValue(range.first, "hostEphemeralStart"),
277
+ TraceLoggingValue(range.second, "hostEphemeralEnd"));
278
+ return -LX_EADDRINUSE;
279
+ }
280
+
281
+ if (IsPortInGuestEphemeralRange(PortNumber))
282
{
283
WSL_LOG(
284
"GuestNetworkService::OnPortAllocationRequest",
src/windows/service/exe/WslCoreGuestNetworkService.h
+12
-1
@@ -54,11 +54,18 @@ private:
54
ULONG ReferenceCount;
55
};
56
57
- // Returns true if the port allocation should be always allowed, without Windows.
57
+ // Returns true if the port allocation should be always allowed, without asking HNS.
58
static bool IsPortAllocationLoopbackException(const SOCKADDR_INET& Address) noexcept;
59
60
static bool IsPortAllocationMulticast(const SOCKADDR_INET& Address, _In_ int Protocol) noexcept;
61
62
+ bool IsPortInHostEphemeralRange(uint16_t PortNumber, int Protocol) const noexcept;
63
+
64
+ _Requires_lock_held_(m_dataLock)
65
+ bool IsPortInGuestEphemeralRange(uint16_t PortNumber) const noexcept;
66
+
67
+ static std::pair<uint16_t, uint16_t> QueryHostEphemeralPortRange(LPCWSTR WmiClassName) noexcept;
68
+
69
static std::optional<LxssDynamicFunction<decltype(HcnReserveGuestNetworkServicePortRange)>> m_allocatePortRange;
70
static std::optional<LxssDynamicFunction<decltype(HcnReserveGuestNetworkServicePort)>> m_allocatePort;
71
static std::optional<LxssDynamicFunction<decltype(HcnReleaseGuestNetworkServicePortReservationHandle)>> m_releasePort;
@@ -70,5 +77,9 @@ private:
77
_Guarded_by_(m_dataLock) std::set<uint16_t> m_ignoredPorts;
78
_Guarded_by_(m_dataLock) std::map<std::pair<HCN_PORT_PROTOCOL, USHORT>, HcnPortReservation> m_reservedPorts;
79
_Guarded_by_(m_dataLock) HCN_PORT_RANGE_RESERVATION m_reservedPortRange {};
80
+
81
+ // Host ephemeral port ranges can change. They are queried once at startup, if a change occurs, the service will need to be restarted.
82
+ std::pair<uint16_t, uint16_t> m_hostTcpEphemeralPortRange{};
83
+ std::pair<uint16_t, uint16_t> m_hostUdpEphemeralPortRange{};
84
};
85
} // namespace wsl::core::networking
test/windows/NetworkTests.cpp
+60
@@ -4241,6 +4241,66 @@ class MirroredTests
4241
VERIFY_IS_TRUE(canBindUdp);
4242
}
4243
4244
+ void VerifyGuestBindToHostEphemeralRangeDenied(LPCWSTR ProtocolSettingClass, LPCWSTR SocatProtocolPrefix)
4245
+ {
4246
+ MIRRORED_NETWORKING_TEST_ONLY();
4247
+
4248
+ m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
4249
+ WaitForMirroredStateInLinux();
4250
+
4251
+ // Query the host ephemeral port range via PowerShell.
4252
+ auto startQuery =
4253
+ std::wstring(L"(") + ProtocolSettingClass +
4254
+ L" | Where-Object { $_.DynamicPortRangeStartPort -gt 0 } | Select-Object -First 1).DynamicPortRangeStartPort";
4255
+ auto [startStr, _1] = LxsstuLaunchPowershellAndCaptureOutput(startQuery.c_str(), 0);
4256
+ const auto hostEphemeralStart = std::stoi(startStr);
4257
+
4258
+ auto countQuery =
4259
+ std::wstring(L"(") + ProtocolSettingClass +
4260
+ L" | Where-Object { $_.DynamicPortRangeNumberOfPorts -gt 0 } | Select-Object -First 1).DynamicPortRangeNumberOfPorts";
4261
+ auto [countStr, _2] = LxsstuLaunchPowershellAndCaptureOutput(countQuery.c_str(), 0);
4262
+ const auto hostEphemeralEnd = hostEphemeralStart + std::stoi(countStr) - 1;
4263
+
4264
+ // Get the guest ephemeral port range.
4265
+ auto [start, err1] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv4/ip_local_port_range | cut -f1", 0);
4266
+ start.pop_back();
4267
+ const auto guestEphemeralRangeStart = std::stoi(start);
4268
+
4269
+ auto [end, err2] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv4/ip_local_port_range | cut -f2", 0);
4270
+ end.pop_back();
4271
+ const auto guestEphemeralRangeEnd = std::stoi(end);
4272
+
4273
+ // Pick a port in the host ephemeral range but not in the guest's assigned range.
4274
+ // The ranges may overlap
4275
+ int testPort = 0;
4276
+ if (hostEphemeralStart < guestEphemeralRangeStart || hostEphemeralStart > guestEphemeralRangeEnd)
4277
+ {
4278
+ testPort = hostEphemeralStart;
4279
+ }
4280
+ else if (guestEphemeralRangeEnd < hostEphemeralEnd)
4281
+ {
4282
+ testPort = guestEphemeralRangeEnd + 1;
4283
+ }
4284
+ else
4285
+ {
4286
+ VERIFY_FAIL(L"Guest ephemeral range fully covers the host ephemeral range, cannot find a test port");
4287
+ }
4288
+
4289
+ auto socatArg = std::wstring(SocatProtocolPrefix) + std::to_wstring(testPort);
4290
+ auto [listener, success, read] = NetworkTests::BindGuestPortHelper(socatArg);
4291
+ VERIFY_IS_FALSE(success);
4292
+ }
4293
+
4294
+ WSL2_TEST_METHOD(GuestTcpBindToHostEphemeralRangeDenied)
4295
+ {
4296
+ VerifyGuestBindToHostEphemeralRangeDenied(L"Get-NetTCPSetting", L"TCP4-LISTEN:");
4297
+ }
4298
+
4299
+ WSL2_TEST_METHOD(GuestUdpBindToHostEphemeralRangeDenied)
4300
+ {
4301
+ VerifyGuestBindToHostEphemeralRangeDenied(L"Get-NetUDPSetting", L"UDP4-LISTEN:");
4302
+ }
4303
+
4304
WSL2_TEST_METHOD(NonRootNamespaceEphemeralBind)
4305
{
4306
MIRRORED_NETWORKING_TEST_ONLY();