master
cpp 810 lines 35.7 KB
Raw
1 // Copyright (C) Microsoft Corporation. All rights reserved.
2
3 #include "precomp.h"
4 #include "NatNetworking.h"
5 #include "WslCoreNetworkEndpointSettings.h"
6 #include "WslCoreHostDnsInfo.h"
7 #include "Stringify.h"
8 #include "WslCoreFirewallSupport.h"
9 #include "hcs.hpp"
10
11 using namespace wsl::core::networking;
12 using namespace wsl::windows::common::stringify;
13 using namespace wsl::windows::common::string;
14 using namespace wsl::windows::common::hcs;
15 using namespace wsl::shared;
16 using wsl::core::NatNetworking;
17 using wsl::windows::common::Context;
18 using wsl::windows::common::ExecutionContext;
19 using wsl::windows::common::hcs::unique_hcn_endpoint;
20
21 // This static list is used to keep of which endpoints are in use by other users.
22 // It's needed because when we see an endpoint with the same ip address we want,
23 // we have no way to differentiate between an endpoint that we previously used
24 // that didn't get deleted, and an endpoint actively in use by another user.
25 static wil::srwlock g_endpointsInUseLock;
26 static std::vector<GUID> g_endpointsInUse;
27
28 NatNetworking::NatNetworking(
29 HCS_SYSTEM system,
30 wsl::windows::common::hcs::unique_hcn_network&& network,
31 GnsChannel&& gnsChannel,
32 Config& config,
33 wil::unique_socket&& dnsHvsocket,
34 LPCWSTR dnsOptions) :
35 m_system(system), m_config(config), m_network(std::move(network)), m_dnsOptions(dnsOptions), m_gnsChannel(std::move(gnsChannel))
36 {
37 m_connectivityTelemetryEnabled = config.EnableTelemetry && !WslTraceLoggingShouldDisableTelemetry();
38
39 if (dnsHvsocket)
40 {
41 // Create the DNS resolver used for DNS tunneling.
42 networking::DnsResolverFlags resolverFlags{};
43 WI_SetFlagIf(resolverFlags, networking::DnsResolverFlags::BestEffortDnsParsing, m_config.BestEffortDnsParsing);
44
45 m_dnsTunnelingResolver.emplace(std::move(dnsHvsocket), resolverFlags);
46
47 m_dnsTunnelingIpAddress = wsl::windows::common::string::IntegerIpv4ToWstring(config.DnsTunnelingIpAddress.value());
48 }
49 else if (!config.EnableDnsProxy)
50 {
51 // EnableDnsProxy indicates to use the DNS/NAT shared access service to proxy DNS requests
52 // If this is false then wsl will assign a prioritized set of DNS servers into the Linux container
53 // prioritized means:
54 // - can only set 3 DNS servers (Linux limitation)
55 // - when there are multiple host connected interfaces, we need to use the DNS servers from the most-likely-to-be-used interface on the host
56 m_useMirrorDnsSettings = true;
57 }
58 }
59
60 NatNetworking::~NatNetworking()
61 {
62 // Stop DNS suffix change notifications first, as those can call into the GNS channel.
63 m_dnsSuffixRegistryWatcher.reset();
64
65 // Stop the GNS channel to unblock any stuck communications with the guest
66 // calling this before m_connectivityTelemetry.Reset() to unblock that callback if it's attempting a connectivity request in Linux
67 m_gnsChannel.Stop();
68
69 // Stop the telemetry timer which could queue work to linux (through m_gnsChannel)
70 m_connectivityTelemetry.Reset();
71
72 // Unregister the network notification callback to prevent notifications from running while the remainder of the object is destroyed.
73 m_networkNotifyHandle.reset();
74
75 auto lock = g_endpointsInUseLock.lock_exclusive();
76 auto eraseRange = std::ranges::remove(g_endpointsInUse, m_endpoint.Id);
77 g_endpointsInUse.erase(eraseRange.begin(), eraseRange.end());
78 }
79
80 void NatNetworking::TelemetryConnectionCallback(NLM_CONNECTIVITY hostConnectivity, uint32_t telemetryCounter) noexcept
81 try
82 {
83 WSL_LOG("NatNetworking::TelemetryConnectionCallback");
84
85 // if this is the inital callback for checking container connectivity, push this through as telemetry, so we can observe the time-to-connect
86 if ((telemetryCounter == 1) || (hostConnectivity & NLM_CONNECTIVITY_IPV4_INTERNET))
87 {
88 int returnedIPv4Value{};
89 const auto requestStatus = wil::ResultFromException([&] {
90 const auto lock = m_lock.lock_exclusive();
91 returnedIPv4Value = m_gnsChannel.SendNetworkDeviceMessageReturnResult(LxGnsMessageConnectTestRequest, c_ipv4TestRequestTarget);
92 });
93
94 // make the same connect requests as we just requested from the container
95 const auto hostConnCheckResult = wsl::shared::conncheck::CheckConnection(c_ipv4TestRequestTargetA, nullptr, "80");
96 const auto WindowsIpv4ConnCheckStatus = static_cast<uint32_t>(hostConnCheckResult.Ipv4Status);
97 const auto WindowsIpv6ConnCheckStatus = static_cast<uint32_t>(hostConnCheckResult.Ipv6Status);
98
99 const auto WindowsIPv4NlmConnectivityLevel = ConnectivityTelemetry::WindowsIPv4NlmConnectivityLevel(hostConnectivity);
100 const auto WindowsIPv6NlmConnectivityLevel = ConnectivityTelemetry::WindowsIPv6NlmConnectivityLevel(hostConnectivity);
101 const auto LinuxIPv4ConnCheckStatus = ConnectivityTelemetry::LinuxIPv4ConnCheckResult(returnedIPv4Value);
102 // NAT doesn't have an IPv6 result because NAT is only IPv4 -- 2 == failed to connect
103 constexpr auto LinuxIPv6ConnCheckStatus = 2;
104
105 const auto timeFromObjectCreation = std::chrono::steady_clock::now() - m_objectCreationTime;
106 WSL_LOG_TELEMETRY(
107 "TelemetryConnectionCallback",
108 PDT_ProductAndServicePerformance,
109 TraceLoggingValue("NAT", "networkingMode"),
110 TraceLoggingValue(telemetryCounter, "telemetryCounter"),
111 TraceLoggingValue(
112 (std::chrono::duration_cast<std::chrono::milliseconds>(timeFromObjectCreation)).count(),
113 "timeFromObjectCreationMs"),
114 TraceLoggingValue(wsl::core::networking::ToString(hostConnectivity).c_str(), "HostConnectivityLevel"),
115 TraceLoggingValue(WindowsIPv4NlmConnectivityLevel, "WindowsIPv4ConnectivityLevel"),
116 TraceLoggingValue(WindowsIPv6NlmConnectivityLevel, "WindowsIPv6ConnectivityLevel"),
117 TraceLoggingValue(LinuxIPv4ConnCheckStatus, "LinuxIPv4ConnCheckStatus"),
118 TraceLoggingValue(LinuxIPv6ConnCheckStatus, "LinuxIPv6ConnCheckStatus"),
119 TraceLoggingValue(WindowsIpv4ConnCheckStatus, "WindowsIpv4ConnCheckStatus"),
120 TraceLoggingValue(WindowsIpv6ConnCheckStatus, "WindowsIpv6ConnCheckStatus"),
121 TraceLoggingHResult(requestStatus, "statusSendingMessageToLinux"),
122 TraceLoggingValue(m_config.EnableDnsTunneling, "DnsTunnelingEnabled"),
123 TraceLoggingValue(m_dnsTunnelingIpAddress.c_str(), "DnsTunnelingIpAddress"),
124 TraceLoggingValue(m_config.FirewallConfig.Enabled(), "HyperVFirewallEnabled"),
125 TraceLoggingValue(m_config.EnableAutoProxy, "AutoProxyFeatureEnabled")); // the feature is enabled, but we don't know if proxy settings are actually configured
126 }
127 else
128 {
129 WSL_LOG(
130 "NatNetworking::TelemetryConnectionCallback - not testing connectivity - host is not connected",
131 TraceLoggingValue(wsl::core::networking::ToString(hostConnectivity).c_str(), "HostConnectivityLevel"));
132 }
133 }
134 CATCH_LOG()
135
136 bool NatNetworking::IsHyperVFirewallSupported(const wsl::core::Config& vmConfig) noexcept
137 {
138 const auto hyperVFirewallSupport = wsl::core::networking::GetHyperVFirewallSupportVersion(vmConfig.FirewallConfig);
139
140 switch (hyperVFirewallSupport)
141 {
142 case HyperVFirewallSupport::None:
143 WSL_LOG("IsHyperVFirewallSupported returning false: No Hyper-V Firewall API present");
144 return false;
145
146 case HyperVFirewallSupport::Version1:
147 // we don't support using a NAT *and* Hyper-V Firewall when Windows only has the V1 APIs
148 WSL_LOG(
149 "IsHyperVFirewallSupported returning false: Hyper-V Firewall not supported with a NAT-network and v1 Hyper-V "
150 "Firewall APIs");
151 return false;
152
153 case HyperVFirewallSupport::Version2:
154 {
155 return true;
156 }
157
158 default:
159 WI_ASSERT(false);
160 return false;
161 }
162 }
163
164 std::pair<wsl::core::networking::EphemeralHcnEndpoint, wsl::shared::hns::HNSEndpoint> NatNetworking::CreateEndpoint(const std::wstring& IpAddress) const
165 {
166 hns::HostComputeEndpoint hnsEndpoint{};
167 hnsEndpoint.SchemaVersion.Major = 2;
168 hnsEndpoint.SchemaVersion.Minor = 16;
169
170 // Network Id
171 hnsEndpoint.HostComputeNetwork = m_config.NatNetworkId();
172
173 // Port name policy
174 hns::EndpointPolicy<hns::PortnameEndpointPolicySetting> endpointPortNamePolicy{};
175 endpointPortNamePolicy.Type = hns::EndpointPolicyType::PortName;
176 hnsEndpoint.Policies.emplace_back(std::move(endpointPortNamePolicy));
177
178 // IP Address
179 if (!IpAddress.empty())
180 {
181 wsl::shared::hns::IpConfig endpointIpConfig{};
182 endpointIpConfig.IpAddress = IpAddress;
183 hnsEndpoint.IpConfigurations.emplace_back(endpointIpConfig);
184 }
185
186 // Firewall policy
187 if (m_config.FirewallConfig.Enabled())
188 {
189 hns::EndpointPolicy<hns::FirewallPolicySetting> endpointFirewallPolicy{};
190 endpointFirewallPolicy.Settings.VmCreatorId = m_config.FirewallConfig.VmCreatorId.value();
191 endpointFirewallPolicy.Settings.PolicyFlags = hns::FirewallPolicyFlags::None;
192 endpointFirewallPolicy.Type = hns::EndpointPolicyType::Firewall;
193 hnsEndpoint.Policies.emplace_back(std::move(endpointFirewallPolicy));
194 }
195
196 auto endpoint = wsl::core::networking::CreateEphemeralHcnEndpoint(m_network.get(), hnsEndpoint);
197
198 return {std::move(endpoint), wsl::windows::common::hcs::GetEndpointProperties(endpoint.Endpoint.get())};
199 }
200
201 void NatNetworking::Initialize()
202 {
203 auto lock = m_lock.lock_exclusive();
204 wil::unique_cotaskmem_string error;
205 wsl::shared::hns::HNSEndpoint endpointProperties{};
206
207 // First try to find an existing endpoint that we can use.
208 if (!m_config.NatIpAddress.empty())
209 {
210 PCSTR executionStep = "";
211 try
212 {
213 // Enumerating and attaching the endpoints need to be an atomic operation between different users.
214 // Keep the lock until endpoint is created.
215 // Its currently safe to take this lock while holding the member m_lock
216 // because g_endpointsInUseLock is only ever locked during the d'tor
217 auto endpointLock = g_endpointsInUseLock.lock_exclusive();
218
219 wil::unique_cotaskmem_string endpointsJson;
220 wil::unique_cotaskmem_string endpointsError;
221
222 // Unfortunately it's not possible to filter endpoints by IP address
223 // (since internally HNS will convert the IP address field to an array of objects, and the objects themselves won't be equal
224 // because the query will only have on field set), so we need to manually iterate through the endpoints on the network.
225 for (const auto& id : EnumerateEndpointsByNetworkId(m_config.NatNetworkId()))
226 {
227 wil::unique_cotaskmem_string openEndpointError;
228 wsl::windows::common::hcs::unique_hcn_endpoint openEndpoint;
229 executionStep = "HcnOpenEndpoint";
230 auto result = HcnOpenEndpoint(id, &openEndpoint, &openEndpointError);
231 THROW_HR_IF_MSG(result, FAILED(result), "HcnOpenEndpoint %ls", openEndpointError.get());
232
233 executionStep = "HcnQueryEndpointProperties";
234 auto properties = wsl::windows::common::hcs::GetEndpointProperties(openEndpoint.get());
235 if (properties.IPAddress == m_config.NatIpAddress)
236 {
237 THROW_HR_IF_MSG(
238 E_UNEXPECTED,
239 std::ranges::find(g_endpointsInUse, id) != g_endpointsInUse.end(),
240 "Endpoint is in use by another address. Refusing to delete it.");
241
242 // TODO: this means WSL just whacked a different container's NAT address
243 // e.g., this just broke MDAG or Sandbox if they happened to use this same address range
244 // this sounds like a really bad idea
245
246 // Found an endpoint on the same network with the IP address we want: delete it so it doesn't conflict
247 // with ours.
248 LOG_HR_MSG(E_UNEXPECTED, "Found a conflicting endpoint. Deleting it");
249 openEndpoint.reset();
250 executionStep = "HcnDeleteEndpoint";
251 result = HcnDeleteEndpoint(id, &openEndpointError);
252 THROW_HR_IF_MSG(result, FAILED(result), "HcnDeleteEndpoint %ls", openEndpointError.get());
253 }
254 }
255
256 // Create and attach the endpoint.
257 wsl::core::networking::EphemeralHcnEndpoint endpoint;
258 executionStep = "HcnCreateEndpoint";
259 std::tie(endpoint, endpointProperties) = CreateEndpoint(m_config.NatIpAddress);
260 executionStep = "AttachEndpoint";
261 AttachEndpoint(std::move(endpoint), endpointProperties);
262 g_endpointsInUse.emplace_back(endpointProperties.ID);
263 }
264 catch (...)
265 {
266 WSL_LOG(
267 "ConstrainedNetworkEndpointCreationFailed",
268 TraceLoggingValue(executionStep, "executionStep"),
269 TraceLoggingValue("NAT", "networkingMode"),
270 TraceLoggingValue(m_config.EnableDnsTunneling, "DnsTunnelingEnabled"),
271 TraceLoggingValue(m_config.FirewallConfig.Enabled(), "HyperVFirewallEnabled"),
272 TraceLoggingValue(m_config.EnableAutoProxy, "AutoProxyFeatureEnabled"), // the feature is enabled, but we don't know if proxy settings are actually configured
273 TraceLoggingHexUInt32(wil::ResultFromCaughtException(), "result"));
274 }
275 }
276
277 if (!m_endpoint.Endpoint)
278 {
279 PCSTR executionStep = "";
280 try
281 {
282 // If no IP address was passed or if the endpoint couldn't be created / attached, create a new one without the IP address requirement.
283 networking::EphemeralHcnEndpoint endpoint;
284 executionStep = "HcnCreateEndpoint";
285 std::tie(endpoint, endpointProperties) = CreateEndpoint(L"");
286 executionStep = "AttachEndpoint";
287 AttachEndpoint(std::move(endpoint), endpointProperties);
288 }
289 catch (...)
290 {
291 const auto hr = wil::ResultFromCaughtException();
292 WSL_LOG(
293 "NewEndpointCreationFailed",
294 TraceLoggingValue(executionStep, "executionStep"),
295 TraceLoggingValue("NAT", "networkingMode"),
296 TraceLoggingValue(m_config.EnableDnsTunneling, "DnsTunnelingEnabled"),
297 TraceLoggingValue(m_config.FirewallConfig.Enabled(), "HyperVFirewallEnabled"),
298 TraceLoggingValue(m_config.EnableAutoProxy, "AutoProxyFeatureEnabled"), // the feature is enabled, but we don't know if proxy settings are actually configured
299 TraceLoggingHexUInt32(hr, "result"));
300 throw;
301 }
302
303 if (!m_config.NatIpAddress.empty())
304 {
305 EMIT_USER_WARNING(wsl::shared::Localization::MessageFailedToCreateNetworkEndpoint(
306 m_config.NatIpAddress.c_str(), endpointProperties.IPAddress.c_str()));
307 }
308
309 // Record the new IP address associated to the endpoint.
310 m_config.NatIpAddress = endpointProperties.IPAddress;
311 }
312
313 WI_ASSERT(m_endpoint.Endpoint);
314
315 // Send the endpoint state (ip address & link) to gns
316 m_gnsChannel.SendEndpointState(endpointProperties);
317
318 // Send the default route to gns
319
320 hns::ModifyGuestEndpointSettingRequest<hns::Route> request;
321 request.RequestType = hns::ModifyRequestType::Add;
322 request.ResourceType = hns::GuestEndpointResourceType::Route;
323 request.Settings.NextHop = endpointProperties.GatewayAddress;
324 request.Settings.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX;
325 request.Settings.Family = AF_INET;
326
327 m_gnsChannel.SendHnsNotification(ToJsonW(request).c_str(), m_endpoint.Id);
328
329 if (m_dnsTunnelingResolver)
330 {
331 // Register notifications for DNS suffix changes after we create the Endpoint
332 //
333 // Note: DNS suffix change notifications are used only if DNS tunneling is enabled. DNS behavior for NAT mode
334 // without DNS tunneling remains unchanged
335 m_dnsSuffixRegistryWatcher.emplace([this] {
336 const auto watcher_lock = m_lock.lock_exclusive();
337 UpdateDns();
338 });
339 }
340
341 // Update DNS information.
342 UpdateDns(endpointProperties.GatewayAddress.c_str());
343
344 // if using the shared access DNS proxy, ensure that the shared access service is allowed inbound UDP access.
345 if (!m_useMirrorDnsSettings && !m_dnsTunnelingResolver)
346 {
347 // N.B. This rule works around a host OS issue that prevents the DNS proxy from working on older versions of Windows.
348 ConfigureSharedAccessFirewallRule();
349 }
350
351 THROW_IF_WIN32_ERROR(NotifyNetworkConnectivityHintChange(&NatNetworking::OnNetworkConnectivityChange, this, true, &m_networkNotifyHandle));
352
353 // once the VM is created, start the telemetry timer
354 if (m_connectivityTelemetryEnabled)
355 {
356 m_connectivityTelemetry.StartTimer([&](NLM_CONNECTIVITY hostConnectivity, uint32_t telemetryCounter) {
357 TelemetryConnectionCallback(hostConnectivity, telemetryCounter);
358 });
359 }
360 }
361
362 void NatNetworking::AttachEndpoint(wsl::core::networking::EphemeralHcnEndpoint&& endpoint, const wsl::shared::hns::HNSEndpoint& properties)
363 {
364
365 // for mirrored endpoints, we will set the InstanceId to the InterfaceGuid of the host interface we mirror - as we add &
366 // remove them dynamically for NAT endpoints, we will just set the InstanceId to the EndpointId
367
368 ModifySettingRequest<NetworkAdapter> networkRequest{};
369 networkRequest.ResourcePath = networking::c_networkAdapterPrefix + wsl::shared::string::GuidToString<wchar_t>(properties.ID);
370 networkRequest.RequestType = ModifyRequestType::Add;
371 networkRequest.Settings.EndpointId = properties.ID;
372 networkRequest.Settings.InstanceId = properties.ID;
373
374 networkRequest.Settings.MacAddress = wsl::shared::string::ParseMacAddress(properties.MacAddress);
375 auto retryCount = 0ul;
376 const auto hr = wsl::shared::retry::RetryWithTimeout<HRESULT>(
377 [&] {
378 HRESULT exceptionHr = wil::ResultFromException(
379 [&] { wsl::windows::common::hcs::ModifyComputeSystem(m_system, wsl::shared::ToJsonW(networkRequest).c_str()); });
380
381 WSL_LOG(
382 "NatNetworking::AttachEndpoint [ModifyComputeSystem(ModifyRequestType::Add)]",
383 TraceLoggingValue(properties.ID, "endpointId"),
384 TraceLoggingValue(exceptionHr, "hr"),
385 TraceLoggingValue(retryCount, "retryCount"));
386
387 ++retryCount;
388 return THROW_IF_FAILED(exceptionHr);
389 },
390 wsl::core::networking::AddEndpointRetryPeriod,
391 wsl::core::networking::AddEndpointRetryTimeout,
392 wsl::core::networking::AddEndpointRetryPredicate);
393
394 if (hr == HCN_E_ENDPOINT_ALREADY_ATTACHED)
395 {
396 WSL_LOG(
397 "NatNetworking::AttachEndpoint [Adding the endpoint returned HCN_E_ENDPOINT_ALREADY_ATTACHED - continuing]",
398 TraceLoggingValue(properties.ID, "endpointId"));
399 }
400 else if (FAILED(hr))
401 {
402 THROW_HR(hr);
403 }
404
405 m_endpoint = std::move(endpoint);
406 m_networkSettings = GetEndpointSettings(properties);
407 }
408
409 void NatNetworking::StartPortTracker(wil::unique_socket&& socket)
410 {
411 WI_ASSERT(false);
412 }
413
414 void NETIOAPI_API_ NatNetworking::OnNetworkConnectivityChange(PVOID context, NL_NETWORK_CONNECTIVITY_HINT hint)
415 {
416 auto* thisPtr = static_cast<NatNetworking*>(context);
417
418 thisPtr->RefreshGuestConnection(hint);
419 thisPtr->m_connectivityTelemetry.UpdateTimer();
420 }
421
422 void NatNetworking::RefreshGuestConnection(NL_NETWORK_CONNECTIVITY_HINT connectivityHint) noexcept
423 try
424 {
425 auto lock = m_lock.lock_exclusive();
426
427 WSL_LOG(
428 "NatNetworking::RefreshGuestConnection",
429 TraceLoggingValue(wsl::windows::common::stringify::ToString(connectivityHint.ConnectivityLevel), "ConnectivityLevel"),
430 TraceLoggingValue(wsl::windows::common::stringify::ToString(connectivityHint.ConnectivityCost), "ConnectivityCost"));
431
432 UpdateMtu();
433 UpdateDns();
434 }
435 CATCH_LOG()
436
437 _Requires_lock_held_(m_lock)
438 void NatNetworking::UpdateDns(std::optional<PCWSTR> gatewayAddress) noexcept
439 try
440 {
441 if (!m_dnsTunnelingResolver && !m_useMirrorDnsSettings && !gatewayAddress)
442 {
443 return;
444 }
445
446 networking::DnsInfo latestDnsSettings{};
447
448 // NAT mode with DNS tunneling
449 if (m_dnsTunnelingResolver)
450 {
451 latestDnsSettings = HostDnsInfo::GetDnsTunnelingSettings(m_dnsTunnelingIpAddress);
452 }
453 // NAT mode without Shared Access DNS proxy
454 else if (m_useMirrorDnsSettings)
455 {
456 latestDnsSettings = HostDnsInfo::GetDnsSettings(DnsSettingsFlags::IncludeVpn);
457 }
458 // NAT mode with Shared Access DNS proxy
459 else if (gatewayAddress)
460 {
461 // set the NAT gateway address when using the NAT IPv4 DNS proxy
462 latestDnsSettings.Servers.emplace_back(wsl::shared::string::WideToMultiByte(gatewayAddress.value()));
463 }
464
465 if (latestDnsSettings != m_trackedDnsSettings)
466 {
467 auto dnsNotification = BuildDnsNotification(latestDnsSettings, m_dnsOptions);
468
469 WSL_LOG(
470 "NatNetworking::UpdateDns",
471 TraceLoggingValue(dnsNotification.Options.c_str(), "options"),
472 TraceLoggingValue(dnsNotification.Search.c_str(), "search"),
473 TraceLoggingValue(dnsNotification.ServerList.c_str(), "serverList"));
474
475 hns::ModifyGuestEndpointSettingRequest<hns::DNS> notification{};
476 notification.RequestType = hns::ModifyRequestType::Update;
477 notification.ResourceType = hns::GuestEndpointResourceType::DNS;
478 notification.Settings = std::move(dnsNotification);
479 m_gnsChannel.SendHnsNotification(ToJsonW(notification).c_str(), m_endpoint.Id);
480
481 m_trackedDnsSettings = std::move(latestDnsSettings);
482 }
483 }
484 CATCH_LOG()
485
486 void NatNetworking::UpdateMtu()
487 {
488 const auto minMtu = GetMinimumConnectedInterfaceMtu();
489
490 // Only send the update if the MTU changed.
491 if (minMtu && minMtu.value() != m_networkMtu)
492 {
493 m_networkMtu = minMtu.value();
494
495 hns::ModifyGuestEndpointSettingRequest<hns::NetworkInterface> notification{};
496 notification.ResourceType = hns::GuestEndpointResourceType::Interface;
497 notification.RequestType = hns::ModifyRequestType::Update;
498 notification.Settings.Connected = true;
499 notification.Settings.NlMtu = m_networkMtu;
500
501 WSL_LOG(
502 "NatNetworking::UpdateMtu", TraceLoggingValue(m_endpoint.Id, "endpointId"), TraceLoggingValue(m_networkMtu, "natMtu"));
503
504 m_gnsChannel.SendHnsNotification(ToJsonW(notification).c_str(), m_endpoint.Id);
505 }
506 }
507
508 void NatNetworking::TraceLoggingRundown() noexcept
509 {
510 auto lock = m_lock.lock_exclusive();
511
512 WSL_LOG(
513 "NatNetworking::TraceLoggingRundown",
514 TraceLoggingValue(m_config.NatNetworkId(), "networkId"),
515 TraceLoggingValue(m_endpoint.Id, "endpointId"),
516 TRACE_NETWORKSETTINGS_OBJECT(m_networkSettings));
517 }
518
519 void NatNetworking::FillInitialConfiguration(LX_MINI_INIT_NETWORKING_CONFIGURATION& message)
520 {
521 message.NetworkingMode = LxMiniInitNetworkingModeNat;
522 message.DisableIpv6 = false;
523 message.EnableDhcpClient = false;
524 message.PortTrackerType = m_config.EnableLocalhostRelay ? LxMiniInitPortTrackerTypeRelay : LxMiniInitPortTrackerTypeNone;
525 }
526
527 // before sending anything to the container, we must wait for the NAT IP Interfaces on the host to be connected.
528 // there's a possible race here if the physical adapter gets connected but the NAT vNIC interface take a bit longer
529 std::optional<ULONGLONG> NatNetworking::FindNatInterfaceLuid(const SOCKADDR_INET& natAddress, const NL_NETWORK_CONNECTIVITY_HINT& currentConnectivityHint)
530 {
531 constexpr ULONGLONG maxTimeToWaitMs = 10ull * 1000ull;
532 constexpr ULONG timeToSleepMs = 100ul;
533 const auto startTickCount = GetTickCount64();
534
535 NET_LUID natLuid{};
536 for (;;)
537 {
538 // HNS does not give us the interface guid/luid/index of the vNIC that is used for this NAT configuration
539 // because we don't constrain our NAT interface to any one host NIC
540 // we only have the assigned IPAddress - we'll have to use that to find the interface to check its state
541 // this is NAT - so it's an IPv4 address
542 unique_address_table addressTable;
543 THROW_IF_WIN32_ERROR(GetUnicastIpAddressTable(AF_INET, &addressTable));
544 for (const auto& address : wil::make_range(addressTable.get()->Table, addressTable.get()->NumEntries))
545 {
546 if (natAddress == address.Address)
547 {
548 natLuid.Value = address.InterfaceLuid.Value;
549 break;
550 }
551
552 WSL_LOG(
553 "NatNetworking::FindNatInterfaceLuid [IP Address comparison mismatch]",
554 TraceLoggingValue(wsl::windows::common::string::SockAddrInetToString(natAddress).c_str(), "natAddress"),
555 TraceLoggingValue(
556 wsl::windows::common::string::SockAddrInetToString(address.Address).c_str(), "enumeratedAddress"));
557 }
558
559 if (natLuid.Value != 0)
560 {
561 break;
562 }
563
564 // give up if something is just broken and taking too long
565 if (GetTickCount64() - startTickCount >= maxTimeToWaitMs)
566 {
567 break;
568 }
569 // else sleep and try again shortly
570 Sleep(timeToSleepMs);
571 // bail if connectivity on the host has completely changed
572 NL_NETWORK_CONNECTIVITY_HINT latestConnectivityHint{};
573 GetNetworkConnectivityHint(&latestConnectivityHint);
574 if (latestConnectivityHint != currentConnectivityHint)
575 {
576 WSL_LOG("NatNetworking::FindNatInterfaceLuid [connectivity changed while waiting for the NAT interface]");
577 THROW_WIN32_MSG(ERROR_RETRY, "connectivity changed while waiting for the NAT interface");
578 }
579 }
580
581 if (natLuid.Value == 0)
582 {
583 WSL_LOG(
584 "NatNetworking::FindNatInterfaceLuid [IP address not found]",
585 TraceLoggingValue(natLuid.Value, "natInterfaceLuid"),
586 TraceLoggingValue(wsl::windows::common::string::SockAddrInetToString(natAddress).c_str(), "natIPAddress"));
587 return {};
588 }
589
590 WSL_LOG(
591 "NatNetworking::FindNatInterfaceLuid [waiting for NAT interface to be connected]",
592 TraceLoggingValue(natLuid.Value, "natInterfaceLuid"),
593 TraceLoggingValue(wsl::windows::common::string::SockAddrInetToString(natAddress).c_str(), "natIPAddress"));
594
595 bool ipv4Connected = false;
596 for (;;)
597 {
598 unique_interface_table interfaceTable{};
599 THROW_IF_WIN32_ERROR(::GetIpInterfaceTable(AF_UNSPEC, &interfaceTable));
600 // we only track the IPv4 interface because we only NAT IPv4 to the container
601 for (auto index = 0ul; index < interfaceTable.get()->NumEntries; ++index)
602 {
603 const auto& ipInterface = interfaceTable.get()->Table[index];
604 if (ipInterface.Family == AF_INET && !!ipInterface.Connected && ipInterface.InterfaceLuid.Value == natLuid.Value)
605 {
606 ipv4Connected = true;
607 break;
608 }
609 }
610 if (ipv4Connected)
611 {
612 break;
613 }
614
615 // give up if something is just broken and taking too long
616 if (GetTickCount64() - startTickCount >= maxTimeToWaitMs)
617 {
618 break;
619 }
620 // else sleep and try again shortly
621 Sleep(timeToSleepMs);
622 // bail if connectivity on the host has completely changed
623 NL_NETWORK_CONNECTIVITY_HINT latestConnectivityHint{};
624 GetNetworkConnectivityHint(&latestConnectivityHint);
625 if (latestConnectivityHint != currentConnectivityHint)
626 {
627 WSL_LOG("NatNetworking::FindNatInterfaceLuid [connectivity changed while waiting for the NAT interface]");
628 THROW_WIN32_MSG(ERROR_RETRY, "connectivity changed while waiting for the NAT interface");
629 }
630 }
631
632 // return zero if it's not connected yet so we can retry the next cycle
633 return ipv4Connected ? natLuid.Value : std::optional<ULONGLONG>();
634 }
635
636 wsl::windows::common::hcs::unique_hcn_network NatNetworking::CreateNetwork(wsl::core::Config& config)
637 {
638 wsl::windows::common::hcs::unique_hcn_network natNetwork;
639 wil::ResultFromException(WI_DIAGNOSTICS_INFO, [&] {
640 try
641 {
642 wsl::core::networking::ConfigureHyperVFirewall(config.FirewallConfig, wsl::windows::common::wslutil::c_vmOwner);
643 natNetwork = CreateNetworkInternal(config);
644 }
645 catch (...)
646 {
647 // Don't retry if no constraints were set.
648 if (config.NatNetwork.empty() && config.NatGateway.empty())
649 {
650 LOG_CAUGHT_EXCEPTION();
651 throw;
652 }
653
654 LOG_CAUGHT_EXCEPTION_MSG(
655 "Failed to create network: '%ls' with gateway: '%ls', retrying without constraints",
656 config.NatNetwork.c_str(),
657 config.NatGateway.c_str());
658
659 const auto error = wil::ResultFromCaughtException();
660 WSL_LOG(
661 "ConstrainedNetworkCreationFailed",
662 TraceLoggingHexUInt32(error, "result"),
663 TraceLoggingValue("NAT", "networkingMode"),
664 TraceLoggingValue(config.EnableDnsTunneling, "DnsTunnelingEnabled"),
665 TraceLoggingValue(config.FirewallConfig.Enabled(), "HyperVFirewallEnabled"),
666 TraceLoggingValue(config.EnableAutoProxy, "AutoProxyFeatureEnabled") // the feature is enabled, but we don't know if proxy settings are actually configured
667 );
668
669 const auto previousRange = std::move(config.NatNetwork);
670 config.NatGateway = {};
671 // Note that the firewall config is NOT cleared here as we MUST always configure firewall if it has been requested
672 natNetwork = CreateNetworkInternal(config);
673
674 EMIT_USER_WARNING(wsl::shared::Localization::MessageFailedToCreateNetwork(
675 previousRange.c_str(), config.NatNetwork.c_str(), wsl::windows::common::wslutil::GetSystemErrorString(error).c_str()));
676 }
677 });
678
679 return natNetwork;
680 }
681
682 wsl::windows::common::hcs::unique_hcn_network NatNetworking::CreateNetworkInternal(wsl::core::Config& config)
683 {
684 HRESULT hr = S_OK;
685 PCSTR executionStep = "";
686
687 // Log telemetry to determine how long it takes to create the network.
688 const auto startTimeMs = GetTickCount64();
689
690 // Log how long it takes for networking to be created
691 WSL_LOG_TELEMETRY(
692 "CreateNetworkBegin",
693 PDT_ProductAndServicePerformance,
694 TraceLoggingValue(config.NatNetworkName(), "NetworkName"),
695 TraceLoggingGuid(config.NatNetworkId(), "NetworkGuid"),
696 TraceLoggingValue("NAT", "networkingMode"),
697 TraceLoggingValue(config.EnableDnsTunneling, "DnsTunnelingEnabled"),
698 TraceLoggingValue(config.FirewallConfig.Enabled(), "HyperVFirewallEnabled"),
699 TraceLoggingValue(config.EnableAutoProxy, "AutoProxyFeatureEnabled")); // the feature is enabled, but we don't know if proxy settings are actually configured
700
701 auto createEnd = wil::scope_exit([&] {
702 const auto TimeToCreateNetworkMs = GetTickCount64() - startTimeMs;
703 WSL_LOG_TELEMETRY(
704 "CreateNetworkEnd",
705 PDT_ProductAndServicePerformance,
706 TraceLoggingValue(config.NatNetworkName(), "NetworkName"),
707 TraceLoggingGuid(config.NatNetworkId(), "NetworkGuid"),
708 TraceLoggingValue(TimeToCreateNetworkMs, "TimeToCreateNetworkMs"),
709 TraceLoggingHResult(hr, "hr"),
710 TraceLoggingValue(executionStep, "executionStep"),
711 TraceLoggingValue("NAT", "networkingMode"),
712 TraceLoggingValue(config.EnableDnsTunneling, "DnsTunnelingEnabled"),
713 TraceLoggingValue(config.FirewallConfig.Enabled(), "HyperVFirewallEnabled"),
714 TraceLoggingValue(config.EnableAutoProxy, "AutoProxyFeatureEnabled")); // the feature is enabled, but we don't know if proxy settings are actually configured
715 });
716
717 auto runAsSelf = wil::run_as_self();
718
719 // Send a HNS request to create the network.
720 hns::Network settings{};
721 settings.Name = config.NatNetworkName();
722 settings.Type = hns::NetworkMode::ICS;
723 settings.IsolateSwitch = true;
724 settings.Flags = hns::NetworkFlags::EnableDns | hns::NetworkFlags::EnableNonPersistent;
725 WI_SetFlagIf(settings.Flags, hns::NetworkFlags::EnableFirewall, config.FirewallConfig.Enabled());
726
727 if (!config.NatNetwork.empty())
728 {
729 hns::IpSubnet netIpSubnet{};
730 netIpSubnet.IpAddressPrefix = config.NatNetwork;
731
732 hns::Subnet subnet{};
733 subnet.AddressPrefix = config.NatNetwork;
734 subnet.GatewayAddress = config.NatGateway;
735 subnet.IpSubnets.emplace_back(std::move(netIpSubnet));
736 settings.Subnets.emplace_back(std::move(subnet));
737 }
738
739 // Determine if the virtual network should be constrained by an external interface on the host.
740 // For example, if the user only wants traffic to be routed if a VPN is connected.
741 try
742 {
743 const auto lxssKey = windows::common::registry::OpenKey(HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, KEY_READ);
744 const auto interfaceConstraint =
745 windows::common::registry::ReadString(lxssKey.get(), nullptr, c_interfaceConstraintKey, L"");
746
747 if (!interfaceConstraint.empty())
748 {
749 settings.Type = hns::NetworkMode::ConstrainedICS;
750 settings.InterfaceConstraint.InterfaceAlias = interfaceConstraint;
751 }
752 }
753 CATCH_LOG()
754
755 wsl::windows::common::hcs::unique_hcn_network network{};
756 try
757 {
758 auto retryCount = 0ul;
759 wsl::shared::retry::RetryWithTimeout<void>(
760 [&] {
761 executionStep = "HcnCreateNetwork";
762 ExecutionContext context(Context::HNS);
763 wil::unique_cotaskmem_string error;
764 HRESULT hns_hr = HcnCreateNetwork(config.NatNetworkId(), ToJsonW(settings).c_str(), &network, &error);
765 WSL_LOG(
766 "NatNetworking::CreateNetwork [HcnCreateNetwork]",
767 TraceLoggingValue(config.NatNetworkId(), "networkGuid"),
768 TraceLoggingValue(settings.Name.c_str(), "settingsName"),
769 TraceLoggingValue(JsonEnumToString(settings.Type).c_str(), "settingsType"),
770 TraceLoggingValue(
771 settings.InterfaceConstraint.InterfaceAlias.c_str(), "settingsInterfaceConstraintInterfaceAlias"),
772 TraceLoggingValue(settings.IsolateSwitch, "settingsIsolateSwitch"),
773 TraceLoggingValue(static_cast<uint32_t>(settings.Flags), "settingsFlags"),
774 TraceLoggingValue(hns_hr, "hr"),
775 TraceLoggingValue(retryCount, "retryCount"));
776
777 ++retryCount;
778
779 // Open the existing network if it already exists.
780 if (hns_hr == HCN_E_NETWORK_ALREADY_EXISTS)
781 {
782 executionStep = "HcnOpenNetwork";
783 network = wsl::core::networking::OpenNetwork(config.NatNetworkId());
784 }
785 else
786 {
787 // Throw other errors to allow for retries
788 THROW_IF_FAILED_MSG(hns_hr, "HcnCreateNetwork %ls", error.get());
789 }
790
791 executionStep = "HcnQueryNetworkProperties";
792 // Save the networks settings in the configuration (used for WSL to save the NAT network configuration)
793 auto [properties, propertiesString] = wsl::core::networking::QueryNetworkProperties(network.get());
794 THROW_HR_IF_MSG(
795 E_UNEXPECTED, properties.Subnets.size() != 1, "Unexpected number of subnets in network: %ls", propertiesString.get());
796
797 config.NatGateway = properties.Subnets[0].GatewayAddress;
798 config.NatNetwork = properties.Subnets[0].AddressPrefix;
799 },
800 std::chrono::milliseconds(100),
801 std::chrono::seconds(3));
802 }
803 catch (...)
804 {
805 hr = wil::ResultFromCaughtException();
806 throw;
807 }
808
809 return network;
810 }