master
cpp 455 lines 20.4 KB
Raw
1 // Copyright (C) Microsoft Corporation. All rights reserved.
2
3 #include "precomp.h"
4
5 #include "WslCoreGuestNetworkService.h"
6 #include "WslCoreNetworkingSupport.h"
7
8 #include <TraceLoggingProvider.h>
9
10 #include "Stringify.h"
11 #include "WmiService.h"
12 #include "WslTelemetry.h"
13 #include "hns_schema.h"
14
15 static constexpr auto c_computeNetworkModuleName = L"ComputeNetwork.dll";
16 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;
25
26 std::optional<LxssDynamicFunction<decltype(HcnReserveGuestNetworkServicePortRange)>> wsl::core::networking::GuestNetworkService::m_allocatePortRange;
27 std::optional<LxssDynamicFunction<decltype(HcnReserveGuestNetworkServicePort)>> wsl::core::networking::GuestNetworkService::m_allocatePort;
28 std::optional<LxssDynamicFunction<decltype(HcnReleaseGuestNetworkServicePortReservationHandle)>> wsl::core::networking::GuestNetworkService::m_releasePort;
29
30 wsl::core::networking::GuestNetworkService::GuestNetworkService() noexcept
31 {
32 if (wsl::core::networking::IsFlowSteeringSupportedByHns())
33 {
34
35 static std::once_flag flag;
36 std::call_once(flag, [&]() {
37 try
38 {
39 m_allocatePortRange.emplace(c_computeNetworkModuleName, "HcnReserveGuestNetworkServicePortRange");
40 m_allocatePort.emplace(c_computeNetworkModuleName, "HcnReserveGuestNetworkServicePort");
41 m_releasePort.emplace(c_computeNetworkModuleName, "HcnReleaseGuestNetworkServicePortReservationHandle");
42 }
43 CATCH_LOG()
44 });
45 }
46 }
47
48 void wsl::core::networking::GuestNetworkService::CreateGuestNetworkService(
49 const bool firewallEnabled, const std::set<uint16_t>& IgnoredPorts, const GUID& VmId, const UUID& ServerUuid, HCN_NOTIFICATION_CALLBACK Callback, void* CallbackContext)
50 {
51 // we must first enable mirrored networking - which must by done by indirectly issuing a query with these special flags
52 wsl::core::networking::EnumerateMirroredNetworksAndHyperVFirewall(firewallEnabled);
53
54 m_ignoredPorts = IgnoredPorts;
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
61 WI_ASSERT(m_hostTcpEphemeralPortRange.first <= m_hostTcpEphemeralPortRange.second);
62 WI_ASSERT(m_hostUdpEphemeralPortRange.first <= m_hostUdpEphemeralPortRange.second);
63
64 WSL_LOG(
65 "GuestNetworkService::CreateGuestNetworkService - host ephemeral port ranges",
66 TraceLoggingValue(m_hostTcpEphemeralPortRange.first, "tcpStartPort"),
67 TraceLoggingValue(m_hostTcpEphemeralPortRange.second, "tcpEndPort"),
68 TraceLoggingValue(m_hostUdpEphemeralPortRange.first, "udpStartPort"),
69 TraceLoggingValue(m_hostUdpEphemeralPortRange.second, "udpEndPort"));
70
71 hns::GuestNetworkService request{};
72 request.VirtualMachineId = VmId;
73 request.MirrorHostNetworking = true;
74 request.SchemaVersion = {2, 0};
75
76 request.GnsRpcServerInformation.EndpointType = hns::RpcEndpointType::LRpc;
77 request.GnsRpcServerInformation.ObjectUuid = ServerUuid;
78 WI_SetFlag(request.Flags, hns::GuestNetworkServiceFlags::IsFlowsteered);
79 WI_SetFlag(request.Flags, hns::GuestNetworkServiceFlags::IsFlowsteeredSelfManaged);
80
81 wil::unique_cotaskmem_string error;
82 const auto result = ::HcnCreateGuestNetworkService(VmId, ToJsonW(request).c_str(), &m_service, &error);
83 WSL_LOG(
84 "GuestNetworkService::CreateGuestNetworkService [HcnCreateGuestNetworkService]",
85 TraceLoggingValue(request.VirtualMachineId, "virtualMachineId"),
86 TraceLoggingValue(request.MirrorHostNetworking, "mirrorHostNetworking"),
87 TraceLoggingValue(request.SchemaVersion.Major, "schemaMajorVersion"),
88 TraceLoggingValue(request.SchemaVersion.Minor, "schemaMinorVersion"),
89 TraceLoggingValue(JsonEnumToString(request.GnsRpcServerInformation.EndpointType).c_str(), "endpointType"),
90 TraceLoggingValue(request.GnsRpcServerInformation.ObjectUuid, "objectUuid"),
91 TraceLoggingValue(static_cast<uint32_t>(request.Flags), "flags-value"),
92 TraceLoggingHResult(result, "result"),
93 TraceLoggingValue(error.is_valid() ? error.get() : L"null", "errorString"));
94 THROW_IF_FAILED_MSG(result, "%ls", error.get());
95 m_id = VmId;
96
97 m_guestNetworkServiceCallback = windows::common::hcs::RegisterGuestNetworkServiceCallback(m_service, Callback, CallbackContext);
98 SetGuestNetworkServiceState(hns::GuestNetworkServiceState::Bootstrapping);
99 }
100
101 void wsl::core::networking::GuestNetworkService::SetGuestNetworkServiceState(_In_ hns::GuestNetworkServiceState State) const
102 {
103 hns::ModifyGuestNetworkServiceSettingRequest modifyRequest{};
104 modifyRequest.RequestType = hns::ModifyRequestType::Update;
105 modifyRequest.ResourceType = hns::GuestNetworkServiceResourceType::State;
106 modifyRequest.Settings.State = State;
107
108 const auto result = ::HcnModifyGuestNetworkService(m_service.get(), ToJsonW(modifyRequest).c_str(), nullptr);
109 WSL_LOG(
110 "GuestNetworkService::SetGuestNetworkServiceState [HcnModifyGuestNetworkService]",
111 TraceLoggingValue(JsonEnumToString(modifyRequest.Settings.State).c_str(), "state"));
112 THROW_IF_FAILED(result);
113 }
114
115 std::pair<uint16_t, uint16_t> wsl::core::networking::GuestNetworkService::AllocateEphemeralPortRange()
116 {
117 FAIL_FAST_IF(!IsFlowSteeringSupportedByHns());
118
119 const auto lock = m_dataLock.lock_exclusive();
120
121 HANDLE port{nullptr};
122 auto releasePortOnError = wil::scope_exit([&] {
123 if (port)
124 {
125 m_releasePort.value()(port);
126 }
127 });
128
129 // N.B. Use an odd number of ports to avoid Linux kernel warning about preferring different parity for start / end values.
130 static constexpr auto c_ephemeralPortRangeSize = 4095;
131 THROW_IF_FAILED(m_allocatePortRange.value()(m_service.get(), c_ephemeralPortRangeSize, &m_reservedPortRange, &port));
132
133 WI_ASSERT(m_reservedPortRange.endingPort - m_reservedPortRange.startingPort == c_ephemeralPortRangeSize);
134
135 // Count the overlap of the guest's reserved ephemeral range with the host ephemeral range
136 // and seed the in-use counters accordingly.
137 m_hostTcpEphemeralPortsInUse = ComputeHostEphemeralOverlap(IPPROTO_TCP);
138 m_hostUdpEphemeralPortsInUse = ComputeHostEphemeralOverlap(IPPROTO_UDP);
139
140 // setting the port to zero as we do not expect any bind requests to be sent to wslcore for ports in this range
141 m_reservedPorts.emplace(std::make_pair(HCN_PORT_PROTOCOL_TCP, static_cast<uint16_t>(0)), HcnPortReservation{port, 1});
142
143 // ownership of the port was transferred successfully
144 releasePortOnError.release();
145
146 WSL_LOG(
147 "GuestNetworkService::AllocateEphemeralPortRange",
148 TraceLoggingValue(m_reservedPortRange.startingPort, "startingPort"),
149 TraceLoggingValue(m_reservedPortRange.endingPort, "endingPort"));
150
151 return std::make_pair(m_reservedPortRange.startingPort, m_reservedPortRange.endingPort);
152 }
153
154 bool wsl::core::networking::GuestNetworkService::IsPortAllocationLoopbackException(const SOCKADDR_INET& Address) noexcept
155 {
156 // Out of IPv4 loopback address range 127.0.0.0/8, only 127.0.0.1 is used by host<->guest loopback networking scenarios.
157 // FSE needs to be aware of binds using address 127.0.0.1, but can ignore binds for other IPv4 loopback addresses.
158 //
159 // Loopback traffic from the guest to the other IPv4 loopback addresses will stay in the guest.
160 //
161 // This also solves the issue of someone wanting to bind on the host to port 53 (known scenario is ICS)
162 // at the same time with someone binding to port 53 in the guest - known scenarios are:
163 // - DNS tunneling server that uses IP 127.0.0.42, port 53
164 // - systemd DNS resolver that uses IP 127.0.0.53, port 53
165 return (Address.si_family == AF_INET && IN4_IS_ADDR_LOOPBACK(&Address.Ipv4.sin_addr) && !IN4_ADDR_EQUAL(&Address.Ipv4.sin_addr, &c_ipv4LoopbackAddr));
166 }
167
168 bool wsl::core::networking::GuestNetworkService::IsPortAllocationMulticast(const SOCKADDR_INET& Address, _In_ int Protocol) noexcept
169 {
170 const auto PortNumber = SS_PORT(&Address);
171
172 if ((Address.si_family == AF_INET && IN4_IS_ADDR_MULTICAST(&Address.Ipv4.sin_addr)) ||
173 (Address.si_family == AF_INET6 && IN6_IS_ADDR_MULTICAST(&Address.Ipv6.sin6_addr)))
174 {
175 return true;
176 }
177 // multicast DNS (mDNS)
178 else if (Protocol == IPPROTO_UDP && PortNumber == c_mdnsPortNumber)
179 {
180 return true;
181 }
182 // LLMNR DNS
183 else if (Protocol == IPPROTO_UDP && PortNumber == c_llmnrPortNumber)
184 {
185 return true;
186 }
187
188 return false;
189 }
190
191 std::pair<uint16_t, uint16_t> wsl::core::networking::GuestNetworkService::QueryHostEphemeralPortRange(LPCWSTR WmiClassName) noexcept
192 try
193 {
194 const auto com = wil::CoInitializeEx();
195 WmiService service(L"ROOT\\StandardCimv2");
196 WmiEnumerate enumSetting(service);
197
198 auto query = std::format(L"SELECT DynamicPortRangeStartPort, DynamicPortRangeNumberOfPorts FROM {}", WmiClassName);
199 for (const auto& instance : enumSetting.query(query.c_str()))
200 {
201 unsigned int startPort = 0;
202 unsigned int numberOfPorts = 0;
203
204 if (instance.get(L"DynamicPortRangeStartPort", &startPort) &&
205 instance.get(L"DynamicPortRangeNumberOfPorts", &numberOfPorts) && startPort > 0 && numberOfPorts > 0)
206 {
207 const auto endPort = static_cast<uint64_t>(startPort) + numberOfPorts - 1;
208 if (startPort > UINT16_MAX || endPort > UINT16_MAX)
209 {
210 LOG_HR_MSG(E_FAIL, "Ephemeral port range overflows uint16_t: start=%u, count=%u", startPort, numberOfPorts);
211 return c_invalidEphemeralPortRange;
212 }
213
214 return {static_cast<uint16_t>(startPort), static_cast<uint16_t>(endPort)};
215 }
216 }
217
218 LOG_HR_MSG(E_FAIL, "No valid ephemeral port range found in WMI class %ls", WmiClassName);
219 return c_invalidEphemeralPortRange;
220 }
221 catch (...)
222 {
223 LOG_CAUGHT_EXCEPTION_MSG("Failed to query host ephemeral port range from WMI class %ls", WmiClassName);
224 return c_invalidEphemeralPortRange;
225 }
226
227 bool wsl::core::networking::GuestNetworkService::IsPortInHostEphemeralRange(uint16_t PortNumber, int Protocol) const noexcept
228 {
229 const auto& range = (Protocol == IPPROTO_UDP) ? m_hostUdpEphemeralPortRange : m_hostTcpEphemeralPortRange;
230 return PortNumber >= range.first && PortNumber <= range.second;
231 }
232
233 bool wsl::core::networking::GuestNetworkService::IsPortInGuestEphemeralRange(uint16_t PortNumber) const noexcept
234 {
235 return PortNumber >= m_reservedPortRange.startingPort && PortNumber <= m_reservedPortRange.endingPort;
236 }
237
238 uint16_t wsl::core::networking::GuestNetworkService::ComputeHostEphemeralPortCap(int Protocol) const noexcept
239 {
240 const auto& range = (Protocol == IPPROTO_UDP) ? m_hostUdpEphemeralPortRange : m_hostTcpEphemeralPortRange;
241
242 // Cap the guest at half of the host ephemeral range so it can't exhaust the host's ports. The
243 // host range is always at least 255, so the cap will fit in uint16_t, no risk of overflow.
244 return static_cast<uint16_t>((range.second - range.first + 1) / 2);
245 }
246
247 uint16_t wsl::core::networking::GuestNetworkService::ComputeHostEphemeralOverlap(int Protocol) const noexcept
248 {
249 const auto& range = (Protocol == IPPROTO_UDP) ? m_hostUdpEphemeralPortRange : m_hostTcpEphemeralPortRange;
250
251 // Number of guest reserved ports that fall within the host ephemeral range.
252 const uint16_t overlapStart = std::max<uint16_t>(m_reservedPortRange.startingPort, range.first);
253 const uint16_t overlapEnd = std::min<uint16_t>(m_reservedPortRange.endingPort, range.second);
254 return (overlapStart <= overlapEnd) ? static_cast<uint16_t>(overlapEnd - overlapStart + 1) : 0;
255 }
256
257 int wsl::core::networking::GuestNetworkService::OnPortAllocationRequest(const SOCKADDR_INET& Address, _In_ int Protocol, _In_ bool Allocate) noexcept
258 try
259 {
260 // The Linux and Windows constants conveniently have the same values for TCP & UDP.
261 WI_ASSERT(Protocol == IPPROTO_TCP || Protocol == IPPROTO_UDP);
262 WI_ASSERT(m_allocatePort.has_value() && m_releasePort.has_value());
263 auto HnsProtocol = Protocol == IPPROTO_TCP ? HCN_PORT_PROTOCOL_TCP : HCN_PORT_PROTOCOL_UDP;
264
265 const auto PortNumber = SS_PORT(&Address);
266 const auto StringAddress = wsl::windows::common::string::SockAddrInetToString(Address);
267
268 if (IsPortAllocationLoopbackException(Address))
269 {
270 WSL_LOG(
271 "GuestNetworkService::OnPortAllocationRequest - allowing port allocation for loopback without asking FSE",
272 TraceLoggingValue(StringAddress.c_str(), "IP address"),
273 TraceLoggingValue(Protocol == IPPROTO_TCP ? "TCP" : "UDP", "protocol"),
274 TraceLoggingValue(PortNumber, "portNumber"),
275 TraceLoggingValue(Address.si_family == AF_INET ? "IPv4" : "IPv6", "address family"),
276 TraceLoggingValue(Allocate, "Allocate"));
277 return 0;
278 }
279
280 if (m_ignoredPorts.find(PortNumber) != m_ignoredPorts.end())
281 {
282
283 WSL_LOG(
284 "GuestNetworkService::OnPortAllocationRequest - allowing port allocation for ignored port without asking FSE",
285 TraceLoggingValue(StringAddress.c_str(), "IP address"),
286 TraceLoggingValue(Protocol == IPPROTO_TCP ? "TCP" : "UDP", "protocol"),
287 TraceLoggingValue(PortNumber, "portNumber"),
288 TraceLoggingValue(Address.si_family == AF_INET ? "IPv4" : "IPv6", "address family"),
289 TraceLoggingValue(Allocate, "Allocate"));
290 return 0;
291 }
292
293 const auto lock = m_dataLock.lock_exclusive();
294
295 if (IsPortInGuestEphemeralRange(PortNumber))
296 {
297 WSL_LOG(
298 "GuestNetworkService::OnPortAllocationRequest",
299 TraceLoggingValue(
300 "Guest attempted to allocate a port but it was already allocated through port reservations", "status"),
301 TraceLoggingValue(HnsProtocol == HCN_PORT_PROTOCOL_TCP ? "TCP" : "UDP", "protocol"),
302 TraceLoggingValue(PortNumber, "portNumber"),
303 TraceLoggingValue(StringAddress.c_str(), "IP address"));
304 return 0;
305 }
306
307 const bool isHostEphemeralPort = IsPortInHostEphemeralRange(PortNumber, Protocol);
308
309 HRESULT result = E_UNEXPECTED;
310 const auto it = m_reservedPorts.find(std::make_pair(HnsProtocol, PortNumber));
311 if (Allocate)
312 {
313 if (it != m_reservedPorts.end())
314 {
315 it->second.ReferenceCount++;
316 WSL_LOG(
317 "GuestNetworkService::OnPortAllocationRequest - incremented reference",
318 TraceLoggingValue(PortNumber, "Port"),
319 TraceLoggingValue(Address.si_family, "Family"),
320 TraceLoggingValue(StringAddress.c_str(), "IP address"),
321 TraceLoggingValue(Protocol, "Protocol"),
322 TraceLoggingValue(it->second.ReferenceCount, "ReferenceCount"));
323 return 0;
324 }
325
326 // New reservation for a port in the host ephemeral range: enforce the cap.
327 if (isHostEphemeralPort)
328 {
329 const auto cap = ComputeHostEphemeralPortCap(Protocol);
330 const auto portsInUse = (Protocol == IPPROTO_UDP) ? m_hostUdpEphemeralPortsInUse : m_hostTcpEphemeralPortsInUse;
331 if (portsInUse >= cap)
332 {
333 WSL_LOG(
334 "GuestNetworkService::OnPortAllocationRequest - denying port in host ephemeral range, cap reached",
335 TraceLoggingValue(StringAddress.c_str(), "IP address"),
336 TraceLoggingValue(Protocol == IPPROTO_TCP ? "TCP" : "UDP", "protocol"),
337 TraceLoggingValue(PortNumber, "portNumber"),
338 TraceLoggingValue(portsInUse, "hostEphemeralPortsInUse"),
339 TraceLoggingValue(cap, "hostEphemeralPortCap"));
340 return -LX_EADDRINUSE;
341 }
342 }
343
344 HANDLE port{nullptr};
345 auto releasePortOnError = wil::scope_exit([&] {
346 if (port)
347 {
348 m_releasePort.value()(port);
349 }
350 });
351
352 bool isMulticast = IsPortAllocationMulticast(Address, Protocol);
353
354 // Multicast port allocations are requested using the "shared" flag.
355 result = m_allocatePort.value()(
356 m_service.get(), HnsProtocol, isMulticast ? HCN_PORT_ACCESS_SHARED : HCN_PORT_ACCESS_EXCLUSIVE, PortNumber, &port);
357
358 if (SUCCEEDED(result))
359 {
360 m_reservedPorts.emplace(std::make_pair(HnsProtocol, PortNumber), HcnPortReservation{port, 1});
361
362 if (isHostEphemeralPort)
363 {
364 auto& portsInUse = Protocol == IPPROTO_UDP ? m_hostUdpEphemeralPortsInUse : m_hostTcpEphemeralPortsInUse;
365 portsInUse++;
366 }
367 }
368 // if the port was reserved, we successfully handed over ownership
369 releasePortOnError.release();
370
371 WSL_LOG(
372 "GuestNetworkService::OnPortAllocationRequest [HcnReserveGuestNetworkServicePort]",
373 TraceLoggingValue(HnsProtocol == HCN_PORT_PROTOCOL_TCP ? "TCP" : "UDP", "protocol"),
374 TraceLoggingValue(PortNumber, "portNumber"),
375 TraceLoggingValue(StringAddress.c_str(), "IP address"),
376 TraceLoggingValue(isMulticast, "isMulticast"),
377 TraceLoggingValue(result, "result"));
378 }
379 else
380 {
381 if (it == m_reservedPorts.end())
382 {
383 RETURN_HR_MSG(E_UNEXPECTED, "Guest attempted to deallocate port (%i, %i), but it's not allocated", Protocol, PortNumber);
384 }
385
386 if (it->second.ReferenceCount == 1)
387 {
388 result = m_releasePort.value()(it->second.Handle);
389 m_reservedPorts.erase(it);
390
391 // Only decrement the in-use counter when the release actually succeeded. If the release
392 // failed the reservation may still exist on the host, and undercounting would let the
393 // guest exceed the intended cap.
394 if (isHostEphemeralPort && SUCCEEDED(result))
395 {
396 auto& portsInUse = (Protocol == IPPROTO_UDP) ? m_hostUdpEphemeralPortsInUse : m_hostTcpEphemeralPortsInUse;
397 WI_ASSERT(portsInUse > 0);
398 portsInUse--;
399 }
400
401 WSL_LOG(
402 "GuestNetworkService::OnPortAllocationRequest - released port",
403 TraceLoggingValue(PortNumber, "Port"),
404 TraceLoggingValue(Address.si_family, "Family"),
405 TraceLoggingValue(StringAddress.c_str(), "IP address"),
406 TraceLoggingValue(Protocol, "Protocol"));
407 }
408 else
409 {
410 it->second.ReferenceCount--;
411 WSL_LOG(
412 "GuestNetworkService::OnPortAllocationRequest - decremented reference",
413 TraceLoggingValue(PortNumber, "Port"),
414 TraceLoggingValue(Address.si_family, "Family"),
415 TraceLoggingValue(StringAddress.c_str(), "IP address"),
416 TraceLoggingValue(Protocol, "Protocol"),
417 TraceLoggingValue(it->second.ReferenceCount, "ReferenceCount"));
418 return 0;
419 }
420 }
421
422 return SUCCEEDED(result) ? 0 : -LX_EADDRINUSE;
423 }
424 catch (...)
425 {
426 LOG_CAUGHT_EXCEPTION();
427 return -LX_ENOBUFS;
428 }
429
430 void wsl::core::networking::GuestNetworkService::Stop() noexcept
431 {
432 if (m_releasePort)
433 {
434 const auto lock = m_dataLock.lock_exclusive();
435
436 for (const auto& reservedPort : m_reservedPorts)
437 {
438 m_releasePort.value()(reservedPort.second.Handle);
439 }
440 m_reservedPorts.clear();
441
442 m_hostTcpEphemeralPortsInUse = 0;
443 m_hostUdpEphemeralPortsInUse = 0;
444 }
445
446 m_guestNetworkServiceCallback.reset();
447
448 if (m_service)
449 {
450 wil::unique_cotaskmem_string error;
451 const auto result = ::HcnDeleteGuestNetworkService(m_id, &error);
452 LOG_IF_FAILED_MSG(result, "HcnDeleteGuestNetworkService failed, %ls", error.get());
453 m_service.reset();
454 }
455 }