| 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 | } |