virtio networking: add support for ipv6 (#14350)

* VirtioProxy: Add IPv6 address, gateway, and route support - Add PreferredIpv6Address field and GetBestGatewayV6* methods to NetworkSettings - Extend GetHostEndpointSettings() to discover IPv6 unicast address and gateway - Add UpdateIpv6Address() using ModifyGuestEndpointSettingRequest<IPAddress> - Push IPv6 default route to guest via UpdateDefaultRoute(AF_INET6) - Remove AF_INET6 early return in ModifyOpenPorts, use INETADDR_PORT() - Add EndpointRoute::DefaultRoute() static factory - Pass client_ip_ipv6 in devicehost options (not yet parsed by devicehost) - Remove gateway_ip from devicehost options (only needed for DHCP) - Include IPv6 DNS servers in non-tunneling DNS settings - Add ConfigurationV6 and DnsResolutionAAAA tests * cleanup and add more ipv6 tests * added test coverage and minor updates * clang format * pr feedback * format source * pr feedback * test fixes --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

Ben Hillis committed Mar 13, 2026 at 16:36 UTC 7cd9ed9603632c55d703416bf53ae02348e2bb28
8 files changed +474 -107
packages.config
+1 -1
@@ -18,7 +18,7 @@
18 <package id="Microsoft.WSL.bsdtar" version="0.0.2-2" />
19 <package id="Microsoft.WSL.Dependencies.amd64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
20 <package id="Microsoft.WSL.Dependencies.arm64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
21 - <package id="Microsoft.WSL.DeviceHost" version="1.1.14-0" />
21 + <package id="Microsoft.WSL.DeviceHost" version="1.1.24-0" />
22 <package id="Microsoft.WSL.Kernel" version="6.6.114.1-1" targetFramework="native" />
23 <package id="Microsoft.WSL.LinuxSdk" version="1.20.0" targetFramework="native" />
24 <package id="Microsoft.WSL.TestDistro" version="2.5.7-47" />
src/windows/common/VirtioNetworking.cpp
+100 -42
@@ -56,7 +56,7 @@ void VirtioNetworking::TraceLoggingRundown() noexcept
56 void VirtioNetworking::FillInitialConfiguration(LX_MINI_INIT_NETWORKING_CONFIGURATION& message)
57 {
58 message.NetworkingMode = LxMiniInitNetworkingModeVirtioProxy;
59 - message.DisableIpv6 = false;
59 + message.DisableIpv6 = WI_IsFlagClear(m_flags, VirtioNetworkingFlags::Ipv6);
60 message.EnableDhcpClient = false;
61 message.PortTrackerType = LX_MINI_INIT_PORT_TRACKER_TYPE::LxMiniInitPortTrackerTypeMirrored;
62 }
@@ -78,6 +78,11 @@ void NETIOAPI_API_ VirtioNetworking::OnNetworkConnectivityChange(PVOID context,
78
79 HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int protocol, bool allocate) const noexcept
80 {
81 + if (addr.si_family == AF_INET6 && WI_IsFlagClear(m_flags, VirtioNetworkingFlags::Ipv6))
82 + {
83 + return S_OK;
84 + }
85 +
86 int result = 0;
87 const auto ipAddress = (addr.si_family == AF_INET) ? reinterpret_cast<const void*>(&addr.Ipv4.sin_addr)
88 : reinterpret_cast<const void*>(&addr.Ipv6.sin6_addr);
@@ -132,19 +137,12 @@ int VirtioNetworking::ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET&
137 LOG_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported bind protocol %d", protocol);
138 return 0;
139 }
135 - else if (addr.si_family == AF_INET6)
136 - {
137 - // The virtio net adapter does not yet support IPv6 packets, so any traffic would arrive via
138 - // IPv4. If the caller wants IPv4 they will also likely listen on an IPv4 address, which will
139 - // be handled as a separate callback to this same code.
140 - return 0;
141 - }
140
141 auto lock = m_lock.lock_exclusive();
142 const auto server = m_guestDeviceManager->GetRemoteFileSystem(VIRTIO_NET_CLASS_ID, c_defaultDeviceTag);
143 if (server)
144 {
147 - std::wstring portString = std::format(L"tag={};port_number={}", tag, addr.Ipv4.sin_port);
145 + std::wstring portString = std::format(L"tag={};port_number={}", tag, INETADDR_PORT(reinterpret_cast<const SOCKADDR*>(&addr)));
146 if (protocol == IPPROTO_UDP)
147 {
148 portString += L";udp";
@@ -185,7 +183,13 @@ try
183
184 std::wstring default_route = networkSettings->GetBestGatewayAddressString();
185 appendOption(L"gateway_ip", default_route);
188 - appendOption(L"gateway_mac", networkSettings->GetBestGatewayMacAddress());
186 + appendOption(L"gateway_mac", networkSettings->GetBestGatewayMacAddress(AF_INET));
187 +
188 + if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6))
189 + {
190 + appendOption(L"client_ip_ipv6", networkSettings->PreferredIpv6Address.AddressString);
191 + appendOption(L"gateway_mac_ipv6", networkSettings->GetBestGatewayMacAddress(AF_INET6));
192 + }
193
194 networking::DnsInfo currentDns{};
195 if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::DnsTunneling))
@@ -194,7 +198,9 @@ try
198 }
199 else
200 {
197 - currentDns = networking::HostDnsInfo::GetDnsSettings(networking::DnsSettingsFlags::IncludeVpn);
201 + wsl::core::networking::DnsSettingsFlags dnsFlags = networking::DnsSettingsFlags::IncludeVpn;
202 + WI_SetFlagIf(dnsFlags, networking::DnsSettingsFlags::IncludeIpv6Servers, WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6));
203 + currentDns = networking::HostDnsInfo::GetDnsSettings(dnsFlags);
204 }
205
206 const auto minMtu = GetMinimumConnectedInterfaceMtu();
@@ -221,32 +227,16 @@ try
227 }
228 }
229
224 - // Update IP address if needed.
225 - if (!m_networkSettings || networkSettings->PreferredIpAddress != m_networkSettings->PreferredIpAddress)
230 + UpdateIpv4Address(networkSettings->PreferredIpAddress);
231 + if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6))
232 {
227 - UpdateIpAddress(networkSettings->PreferredIpAddress);
233 + UpdateIpv6Address(networkSettings->PreferredIpv6Address);
234 }
235
230 - // Send default route update if needed.
231 - if (default_route != m_trackedDefaultRoute)
232 - {
233 - m_trackedDefaultRoute = default_route;
234 - UpdateDefaultRoute(default_route, AF_INET);
235 - }
236 -
237 - // Send DNS update if needed.
238 - if (currentDns != m_trackedDnsSettings)
239 - {
240 - m_trackedDnsSettings = currentDns;
241 - UpdateDnsSettings(currentDns);
242 - }
236 + UpdateDefaultRoute(default_route);
237
244 - // Send MTU update if needed.
245 - if (minMtu && minMtu.value() != m_networkMtu)
246 - {
247 - m_networkMtu = minMtu.value();
248 - UpdateMtu(m_networkMtu);
249 - }
238 + UpdateDnsSettings(currentDns);
239 + UpdateMtu(minMtu);
240
241 m_networkSettings = std::move(networkSettings);
242 }
@@ -282,27 +272,46 @@ void VirtioNetworking::SetupLoopbackDevice()
272 m_gnsChannel.SendNetworkDeviceMessage(loopbackType, ToJsonW(createLoopbackDevice).c_str());
273 }
274
285 -void VirtioNetworking::UpdateDefaultRoute(const std::wstring& gateway, ADDRESS_FAMILY family)
275 +void VirtioNetworking::SendDefaultRoute(const std::wstring& gateway, hns::ModifyRequestType requestType)
276 {
287 - if (gateway.empty())
277 + if (gateway.empty() || !m_adapterId.has_value())
278 {
279 return;
280 }
281
282 wsl::shared::hns::Route route;
283 route.NextHop = gateway;
294 - route.DestinationPrefix = (family == AF_INET) ? LX_INIT_DEFAULT_ROUTE_PREFIX : LX_INIT_DEFAULT_ROUTE_V6_PREFIX;
295 - route.Family = family;
284 + route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX;
285 + route.Family = AF_INET;
286
287 hns::ModifyGuestEndpointSettingRequest<hns::Route> request;
298 - request.RequestType = hns::ModifyRequestType::Add;
288 + request.RequestType = requestType;
289 request.ResourceType = hns::GuestEndpointResourceType::Route;
290 request.Settings = route;
291 m_gnsChannel.SendHnsNotification(ToJsonW(request).c_str(), m_adapterId.value());
292 }
293
294 +void VirtioNetworking::UpdateDefaultRoute(const std::wstring& gateway)
295 +{
296 + if (gateway == m_trackedDefaultRoute || !m_adapterId.has_value())
297 + {
298 + return;
299 + }
300 +
301 + SendDefaultRoute(m_trackedDefaultRoute, hns::ModifyRequestType::Remove);
302 + m_trackedDefaultRoute = gateway;
303 + SendDefaultRoute(gateway, hns::ModifyRequestType::Add);
304 +}
305 +
306 void VirtioNetworking::UpdateDnsSettings(const networking::DnsInfo& dns)
307 {
308 + if (dns == m_trackedDnsSettings || !m_adapterId.has_value())
309 + {
310 + return;
311 + }
312 +
313 + m_trackedDnsSettings = dns;
314 +
315 hns::ModifyGuestEndpointSettingRequest<hns::DNS> notification{};
316 notification.RequestType = hns::ModifyRequestType::Update;
317 notification.ResourceType = hns::GuestEndpointResourceType::DNS;
@@ -310,9 +319,17 @@ void VirtioNetworking::UpdateDnsSettings(const networking::DnsInfo& dns)
319 m_gnsChannel.SendHnsNotification(ToJsonW(notification).c_str(), m_adapterId.value());
320 }
321
313 -void VirtioNetworking::UpdateIpAddress(const networking::EndpointIpAddress& ipAddress)
322 +void VirtioNetworking::UpdateIpv4Address(const networking::EndpointIpAddress& ipAddress)
323 {
315 - // N.B. The MAC address is advertised with the virtio device so doesn't need to be explicitly set.
324 + if (ipAddress == m_trackedIpv4Address || ipAddress.AddressString.empty() || !m_adapterId.has_value())
325 + {
326 + return;
327 + }
328 +
329 + m_trackedIpv4Address = ipAddress;
330 +
331 + // N.B. SendEndpointState triggers SetAdapterConfiguration on the Linux side
332 + // which brings the interface UP and configures the full adapter state.
333 hns::HNSEndpoint endpointProperties;
334 endpointProperties.ID = m_adapterId.value();
335 endpointProperties.IPAddress = ipAddress.AddressString;
@@ -320,12 +337,53 @@ void VirtioNetworking::UpdateIpAddress(const networking::EndpointIpAddress& ipAd
337 m_gnsChannel.SendEndpointState(endpointProperties);
338 }
339
323 -void VirtioNetworking::UpdateMtu(ULONG mtu)
340 +void VirtioNetworking::SendIpv6Address(const networking::EndpointIpAddress& ipAddress, hns::ModifyRequestType requestType)
341 +{
342 + WI_ASSERT(WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6));
343 +
344 + if (ipAddress.AddressString.empty() || !m_adapterId.has_value())
345 + {
346 + return;
347 + }
348 +
349 + // The HNSEndpoint schema doesn't support IPv6 addresses, so use ModifyGuestEndpointSettingRequest.
350 + hns::ModifyGuestEndpointSettingRequest<hns::IPAddress> request;
351 + request.RequestType = requestType;
352 + request.ResourceType = hns::GuestEndpointResourceType::IPAddress;
353 + request.Settings.Address = ipAddress.AddressString;
354 + request.Settings.Family = ipAddress.Address.si_family;
355 + request.Settings.OnLinkPrefixLength = ipAddress.PrefixLength;
356 + request.Settings.PreferredLifetime = ULONG_MAX;
357 + m_gnsChannel.SendHnsNotification(ToJsonW(request).c_str(), m_adapterId.value());
358 +}
359 +
360 +void VirtioNetworking::UpdateIpv6Address(const networking::EndpointIpAddress& ipAddress)
361 +{
362 + WI_ASSERT(WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6));
363 +
364 + if (ipAddress == m_trackedIpv6Address || !m_adapterId.has_value())
365 + {
366 + return;
367 + }
368 +
369 + SendIpv6Address(m_trackedIpv6Address, hns::ModifyRequestType::Remove);
370 + m_trackedIpv6Address = ipAddress;
371 + SendIpv6Address(ipAddress, hns::ModifyRequestType::Add);
372 +}
373 +
374 +void VirtioNetworking::UpdateMtu(std::optional<ULONG> mtu)
375 {
376 + if (!mtu || mtu.value() == m_networkMtu || !m_adapterId.has_value())
377 + {
378 + return;
379 + }
380 +
381 + m_networkMtu = mtu.value();
382 +
383 hns::ModifyGuestEndpointSettingRequest<hns::NetworkInterface> notification{};
384 notification.ResourceType = hns::GuestEndpointResourceType::Interface;
385 notification.RequestType = hns::ModifyRequestType::Update;
386 notification.Settings.Connected = true;
329 - notification.Settings.NlMtu = mtu;
387 + notification.Settings.NlMtu = m_networkMtu;
388 m_gnsChannel.SendHnsNotification(ToJsonW(notification).c_str(), m_adapterId.value());
389 }
src/windows/common/VirtioNetworking.h
+9 -3
@@ -15,6 +15,7 @@ enum class VirtioNetworkingFlags
15 None = 0x0,
16 LocalhostRelay = 0x1,
17 DnsTunneling = 0x2,
18 + Ipv6 = 0x4,
19 };
20 DEFINE_ENUM_FLAG_OPERATORS(VirtioNetworkingFlags);
21
@@ -43,10 +44,13 @@ private:
44 int ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& addr, _In_ int protocol, _In_ bool isOpen) const;
45 void RefreshGuestConnection() noexcept;
46 void SetupLoopbackDevice();
46 - void UpdateDefaultRoute(const std::wstring& gateway, ADDRESS_FAMILY family);
47 + void SendDefaultRoute(const std::wstring& gateway, wsl::shared::hns::ModifyRequestType requestType);
48 + void SendIpv6Address(const networking::EndpointIpAddress& ipAddress, wsl::shared::hns::ModifyRequestType requestType);
49 + void UpdateDefaultRoute(const std::wstring& gateway);
50 void UpdateDnsSettings(const networking::DnsInfo& dns);
48 - void UpdateIpAddress(const networking::EndpointIpAddress& ipAddress);
49 - void UpdateMtu(ULONG mtu);
51 + void UpdateIpv4Address(const networking::EndpointIpAddress& ipAddress);
52 + void UpdateIpv6Address(const networking::EndpointIpAddress& ipAddress);
53 + void UpdateMtu(std::optional<ULONG> mtu);
54
55 mutable wil::srwlock m_lock;
56
@@ -62,6 +66,8 @@ private:
66
67 ULONG m_networkMtu = 0;
68 std::wstring m_trackedDeviceOptions;
69 + networking::EndpointIpAddress m_trackedIpv4Address{};
70 + networking::EndpointIpAddress m_trackedIpv6Address{};
71 std::wstring m_trackedDefaultRoute;
72 networking::DnsInfo m_trackedDnsSettings{};
73
src/windows/common/WslCoreNetworkEndpointSettings.cpp
+64 -22
@@ -24,7 +24,9 @@ std::shared_ptr<wsl::core::networking::NetworkSettings> wsl::core::networking::G
24 return std::make_shared<wsl::core::networking::NetworkSettings>(
25 properties.InterfaceConstraint.InterfaceGuid,
26 address,
27 + EndpointIpAddress{},
28 route,
29 + EndpointRoute{},
30 properties.MacAddress,
31 properties.InterfaceConstraint.InterfaceIndex,
32 properties.InterfaceConstraint.InterfaceMediaType);
@@ -61,44 +63,84 @@ std::shared_ptr<wsl::core::networking::NetworkSettings> wsl::core::networking::G
63 }
64 if (firstIpv4Address)
65 {
64 - address.Address = *reinterpret_cast<SOCKADDR_INET*>(firstIpv4Address->Address.lpSockaddr);
66 + address.Address.Ipv4 = *reinterpret_cast<SOCKADDR_IN*>(firstIpv4Address->Address.lpSockaddr);
67 address.AddressString = windows::common::string::SockAddrInetToWstring(address.Address);
68 address.PrefixLength = firstIpv4Address->OnLinkPrefixLength;
69 }
70
69 - EndpointRoute route{};
70 - PIP_ADAPTER_GATEWAY_ADDRESS nextGatewayAddress = bestInterface->FirstGatewayAddress;
71 - while (nextGatewayAddress && nextGatewayAddress->Address.lpSockaddr->sa_family != AF_INET)
71 + // Find the first global-scope (non-link-local) IPv6 unicast address.
72 + EndpointIpAddress ipv6Address{};
73 + auto nextUnicastAddress = bestInterface->FirstUnicastAddress;
74 + while (nextUnicastAddress)
75 {
73 - nextGatewayAddress = nextGatewayAddress->Next;
76 + if (nextUnicastAddress->Address.lpSockaddr->sa_family == AF_INET6)
77 + {
78 + const auto& sin6 = *reinterpret_cast<SOCKADDR_IN6*>(nextUnicastAddress->Address.lpSockaddr);
79 + if (!IN6_IS_ADDR_LINKLOCAL(&sin6.sin6_addr) && !IN6_IS_ADDR_LOOPBACK(&sin6.sin6_addr))
80 + {
81 + ipv6Address.Address.Ipv6 = sin6;
82 + ipv6Address.AddressString = windows::common::string::SockAddrInetToWstring(ipv6Address.Address);
83 + ipv6Address.PrefixLength = nextUnicastAddress->OnLinkPrefixLength;
84 + break;
85 + }
86 + }
87 + nextUnicastAddress = nextUnicastAddress->Next;
88 }
75 - if (nextGatewayAddress)
89 +
90 + // Helper to find the first gateway address of a given family.
91 + auto findGatewayAddress = [](PIP_ADAPTER_GATEWAY_ADDRESS list, ADDRESS_FAMILY family) -> PIP_ADAPTER_GATEWAY_ADDRESS {
92 + while (list && list->Address.lpSockaddr->sa_family != family)
93 + {
94 + list = list->Next;
95 + }
96 + return list;
97 + };
98 +
99 + // Build IPv4 default route.
100 + EndpointRoute route{};
101 + const auto v4Gateway = findGatewayAddress(bestInterface->FirstGatewayAddress, AF_INET);
102 + if (v4Gateway)
103 {
77 - route.DestinationPrefix.PrefixLength = 0;
78 - IN4ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv4);
79 - route.DestinationPrefixString = LX_INIT_UNSPECIFIED_ADDRESS;
80 - route.NextHop = *reinterpret_cast<SOCKADDR_INET*>(nextGatewayAddress->Address.lpSockaddr);
81 - route.NextHopString = windows::common::string::SockAddrInetToWstring(route.NextHop);
104 + SOCKADDR_INET v4NextHop{};
105 + v4NextHop.Ipv4 = *reinterpret_cast<SOCKADDR_IN*>(v4Gateway->Address.lpSockaddr);
106 + route = EndpointRoute::DefaultRoute(AF_INET, v4NextHop);
107 }
108 else if (address.Address.si_family == AF_INET)
109 {
85 - IN_ADDR default_route{};
86 - default_route.s_addr = htonl((ntohl(address.Address.Ipv4.sin_addr.s_addr) & ~((1 << (32 - address.PrefixLength)) - 1)) | 1);
87 - route.DestinationPrefix.PrefixLength = 0;
88 - IN4ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv4);
89 - route.DestinationPrefixString = LX_INIT_UNSPECIFIED_ADDRESS;
90 - IN4ADDR_SETSOCKADDR(&route.NextHop.Ipv4, &default_route, 0);
91 - route.NextHopString = windows::common::string::SockAddrInetToWstring(route.NextHop);
110 + // Synthesize a gateway from the first host address in the subnet.
111 + SOCKADDR_INET gatewayAddr{};
112 + gatewayAddr.si_family = AF_INET;
113 + const uint32_t hostAddr = ntohl(address.Address.Ipv4.sin_addr.s_addr);
114 + const uint32_t mask = (address.PrefixLength == 0) ? 0u : ~((1u << (32u - address.PrefixLength)) - 1u);
115 + gatewayAddr.Ipv4.sin_addr.s_addr = htonl((hostAddr & mask) | 1u);
116 + route = EndpointRoute::DefaultRoute(AF_INET, gatewayAddr);
117 + }
118 +
119 + // Build IPv6 default route.
120 + EndpointRoute v6Route{};
121 + const auto v6Gateway = findGatewayAddress(bestInterface->FirstGatewayAddress, AF_INET6);
122 + if (v6Gateway)
123 + {
124 + SOCKADDR_INET v6NextHop{};
125 + v6NextHop.Ipv6 = *reinterpret_cast<SOCKADDR_IN6*>(v6Gateway->Address.lpSockaddr);
126 + v6Route = EndpointRoute::DefaultRoute(AF_INET6, v6NextHop);
127 }
128
129 return std::make_shared<NetworkSettings>(
95 - bestInterface->NetworkGuid, address, route, macAddress, bestInterface->IfIndex, bestInterface->IfType);
130 + bestInterface->NetworkGuid,
131 + std::move(address),
132 + std::move(ipv6Address),
133 + std::move(route),
134 + std::move(v6Route),
135 + std::move(macAddress),
136 + bestInterface->IfIndex,
137 + bestInterface->IfType);
138 }
139
98 -std::wstring wsl::core::networking::NetworkSettings::GetBestGatewayMacAddress() const
140 +std::wstring wsl::core::networking::NetworkSettings::GetBestGatewayMacAddress(ADDRESS_FAMILY addressFamily) const
141 {
100 - auto gatewayAddress = GetBestGatewayAddress();
101 - if (gatewayAddress.si_family != AF_INET)
142 + auto gatewayAddress = GetBestGatewayAddress(addressFamily);
143 + if (gatewayAddress.si_family != addressFamily)
144 {
145 return {};
146 }
src/windows/common/WslCoreNetworkEndpointSettings.h
+56 -10
@@ -172,6 +172,27 @@ struct EndpointRoute
172 EndpointRoute(const EndpointRoute&) = default;
173 EndpointRoute& operator=(const EndpointRoute&) = default;
174
175 + // Build a default route (0.0.0.0/0 or ::/0) with the given next hop.
176 + static EndpointRoute DefaultRoute(ADDRESS_FAMILY family, const SOCKADDR_INET& nextHop)
177 + {
178 + EndpointRoute route{};
179 + route.Family = family;
180 + route.DestinationPrefix.PrefixLength = 0;
181 + if (family == AF_INET)
182 + {
183 + IN4ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv4);
184 + route.DestinationPrefixString = LX_INIT_UNSPECIFIED_ADDRESS;
185 + }
186 + else
187 + {
188 + IN6ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv6);
189 + route.DestinationPrefixString = LX_INIT_UNSPECIFIED_V6_ADDRESS;
190 + }
191 + route.NextHop = nextHop;
192 + route.NextHopString = windows::common::string::SockAddrInetToWstring(nextHop);
193 + return route;
194 + }
195 +
196 EndpointRoute(const MIB_IPFORWARD_ROW2& RouteRow) :
197 Family(RouteRow.NextHop.si_family),
198 DestinationPrefix(RouteRow.DestinationPrefix),
@@ -258,19 +279,39 @@ struct NetworkSettings
279 {
280 NetworkSettings() = default;
281
261 - NetworkSettings(const GUID& interfaceGuid, EndpointIpAddress preferredIpAddress, EndpointRoute gateway, std::wstring macAddress, uint32_t interfaceIndex, uint32_t mediaType) :
282 + NetworkSettings(
283 + const GUID& interfaceGuid,
284 + EndpointIpAddress preferredIpAddress,
285 + EndpointIpAddress preferredIpv6Address,
286 + EndpointRoute gateway,
287 + EndpointRoute v6Gateway,
288 + std::wstring macAddress,
289 + uint32_t interfaceIndex,
290 + uint32_t mediaType) :
291 InterfaceGuid(interfaceGuid),
292 PreferredIpAddress(std::move(preferredIpAddress)),
293 + PreferredIpv6Address(std::move(preferredIpv6Address)),
294 MacAddress(std::move(macAddress)),
295 InterfaceIndex(interfaceIndex),
296 InterfaceType(mediaType)
297 {
268 - Routes.emplace(std::move(gateway));
298 + // Only insert routes that have a valid next hop. A default-constructed or empty
299 + // EndpointRoute indicates no gateway was found for that address family.
300 + if (!gateway.NextHopString.empty())
301 + {
302 + Routes.emplace(std::move(gateway));
303 + }
304 +
305 + if (!v6Gateway.NextHopString.empty())
306 + {
307 + Routes.emplace(std::move(v6Gateway));
308 + }
309 }
310
311 GUID InterfaceGuid{};
312 EndpointIpAddress PreferredIpAddress{};
273 - std::set<EndpointIpAddress> IpAddresses{}; // Does not include PreferredIpAddress.
313 + EndpointIpAddress PreferredIpv6Address{};
314 + std::set<EndpointIpAddress> IpAddresses{}; // Does not include PreferredIpAddress or PreferredIpv6Address.
315 std::set<EndpointRoute> Routes{};
316 std::wstring MacAddress;
317 IF_INDEX InterfaceIndex = 0;
@@ -290,12 +331,13 @@ struct NetworkSettings
331
332 auto operator<=>(const NetworkSettings&) const = default;
333
293 - std::wstring GetBestGatewayAddressString() const
334 + // Returns the next-hop string of the first default route matching the given address family.
335 + std::wstring GetBestGatewayAddressString(ADDRESS_FAMILY family = AF_INET) const
336 {
295 - // Best is currently defined as simply the first IPv4 gateway.
337 + const auto& unspecified = (family == AF_INET) ? LX_INIT_UNSPECIFIED_ADDRESS : LX_INIT_UNSPECIFIED_V6_ADDRESS;
338 for (const auto& route : Routes)
339 {
298 - if (route.Family == AF_INET && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == LX_INIT_UNSPECIFIED_ADDRESS)
340 + if (route.Family == family && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == unspecified)
341 {
342 return route.NextHopString;
343 }
@@ -304,12 +346,13 @@ struct NetworkSettings
346 return {};
347 }
348
307 - SOCKADDR_INET GetBestGatewayAddress() const
349 + // Returns the next-hop address of the first default route matching the given address family.
350 + SOCKADDR_INET GetBestGatewayAddress(ADDRESS_FAMILY family = AF_INET) const
351 {
309 - // Best is currently defined as simply the first IPv4 gateway.
352 + const auto& unspecified = (family == AF_INET) ? LX_INIT_UNSPECIFIED_ADDRESS : LX_INIT_UNSPECIFIED_V6_ADDRESS;
353 for (const auto& route : Routes)
354 {
312 - if (route.Family == AF_INET && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == LX_INIT_UNSPECIFIED_ADDRESS)
355 + if (route.Family == family && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == unspecified)
356 {
357 return route.NextHop;
358 }
@@ -318,7 +361,7 @@ struct NetworkSettings
361 return {};
362 }
363
321 - std::wstring GetBestGatewayMacAddress() const;
364 + std::wstring GetBestGatewayMacAddress(ADDRESS_FAMILY addressFamily) const;
365
366 std::wstring IpAddressesString() const
367 {
@@ -369,6 +412,9 @@ std::shared_ptr<NetworkSettings> GetHostEndpointSettings();
412 TraceLoggingValue((settings)->GetBestGatewayAddressString().c_str(), "bestGatewayAddress"), \
413 TraceLoggingValue((settings)->PreferredIpAddress.AddressString.c_str(), "preferredIpAddress"), \
414 TraceLoggingValue((settings)->PreferredIpAddress.PrefixLength, "preferredIpAddressPrefixLength"), \
415 + TraceLoggingValue((settings)->PreferredIpv6Address.AddressString.c_str(), "preferredIpv6Address"), \
416 + TraceLoggingValue((settings)->PreferredIpv6Address.PrefixLength, "preferredIpv6AddressPrefixLength"), \
417 + TraceLoggingValue((settings)->GetBestGatewayAddressString(AF_INET6).c_str(), "bestGatewayV6Address"), \
418 TraceLoggingValue((settings)->IpAddressesString().c_str(), "ipAddresses"), \
419 TraceLoggingValue((settings)->RoutesString().c_str(), "routes"), \
420 TraceLoggingValue((settings)->MacAddress.c_str(), "macAddress"), \
src/windows/common/WslCoreNetworkingSupport.h
+1 -1
@@ -544,7 +544,7 @@ private:
544 m_buffer.resize(BufferSize);
545 Result = GetAdaptersAddresses(
546 AF_UNSPEC,
547 - (GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST),
547 + (GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS),
548 nullptr,
549 (PIP_ADAPTER_ADDRESSES)m_buffer.data(),
550 &BufferSize);
src/windows/service/exe/WslCoreVm.cpp
+1 -1
@@ -576,7 +576,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
576 }
577 else if (m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy)
578 {
579 - wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::None;
579 + wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::Ipv6;
580 WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::LocalhostRelay, m_vmConfig.EnableLocalhostRelay);
581 m_networkingEngine = std::make_unique<wsl::core::VirtioNetworking>(
582 std::move(gnsChannel), flags, LX_INIT_RESOLVCONF_FULL_HEADER, m_guestDeviceManager, m_userToken);
test/windows/NetworkTests.cpp
+242 -27
@@ -2729,7 +2729,10 @@ class NetworkTests
2729 {
2730 if (std::regex_search(line, match, defaultRoutePattern) && match.size() >= 3)
2731 {
2732 - VERIFY_IS_FALSE(state.DefaultRoute.has_value());
2732 + if (state.DefaultRoute.has_value())
2733 + {
2734 + continue;
2735 + }
2736
2737 state.DefaultRoute = {{match.str(1), match.str(2), {}, match.size() > 4 && match[4].matched ? std::stoi(match.str(4)) : 0}};
2738 }
@@ -2754,6 +2757,24 @@ class NetworkTests
2757 return GetRoutingTableState(out, defaultRoutePattern, routePattern);
2758 }
2759
2760 + static void WaitForIpv6DefaultRoute()
2761 + {
2762 + const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(30);
2763 + while (std::chrono::steady_clock::now() < timeout)
2764 + {
2765 + auto state = GetIpv6RoutingTableState();
2766 + if (state.DefaultRoute.has_value())
2767 + {
2768 + return;
2769 + }
2770 +
2771 + LogInfo("Waiting for IPv6 default route...");
2772 + std::this_thread::sleep_for(std::chrono::seconds(1));
2773 + }
2774 +
2775 + VERIFY_FAIL(L"Timed out waiting for IPv6 default route");
2776 + }
2777 +
2778 static RoutingTableState GetIpv6RoutingTableState()
2779 {
2780 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"ip -6 route show");
@@ -3255,7 +3276,8 @@ class NetworkTests
3276 NET_LUID interfaceLuid{};
3277 CONTINUE_IF_FAILED_WIN32(ConvertInterfaceGuidToLuid(&interfaceGuid, &interfaceLuid));
3278
3258 - for (const IP_ADAPTER_ADDRESSES* adapter = adapterAddresses.get(); adapter != nullptr; adapter = adapter->Next)
3279 + for (auto* adapter = reinterpret_cast<const IP_ADAPTER_ADDRESSES*>(adapterAddresses.data()); adapter != nullptr;
3280 + adapter = adapter->Next)
3281 {
3282 if (interfaceLuid.Value == adapter->Luid.Value && adapter->FirstUnicastAddress != nullptr && adapter->FirstGatewayAddress != nullptr)
3283 {
@@ -3267,18 +3289,63 @@ class NetworkTests
3289 return false;
3290 }
3291
3270 - static std::unique_ptr<IP_ADAPTER_ADDRESSES> GetAdapterAddresses(ADDRESS_FAMILY family)
3292 + static bool HostHasIpv6DnsServers()
3293 + {
3294 + ULONG bufferSize = 0;
3295 + constexpr ULONG flags = GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS;
3296 + std::vector<BYTE> buffer;
3297 + ULONG result = GetAdaptersAddresses(AF_INET6, flags, nullptr, nullptr, &bufferSize);
3298 + while (result == ERROR_BUFFER_OVERFLOW)
3299 + {
3300 + buffer.resize(bufferSize);
3301 + result = GetAdaptersAddresses(AF_INET6, flags, nullptr, reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data()), &bufferSize);
3302 + }
3303 +
3304 + if (result != NO_ERROR)
3305 + {
3306 + return false;
3307 + }
3308 +
3309 + DWORD bestIndex = 0;
3310 + SOCKADDR_IN6 dest{};
3311 + dest.sin6_family = AF_INET6;
3312 + InetPtonW(AF_INET6, L"2001:4860:4860::8888", &dest.sin6_addr);
3313 +
3314 + if (GetBestInterfaceEx(reinterpret_cast<SOCKADDR*>(&dest), &bestIndex) != NO_ERROR)
3315 + {
3316 + return false;
3317 + }
3318 +
3319 + for (auto* adapter = reinterpret_cast<const IP_ADAPTER_ADDRESSES*>(buffer.data()); adapter != nullptr; adapter = adapter->Next)
3320 + {
3321 + if (adapter->IfIndex != bestIndex)
3322 + {
3323 + continue;
3324 + }
3325 +
3326 + for (auto* dns = adapter->FirstDnsServerAddress; dns != nullptr; dns = dns->Next)
3327 + {
3328 + if (dns->Address.lpSockaddr->sa_family == AF_INET6)
3329 + {
3330 + return true;
3331 + }
3332 + }
3333 + }
3334 +
3335 + return false;
3336 + }
3337 +
3338 + static std::vector<BYTE> GetAdapterAddresses(ADDRESS_FAMILY family)
3339 {
3272 - ULONG result;
3340 constexpr ULONG flags =
3341 (GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_GATEWAYS);
3342 ULONG bufferSize = 0;
3276 - std::unique_ptr<IP_ADAPTER_ADDRESSES> buffer;
3277 -
3278 - while ((result = GetAdaptersAddresses(family, flags, nullptr, buffer.get(), &bufferSize)) == ERROR_BUFFER_OVERFLOW)
3343 + std::vector<BYTE> buffer;
3344 + ULONG result = GetAdaptersAddresses(family, flags, nullptr, nullptr, &bufferSize);
3345 + while (result == ERROR_BUFFER_OVERFLOW)
3346 {
3280 - buffer.reset(static_cast<IP_ADAPTER_ADDRESSES*>(malloc(bufferSize)));
3281 - VERIFY_IS_NOT_NULL(buffer.get());
3347 + buffer.resize(bufferSize);
3348 + result = GetAdaptersAddresses(family, flags, nullptr, reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data()), &bufferSize);
3349 }
3350
3351 VERIFY_WIN32_SUCCEEDED(result);
@@ -4566,6 +4633,8 @@ class VirtioProxyTests
4633 return;
4634 }
4635
4636 + NetworkTests::WaitForIpv6DefaultRoute();
4637 +
4638 NetworkTests::GuestClient(L"tcp6-connect:bing.com:80");
4639 }
4640
@@ -4584,8 +4653,14 @@ class VirtioProxyTests
4653
4654 VERIFY_IS_TRUE(std::regex_match(out, pattern));
4655
4587 - // Verify that /etc/resolv.conf contains a 'search' line with DNS suffixes
4588 - VERIFY_IS_TRUE(out.find(L"search ") != std::wstring::npos);
4656 + // Verify that /etc/resolv.conf contains a 'search' line if the host has DNS suffixes
4657 + auto [suffixOut, suffixErr] = LxsstuLaunchPowershellAndCaptureOutput(
4658 + L"(@((Get-DnsClientGlobalSetting).SuffixSearchList) + @((Get-DnsClient).ConnectionSpecificSuffix) | Where-Object "
4659 + L"{$_}).Count");
4660 + if (_wtoi(suffixOut.c_str()) > 0)
4661 + {
4662 + VERIFY_IS_TRUE(out.find(L"search ") != std::wstring::npos);
4663 + }
4664 }
4665
4666 TEST_METHOD(GuestPortIsReleased)
@@ -4602,23 +4677,18 @@ class VirtioProxyTests
4677 NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false);
4678 }
4679
4605 - const wil::unique_socket listenSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
4606 - VERIFY_IS_TRUE(!!listenSocket);
4680 + wsl::shared::retry::RetryWithTimeout<void>(
4681 + [&]() {
4682 + const wil::unique_socket listenSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
4683 + THROW_HR_IF(E_ABORT, !listenSocket);
4684
4608 - SOCKADDR_IN Address{};
4609 - Address.sin_family = AF_INET;
4610 - Address.sin_port = htons(1234);
4611 -
4612 - const auto timeout = std::chrono::steady_clock::now() + std::chrono::minutes(2);
4613 -
4614 - bool bound = false;
4615 - while (!bound && std::chrono::steady_clock::now() < timeout)
4616 - {
4617 - bound = bind(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&Address), sizeof(Address)) != SOCKET_ERROR;
4618 - std::this_thread::sleep_for(std::chrono::seconds(1));
4619 - }
4620 -
4621 - VERIFY_IS_TRUE(bound);
4685 + SOCKADDR_IN Address{};
4686 + Address.sin_family = AF_INET;
4687 + Address.sin_port = htons(1234);
4688 + THROW_HR_IF(E_FAIL, bind(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&Address), sizeof(Address)) == SOCKET_ERROR);
4689 + },
4690 + std::chrono::seconds(1),
4691 + std::chrono::minutes(2));
4692 }
4693
4694 TEST_METHOD(LoopbackGuestToHost)
@@ -4690,5 +4760,150 @@ class VirtioProxyTests
4760 m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .autoProxy = true}));
4761 NetworkTests::VerifyHttpProxySimple();
4762 }
4763 +
4764 + TEST_METHOD(ConfigurationV6)
4765 + {
4766 + VIRTIOPROXY_TEST_ONLY();
4767 +
4768 + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
4769 +
4770 + if (!NetworkTests::HostHasInternetConnectivity(AF_INET6))
4771 + {
4772 + LogSkipped("Host does not have IPv6 internet connectivity. Skipping...");
4773 + return;
4774 + }
4775 +
4776 + // Wait for the device host to send an IPv6 Router Advertisement
4777 + // before querying the interface state.
4778 + NetworkTests::WaitForIpv6DefaultRoute();
4779 +
4780 + const auto state = NetworkTests::GetInterfaceState(L"eth0");
4781 +
4782 + // Verify that the guest has a global IPv6 address assigned
4783 + VERIFY_IS_FALSE(state.V6Addresses.empty());
4784 +
4785 + // Verify that the guest has an IPv6 default gateway
4786 + VERIFY_IS_TRUE(state.V6Gateway.has_value());
4787 +
4788 + // Verify the guest IPv6 address matches the host global IPv6 address
4789 + const auto adapterAddresses = NetworkTests::GetAdapterAddresses(AF_INET6);
4790 + DWORD bestIndex = 0;
4791 + SOCKADDR_IN6 dest{};
4792 + dest.sin6_family = AF_INET6;
4793 + InetPtonW(AF_INET6, L"2001:4860:4860::8888", &dest.sin6_addr);
4794 + VERIFY_ARE_EQUAL(NO_ERROR, GetBestInterfaceEx(reinterpret_cast<SOCKADDR*>(&dest), &bestIndex));
4795 +
4796 + for (auto* adapter = reinterpret_cast<const IP_ADAPTER_ADDRESSES*>(adapterAddresses.data()); adapter != nullptr;
4797 + adapter = adapter->Next)
4798 + {
4799 + if (adapter->IfIndex != bestIndex)
4800 + {
4801 + continue;
4802 + }
4803 +
4804 + for (auto* unicast = adapter->FirstUnicastAddress; unicast != nullptr; unicast = unicast->Next)
4805 + {
4806 + if (unicast->Address.lpSockaddr->sa_family != AF_INET6)
4807 + {
4808 + continue;
4809 + }
4810 +
4811 + const auto& sin6 = *reinterpret_cast<SOCKADDR_IN6*>(unicast->Address.lpSockaddr);
4812 + if (IN6_IS_ADDR_LINKLOCAL(&sin6.sin6_addr) || IN6_IS_ADDR_LOOPBACK(&sin6.sin6_addr))
4813 + {
4814 + continue;
4815 + }
4816 +
4817 + SOCKADDR_INET hostAddr{};
4818 + hostAddr.Ipv6 = sin6;
4819 + const auto hostAddrString = wsl::windows::common::string::SockAddrInetToWstring(hostAddr);
4820 +
4821 + // The host address may not be at index 0 due to SLAAC addresses from RA
4822 + bool addressFound = false;
4823 + for (const auto& v6Addr : state.V6Addresses)
4824 + {
4825 + if (v6Addr.Address == hostAddrString)
4826 + {
4827 + addressFound = true;
4828 + break;
4829 + }
4830 + }
4831 + VERIFY_IS_TRUE(addressFound);
4832 + break;
4833 + }
4834 +
4835 + break;
4836 + }
4837 + }
4838 +
4839 + TEST_METHOD(DnsResolutionDigV6)
4840 + {
4841 + VIRTIOPROXY_TEST_ONLY();
4842 +
4843 + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
4844 +
4845 + if (!NetworkTests::HostHasInternetConnectivity(AF_INET6))
4846 + {
4847 + LogSkipped("Host does not have IPv6 internet connectivity. Skipping...");
4848 + return;
4849 + }
4850 +
4851 + // Test AAAA record resolution (IPv6) with both UDP and TCP
4852 + NetworkTests::VerifyDigDnsResolution(L"dig +short +time=5 AAAA bing.com");
4853 + NetworkTests::VerifyDigDnsResolution(L"dig +tcp +short +time=5 AAAA bing.com");
4854 + }
4855 +
4856 + TEST_METHOD(GuestPortIsReleasedV6)
4857 + {
4858 + VIRTIOPROXY_TEST_ONLY();
4859 +
4860 + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
4861 +
4862 + // Make sure the VM doesn't time out
4863 + WslKeepAlive keepAlive;
4864 +
4865 + {
4866 + auto guestProcess = NetworkTests::BindGuestPort(L"TCP6-LISTEN:1234,bind=::", true);
4867 + NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false, true);
4868 + }
4869 +
4870 + wsl::shared::retry::RetryWithTimeout<void>(
4871 + [&]() {
4872 + const wil::unique_socket listenSocket(socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP));
4873 + THROW_HR_IF(E_ABORT, !listenSocket);
4874 +
4875 + SOCKADDR_IN6 Address{};
4876 + Address.sin6_family = AF_INET6;
4877 + Address.sin6_port = htons(1234);
4878 + THROW_HR_IF(E_FAIL, bind(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&Address), sizeof(Address)) == SOCKET_ERROR);
4879 + },
4880 + std::chrono::seconds(1),
4881 + std::chrono::minutes(2));
4882 + }
4883 +
4884 + TEST_METHOD(ConfigurationV6DnsServers)
4885 + {
4886 + VIRTIOPROXY_TEST_ONLY();
4887 +
4888 + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
4889 +
4890 + if (!NetworkTests::HostHasInternetConnectivity(AF_INET6))
4891 + {
4892 + LogSkipped("Host does not have IPv6 internet connectivity. Skipping...");
4893 + return;
4894 + }
4895 +
4896 + if (!NetworkTests::HostHasIpv6DnsServers())
4897 + {
4898 + LogSkipped("Host does not have IPv6 DNS servers configured. Skipping...");
4899 + return;
4900 + }
4901 +
4902 + auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/resolv.conf", 0);
4903 +
4904 + // Verify that /etc/resolv.conf contains at least one IPv6 nameserver
4905 + const std::wregex v6Pattern(L"(.|\\n)*nameserver [0-9a-fA-F:]+\\n(.|\\n)*");
4906 + VERIFY_IS_TRUE(std::regex_match(out, v6Pattern));
4907 + }
4908 };
4909 } // namespace NetworkTests