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();