Files
WSL/test/windows/NetworkTests.cpp
Ben Hillis a8205a85ba merge master -> feature/wsl-for-apps (#14537)
* test: enable virtiofs tests and enable WSLG during testing (#14387)

* test: enable virtiofs tests and enable WSLG during testing

* test fix

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* chore(distributions): Almalinux auto-update - 20260311 14:52:02 (#14404)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Fix CVE-2026-26127: bump .NET runtime from 10.0.0 to 10.0.4 (#14421)

Addresses Dependabot alerts #10 and #11. The Microsoft.NETCore.App.Runtime
packages (win-x64 and win-arm64) at version 10.0.0 are vulnerable to a
denial of service via out-of-bounds read when decoding malformed Base64Url
input (CVSS 7.5 High). Bumped to 10.0.4 which includes the fix.

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Notice change from build: 141806547 (#14423)

Co-authored-by: WSL notice <noreply@microsoft.com>

* Ship initrd.img in MSI using build-time generation via powershell script (#14424)

* Ship initrd.img in MSI using build-time generation via tar.exe

Replace the install-time CreateInitrd/RemoveInitrd custom actions with a
build-time step that generates initrd.img using the Windows built-in
tar.exe (libarchive/bsdtar) and ships it directly in the MSI.

The install-time approach had a race condition: wsl.exe could launch
before the CreateInitrd custom action completed, causing
ERROR_FILE_NOT_FOUND for initrd.img.

Changes:
- Add CMake custom command to generate initrd.img via tar.exe --format=newc
- Add initrd.img as a regular file in the MSI tools component
- Remove CreateInitrd/RemoveInitrd custom actions from WiX, DllMain,
  and wslinstall.def
- Remove CreateCpioInitrd helper and its tests (no longer needed)
- Update pipeline build targets to build initramfs instead of init

* pr feedback

* more pr feedback

* switch to using a powershell script instead of tar.exe

* powershell script feedback

* hopefully final pr feedback

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* virtiofs: update logic so querying virtiofs mount source does not require a call to the service (#14380)

* virtiofs: update logic so querying virtiofs mount source does not require a call to the service

* more pr feedback

* use std::filesystem::read_symlink

* pr feedback and use canonical path in virtiofs symlink

* make sure canonical path is always used

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

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

* Track `bind` syscall when port is 0 (#14333)

* Initial work

* .

* pr feedback and add unit test

* minor tweaks an fix use after free in logging statement

* implement PR feedback

* hopefully final pr feedback

* pr feedback in test function

* Address PR feedback: add try/catch to TrackPort and PortZeroBind queue push

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Add iptables to list of apps to install in WSL (#14459)

There were instructions already on how to install tcpdump in WSL, but
iptables are also needed for the log collection to be complete, so this
PR adds instructions on how to also install iptables.

Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com>

* Update Microsoft.WSL.DeviceHost to version 1.1.39-0 (#14460)

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Moves all Ubuntu distros to the tar-based format (#14463)

* Move all supported Ubuntu images to the new format

We backported the build pipeline so all current LTSes come out in the new tar-based format

* Remove the appx based distros

All WSL users can run tar-based distros by now, right?
There is no benefit in maintaining both formats.

* Enable DNS tunneling for VirtioProxy networking mode (#14461)

- Allow VirtioProxy to keep EnableDnsTunneling=true in config, but clear
  socket-specific options (BestEffortDnsParsing, DnsTunnelingIpAddress)
- Suppress dedicated DNS tunneling hvsocket for VirtioProxy; tunneling
  is handled through the VirtioNetworking device host instead
- Set DnsTunneling flag on VirtioNetworkingFlags so the device host
  knows to tunnel DNS
- Expand SWIOTLB kernel cmdline to cover VirtioFs and VirtioProxy
- Bump DeviceHost package to 1.1.39-0
- Add VirtioProxy DNS test coverage for tunneling on/off
- Skip GuestPortIsReleasedV6 on Windows 10

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* test: disable LoopbackExplicit due to OS build 29555 regression (#14477)

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES (#14426)

* Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES

- Split MSI/Wintrust install functions from wslutil.cpp into install.cpp
- Remove MI.lib, wsldeps.lib, msi.lib, Wintrust.lib, computecore.lib,
  computenetwork.lib, Iphlpapi.lib from COMMON_LINK_LIBRARIES
- Add per-target MSI_LINK_LIBRARIES, HCS_LINK_LIBRARIES, SERVICE_LINK_LIBRARIES
- Delay-load msi.dll and WINTRUST.dll for wsl.exe and wslg.exe
- Result: wslhost, wslrelay, wslcsdk, testplugin lose msi/wintrust startup imports;
  wsl.exe and wslg.exe defer msi/wintrust loading until actually needed;
  wslservice is the only target that imports computecore/computenetwork/Iphlpapi

* minor fixes to install.cpp that were caught during PR

* move to wsl::windows::common::install namespace

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Fix wsl stuck when misconfigured cifs mount presents (#14466)

* detach terminal before running mount -a

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* use _exit on error before execv in child process to avoid unintentional resource release

* Add regression test

* Fix clang format issue

* fix all clang format issue

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* resolve ai comments

* move test to unit test

* Fix string literal

* Overwrite fstab to resolve pipeline missing file issue

---------

Co-authored-by: Feng Wang <wangfen@microsoft.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Update localization and notice scripts to target the branch that the pipeline is running on (#14492)

* test: Add arm64 test distro support (#14500)

* test: Add arm64 test distro support

* update unit test baseline

* more test baseline updates

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* test: remove duplicated DNS test coverage (#14522)

* test: remove duplicated DNS test coverage

* format source

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* Fix: Fail and warn the user when --uninstall is given parameters (#14524)

Fail and warn the user when --uninstall is given parameters.

* Localization change from build: 142847827 (#14525)

Co-authored-by: WSL localization <noreply@microsoft.com>

* virito net: revert to previous DNS behavior while we debug an issue with DNS over TCP (#14532)

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* devicehost: update to latest devicehost nuget with tracing improvements (#14531)

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>

* fix merge issues

---------

Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com>
Co-authored-by: AlmaLinux Autobot <107999298+almalinuxautobot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Blue <OneBlue@users.noreply.github.com>
Co-authored-by: WSL notice <noreply@microsoft.com>
Co-authored-by: Daman Mulye <daman_mulye@hotmail.com>
Co-authored-by: Andre Muezerie <108841174+andremueiot@users.noreply.github.com>
Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com>
Co-authored-by: Carlos Nihelton <carlos.santanadeoliveira@canonical.com>
Co-authored-by: Feng Wang <wang6922@outlook.com>
Co-authored-by: Feng Wang <wangfen@microsoft.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-26 17:10:59 -07:00

5046 lines
208 KiB
C++

/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
NetworkTests.cpp
Abstract:
This file contains test cases for the networking logic.
--*/
#include "precomp.h"
#include "computenetwork.h"
#include "Common.h"
#include "wslpolicies.h"
#include "hns_schema.h"
#include <mstcpip.h>
#include <winhttp.h>
#include <winsock2.h>
#include <netlistmgr.h>
using wsl::shared::hns::GuestEndpointResourceType;
using wsl::shared::hns::ModifyGuestEndpointSettingRequest;
using wsl::shared::hns::ModifyRequestType;
bool TryLoadWinhttpProxyMethods() noexcept
{
constexpr auto winhttpModuleName = L"Winhttp.dll";
const wil::shared_hmodule winhttpModule{LoadLibraryEx(winhttpModuleName, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)};
if (!winhttpModule)
{
return false;
}
try
{
// attempt to find the functions for the Winhttp proxy APIs.
static LxssDynamicFunction<decltype(WinHttpRegisterProxyChangeNotification)> WinHttpRegisterProxyChangeNotification{
winhttpModule, "WinHttpRegisterProxyChangeNotification"};
static LxssDynamicFunction<decltype(WinHttpUnregisterProxyChangeNotification)> WinHttpUnregisterProxyChangeNotification{
winhttpModule, "WinHttpUnregisterProxyChangeNotification"};
static LxssDynamicFunction<decltype(WinHttpGetProxySettingsEx)> WinHttpGetProxySettingsEx{
winhttpModule, "WinHttpGetProxySettingsEx"};
static LxssDynamicFunction<decltype(WinHttpGetProxySettingsResultEx)> WinHttpGetProxySettingsResultEx{
winhttpModule, "WinHttpGetProxySettingsResultEx"};
static LxssDynamicFunction<decltype(WinHttpFreeProxySettingsEx)> WinHttpFreeProxySettingsEx{
winhttpModule, "WinHttpFreeProxySettingsEx"};
}
catch (...)
{
return false;
}
return true;
}
#define HYPERV_FIREWALL_TEST_ONLY() \
{ \
WSL2_TEST_ONLY(); \
WINDOWS_11_TEST_ONLY(); \
if (!AreExperimentalNetworkingFeaturesSupported() || !IsHyperVFirewallSupported()) \
{ \
LogSkipped("Hyper-V Firewall not supported on this OS. Skipping test..."); \
return; \
} \
}
#define MIRRORED_NETWORKING_TEST_ONLY() \
{ \
WSL2_TEST_ONLY(); \
WINDOWS_11_TEST_ONLY(); \
if (!AreExperimentalNetworkingFeaturesSupported() || !IsHyperVFirewallSupported()) \
{ \
LogSkipped("Mirrored networking not supported on this OS. Skipping test.."); \
return; \
} \
}
#define DNS_TUNNELING_TEST_ONLY() \
{ \
WSL2_TEST_ONLY(); \
WINDOWS_11_TEST_ONLY(); \
if (!AreExperimentalNetworkingFeaturesSupported()) \
{ \
LogSkipped("DNS tunneling not supported on this OS. Skipping test..."); \
return; \
} \
if (!TryLoadDnsResolverMethods()) \
{ \
LogSkipped("DNS tunneling APIs not present on this OS. Skipping test..."); \
return; \
} \
}
#define WINHTTP_PROXY_TEST_ONLY() \
{ \
WSL2_TEST_ONLY(); \
if (!TryLoadWinhttpProxyMethods()) \
{ \
LogSkipped("Winhttp proxy APIs not present on this OS. Skipping test..."); \
return; \
} \
}
#define VIRTIOPROXY_TEST_ONLY() \
{ \
WSL2_TEST_ONLY(); \
}
static constexpr auto c_wslVmCreatorId = L"\'{40e0ac32-46a5-438a-A0B2-2B479E8F2E90}\'";
static constexpr auto c_wsaVmCreatorId = L"\'{9E288F02-CE00-4D9E-BE2B-14CE463B0298}\'";
static constexpr auto c_anyVmCreatorId = L"\'{00000000-0000-0000-0000-000000000000}\'";
static constexpr auto c_firewallRuleActionBlock = L"Block";
static constexpr auto c_firewallRuleActionAllow = L"Allow";
static constexpr auto c_firewallTrafficTestCmd = L"ping -c 3 -W 5 1.1.1.1";
static const std::wstring c_firewallTrafficTestPort = L"80";
static const std::wstring c_firewallTestOtherPort = L"443";
static const std::wstring c_dnsTunnelingDefaultIp = L"10.255.255.254";
// Set ManualConnectivityValidation to true to manually check stdout from the test to verify the correct calls are made in Linux/Init
static constexpr bool ManualConnectivityValidation = false;
namespace {
std::wstring GetMacAddress(const std::wstring& adapter = L"eth0")
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /sys/class/net/" + adapter + L"/address", 0);
out.pop_back(); // remove LF
return out;
}
template <class T>
class Stopwatch
{
private:
LARGE_INTEGER m_startQpc;
LARGE_INTEGER m_frequencyQpc;
T m_timeoutInterval;
public:
Stopwatch(_In_opt_ T TimeoutInterval = T::max()) : m_timeoutInterval(TimeoutInterval)
{
QueryPerformanceFrequency(&m_frequencyQpc);
QueryPerformanceCounter(&m_startQpc);
}
T Elapsed()
{
LARGE_INTEGER End;
UINT64 ElapsedQpc;
QueryPerformanceCounter(&End);
ElapsedQpc = End.QuadPart - m_startQpc.QuadPart;
return T((ElapsedQpc * T::period::den) / T::period::num / m_frequencyQpc.QuadPart);
}
bool IsExpired()
{
return Elapsed() >= m_timeoutInterval;
}
};
} // namespace
namespace NetworkTests {
class VirtioProxyTests;
class NetworkTests
{
WSL_TEST_CLASS(NetworkTests)
friend class MirroredTests;
friend class BridgedTests;
friend class VirtioProxyTests;
struct IpAddress
{
std::wstring Address;
uint8_t PrefixLength;
bool Preferred = false;
bool operator==(const IpAddress& other) const
{
return Address == other.Address && PrefixLength == other.PrefixLength;
}
std::wstring GetPrefix() const
{
DWORD status = ERROR_INVALID_FUNCTION;
SOCKADDR_INET* address = nullptr;
unsigned char* addressPointer{};
NET_ADDRESS_INFO netAddrInfo{};
status = ParseNetworkString(Address.c_str(), NET_STRING_IP_ADDRESS, &netAddrInfo, nullptr, nullptr);
if (status != NO_ERROR)
{
return std::wstring(L"");
}
address = reinterpret_cast<SOCKADDR_INET*>(&netAddrInfo.IpAddress);
addressPointer = (address->si_family == AF_INET) ? reinterpret_cast<unsigned char*>(&address->Ipv4.sin_addr)
: address->Ipv6.sin6_addr.u.Byte;
constexpr int c_numBitsPerByte = 8;
for (int i = 0, currPrefixLength = PrefixLength; i < INET_ADDR_LENGTH(address->si_family); i++, currPrefixLength -= c_numBitsPerByte)
{
if (currPrefixLength < c_numBitsPerByte)
{
const int bitShiftAmt = (c_numBitsPerByte - std::max(currPrefixLength, 0));
addressPointer[i] &= (0xFF >> bitShiftAmt) << bitShiftAmt;
}
}
return wsl::windows::common::string::SockAddrInetToWstring(*address) + L"/" + std::to_wstring(PrefixLength);
}
};
struct InterfaceState
{
std::wstring Name;
std::vector<IpAddress> V4Addresses;
std::optional<std::wstring> Gateway;
std::vector<IpAddress> V6Addresses;
std::optional<std::wstring> V6Gateway;
bool Up = false;
int Mtu = 0;
bool Rename = false;
};
struct Route
{
std::wstring Via;
std::wstring Device;
std::optional<std::wstring> Prefix;
int Metric = 0;
bool operator==(const Route& other) const
{
return Via == other.Via && Device == other.Device && Prefix == other.Prefix;
}
};
struct RoutingTableState
{
std::optional<Route> DefaultRoute;
std::vector<Route> Routes;
};
enum class FirewallType
{
Host,
HyperV
};
struct FirewallRule
{
FirewallType Type;
std::wstring Name;
std::wstring RemotePorts;
std::wstring Action;
std::wstring VmCreatorId;
};
GUID AdapterId;
TEST_CLASS_SETUP(TestClassSetup)
{
VERIFY_ARE_EQUAL(LxsstuInitialize(false), TRUE);
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
if (LxsstuVmMode())
{
WslShutdown();
}
VERIFY_NO_THROW(LxsstuUninitialize(false));
return true;
}
TEST_METHOD_SETUP(MethodSetup)
{
if (!LxsstuVmMode())
{
return true;
}
AdapterId = NetworkTests::QueryAdapterId();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ln -f -s /init /gns"), (DWORD)0);
return true;
}
TEST_METHOD(RemoveAndAddDefaultRoute)
{
WSL2_TEST_ONLY();
TestCase({{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"}});
// Verify that the default routes are set
auto state = GetIpv4RoutingTableState();
VERIFY_IS_TRUE(state.DefaultRoute.has_value());
VERIFY_ARE_EQUAL(state.DefaultRoute->Via, L"192.168.0.1");
auto v6State = GetIpv6RoutingTableState();
VERIFY_IS_TRUE(v6State.DefaultRoute.has_value());
VERIFY_ARE_EQUAL(v6State.DefaultRoute->Via, L"fc00::1");
// Now remove them
wsl::shared::hns::Route route;
route.NextHop = L"192.168.0.1";
route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX;
route.Family = AF_INET;
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Remove, GuestEndpointResourceType::Route);
wsl::shared::hns::Route v6Route;
v6Route.NextHop = L"fc00::1";
v6Route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_V6_PREFIX;
v6Route.Family = AF_INET6;
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Remove, GuestEndpointResourceType::Route);
// Verify that the routes are removed
state = GetIpv4RoutingTableState();
VERIFY_IS_FALSE(state.DefaultRoute.has_value());
v6State = GetIpv6RoutingTableState();
VERIFY_IS_FALSE(v6State.DefaultRoute.has_value());
// Add them again
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Add, GuestEndpointResourceType::Route);
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Add, GuestEndpointResourceType::Route);
// Verify that the routes are restored
state = GetIpv4RoutingTableState();
VERIFY_IS_TRUE(state.DefaultRoute.has_value());
VERIFY_ARE_EQUAL(state.DefaultRoute->Via, L"192.168.0.1");
VERIFY_ARE_EQUAL(state.DefaultRoute->Device, L"eth0");
v6State = GetIpv6RoutingTableState();
VERIFY_IS_TRUE(v6State.DefaultRoute.has_value());
VERIFY_ARE_EQUAL(v6State.DefaultRoute->Via, L"fc00::1");
VERIFY_ARE_EQUAL(v6State.DefaultRoute->Device, L"eth0");
}
TEST_METHOD(SetInterfaceDownAndUp)
{
WSL2_TEST_ONLY();
// Disconnect interface
wsl::shared::hns::NetworkInterface link;
link.Connected = false;
RunGns(link, ModifyRequestType::Update, GuestEndpointResourceType::Interface);
VERIFY_IS_FALSE(GetInterfaceState(L"eth0").Up);
// Connect it again
link.Connected = true;
RunGns(link, ModifyRequestType::Update, GuestEndpointResourceType::Interface);
VERIFY_IS_TRUE(GetInterfaceState(L"eth0").Up);
}
TEST_METHOD(SetMtu)
{
WSL2_TEST_ONLY();
// Set MTU - must be 1280 bytes or above to meet IPv6 minimum MTU requirement
wsl::shared::hns::NetworkInterface link;
link.Connected = true;
link.NlMtu = 1280;
RunGns(link, ModifyRequestType::Update, GuestEndpointResourceType::Interface);
VERIFY_ARE_EQUAL(GetInterfaceState(L"eth0").Mtu, 1280);
}
TEST_METHOD(AddAndRemoveCustomRoute)
{
WSL2_TEST_ONLY();
TestCase({{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"}});
// Add custom routes, one per address family
wsl::shared::hns::Route route;
route.NextHop = L"192.168.0.12";
route.DestinationPrefix = L"192.168.2.0/24";
route.Family = AF_INET;
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
wsl::shared::hns::Route v6Route;
v6Route.NextHop = L"fc00::12";
v6Route.DestinationPrefix = L"fc00:abcd::/80";
v6Route.Family = AF_INET6;
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
// Check that the routes are there
const bool v4CustomRouteExists = RouteExists({L"192.168.0.12", L"eth0", L"192.168.2.0/24"});
const bool v6CustomRouteExists = RouteExists({L"fc00::12", L"eth0", L"fc00:abcd::/80"});
// Now remove them
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Remove, GuestEndpointResourceType::Route);
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Remove, GuestEndpointResourceType::Route);
// Check that the routes are gone
const bool v4CustomRouteGone = !RouteExists({L"192.168.0.12", L"eth0", L"192.168.2.0/24"});
const bool v6CustomRouteGone = !RouteExists({L"fc00::12", L"eth0", L"fc00:abcd::/80"});
VERIFY_IS_TRUE(v4CustomRouteExists);
VERIFY_IS_TRUE(v6CustomRouteExists);
VERIFY_IS_TRUE(v4CustomRouteGone);
VERIFY_IS_TRUE(v6CustomRouteGone);
}
TEST_METHOD(AddRouteWithMetrics)
{
WSL2_TEST_ONLY();
TestCase({{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"}});
// Add a custom route per address family
wsl::shared::hns::Route route;
route.NextHop = L"192.168.0.12";
route.DestinationPrefix = L"192.168.2.0/24";
route.Family = AF_INET;
route.Metric = 12;
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
wsl::shared::hns::Route v6Route;
v6Route.NextHop = L"fc00::12";
v6Route.DestinationPrefix = L"fc00:abcd::/64";
v6Route.Family = AF_INET6;
v6Route.Metric = 12;
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
// Check that the routes are there
const bool v4CustomRouteExists = RouteExists({L"192.168.0.12", L"eth0", L"192.168.2.0/24", 12});
const bool v6CustomRouteExists = RouteExists({L"fc00::12", L"eth0", L"fc00:abcd::/64", 12});
// Now remove them
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Remove, GuestEndpointResourceType::Route);
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Remove, GuestEndpointResourceType::Route);
// Check that the routes are gone
const bool v4CustomRouteGone = !RouteExists({L"192.168.0.12", L"eth0", L"192.168.2.0/24", 12});
const bool v6CustomRouteGone = !RouteExists({L"fc00::12", L"eth0", L"fc00:abcd::/64", 12});
VERIFY_IS_TRUE(v4CustomRouteExists);
VERIFY_IS_TRUE(v6CustomRouteExists);
VERIFY_IS_TRUE(v4CustomRouteGone);
VERIFY_IS_TRUE(v6CustomRouteGone);
}
TEST_METHOD(ResetRoutes)
{
WSL2_TEST_ONLY();
TestCase({{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"}});
// Add a custom route per address family
wsl::shared::hns::Route route;
route.NextHop = L"192.168.0.12";
route.DestinationPrefix = L"192.168.2.0/24";
route.Family = AF_INET;
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
wsl::shared::hns::Route v6Route;
v6Route.NextHop = L"fc00::12";
v6Route.DestinationPrefix = L"fc00:abcd::/80";
v6Route.Family = AF_INET6;
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
// Check that the custom routes are there
bool v4RouteExists = RouteExists({L"192.168.0.12", L"eth0", L"192.168.2.0/24"});
bool v6RouteExists = RouteExists({L"fc00::12", L"eth0", L"fc00:abcd::/80"});
// Reset the routing table
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Reset, GuestEndpointResourceType::Route);
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Reset, GuestEndpointResourceType::Route);
// Check that both routes are gone, per address family
bool v4RouteGoneAfterReset = !RouteExists({L"192.168.0.12", L"eth0", L"192.168.2.0/24"});
auto state = GetIpv4RoutingTableState();
bool v4GwGoneAfterReset = !state.DefaultRoute.has_value();
bool v6RouteGoneAfterReset = !RouteExists({L"fc00::12", L"eth0", L"fc00:abcd::/80"});
auto v6State = GetIpv6RoutingTableState();
bool v6GwGoneAfterReset = !v6State.DefaultRoute.has_value();
// Add the custom and default routes back
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX;
route.NextHop = L"192.168.0.1";
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
v6Route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_V6_PREFIX;
v6Route.NextHop = L"fc00::1";
SendDeviceSettingsRequest(L"eth0", v6Route, ModifyRequestType::Update, GuestEndpointResourceType::Route);
// Verify that all the routes are there
bool v4RouteRestored = RouteExists({L"192.168.0.12", L"eth0", L"192.168.2.0/24"});
state = GetIpv4RoutingTableState();
bool v4GwRestored = state.DefaultRoute.has_value();
bool v4GwRestoredCorrectly = state.DefaultRoute->Via == L"192.168.0.1";
bool v6RouteRestored = RouteExists({L"fc00::12", L"eth0", L"fc00:abcd::/80"});
v6State = GetIpv6RoutingTableState();
bool v6GwRestored = v6State.DefaultRoute.has_value();
bool v6GwRestoredCorrectly = v6State.DefaultRoute->Via == L"fc00::1";
VERIFY_IS_TRUE(v4RouteExists);
VERIFY_IS_TRUE(v6RouteExists);
VERIFY_IS_TRUE(v4RouteGoneAfterReset);
VERIFY_IS_TRUE(v4GwGoneAfterReset);
VERIFY_IS_TRUE(v6RouteGoneAfterReset);
VERIFY_IS_TRUE(v6GwGoneAfterReset);
VERIFY_IS_TRUE(v4RouteRestored);
VERIFY_IS_TRUE(v4GwRestored);
VERIFY_IS_TRUE(v4GwRestoredCorrectly);
VERIFY_IS_TRUE(v6RouteRestored);
VERIFY_IS_TRUE(v6GwRestored);
VERIFY_IS_TRUE(v6GwRestoredCorrectly);
}
TEST_METHOD(ResetRoutesTwice)
{
WSL2_TEST_ONLY();
TestCase({{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"}});
auto state = GetIpv4RoutingTableState();
VERIFY_IS_TRUE(state.DefaultRoute.has_value());
auto v6State = GetIpv6RoutingTableState();
VERIFY_IS_TRUE(v6State.DefaultRoute.has_value());
// Reset the IPv4 table twice
wsl::shared::hns::Route route;
route.Family = AF_INET;
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Reset, GuestEndpointResourceType::Route);
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Reset, GuestEndpointResourceType::Route);
state = GetIpv4RoutingTableState();
VERIFY_IS_FALSE(state.DefaultRoute.has_value());
VERIFY_IS_TRUE(state.Routes.empty());
// Then reset the IPv6 table twice
route.Family = AF_INET6;
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Reset, GuestEndpointResourceType::Route);
SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Reset, GuestEndpointResourceType::Route);
state = GetIpv6RoutingTableState();
VERIFY_IS_FALSE(state.DefaultRoute.has_value());
VERIFY_IS_TRUE(state.Routes.empty());
}
TEST_METHOD(UpdateIpAddress)
{
WSL2_TEST_ONLY();
TestCase({{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"}});
// Verify that the IPs are in the preferred state
auto interfaceState = GetInterfaceState(L"eth0");
VERIFY_ARE_EQUAL(1, interfaceState.V4Addresses.size());
VERIFY_ARE_EQUAL(L"192.168.0.2", interfaceState.V4Addresses[0].Address);
VERIFY_IS_TRUE(interfaceState.V4Addresses[0].Preferred);
VERIFY_ARE_EQUAL(1, interfaceState.V6Addresses.size());
VERIFY_ARE_EQUAL(L"fc00::2", interfaceState.V6Addresses[0].Address);
VERIFY_IS_TRUE(interfaceState.V6Addresses[0].Preferred);
// Change current ip addresses to be deprecated
wsl::shared::hns::IPAddress address;
address.Address = L"192.168.0.2";
address.OnLinkPrefixLength = 24;
address.Family = AF_INET;
address.PreferredLifetime = 0;
SendDeviceSettingsRequest(L"eth0", address, ModifyRequestType::Update, GuestEndpointResourceType::IPAddress);
wsl::shared::hns::IPAddress v6Address;
v6Address.Address = L"fc00::2";
v6Address.OnLinkPrefixLength = 64;
v6Address.Family = AF_INET6;
address.PreferredLifetime = 0;
SendDeviceSettingsRequest(L"eth0", v6Address, ModifyRequestType::Update, GuestEndpointResourceType::IPAddress);
// Validate that the IPs are no longer preferred
interfaceState = GetInterfaceState(L"eth0");
VERIFY_ARE_EQUAL(1, interfaceState.V4Addresses.size());
VERIFY_ARE_EQUAL(L"192.168.0.2", interfaceState.V4Addresses[0].Address);
VERIFY_IS_FALSE(interfaceState.V4Addresses[0].Preferred);
VERIFY_ARE_EQUAL(1, interfaceState.V6Addresses.size());
VERIFY_ARE_EQUAL(L"fc00::2", interfaceState.V6Addresses[0].Address);
VERIFY_IS_FALSE(interfaceState.V6Addresses[0].Preferred);
}
enum IpPrefixOrigin
{
IpPrefixOriginOther = 0,
IpPrefixOriginManual,
IpPrefixOriginWellKnown,
IpPrefixOriginDhcp,
IpPrefixOriginRouterAdvertisement,
};
enum IpSuffixOrigin
{
IpSuffixOriginOther = 0,
IpSuffixOriginManual,
IpSuffixOriginWellKnown,
IpSuffixOriginDhcp,
IpSuffixOriginLinkLayerAddress,
IpSuffixOriginRandom,
};
TEST_METHOD(TemporaryAddress)
{
WSL2_TEST_ONLY();
TestCase({{L"eth0", {}, {}, {{L"fc00::2", 64}}, L"fc00::1"}});
// Make the address public
wsl::shared::hns::IPAddress v6Address;
v6Address.Address = L"fc00::2";
v6Address.OnLinkPrefixLength = 64;
v6Address.Family = AF_INET6;
v6Address.PrefixOrigin = IpPrefixOriginRouterAdvertisement;
v6Address.SuffixOrigin = IpSuffixOriginLinkLayerAddress;
v6Address.PreferredLifetime = 0xFFFFFFFF;
SendDeviceSettingsRequest(L"eth0", v6Address, ModifyRequestType::Update, GuestEndpointResourceType::IPAddress);
// Add a temporary address
v6Address.Address = L"fc00::abcd:1234:5678:9999";
v6Address.OnLinkPrefixLength = 64;
v6Address.Family = AF_INET6;
v6Address.PrefixOrigin = IpPrefixOriginRouterAdvertisement;
v6Address.SuffixOrigin = IpSuffixOriginRandom;
v6Address.PreferredLifetime = 0xFFFFFFFF;
SendDeviceSettingsRequest(L"eth0", v6Address, ModifyRequestType::Add, GuestEndpointResourceType::IPAddress);
// Wait for DAD to finish to avoid it being a factor in source address selection
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
VERIFY_ARE_EQUAL(2, GetInterfaceState(L"eth0").V6Addresses.size());
// Ensure that the temporary address is preferred during source address selection
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"ip route get 2001::5");
LogInfo("'ip route get 2001::5' - '%ls'", out.c_str());
auto [out5, _5] = LxsstuLaunchWslAndCaptureOutput(L"ip addr show eth0");
LogInfo("[TemporaryAddress] ip addr show output:\r\n%ls", FixLineEndings(out5).c_str());
std::wsmatch match;
std::wregex pattern(L"2001::5 from :: via fc00::1 dev eth0 proto kernel src ([a-f,A-F,0-9,:]+)");
VERIFY_IS_TRUE(std::regex_search(out, match, pattern));
VERIFY_ARE_EQUAL(2, match.size());
VERIFY_ARE_EQUAL(L"fc00::abcd:1234:5678:9999", match.str(1));
// Make another public address
v6Address.Address = L"fc00::3";
v6Address.OnLinkPrefixLength = 64;
v6Address.Family = AF_INET6;
v6Address.PrefixOrigin = IpPrefixOriginRouterAdvertisement;
v6Address.SuffixOrigin = IpSuffixOriginLinkLayerAddress;
v6Address.PreferredLifetime = 0xFFFFFFFF;
SendDeviceSettingsRequest(L"eth0", v6Address, ModifyRequestType::Add, GuestEndpointResourceType::IPAddress);
// Test source address selection again
auto [out2, _2] = LxsstuLaunchWslAndCaptureOutput(L"ip route get 2001::6");
LogInfo("'ip route get 2001::6' - '%ls'", out2.c_str());
std::wregex pattern2(L"2001::6 from :: via fc00::1 dev eth0 proto kernel src ([a-f,A-F,0-9,:]+)");
VERIFY_IS_TRUE(std::regex_search(out2, match, pattern2));
VERIFY_ARE_EQUAL(2, match.size());
VERIFY_ARE_EQUAL(L"fc00::abcd:1234:5678:9999", match.str(1));
}
TEST_METHOD(SimpleCase)
{
TestCase({{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"}});
}
TEST_METHOD(AddressChange)
{
TestCase(
{{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"},
{L"eth0", {{L"192.168.0.3", 24}}, L"192.168.0.1", {{L"fc00::3", 64}}, L"fc00::1"}});
}
TEST_METHOD(GatewayChange)
{
TestCase(
{{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"},
{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.3", {{L"fc00::2", 64}}, L"fc00::3"}});
}
TEST_METHOD(NetworkChange)
{
TestCase(
{{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"},
{L"eth0", {{L"10.0.0.2", 16}}, L"10.0.0.1", {{L"fc00:abcd::5", 80}}, L"fc00:abcd::1"}});
}
TEST_METHOD(NetworkChangeAndBack)
{
TestCase(
{{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"},
{L"eth0", {{L"10.0.0.2", 16}}, L"10.0.0.1", {{L"fc00:abcd::5", 80}}, L"fc00:abcd::1"},
{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"}});
}
TEST_METHOD(NoChange)
{
TestCase(
{{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"},
{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1"}});
}
TEST_METHOD(MultipleIps)
{
TestCase(
{{L"eth0",
{{L"192.168.0.2", 24}, {L"192.168.0.3", 24}},
L"192.168.0.1",
{{L"fc00::2", 64}, {L"fc00::3", 64}},
L"fc00::1"}});
}
TEST_METHOD(MacAddressChangeAndBack)
{
WSL2_TEST_ONLY();
const auto originalMac = GetMacAddress();
wsl::shared::hns::MacAddress macAddress;
macAddress.PhysicalAddress = "AA-AA-FF-FF-FF-FF";
SendDeviceSettingsRequest(L"eth0", macAddress, ModifyRequestType::Update, GuestEndpointResourceType::MacAddress);
VERIFY_ARE_EQUAL(GetMacAddress(), L"aa:aa:ff:ff:ff:ff");
macAddress.PhysicalAddress = wsl::shared::string::WideToMultiByte(originalMac);
std::replace(macAddress.PhysicalAddress.begin(), macAddress.PhysicalAddress.end(), ':', '-');
SendDeviceSettingsRequest(L"eth0", macAddress, ModifyRequestType::Update, GuestEndpointResourceType::MacAddress);
VERIFY_ARE_EQUAL(GetMacAddress(), originalMac);
}
static void VerifyDigDnsResolution(const std::wstring& digCommandLine)
{
// dig has exit code 0 when it receives a DNS response
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(digCommandLine.data(), 0);
// Verify dig returned a non-empty output
VERIFY_IS_TRUE(!out.empty());
}
static void VerifyDnsResolutionBasic()
{
// Verify basic DNS resolution using getent
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"getent ahosts bing.com", 0);
VERIFY_IS_TRUE(!out.empty());
}
static void VerifyDnsResolutionDig()
{
if (HostHasInternetConnectivity(AF_INET))
{
// Test A record resolution (IPv4) with both UDP and TCP
VerifyDigDnsResolution(L"dig +short +time=5 A bing.com");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 A bing.com");
// Test reverse DNS lookup
VerifyDigDnsResolution(L"dig +short +time=5 -x 8.8.8.8");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 -x 8.8.8.8");
}
else
{
LogInfo("Host does not have IPv4 internet connectivity. Skipping IPv4 DNS tests.");
}
if (HostHasInternetConnectivity(AF_INET6))
{
// Test AAAA record resolution (IPv6) with both UDP and TCP
VerifyDigDnsResolution(L"dig +short +time=5 AAAA bing.com");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 AAAA bing.com");
}
else
{
LogInfo("Host does not have IPv6 internet connectivity. Skipping IPv6 DNS tests.");
}
}
static void VerifyDnsResolutionRecordTypes()
{
// Test various DNS record types
VerifyDigDnsResolution(L"dig +short +time=5 MX bing.com");
VerifyDigDnsResolution(L"dig +short +time=5 NS bing.com");
VerifyDigDnsResolution(L"dig +short +time=5 TXT bing.com");
VerifyDigDnsResolution(L"dig +short +time=5 SOA bing.com");
}
static void VerifyDnsQueries()
{
// query for A/IPv4 records
VerifyDigDnsResolution(L"dig +short +time=5 A bing.com");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 A bing.com");
// query for AAAA/IPv6 records
VerifyDigDnsResolution(L"dig +short +time=5 AAAA bing.com");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 AAAA bing.com");
// query for MX records
VerifyDigDnsResolution(L"dig +short +time=5 MX bing.com");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 MX bing.com");
// query for NS records
VerifyDigDnsResolution(L"dig +short +time=5 NS bing.com");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 NS bing.com");
// reverse DNS lookup
VerifyDigDnsResolution(L"dig +short +time=5 -x 8.8.8.8");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 -x 8.8.8.8");
// query for SOA records
VerifyDigDnsResolution(L"dig +short +time=5 SOA bing.com");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 SOA bing.com");
// query for TXT records
VerifyDigDnsResolution(L"dig +short +time=5 TXT bing.com");
VerifyDigDnsResolution(L"dig +tcp +short +time=5 TXT bing.com");
// query for CNAME records
VerifyDigDnsResolution(L"dig +time=5 CNAME bing.com");
VerifyDigDnsResolution(L"dig +tcp +time=5 CNAME bing.com");
// query for SRV records
VerifyDigDnsResolution(L"dig +time=5 SRV bing.com");
VerifyDigDnsResolution(L"dig +tcp +time=5 SRV bing.com");
// query for ANY - for this option dig expects a large response so it will query directly over TCP,
// instead of trying UDP first and falling back to TCP.
VerifyDigDnsResolution(L"dig +short ANY bing.com");
}
static void VerifyDnsSuffixes()
{
bool foundSuffix = false;
// Verify global DNS suffixes are reflected in Linux
auto [outGlobal, errGlobal] = LxsstuLaunchPowershellAndCaptureOutput(
L"Get-DnsClientGlobalSetting | Select-Object -Property SuffixSearchList | ForEach-Object {$_.SuffixSearchList}");
const std::wstring separators = L" \n\t\r";
for (const auto& suffix : wsl::shared::string::SplitByMultipleSeparators(outGlobal, separators))
{
if (!suffix.empty())
{
foundSuffix = true;
// use grep -F as suffixes can contain '.'
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"cat /etc/resolv.conf | grep search | grep -F " + suffix), static_cast<DWORD>(0));
}
}
// Verify per-interface DNS suffixes are reflected in Linux
auto [outPerInterface, errPerInterface] =
LxsstuLaunchPowershellAndCaptureOutput(L"Get-DnsClient | ForEach-Object {$_.ConnectionSpecificSuffix}");
for (const auto& suffix : wsl::shared::string::SplitByMultipleSeparators(outPerInterface, separators))
{
if (!suffix.empty())
{
foundSuffix = true;
// use grep -F as suffixes can contain '.'
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"cat /etc/resolv.conf | grep search | grep -F " + suffix), static_cast<DWORD>(0));
}
}
// No suffix was found - configure a dummy global suffix, verify it's reflected in Linux, then delete it
if (!foundSuffix)
{
LxsstuLaunchPowershellAndCaptureOutput(L"Set-DnsClientGlobalSetting -SuffixSearchList @('test.com')");
auto restoreGlobalSuffixes = wil::scope_exit(
[&] { LxsstuLaunchPowershellAndCaptureOutput(L"Set-DnsClientGlobalSetting -SuffixSearchList @()"); });
std::this_thread::sleep_for(std::chrono::seconds(1));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"cat /etc/resolv.conf | grep search | grep -F test.com"), static_cast<DWORD>(0));
LxsstuLaunchPowershellAndCaptureOutput(L"Set-DnsClientGlobalSetting -SuffixSearchList @()");
std::this_thread::sleep_for(std::chrono::seconds(1));
VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"cat /etc/resolv.conf | grep search | grep -F test.com"), static_cast<DWORD>(0));
}
}
static void VerifyEtcHosts()
{
const auto windowsHostsPath = "C:\\Windows\\System32\\drivers\\etc\\hosts";
// Save existing Windows /etc/hosts
std::wifstream windowsHostsRead(windowsHostsPath);
const auto oldWindowsHosts = std::wstring{std::istreambuf_iterator<wchar_t>(windowsHostsRead), {}};
windowsHostsRead.close();
auto restoreWindowsHosts = wil::scope_exit([&] {
std::wofstream windowsHostsWrite(windowsHostsPath);
windowsHostsWrite << oldWindowsHosts;
});
// Add dummy entry matching bing.com to IP 1.2.3.4
std::wofstream windowsHostsWrite(windowsHostsPath, std::ios_base::app);
windowsHostsWrite << "\n1.2.3.4 bing.com";
windowsHostsWrite.close();
// Verify Linux /etc/hosts does *not* contain 1.2.3.4
VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(L"cat /etc/hosts | grep -F 1.2.3.4"), static_cast<DWORD>(0));
// Verify bing.com gets resolved to 1.2.3.4 by dig
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"dig bing.com | grep -F 1.2.3.4"), static_cast<DWORD>(0));
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"dig +tcp bing.com | grep -F 1.2.3.4"), static_cast<DWORD>(0));
}
static void VerifyDnsTunneling(const std::wstring& dnsTunnelingIpAddress)
{
// Verify /etc/resolv.conf is configured with the expected nameserver
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"cat /etc/resolv.conf | grep nameserver | grep -F " + dnsTunnelingIpAddress), static_cast<DWORD>(0));
// Verify that we have a working connection.
GuestClient(L"tcp-connect:bing.com:80");
// Verify multiple types of DNS queries
VerifyDnsQueries();
// Verify resolution via Windows /etc/hosts
VerifyEtcHosts();
// Verify DNS tunneling works with systemd enabled
auto revert = EnableSystemd();
GuestClient(L"tcp-connect:bing.com:80");
VerifyDnsQueries();
}
TEST_METHOD(NatDnsTunneling)
{
DNS_TUNNELING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.dnsTunneling = true}));
VerifyDnsTunneling(c_dnsTunnelingDefaultIp);
}
TEST_METHOD(NatDnsTunnelingWithSpecificIp)
{
DNS_TUNNELING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.dnsTunneling = true, .dnsTunnelingIpAddress = L"10.255.255.1"}));
VerifyDnsTunneling(L"10.255.255.1");
}
TEST_METHOD(NatDnsTunnelingVerifySuffixes)
{
DNS_TUNNELING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.dnsTunneling = true}));
VerifyDnsSuffixes();
}
TEST_METHOD(NatWithoutIcsDnsProxy)
{
WSL2_TEST_ONLY();
// Verify WSL has connectivity in NAT mode when the ICS DNS proxy is turned off (in which case the DNS servers
// from Windows are mirrored in Linux)
WslConfigChange config(LxssGenerateTestConfig({.dnsProxy = false}));
GuestClient(L"tcp-connect:bing.com:80");
}
TEST_METHOD(DnsChange)
{
WSL2_TEST_ONLY();
wsl::shared::hns::DNS dns;
dns.ServerList = {L"1.1.1.1"};
dns.Options = LX_INIT_RESOLVCONF_FULL_HEADER;
RunGns(dns, ModifyRequestType::Update, GuestEndpointResourceType::DNS);
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/resolv.conf", 0);
const std::wstring expected = std::wstring(LX_INIT_RESOLVCONF_FULL_HEADER) + L"nameserver 1.1.1.1\n";
VERIFY_ARE_EQUAL(expected, out.c_str());
}
TEST_METHOD(DnsChangeMultipleServerAndSearch)
{
WSL2_TEST_ONLY();
wsl::shared::hns::DNS dns;
dns.ServerList = L"1.1.1.1,1.1.1.2";
dns.Search = L"foo.microsoft.com,bar.microsoft.com";
dns.Options = LX_INIT_RESOLVCONF_FULL_HEADER;
RunGns(dns, ModifyRequestType::Update, GuestEndpointResourceType::DNS);
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/resolv.conf", 0);
const std::wstring expected = std::wstring(LX_INIT_RESOLVCONF_FULL_HEADER) +
L"nameserver 1.1.1.1\n"
L"nameserver 1.1.1.2\n"
L"search foo.microsoft.com bar.microsoft.com\n";
VERIFY_ARE_EQUAL(expected, out.c_str());
}
TEST_METHOD(DnsResolutionBasic)
{
WSL2_TEST_ONLY();
NetworkTests::VerifyDnsResolutionBasic();
}
TEST_METHOD(DnsResolutionDig)
{
WSL2_TEST_ONLY();
NetworkTests::VerifyDnsResolutionDig();
}
TEST_METHOD(DnsResolutionRecordTypes)
{
WSL2_TEST_ONLY();
NetworkTests::VerifyDnsResolutionRecordTypes();
}
static void ClearHttpProxySettings(bool userScope)
{
auto command = L"Set-WinhttpProxy -SettingScope Machine -Proxy \\\"\\\"";
if (userScope)
{
command = L"Set-WinhttpProxy -SettingScope User -Proxy \\\"\\\"";
}
LxsstuLaunchPowershellAndCaptureOutput(command);
}
static void SetHttpProxySettings(const std::wstring& proxyString, const std::wstring& bypasses, const std::wstring& autoconfigUrl, bool userScope)
{
std::wstringstream proxySettings{};
if (userScope)
{
proxySettings << L" -SettingScope User";
}
else
{
proxySettings << L" -SettingScope Machine";
}
if (!proxyString.empty())
{
proxySettings << L" -Proxy " + proxyString;
}
if (!bypasses.empty())
{
proxySettings << L" -ProxyBypass \\\"" + bypasses + L"\\\"";
}
if (!autoconfigUrl.empty())
{
proxySettings << L" -AutoconfigUrl " + autoconfigUrl;
}
LogInfo("SetHttpProxySettings %ls", proxySettings.str().c_str());
auto [out, _] = LxsstuLaunchPowershellAndCaptureOutput(L"Set-WinhttpProxy" + proxySettings.str());
LogInfo("WinhttpProxy %ls", out.c_str());
}
static constexpr auto c_httpProxyLower = L"http_proxy";
static constexpr auto c_httpProxyUpper = L"HTTP_PROXY";
static constexpr auto c_httpsProxyLower = L"https_proxy";
static constexpr auto c_httpsProxyUpper = L"HTTPS_PROXY";
static constexpr auto c_proxyBypassLower = L"no_proxy";
static constexpr auto c_proxyBypassUpper = L"NO_PROXY";
static constexpr auto c_pacProxy = L"WSL_PAC_URL";
static constexpr auto c_httpProxyString = L"http://test.com:8888";
static constexpr auto c_httpProxyString2 = L"http://otherServer.com:1234";
static constexpr auto c_httpProxyLocalhost = L"http://localhost:8888";
static constexpr auto c_httpProxyLoopback = L"http://loopback:8888";
static constexpr auto c_httpProxyLocalhostv4 = L"http://127.0.0.1:8888";
static constexpr auto c_httpProxyLocalhostv6 = L"http://[::1]:8888";
static constexpr auto c_httpProxyIpV4 = L"http://198.168.1.128:8888";
static constexpr auto c_httpProxyIpV6 = L"http://[2001::1]:8888";
static constexpr auto c_httpProxyBypassString = L"test";
static constexpr auto c_httpProxyPACurl = L"testpac.pac";
static void VerifyWslEnvVariable(const std::wstring& envVar, const std::wstring& proxyString)
{
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"echo -n $" + envVar);
VERIFY_ARE_EQUAL(proxyString, output);
}
static void VerifyHttpProxyBypassesMirrored(const std::wstring& bypassString)
{
VerifyWslEnvVariable(c_proxyBypassLower, bypassString);
VerifyWslEnvVariable(c_proxyBypassUpper, bypassString);
}
static void VerifyHttpProxyPacUrlMirrored(const std::wstring& pacUrl)
{
VerifyWslEnvVariable(c_pacProxy, pacUrl);
}
static void VerifyHttpProxyStringMirrored(const std::wstring& proxyString)
{
VerifyWslEnvVariable(c_httpProxyLower, proxyString);
VerifyWslEnvVariable(c_httpProxyUpper, proxyString);
VerifyWslEnvVariable(c_httpsProxyLower, proxyString);
VerifyWslEnvVariable(c_httpsProxyUpper, proxyString);
}
static void VerifyHttpProxyEnvVariables(const std::wstring& proxyString, const std::wstring& bypassString, const std::wstring& pacUrl)
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"printenv");
LogInfo("VerifyHttpProxyEnvVariables:\r\n%ls", FixLineEndings(out).c_str());
VerifyHttpProxyStringMirrored(proxyString);
VerifyHttpProxyBypassesMirrored(bypassString);
VerifyHttpProxyPacUrlMirrored(pacUrl);
}
static void VerifyHttpProxySimple(bool userScope = true)
{
auto restoreProxySettings = wil::scope_exit([&] { ClearHttpProxySettings(userScope); });
SetHttpProxySettings(c_httpProxyString, L"", L"", userScope);
VerifyHttpProxyEnvVariables(c_httpProxyString, L"", L"");
}
static void VerifyNoHttpProxyConfigured(bool userScope = true)
{
ClearHttpProxySettings(userScope);
VerifyHttpProxyEnvVariables(L"", L"", L"");
}
static void VerifyHttpProxyWithBypassesConfigured(bool userScope = true)
{
auto restoreProxySettings = wil::scope_exit([&] { ClearHttpProxySettings(userScope); });
SetHttpProxySettings(c_httpProxyString, c_httpProxyBypassString, L"", userScope);
VerifyHttpProxyEnvVariables(c_httpProxyString, c_httpProxyBypassString, L"");
}
static void VerifyHttpProxyChange(bool userScope = true)
{
auto restoreProxySettings = wil::scope_exit([&] { ClearHttpProxySettings(userScope); });
SetHttpProxySettings(c_httpProxyString, L"", L"", userScope);
VerifyHttpProxyEnvVariables(c_httpProxyString, L"", L"");
SetHttpProxySettings(c_httpProxyString2, L"", L"", userScope);
VerifyHttpProxyEnvVariables(c_httpProxyString2, L"", L"");
}
static void VerifyHttpProxyAndWslEnv(bool userScope = true)
{
auto restoreProxySettings = wil::scope_exit([&] {
ClearHttpProxySettings(userScope);
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(c_httpProxyLower, nullptr));
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"WSLENV", nullptr));
});
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(c_httpProxyLower, c_httpProxyString));
std::wstring wslEnvVal{c_httpProxyLower};
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"WSLENV", wslEnvVal.append(L"/u").c_str()));
VerifyWslEnvVariable(c_httpProxyLower, c_httpProxyString);
SetHttpProxySettings(c_httpProxyString2, L"", L"", true);
// the user set environment variable should have priority over the proxy configured on host
VerifyWslEnvVariable(c_httpProxyLower, c_httpProxyString);
// this variable was not configured by user, so we use host configured proxy
VerifyWslEnvVariable(c_httpProxyUpper, c_httpProxyString2);
}
static void VerifyHttpProxyFilterByNetworkConfiguration(bool isNatMode)
{
auto restoreProxySettings = wil::scope_exit([&] { ClearHttpProxySettings(true); });
SetHttpProxySettings(c_httpProxyLocalhost, L"", L"", true);
if (isNatMode)
{
VerifyHttpProxyEnvVariables(L"", L"", L"");
}
else
{
VerifyHttpProxyEnvVariables(c_httpProxyLocalhost, L"", L"");
}
ClearHttpProxySettings(true);
SetHttpProxySettings(c_httpProxyLoopback, L"", L"", true);
if (isNatMode)
{
VerifyHttpProxyEnvVariables(L"", L"", L"");
}
else
{
VerifyHttpProxyEnvVariables(c_httpProxyLoopback, L"", L"");
}
ClearHttpProxySettings(true);
SetHttpProxySettings(c_httpProxyLocalhostv4, L"", L"", true);
if (isNatMode)
{
VerifyHttpProxyEnvVariables(L"", L"", L"");
}
else
{
VerifyHttpProxyEnvVariables(c_httpProxyLocalhostv4, L"", L"");
}
ClearHttpProxySettings(true);
SetHttpProxySettings(c_httpProxyLocalhostv4, c_httpProxyBypassString, L"", true);
if (isNatMode)
{
VerifyHttpProxyEnvVariables(L"", L"", L"");
}
else
{
VerifyHttpProxyEnvVariables(c_httpProxyLocalhostv4, c_httpProxyBypassString, L"");
}
ClearHttpProxySettings(true);
// validate nonloopback v4 still works
SetHttpProxySettings(c_httpProxyIpV4, L"", L"", true);
VerifyHttpProxyEnvVariables(c_httpProxyIpV4, L"", L"");
ClearHttpProxySettings(true);
SetHttpProxySettings(c_httpProxyIpV6, c_httpProxyBypassString, L"", true);
// v6 addresses is only supported in mirrored mode
if (isNatMode)
{
VerifyHttpProxyEnvVariables(L"", L"", L"");
}
else
{
VerifyHttpProxyEnvVariables(c_httpProxyIpV6, c_httpProxyBypassString, L"");
}
ClearHttpProxySettings(true);
// v6 loopback is unsupported in both network modes
SetHttpProxySettings(c_httpProxyLocalhostv6, L"", L"", true);
VerifyHttpProxyEnvVariables(L"", L"", L"");
}
static void VerifyHttpProxyFilterByNetworkConfigurationNAT()
{
VerifyHttpProxyFilterByNetworkConfiguration(true);
}
static void VerifyHttpProxyFilterByNetworkConfigurationMirrored()
{
VerifyHttpProxyFilterByNetworkConfiguration(false);
}
TEST_METHOD(NatHttpProxyVerifyConfigDisabled)
{
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.autoProxy = false}));
auto restoreProxySettings = wil::scope_exit([&] { ClearHttpProxySettings(true); });
SetHttpProxySettings(c_httpProxyString, L"", L"", true);
VerifyHttpProxyEnvVariables(L"", L"", L"");
}
TEST_METHOD(NatHttpProxySimple)
{
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.autoProxy = true}));
VerifyHttpProxySimple();
}
TEST_METHOD(NatHttpProxySimpleMachineScope)
{
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.autoProxy = true}));
// verify with machine scope
VerifyHttpProxySimple(false);
}
TEST_METHOD(NatNoHttpProxyConfigured)
{
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.autoProxy = true}));
VerifyNoHttpProxyConfigured();
}
TEST_METHOD(NatHttpProxyWithBypassesConfigured)
{
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.autoProxy = true}));
VerifyHttpProxyWithBypassesConfigured();
}
TEST_METHOD(NatHttpProxyChange)
{
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.autoProxy = true}));
VerifyHttpProxyChange();
}
TEST_METHOD(NatHttpProxyAndWslEnv)
{
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.autoProxy = true}));
VerifyHttpProxyAndWslEnv();
}
TEST_METHOD(NatHttpProxyFilterByNetworkConfiguration)
{
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.autoProxy = true}));
VerifyHttpProxyFilterByNetworkConfigurationNAT();
}
TEST_METHOD(RenameInterface)
{
WSL2_TEST_ONLY();
// Disconnect "eth0" interface so it can be renamed
wsl::shared::hns::NetworkInterface link;
link.Connected = false;
RunGns(link, ModifyRequestType::Update, GuestEndpointResourceType::Interface);
const bool eth0Disconnected = !GetInterfaceState(L"eth0").Up;
TestCase({{L"myeth", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1", false, 1500, true}});
const bool myethConnected = GetInterfaceState(L"myeth").Up;
// Disconnect "myeth" interface so it can be restored
link.Connected = false;
RunGns(link, ModifyRequestType::Update, GuestEndpointResourceType::Interface);
const bool myethDisconnected = !GetInterfaceState(L"myeth").Up;
TestCase({{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1", false, 1500, true}});
const bool eth0Connected = GetInterfaceState(L"eth0").Up;
VERIFY_IS_TRUE(eth0Disconnected);
VERIFY_IS_TRUE(myethConnected);
VERIFY_IS_TRUE(myethDisconnected);
VERIFY_IS_TRUE(eth0Connected);
}
TEST_METHOD(RenameWifiInterface)
{
WSL2_TEST_ONLY();
std::wstring commandLine(L"wsl.exe bash -c \"zcat /proc/config.gz | grep CONFIG_PROXY_WIFI=y\"");
const auto out = std::get<0>(LxsstuLaunchCommandAndCaptureOutputWithResult(commandLine.data()));
if (out.empty())
{
LogSkipped("Kernel does not support PROXY_WIFI. Skipping test...");
return;
}
// Disconnect "eth0" interface so it can be renamed
wsl::shared::hns::NetworkInterface link;
link.Connected = false;
RunGns(link, ModifyRequestType::Update, GuestEndpointResourceType::Interface);
const bool eth0Disconnected = !GetInterfaceState(L"eth0").Up;
TestCase({{L"wlan0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1", false, 1500, true}});
const bool _wlan0Connected = GetInterfaceState(L"_wlan0").Up;
const bool _wlan0Deleted = LxsstuLaunchWsl(L"ip link del wlan0") == (DWORD)0;
TestCase({{L"eth0", {{L"192.168.0.2", 24}}, L"192.168.0.1", {{L"fc00::2", 64}}, L"fc00::1", false, 1500, true}});
const bool eth0Connected = GetInterfaceState(L"eth0").Up;
VERIFY_IS_TRUE(eth0Disconnected);
VERIFY_IS_TRUE(_wlan0Connected);
VERIFY_IS_TRUE(_wlan0Deleted);
VERIFY_IS_TRUE(eth0Connected);
}
TEST_METHOD(EnableLoopbackRouting)
{
WSL2_TEST_ONLY();
// Enable accept_local and route_localnet settings for eth0
wsl::shared::hns::VmNicCreatedNotification creationNotification{AdapterId};
RunGns(creationNotification, LxGnsMessageVmNicCreatedNotification);
// Verify the settings were enabled
const bool acceptLocalEnabled = LxsstuLaunchWsl(L"sysctl net.ipv4.conf.eth0.accept_local | grep -w 1") == (DWORD)0;
const bool routeLocalnetEnabled = LxsstuLaunchWsl(L"sysctl net.ipv4.conf.eth0.route_localnet | grep -w 1") == (DWORD)0;
VERIFY_IS_TRUE(acceptLocalEnabled);
VERIFY_IS_TRUE(routeLocalnetEnabled);
}
TEST_METHOD(InitializeLoopbackConfiguration)
{
WSL2_TEST_ONLY();
// Assume eth0 is the GELNIC
wsl::shared::hns::CreateDeviceRequest createDeviceRequest{wsl::shared::hns::DeviceType::Loopback, L"loopback", AdapterId};
RunGns(createDeviceRequest, LxGnsMessageCreateDeviceRequest);
// Verify the expected ip rules are present
const bool gelnicRuleTcpExists =
LxsstuLaunchWsl(L"ip rule show | grep \"from all iif eth0 ipproto tcp lookup local\" | grep ^0:") == (DWORD)0;
const bool gelnicRuleUdpExists =
LxsstuLaunchWsl(L"ip rule show | grep \"from all iif eth0 ipproto tcp lookup local\" | grep ^0:") == (DWORD)0;
const bool table127RuleTcpExists =
LxsstuLaunchWsl(L"ip rule show | grep \"from all ipproto tcp lookup 127\" | grep ^1:") == (DWORD)0;
const bool table127RuleUdpExists =
LxsstuLaunchWsl(L"ip rule show | grep \"from all ipproto udp lookup 127\" | grep ^1:") == (DWORD)0;
const bool table128RuleTcpExists =
LxsstuLaunchWsl(L"ip rule show | grep \"from all ipproto tcp lookup 128\" | grep ^1:") == (DWORD)0;
const bool table128RuleUdpExists =
LxsstuLaunchWsl(L"ip rule show | grep \"from all ipproto udp lookup 128\" | grep ^1:") == (DWORD)0;
const bool localTableRuleExists = LxsstuLaunchWsl(L"ip rule show | grep \"from all lookup local\" | grep ^2:") == (DWORD)0;
// Verify that the static neighbor entry was added for the gateway
const bool gatewayArpEntryExists =
LxsstuLaunchWsl(L"ip neigh show dev eth0 | grep \"169\\.254\\.73\\.152 lladdr 00:11:22:33:44:55 PERMANENT\"") == (DWORD)0;
// Verify route was added for destination 127.0.0.1, with preferred source 127.0.0.1
const bool routeToLoopbackRangeExists =
LxsstuLaunchWsl(
L"ip route show table 127 | grep \"127\\.0\\.0\\.1 via 169\\.254\\.73\\.152 dev eth0\" | grep "
L"\"src 127\\.0\\.0\\.1\" | grep onlink") == (DWORD)0;
const bool shutdownSuccessful = WslShutdown();
VERIFY_IS_TRUE(gelnicRuleTcpExists);
VERIFY_IS_TRUE(gelnicRuleUdpExists);
VERIFY_IS_TRUE(table127RuleTcpExists);
VERIFY_IS_TRUE(table127RuleUdpExists);
VERIFY_IS_TRUE(table128RuleTcpExists);
VERIFY_IS_TRUE(table128RuleUdpExists);
VERIFY_IS_TRUE(localTableRuleExists);
VERIFY_IS_TRUE(gatewayArpEntryExists);
VERIFY_IS_TRUE(routeToLoopbackRangeExists);
VERIFY_IS_TRUE(shutdownSuccessful);
}
TEST_METHOD(AddRemoveLoopbackRoutesv4)
{
WSL2_TEST_ONLY();
const std::wstring interfaceName = L"eth0";
const std::vector<std::wstring> ipAddresses = {L"127.0.0.1", L"127.0.0.2"};
// Add routes on interface eth0 and verify that the routes were added in the custom local routing table (id 128)
for (const auto address : ipAddresses)
{
wsl::shared::hns::LoopbackRoutesRequest addRequest{interfaceName, wsl::shared::hns::OperationType::Create, AF_INET, address};
RunGns(addRequest, LxGnsMessageLoopbackRoutesRequest);
}
const bool firstRouteExists =
LxsstuLaunchWsl(
L"ip route show table 128 | grep \"127\\.0\\.0\\.1 via 169\\.254\\.73\\.152 dev eth0\" | grep \"src "
L"127\\.0\\.0\\.1\" | grep onlink") == (DWORD)0;
const bool secondRouteExists =
LxsstuLaunchWsl(
L"ip route show table 128 | grep \"127\\.0\\.0\\.2 via 169\\.254\\.73\\.152 dev eth0\" | grep \"src "
L"127\\.0\\.0\\.2\" | grep onlink") == (DWORD)0;
// Verify that the static neighbor entry was added for the gateway
const bool gatewayArpEntryExists =
LxsstuLaunchWsl(L"ip neigh show dev eth0 | grep \"169\\.254\\.73\\.152 lladdr 00:11:22:33:44:55 PERMANENT\"") == (DWORD)0;
// Verify that the routes are deleted
for (const auto address : ipAddresses)
{
wsl::shared::hns::LoopbackRoutesRequest removeRequest{interfaceName, wsl::shared::hns::OperationType::Remove, AF_INET, address};
RunGns(removeRequest, LxGnsMessageLoopbackRoutesRequest);
}
const bool firstRouteDeleted = LxsstuLaunchWsl(L"ip route show table 128 | grep 127\\.0\\.0\\.1") == (DWORD)1;
const bool secondRouteDeleted = LxsstuLaunchWsl(L"ip route show table 128 | grep 127\\.0\\.0\\.2") == (DWORD)1;
const bool shutdownSuccessful = WslShutdown();
VERIFY_IS_TRUE(firstRouteExists);
VERIFY_IS_TRUE(secondRouteExists);
VERIFY_IS_TRUE(gatewayArpEntryExists);
VERIFY_IS_TRUE(firstRouteDeleted);
VERIFY_IS_TRUE(secondRouteDeleted);
VERIFY_IS_TRUE(shutdownSuccessful);
}
/*
The test uses the "ip route get" command, which is equivalent to asking the OS what route it will take for a packet. It
functions as a small integration test.
*/
TEST_METHOD(LoopbackGetRoute)
{
WSL2_TEST_ONLY();
// Verify that before configurations are applied, the route chosen for 127.0.0.1 tcp/udp uses the local routing table
const bool loopbackTcpUsesLocalTable =
LxsstuLaunchWsl(L"ip route get from 127.0.0.1 127.0.0.1 ipproto tcp | grep local") == (DWORD)0;
const bool loopbackUdpUsesLocalTable =
LxsstuLaunchWsl(L"ip route get from 127.0.0.1 127.0.0.1 ipproto udp | grep local") == (DWORD)0;
// Assume eth0 is the GELNIC
wsl::shared::hns::CreateDeviceRequest createDeviceRequest{wsl::shared::hns::DeviceType::Loopback, L"loopback", AdapterId};
RunGns(createDeviceRequest, LxGnsMessageCreateDeviceRequest);
// Verify that after configurations are applied, the route chosen for 127.0.0.1 tcp/udp is the desired one
const bool loopbackTcpUsesCustomTable =
LxsstuLaunchWsl(L"ip route get from 127.0.0.1 127.0.0.1 ipproto tcp | grep \"via 169\\.254\\.73\\.152 dev eth0\"") == (DWORD)0;
const bool loopbackUdpUsesCustomTable =
LxsstuLaunchWsl(L"ip route get from 127.0.0.1 127.0.0.1 ipproto udp | grep \"via 169\\.254\\.73\\.152 dev eth0\"") == (DWORD)0;
const bool shutdownSuccessful = WslShutdown();
VERIFY_IS_TRUE(loopbackTcpUsesLocalTable);
VERIFY_IS_TRUE(loopbackUdpUsesLocalTable);
VERIFY_IS_TRUE(loopbackTcpUsesCustomTable);
VERIFY_IS_TRUE(loopbackUdpUsesCustomTable);
VERIFY_IS_TRUE(shutdownSuccessful);
}
// Validate that adapter has an ip address, default route and DNS configuration in NAT mode
TEST_METHOD(NatConfiguration)
{
WSL2_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig());
const auto state = GetInterfaceState(L"eth0");
VERIFY_IS_FALSE(state.V4Addresses.empty());
VERIFY_IS_TRUE(state.Gateway.has_value());
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/resolv.conf", 0);
const std::wregex pattern(L"(.|\n)*nameserver [0-9\\. ]+(.|\n)*", std::regex::extended);
VERIFY_IS_TRUE(std::regex_match(out, pattern));
}
static void WriteNatConfiguration(const std::wstring& network, const std::wstring& gateway, const std::wstring& ipAddress)
{
using namespace wsl::windows::common;
const auto key = registry::OpenLxssMachineKey(KEY_SET_VALUE);
if (gateway == L"delete")
{
registry::DeleteValue(key.get(), L"NatGatewayIpAddress");
}
else if (!gateway.empty())
{
registry::WriteString(key.get(), nullptr, L"NatGatewayIpAddress", gateway.c_str());
}
if (network == L"delete")
{
registry::DeleteValue(key.get(), L"NatNetwork");
}
else if (!network.empty())
{
registry::WriteString(key.get(), nullptr, L"NatNetwork", network.c_str());
}
const auto userKey = registry::OpenLxssUserKey();
if (ipAddress == L"delete")
{
registry::DeleteValue(userKey.get(), L"NatIpAddress");
}
else if (!ipAddress.empty())
{
registry::WriteString(userKey.get(), nullptr, L"NatIpAddress", ipAddress.c_str());
}
}
struct NatNetworkingConfiguration
{
std::wstring networkRange;
std::wstring gatewayIpAddress;
std::wstring ipAddress;
};
static NatNetworkingConfiguration GetNatConfiguration()
{
using namespace wsl::windows::common;
const auto key = registry::OpenLxssMachineKey();
const auto userKey = registry::OpenLxssUserKey();
return {
registry::ReadString(key.get(), nullptr, L"NatNetwork", L""),
registry::ReadString(key.get(), nullptr, L"NatGatewayIpAddress", L""),
registry::ReadString(userKey.get(), nullptr, L"NatIpAddress", L"")};
}
static void ResetWslNetwork()
{
// N.B. This must be kept in sync with the network IDs in NatNetworking.cpp.
GUID natNetworkId;
if (!AreExperimentalNetworkingFeaturesSupported() || !IsHyperVFirewallSupported())
{
natNetworkId = {0xb95d0c5e, 0x57d4, 0x412b, {0xb5, 0x71, 0x18, 0xa8, 0x1a, 0x16, 0xe0, 0x05}};
}
else
{
natNetworkId = {0x790e58b4, 0x7939, 0x4434, {0x93, 0x58, 0x89, 0xae, 0x7d, 0xdb, 0xe8, 0x7e}};
}
wil::unique_cotaskmem_string error;
const auto hr = HcnDeleteNetwork(natNetworkId, &error);
VERIFY_SUCCEEDED(hr, error.get());
}
TEST_METHOD(NatInvalidRange)
{
WSL2_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig());
WriteNatConfiguration(L"InvalidRange", {}, {L"delete"});
ResetWslNetwork();
RestartWslService();
const auto state = GetInterfaceState(
L"eth0",
L"wsl: Failed to create virtual network with address range: 'InvalidRange', created new network with range: "
L"'*.*.*.*/*', *.*");
VERIFY_IS_FALSE(state.V4Addresses.empty());
VERIFY_IS_TRUE(state.Gateway.has_value());
const auto networkConfiguration = GetNatConfiguration();
VERIFY_IS_FALSE(networkConfiguration.networkRange.empty());
VERIFY_ARE_EQUAL(state.V4Addresses[0].Address, networkConfiguration.ipAddress);
VERIFY_ARE_EQUAL(state.Gateway.value_or(L""), networkConfiguration.gatewayIpAddress);
}
TEST_METHOD(NatInvalidGateway)
{
WSL2_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig());
WriteNatConfiguration({}, L"InvalidGateway", {});
ResetWslNetwork();
RestartWslService();
const auto state = GetInterfaceState(
L"eth0",
L"wsl: Failed to create virtual network with address range: '*.*.*.*/*', created new network with range: "
L"'*.*.*.*/*', *.*");
VERIFY_IS_FALSE(state.V4Addresses.empty());
VERIFY_IS_TRUE(state.Gateway.has_value());
const auto networkConfiguration = GetNatConfiguration();
VERIFY_IS_FALSE(networkConfiguration.networkRange.empty());
VERIFY_ARE_EQUAL(state.V4Addresses[0].Address, networkConfiguration.ipAddress);
VERIFY_ARE_EQUAL(state.Gateway.value_or(L""), networkConfiguration.gatewayIpAddress);
}
TEST_METHOD(NatInvalidAddress)
{
WSL2_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig());
const auto previousConfiguration = GetNatConfiguration();
WriteNatConfiguration({}, {}, L"InvalidAddress");
ResetWslNetwork();
RestartWslService();
const auto state = GetInterfaceState(
L"eth0", L"wsl: Failed to create network endpoint with address: 'InvalidAddress', assigned new address: '*.*.*.*'*");
VERIFY_IS_FALSE(state.V4Addresses.empty());
VERIFY_IS_TRUE(state.Gateway.has_value());
const auto networkConfiguration = GetNatConfiguration();
// The network range should be the same
VERIFY_ARE_EQUAL(networkConfiguration.networkRange, previousConfiguration.networkRange);
VERIFY_IS_FALSE(networkConfiguration.networkRange.empty());
VERIFY_ARE_EQUAL(state.V4Addresses[0].Address, networkConfiguration.ipAddress);
VERIFY_ARE_EQUAL(state.Gateway.value_or(L""), networkConfiguration.gatewayIpAddress);
}
struct unique_kill_process
{
unique_kill_process()
{
}
unique_kill_process(wil::unique_handle&& process) : m_process(std::move(process))
{
}
unique_kill_process(unique_kill_process&&) = default;
unique_kill_process& operator=(unique_kill_process&&) = default;
unique_kill_process& operator=(const unique_kill_process&) = delete;
unique_kill_process(const unique_kill_process&) = delete;
~unique_kill_process()
{
reset();
}
void reset()
{
if (m_process)
{
TerminateProcess(m_process.get(), 0);
m_process.reset();
}
}
wil::unique_handle m_process;
};
static void VerifyLoopbackHostToGuest(const std::wstring& address, int protocol, std::chrono::duration<int> timeout = std::chrono::minutes(5))
{
LogInfo("VerifyLoopbackHostToGuest(address=%ls, protocol=%d)", address.c_str(), protocol);
SOCKADDR_INET addr = wsl::windows::common::string::StringToSockAddrInet(address);
SS_PORT(&addr) = htons(1234);
{
// Create listener in guest
std::optional<GuestListener> listener;
// Note: If a previous test case had the same port bound it can take a bit of time for the port to be released on the host.
auto createListener = [&]() { listener.emplace(addr, protocol); };
try
{
wsl::shared::retry::RetryWithTimeout<void>(
createListener, std::chrono::seconds(1), timeout, []() { return wil::ResultFromCaughtException() == E_FAIL; });
}
catch (...)
{
LogError("Failed to bind %ls in the guest, 0x%x", address.c_str(), wil::ResultFromCaughtException());
VERIFY_FAIL();
}
// If the guest is listening on any address, connect via loopback.
const auto ipAddress = (addr.si_family == AF_INET) ? reinterpret_cast<const void*>(&addr.Ipv4.sin_addr)
: reinterpret_cast<const void*>(&addr.Ipv6.sin6_addr);
if (INET_IS_ADDR_UNSPECIFIED(addr.si_family, ipAddress))
{
INETADDR_SETLOOPBACK(reinterpret_cast<PSOCKADDR>(&addr));
SS_PORT(&addr) = htons(1234);
}
// Connect from a client on the host
const wil::unique_socket clientSocket(socket(addr.si_family, (protocol == IPPROTO_UDP) ? SOCK_DGRAM : SOCK_STREAM, protocol));
VERIFY_ARE_NOT_EQUAL(clientSocket.get(), INVALID_SOCKET);
// The WSL2 loopback relay may have a one second delay after creation.
auto pred = [&]() {
if (protocol == IPPROTO_UDP)
{
const char buffer = 'A';
THROW_HR_IF(
E_FAIL,
sendto(clientSocket.get(), &buffer, sizeof(buffer), 0, reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) !=
sizeof(buffer));
}
else
{
THROW_HR_IF(E_FAIL, connect(clientSocket.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
}
};
try
{
wsl::shared::retry::RetryWithTimeout<void>(pred, std::chrono::seconds(1), timeout);
}
catch (...)
{
LogError("Timed out trying to connect to %ls", address.c_str());
VERIFY_FAIL();
}
// Verify the connection was accepted on the listener
listener->AcceptConnection();
}
// Wait until the guest has released its port
VerifyNotBound(addr, addr.si_family, protocol);
}
TEST_METHOD(HostToGuestLoopback)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:NetConfig", L"{1, 2, 3, 4}")
END_TEST_METHOD_PROPERTIES()
// All networking modes for both WSL1/2 are expected to support TCP/IPv4 host to guest loopback by default.
int networkingModeVal = 0;
WEX::TestExecution::TestData::TryGetValue(L"NetConfig", networkingModeVal);
auto networkingMode = static_cast<wsl::core::NetworkingMode>(networkingModeVal);
switch (networkingMode)
{
case wsl::core::NetworkingMode::Bridged:
WINDOWS_11_TEST_ONLY();
__fallthrough;
case wsl::core::NetworkingMode::Mirrored:
case wsl::core::NetworkingMode::VirtioProxy:
WSL2_TEST_ONLY();
break;
}
LogInfo("HostToGuestLoopback (networkingMode=%hs)", ToString(networkingMode));
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = networkingMode, .vmSwitch = L"Default Switch"}));
VerifyLoopbackHostToGuest(L"127.0.0.1", IPPROTO_TCP);
VerifyLoopbackHostToGuest(L"0.0.0.0", IPPROTO_TCP);
}
static void VerifyLoopbackGuestToHost(const std::wstring& address, int protocol)
{
LogInfo("VerifyLoopbackGuestToHost(address=%ls, protocol=%d)", address.c_str(), protocol);
SOCKADDR_INET addr = wsl::windows::common::string::StringToSockAddrInet(address);
SS_PORT(&addr) = htons(1234);
// Create a listener on the host
const wil::unique_socket listenSocket(socket(addr.si_family, (protocol == IPPROTO_UDP) ? SOCK_DGRAM : SOCK_STREAM, protocol));
VERIFY_ARE_NOT_EQUAL(listenSocket.get(), INVALID_SOCKET);
VERIFY_ARE_NOT_EQUAL(bind(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)), SOCKET_ERROR);
if (protocol == IPPROTO_TCP)
{
VERIFY_ARE_NOT_EQUAL(listen(listenSocket.get(), SOMAXCONN), SOCKET_ERROR);
}
// Connect from a client in the guest
GuestClient client(addr, protocol);
// Accept the connection on the listener
SOCKADDR_INET remoteAddr{};
int remoteAddrLen = sizeof(remoteAddr);
if (protocol == IPPROTO_UDP)
{
char buffer[2048];
int Timeout = 3000;
VERIFY_ARE_NOT_EQUAL(setsockopt(listenSocket.get(), SOL_SOCKET, SO_RCVTIMEO, (char*)&Timeout, sizeof(Timeout)), SOCKET_ERROR);
VERIFY_ARE_NOT_EQUAL(
recvfrom(listenSocket.get(), buffer, sizeof(buffer), 0, reinterpret_cast<SOCKADDR*>(&remoteAddr), &remoteAddrLen), SOCKET_ERROR);
}
else
{
// TODO: this accept call needs to timeout to avoid indefinite wait
const wil::unique_socket acceptSocket(accept(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&remoteAddr), &remoteAddrLen));
VERIFY_ARE_NOT_EQUAL(acceptSocket.get(), INVALID_SOCKET);
}
}
static void VerifyLoopbackGuestToGuest(const std::wstring& address, int protocol)
{
LogInfo("VerifyLoopbackGuestToGuest(address=%ls, protocol=%d)", address.c_str(), protocol);
SOCKADDR_INET addr = wsl::windows::common::string::StringToSockAddrInet(address);
SS_PORT(&addr) = htons(1234);
{
std::optional<GuestListener> listener;
auto createListener = [&]() { listener.emplace(addr, protocol); };
try
{
wsl::shared::retry::RetryWithTimeout<void>(createListener, std::chrono::seconds(1), std::chrono::minutes(1), []() {
return wil::ResultFromCaughtException() == E_FAIL;
});
}
catch (...)
{
LogError("Failed to bind %ls", address.c_str());
VERIFY_FAIL();
}
// Create listener in guest
// Connect from a client in the guest
GuestClient client(addr, protocol);
// Verify the connection was accepted on the listener
listener->AcceptConnection();
}
// Wait until the guest has released its port
VerifyNotBound(addr, addr.si_family, protocol);
}
static void VerifyLoopbackConnectivity(const std::wstring& address)
{
// Verify guest to host
VerifyLoopbackGuestToHost(address, IPPROTO_UDP);
VerifyLoopbackGuestToHost(address, IPPROTO_TCP);
// Verify host to guest
VerifyLoopbackHostToGuest(address, IPPROTO_UDP);
VerifyLoopbackHostToGuest(address, IPPROTO_TCP);
// Verify guest to guest
VerifyLoopbackGuestToGuest(address, IPPROTO_UDP);
VerifyLoopbackGuestToGuest(address, IPPROTO_TCP);
}
static wil::unique_socket BindHostPort(uint16_t Port, int Type, int Protocol, bool ExpectSuccess, bool Ipv6 = false, bool Localhost = false)
{
int AddressFamily{};
const SOCKADDR* Address{};
int AddressSize{};
SOCKADDR_IN Address4{};
SOCKADDR_IN6 Address6{};
if (Ipv6)
{
AddressFamily = AF_INET6;
Address6.sin6_family = AF_INET6;
Address6.sin6_port = htons(Port);
if (Localhost)
{
Address6.sin6_addr = IN6ADDR_LOOPBACK_INIT;
}
Address = reinterpret_cast<SOCKADDR*>(&Address6);
AddressSize = sizeof(Address6);
}
else
{
AddressFamily = AF_INET;
Address4.sin_family = AF_INET;
Address4.sin_port = htons(Port);
if (Localhost)
{
Address4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
}
Address = reinterpret_cast<SOCKADDR*>(&Address4);
AddressSize = sizeof(Address4);
}
wil::unique_socket listenSocket(socket(AddressFamily, Type, Protocol));
VERIFY_IS_TRUE(!!listenSocket);
VERIFY_ARE_EQUAL(bind(listenSocket.get(), Address, AddressSize) != SOCKET_ERROR, ExpectSuccess);
return listenSocket;
}
static std::tuple<unique_kill_process, bool, wil::unique_handle> BindGuestPortHelper(std::wstring_view BindSpec)
{
auto [stdErrRead, stdErrWrite] = CreateSubprocessPipe(false, true);
auto [stdOutRead, stdOutWrite] = CreateSubprocessPipe(false, true);
const std::wstring wslCmd = L"socat -dd " + std::wstring(BindSpec) + L" STDOUT";
auto cmd = LxssGenerateWslCommandLine(wslCmd.data());
auto process = LxsstuStartProcess(cmd.data(), nullptr, stdOutWrite.get(), stdErrWrite.get());
stdErrWrite.reset();
stdOutWrite.reset();
const std::map<std::string_view, bool> patterns = {
{"listening on", true},
{"Address already in use", false},
};
bool success = false;
bool finished = false;
DWORD writeOffset = 0;
constexpr DWORD readOffset = 0;
std::string output(512, '\0');
while (!finished)
{
DWORD bytesRead = 0;
if (!ReadFile(stdErrRead.get(), output.data() + writeOffset, static_cast<DWORD>(output.size() - writeOffset), &bytesRead, nullptr))
{
break;
}
writeOffset += bytesRead;
LogInfo("output %hs", output.c_str());
std::string_view outputView = output;
for (const auto& pattern : patterns)
{
DWORD patternOffset = readOffset;
auto matchString = pattern.first;
while (!finished && (patternOffset + matchString.length() < writeOffset))
{
if (outputView.substr(patternOffset).starts_with(matchString))
{
finished = true;
success = pattern.second;
}
patternOffset++;
}
}
}
VERIFY_IS_TRUE(finished);
return std::tuple(std::move(process), success, std::move(stdOutRead));
}
static std::tuple<unique_kill_process, wil::unique_handle> BindGuestPort(std::wstring_view BindSpec, bool ExpectSuccess)
{
auto [process, success, read] = BindGuestPortHelper(BindSpec);
VERIFY_ARE_EQUAL(ExpectSuccess, success);
return std::tuple(std::move(process), std::move(read));
}
// Bind port 0 in the guest and return the process handle and the kernel-assigned port.
// Uses socat's -dd output to extract the actual port from the "listening on" line.
static std::tuple<unique_kill_process, uint16_t> BindGuestPortZero(bool Ipv6 = false)
{
auto [stdErrRead, stdErrWrite] = CreateSubprocessPipe(false, true);
const std::wstring protocol = Ipv6 ? L"TCP6-LISTEN:0" : L"TCP4-LISTEN:0";
const std::wstring wslCmd = L"socat -dd " + protocol + L" STDOUT";
auto cmd = LxssGenerateWslCommandLine(wslCmd.data());
auto process = LxsstuStartProcess(cmd.data(), nullptr, nullptr, stdErrWrite.get());
stdErrWrite.reset();
// Parse the assigned port from socat's debug output.
// socat -dd prints a line like: "... listening on AF=2 0.0.0.0:PORT"
std::string output(512, '\0');
DWORD writeOffset = 0;
uint16_t assignedPort = 0;
bool found = false;
while (!found)
{
// Grow the buffer if full to avoid zero-byte reads and infinite loops.
if (writeOffset == output.size())
{
output.resize(output.size() * 2);
}
DWORD bytesRead = 0;
if (!ReadFile(stdErrRead.get(), output.data() + writeOffset, static_cast<DWORD>(output.size() - writeOffset), &bytesRead, nullptr))
{
break;
}
if (bytesRead == 0)
{
break;
}
writeOffset += bytesRead;
LogInfo("output %hs", output.c_str());
std::string_view outputView(output.data(), writeOffset);
auto pos = outputView.find("listening on");
if (pos != std::string_view::npos)
{
// Limit the search to just the "listening on" line to avoid
// matching colons in subsequent debug lines socat may emit.
auto lineEnd = outputView.find('\n', pos);
auto line = outputView.substr(pos, lineEnd != std::string_view::npos ? lineEnd - pos : std::string_view::npos);
// Find the last ':' before the port digits. For IPv6, socat outputs
// "listening on AF=10 :::PORT", so using find() would match the
// first colon in the address instead of the port separator.
auto colonPos = line.rfind(':');
if (colonPos != std::string_view::npos)
{
auto portStr = line.substr(colonPos + 1);
auto end = portStr.find_first_not_of("0123456789");
if (end != std::string_view::npos)
{
portStr = portStr.substr(0, end);
}
if (portStr.empty())
{
continue;
}
assignedPort = static_cast<uint16_t>(std::stoi(std::string(portStr)));
found = true;
}
}
}
VERIFY_IS_TRUE(found);
VERIFY_IS_TRUE(assignedPort > 0);
LogInfo("Port-0 bind resolved to port %u", assignedPort);
return {std::move(process), assignedPort};
}
static void VerifyPortZeroBindIsTracked(bool verifyRelease = true)
{
// Make sure the VM doesn't time out while we wait for async port resolution
WslKeepAlive keepAlive;
// Bind port 0 in the guest - the kernel assigns an ephemeral port.
// The port tracker intercepts the bind() via seccomp and defers lookup
// to a background thread that resolves the actual port via getsockname().
auto [guestProcess, assignedPort] = BindGuestPortZero();
// The port-0 resolution is asynchronous (deferred to a background thread).
// Retry until the host port tracker registers the port, blocking the host bind.
VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
[&assignedPort]() {
wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
THROW_LAST_ERROR_IF(!sock);
SOCKADDR_IN addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(assignedPort);
THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) != SOCKET_ERROR);
},
std::chrono::seconds(1),
std::chrono::seconds(30)));
if (!verifyRelease)
{
return;
}
// Kill the guest process so the port tracker releases the port.
guestProcess.reset();
// Retry until the host can bind the port again, confirming it was released.
VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
[&assignedPort]() {
wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
THROW_LAST_ERROR_IF(!sock);
SOCKADDR_IN addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(assignedPort);
THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
},
std::chrono::seconds(1),
std::chrono::minutes(2)));
}
template <typename T>
static void VerifyNotBound(T& Address, int AddressFamily, int Protocol)
{
const wil::unique_socket listenSocket(socket(AddressFamily, (Protocol == IPPROTO_TCP) ? SOCK_STREAM : SOCK_DGRAM, Protocol));
VERIFY_IS_TRUE(!!listenSocket);
const auto timeout = std::chrono::steady_clock::now() + std::chrono::minutes(2);
bool bound = false;
while (!bound && std::chrono::steady_clock::now() < timeout)
{
bound = bind(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&Address), sizeof(Address)) != SOCKET_ERROR;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
VERIFY_IS_TRUE(bound);
}
static void VerifyNotBoundLoopback(uint16_t port, bool Ipv6)
{
if (Ipv6)
{
SOCKADDR_IN6 Address{};
Address.sin6_family = AF_INET6;
Address.sin6_port = htons(port);
Address.sin6_addr = IN6ADDR_LOOPBACK_INIT;
VerifyNotBound(Address, Address.sin6_family, IPPROTO_TCP);
}
else
{
SOCKADDR_IN Address{};
Address.sin_family = AF_INET;
Address.sin_port = htons(port);
Address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
VerifyNotBound(Address, Address.sin_family, IPPROTO_TCP);
}
}
struct
{
wchar_t const* const SocatServer = {};
bool const Ipv6 = false;
bool const expectRelay = true;
} LoopbackBindTests[5] = {
{
.SocatServer = L"TCP4-LISTEN:1234,bind=127.0.0.1",
},
{
.SocatServer = L"TCP4-LISTEN:1234,bind=127.0.0.2",
.expectRelay = false,
},
{
.SocatServer = L"TCP4-LISTEN:1234,bind=0.0.0.0",
},
{
.SocatServer = L"TCP6-LISTEN:1234,bind=::1",
.Ipv6 = true,
},
{
.SocatServer = L"TCP6-LISTEN:1234,bind=::",
.Ipv6 = true,
},
};
void NatGuestPortIsReleased()
{
constexpr uint16_t port = 1234;
for (auto const& test : LoopbackBindTests)
{
{
auto guestProcess = BindGuestPort(test.SocatServer, true);
std::this_thread::sleep_for(std::chrono::seconds(3));
BindHostPort(port, SOCK_STREAM, IPPROTO_TCP, !test.expectRelay, test.Ipv6, true);
}
VerifyNotBoundLoopback(port, test.Ipv6);
}
}
void NatHostPortCantBeBoundByGuest()
{
constexpr uint16_t port = 1234;
for (auto const& test : LoopbackBindTests)
{
{
auto hostPort = BindHostPort(port, SOCK_STREAM, IPPROTO_TCP, true, test.Ipv6, true);
BindGuestPort(test.SocatServer, !test.expectRelay);
}
VerifyNotBoundLoopback(port, test.Ipv6);
}
}
static void NatReusePortOnGuest()
{
constexpr uint16_t port = 1234;
{
auto [guestLocal, write] = BindGuestPort(L"TCP4-LISTEN:1234,bind=127.0.0.1,reuseport", true);
BindHostPort(port, SOCK_STREAM, IPPROTO_TCP, false, false, true);
auto guestWild = BindGuestPort(L"TCP4-LISTEN:1234,bind=0.0.0.0,reuseport", true);
BindHostPort(port, SOCK_STREAM, IPPROTO_TCP, false, false, true);
guestLocal.reset();
BindHostPort(port, SOCK_STREAM, IPPROTO_TCP, false, false, true);
}
VerifyNotBoundLoopback(port, false);
}
static void ValidateLocalhostRelayTraffic(ADDRESS_FAMILY addressFamily)
{
THROW_HR_IF(E_INVALIDARG, addressFamily != AF_INET && addressFamily != AF_INET6);
// Bind a port in the guest.
auto [guestProcess, read] =
BindGuestPort(addressFamily == AF_INET6 ? L"TCP6-LISTEN:1234,bind=::1" : L"TCP4-LISTEN:1234,bind=127.0.0.1", true);
// Connect to the port via the localhost relay
wil::unique_socket hostSocket;
SOCKADDR_INET addr{};
addr.si_family = addressFamily;
INETADDR_SETLOOPBACK((PSOCKADDR)&addr);
SS_PORT(&addr) = htons(1234);
auto pred = [&]() {
hostSocket.reset(socket(addressFamily, SOCK_STREAM, IPPROTO_TCP));
THROW_HR_IF(E_ABORT, !hostSocket);
THROW_HR_IF(E_FAIL, connect(hostSocket.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
};
try
{
wsl::shared::retry::RetryWithTimeout<void>(pred, std::chrono::seconds(1), std::chrono::minutes(1));
}
catch (...)
{
LogError("Timed out trying to connect to relay, 0x%x", wil::ResultFromCaughtException());
VERIFY_FAIL();
}
// Send data from host to guest.
constexpr auto buffer = "test-relay-buffer";
VERIFY_ARE_EQUAL(send(hostSocket.get(), buffer, static_cast<int>(strlen(buffer)), 0), strlen(buffer));
{
// Validate that the guest received the correct data.
std::string content(strlen(buffer), '\0');
DWORD totalRead{};
while (totalRead < content.size())
{
DWORD bytesRead{};
VERIFY_IS_TRUE(ReadFile(read.get(), content.data() + totalRead, static_cast<DWORD>(content.size()) - totalRead, &bytesRead, nullptr));
LogInfo("Read %lu bytes", bytesRead);
totalRead += bytesRead;
}
VERIFY_ARE_EQUAL(content, buffer);
}
}
TEST_METHOD(NatLocalhostRelay)
{
WSL2_TEST_ONLY();
WslKeepAlive keepAlive;
ValidateLocalhostRelayTraffic(AF_INET);
ValidateLocalhostRelayTraffic(AF_INET6);
}
TEST_METHOD(NatLocalhostRelayNoIpv6)
{
WSL2_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.kernelCommandLine = L"ipv6.disable=1"}));
WslKeepAlive keepAlive;
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -f /proc/net/tcp6"), 1L);
ValidateLocalhostRelayTraffic(AF_INET);
}
static void TestNonRootNamespaceEphemeralBind()
{
// Get the forwarding state.
auto [oldIpForwardState, _1] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv4/ip_forward", 0);
std::wstring restoreIpForwardCommand = std::format(L"sysctl -w net.ipv4.ip_forward={}", oldIpForwardState.c_str());
// Ensure the ephemeral port range configured in the non-root networking namespace does not
// overlap with the ephemeral port range in the root networking namespace (use the 300 ports
// preceding the root networking namespace ephemeral port range).
auto [start, _2] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv4/ip_local_port_range | cut -f1", 0);
start.pop_back();
int ephemeralRangeStart = std::stoi(start);
int ephemeralRangeEnd = ephemeralRangeStart - 1;
ephemeralRangeStart = ephemeralRangeEnd - 299;
VERIFY_IS_GREATER_THAN(ephemeralRangeStart, 1024);
VERIFY_IS_LESS_THAN_OR_EQUAL(ephemeralRangeEnd, UINT16_MAX);
const std::wstring ephemeralRangeCommand =
std::format(L"ip netns exec testns sysctl -w net.ipv4.ip_local_port_range=\"{} {}\"", ephemeralRangeStart, ephemeralRangeEnd);
// Clean up the below configurations.
auto revertConfig = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&restoreIpForwardCommand] {
LxsstuLaunchWsl(restoreIpForwardCommand.c_str());
LxsstuLaunchWsl(L"--system --user root nft flush chain nat POSTROUTING");
LxsstuLaunchWsl(L"ip link delete veth-test-br");
LxsstuLaunchWsl(L"ip link delete testbridge");
LxsstuLaunchWsl(L"ip netns delete testns");
});
// Set up a networking namespace and provide it external network access via a bridge, veth
// pair, SRCNAT iptables rule and forwarding.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip netns add testns"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(ephemeralRangeCommand.c_str()), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link add testbridge type bridge"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link add veth-test type veth peer name veth-test-br"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set veth-test netns testns"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set veth-test-br master testbridge"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip -n testns link set veth-test up"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set veth-test-br up"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set testbridge up"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip -n testns addr add 192.168.15.2/24 dev veth-test"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip addr add 192.168.15.1/24 dev testbridge"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip -n testns route add default via 192.168.15.1 dev veth-test"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft add table nat"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft \"add chain nat POSTROUTING { type nat hook postrouting priority srcnat; }\""), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft add rule nat POSTROUTING ip saddr 192.168.15.0/24 oif != testbridge masquerade"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sysctl -w net.ipv4.ip_forward=1"), 0);
// Verify we have connectivity from the networking namespace when using ephemeral port selection.
auto [output, warnings] =
LxsstuLaunchWslAndCaptureOutput(L"ip netns exec testns socat -dd tcp-connect:bing.com:80 create:/tmp/nonexistent", 1);
LogInfo("output %s", output.c_str());
LogInfo("warnings %s", warnings.c_str());
VERIFY_ARE_NOT_EQUAL(warnings.find(L"starting data transfer loop"), std::string::npos);
}
TEST_METHOD(NatNonRootNamespaceEphemeralBind)
{
WSL2_TEST_ONLY();
// Because the test creates a new network namespace, the resolv.conf from the root network namespace
// is copied in the resolv.conf of the new network namespace. The DNS tunneling listener running in the root namespace
// needs to be accessible from the new namespace, so it can't use a 127* IP.
WslConfigChange config(LxssGenerateTestConfig({
.guiApplications = true,
.dnsTunneling = true,
.dnsTunnelingIpAddress = L"10.255.255.254",
}));
// Configure the root namespace ephemeral port range so we can guarantee a valid,
// non-overlapping ephemeral port range in the non-root namespace using the very simple port
// range selection logic in TestNonRootNamespaceEphemeralBind.
auto [originalRange, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv4/ip_local_port_range", 0);
std::wstring restoreEphemeralPortRangeCommand =
std::format(L"sysctl -w net.ipv4.ip_local_port_range=\"{}\"", originalRange.c_str());
auto revertEphemeralPortRange = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&restoreEphemeralPortRangeCommand] {
LxsstuLaunchWsl(restoreEphemeralPortRangeCommand.c_str());
});
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sysctl -w net.ipv4.ip_local_port_range=\"60400 60700\""), 0);
TestNonRootNamespaceEphemeralBind();
}
enum class FirewallObjects
{
Required,
NotRequired
};
static void ValidateInitialFirewallState(FirewallObjects expectHyperVFirewallObjects)
{
// Verify that we have an initially working connection.
// This also ensures that WSL is started to allow for
// validating the initial Hyper-V port state
GuestClient(L"tcp-connect:bing.com:80");
if (expectHyperVFirewallObjects == FirewallObjects::Required)
{
// Query for Hyper-V objects. At least one Hyper-V port is expected
auto [out, err] = LxsstuLaunchPowershellAndCaptureOutput(L"Get-NetFirewallHyperVPort");
LogInfo("out:[%ls] err:[%ls]", out.c_str(), err.c_str());
VERIFY_IS_TRUE(!out.empty());
}
}
static auto AddFirewallRule(const FirewallRule& rule)
{
try
{
std::wstring cmdPrefix;
if (rule.Type == FirewallType::HyperV)
{
cmdPrefix = L"New-NetFirewallHyperVRule -VmCreatorId " + rule.VmCreatorId + L" -RemotePorts " + rule.RemotePorts;
}
else
{
cmdPrefix = L"New-NetFirewallRule -Protocol TCP -RemotePort " + rule.RemotePorts;
}
auto [out, _] = LxsstuLaunchPowershellAndCaptureOutput(
cmdPrefix + L" -Name " + rule.Name + L" -DisplayName " + rule.Name + L" -Action " + rule.Action +
L" -Direction Outbound");
LogInfo("AddRule output:\r\n%ls", FixLineEndings(out).c_str());
// output what, if any, Hyper-V Firewall rules were created in response to the above
auto [query_output, __] = LxsstuLaunchPowershellAndCaptureOutput(L"Get-NetFirewallHyperVRule -Name " + rule.Name);
LogInfo("Get-NetFirewallHyperVRule output:\r\n%ls", FixLineEndings(query_output).c_str());
}
CATCH_LOG()
return wil::scope_exit([rule]() {
try
{
LogInfo("Removing the test rule %ls\n", rule.Name.c_str());
std::wstring cmdPrefix;
if (rule.Type == FirewallType::HyperV)
{
cmdPrefix = L"Remove-NetFirewallHyperVRule";
}
else
{
cmdPrefix = L"Remove-NetFirewallRule";
}
LxsstuLaunchPowershellAndCaptureOutput(cmdPrefix + L" -Name " + rule.Name);
}
CATCH_LOG()
});
}
enum class FirewallTestConnectivity
{
Allowed,
Blocked
};
static auto AddFirewallRuleAndValidateTraffic(const FirewallRule& rule, FirewallTestConnectivity expectedConnectivityAfterRule)
{
LogInfo(
"Validating ruleType=[%ls] name=[%ls] and expectedConnectivity=[%ls]",
(rule.Type == FirewallType::Host) ? L"Host" : L"HyperV",
rule.Name.c_str(),
expectedConnectivityAfterRule == FirewallTestConnectivity::Allowed ? L"Allowed" : L"Blocked");
// Add rule and verify the connection is allowed/blocked as expected
auto firewallRuleCleanup = AddFirewallRule(rule);
GuestClient(L"tcp-connect:bing.com:80,connect-timeout=5", expectedConnectivityAfterRule);
return firewallRuleCleanup;
}
static auto ConfigureFirewallEnabled(FirewallType firewallType, bool settingValue, std::wstring vmCreatorId = L"")
{
LogInfo(
"Configure FirewallEnabled for Type=[%ls] enabled=[%ls]",
(firewallType == FirewallType::Host) ? L"Host" : L"HyperV",
settingValue ? L"True" : L"False");
try
{
std::wstring prefix;
if (firewallType == FirewallType::HyperV)
{
prefix = L"Set-NetFirewallHyperVProfile -VmCreatorId " + vmCreatorId;
}
else
{
prefix = L"Set-NetFirewallProfile";
}
LxsstuLaunchPowershellAndCaptureOutput(prefix + L" -Profile Public -Enabled " + (settingValue ? L"True" : L"False"));
LxsstuLaunchPowershellAndCaptureOutput(prefix + L" -Profile Private -Enabled " + (settingValue ? L"True" : L"False"));
LxsstuLaunchPowershellAndCaptureOutput(prefix + L" -Profile Domain -Enabled " + (settingValue ? L"True" : L"False"));
}
CATCH_LOG()
return wil::scope_exit([vmCreatorId, firewallType]() {
try
{
std::wstring prefix;
if (firewallType == FirewallType::HyperV)
{
prefix = L"Set-NetFirewallHyperVProfile -VmCreatorId " + vmCreatorId;
}
else
{
prefix = L"Set-NetFirewallProfile";
}
LxsstuLaunchPowershellAndCaptureOutput(prefix + L" -Profile Public -Enabled NotConfigured");
LxsstuLaunchPowershellAndCaptureOutput(prefix + L" -Profile Private -Enabled NotConfigured");
LxsstuLaunchPowershellAndCaptureOutput(prefix + L" -Profile Domain -Enabled NotConfigured");
}
CATCH_LOG()
});
}
static auto ConfigureHyperVFirewallLoopbackEnabled(bool settingValue, std::wstring vmCreatorId)
{
LogInfo("Configuring LoopbackEnabled=[%d]", settingValue);
try
{
LxsstuLaunchPowershellAndCaptureOutput(
L"Set-NetFirewallHyperVVMSetting -VmCreatorId " + vmCreatorId + L" -LoopbackEnabled " + (settingValue ? L"True" : L"False"));
}
CATCH_LOG()
return wil::scope_exit([vmCreatorId]() {
try
{
LxsstuLaunchPowershellAndCaptureOutput(
L"Set-NetFirewallHyperVVMSetting -VmCreatorId " + vmCreatorId + L" -LoopbackEnabled NotConfigured");
}
CATCH_LOG()
});
}
static void FirewallRuleBlockedTests(FirewallTestConnectivity expectedConnectivity)
{
// Adding a block rule should result in traffic being blocked
FirewallRule blockRule = {FirewallType::Host, L"WSLTestBlockRule", c_firewallTrafficTestPort, c_firewallRuleActionBlock};
AddFirewallRuleAndValidateTraffic(blockRule, expectedConnectivity);
// Adding both an allow and block rule should result in traffic being blocked
FirewallRule allowRule = {FirewallType::Host, L"WSLTestAllowRule", c_firewallTrafficTestPort, c_firewallRuleActionAllow};
auto allowRuleCleanup = AddFirewallRuleAndValidateTraffic(allowRule, FirewallTestConnectivity::Allowed);
AddFirewallRuleAndValidateTraffic(blockRule, expectedConnectivity);
allowRuleCleanup.reset();
// Adding a block rule should result in traffic being blocked
FirewallRule hyperVBlockRule = {
FirewallType::HyperV, L"WSLTestBlockRuleHyperV", c_firewallTrafficTestPort, c_firewallRuleActionBlock, c_wslVmCreatorId};
AddFirewallRuleAndValidateTraffic(hyperVBlockRule, expectedConnectivity);
// Adding both an allow and block rule should result in traffic being blocked
FirewallRule hyperVAllowRule = {
FirewallType::HyperV, L"WSLTestAllowRuleHyperV", c_firewallTrafficTestPort, c_firewallRuleActionAllow, c_wslVmCreatorId};
auto hyperVAllowRuleCleanup = AddFirewallRuleAndValidateTraffic(hyperVAllowRule, FirewallTestConnectivity::Allowed);
AddFirewallRuleAndValidateTraffic(hyperVBlockRule, expectedConnectivity);
hyperVAllowRuleCleanup.reset();
// Adding a rule with vm creator 'any' should result in traffic being blocked
FirewallRule anyHyperVBlockRule = {
FirewallType::HyperV, L"WSLTestBlockRuleHyperVAny", c_firewallTrafficTestPort, c_firewallRuleActionBlock, c_wslVmCreatorId};
AddFirewallRuleAndValidateTraffic(hyperVBlockRule, expectedConnectivity);
}
TEST_METHOD(NatFirewallRulesExpectedBlock)
{
HYPERV_FIREWALL_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.firewall = true}));
ValidateInitialFirewallState(FirewallObjects::Required);
FirewallRuleBlockedTests(FirewallTestConnectivity::Blocked);
}
TEST_METHOD(NatFirewallRulesExpectedBlockFirewallDisabled)
{
HYPERV_FIREWALL_TEST_ONLY();
SKIP_TEST_UNSTABLE();
WslConfigChange config(LxssGenerateTestConfig({.firewall = false}));
ValidateInitialFirewallState(FirewallObjects::NotRequired);
FirewallRuleBlockedTests(FirewallTestConnectivity::Allowed);
}
TEST_METHOD(NatFirewallRulesExpectedBlockFirewallDisabledByPolicy)
{
HYPERV_FIREWALL_TEST_ONLY();
RegistryKeyChange<DWORD> change(
HKEY_LOCAL_MACHINE, wsl::windows::policies::c_registryKey, wsl::windows::policies::c_allowCustomFirewallUserSetting, 0);
// the user tries to disable Hyper-V FW in the config file, but the admin disabled user control
WslConfigChange config(LxssGenerateTestConfig({.firewall = false}));
ValidateInitialFirewallState(FirewallObjects::NotRequired);
FirewallRuleBlockedTests(FirewallTestConnectivity::Blocked);
}
static void FirewallRuleAllowedTests(FirewallTestConnectivity expectedConnectivity)
{
// A host rule with different IP address should not affect traffic
FirewallRule differentIPRule = {FirewallType::Host, L"WSLTestDifferentIPRule", c_firewallTestOtherPort, c_firewallRuleActionBlock};
AddFirewallRuleAndValidateTraffic(differentIPRule, expectedConnectivity);
// A host rule with action allow should not affect traffic
FirewallRule allowRule = {FirewallType::Host, L"WSLTestAllowRule", c_firewallTrafficTestPort, c_firewallRuleActionAllow};
AddFirewallRuleAndValidateTraffic(allowRule, expectedConnectivity);
// A hyperv- rule with a different VM creator ID should not affect this traffic
FirewallRule differentVmCreatorRule = {
FirewallType::HyperV, L"WSLTestDifferentVMCreatorIdRule", c_firewallTrafficTestPort, c_firewallRuleActionBlock, c_wsaVmCreatorId};
AddFirewallRuleAndValidateTraffic(differentVmCreatorRule, expectedConnectivity);
// A hyper-v rule with a different IP address should not affect this traffic
FirewallRule differentIPHyperVRule = {
FirewallType::HyperV, L"WSLTestDifferentIPRuleHyperV", c_firewallTestOtherPort, c_firewallRuleActionBlock, c_wslVmCreatorId};
AddFirewallRuleAndValidateTraffic(differentIPHyperVRule, expectedConnectivity);
// A hyper-v rule with action allow should not affect traffic
FirewallRule allowHyperVRule = {
FirewallType::HyperV, L"WSLTestAllowRuleHyperV", c_firewallTrafficTestPort, c_firewallRuleActionAllow, c_wslVmCreatorId};
AddFirewallRuleAndValidateTraffic(allowHyperVRule, expectedConnectivity);
}
TEST_METHOD(NatFirewallRulesExpectedAllow)
{
HYPERV_FIREWALL_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.firewall = true}));
ValidateInitialFirewallState(FirewallObjects::Required);
FirewallRuleAllowedTests(FirewallTestConnectivity::Allowed);
}
TEST_METHOD(NatFirewallRulesExpectedAllowFirewallDisabled)
{
HYPERV_FIREWALL_TEST_ONLY();
SKIP_TEST_UNSTABLE();
WslConfigChange config(LxssGenerateTestConfig({.firewall = false}));
ValidateInitialFirewallState(FirewallObjects::NotRequired);
FirewallRuleAllowedTests(FirewallTestConnectivity::Allowed);
}
static void FirewallSettingEnabledTests(bool isHyperVFirewallEnabled)
{
// Configure Firewall disabled
auto hostDisabledCleanup = ConfigureFirewallEnabled(FirewallType::Host, false);
// Add host block rule, which is expected to be enforced
FirewallRule blockRule = {FirewallType::Host, L"WSLTestBlockRule", c_firewallTrafficTestPort, c_firewallRuleActionBlock, c_wslVmCreatorId};
AddFirewallRuleAndValidateTraffic(blockRule, FirewallTestConnectivity::Allowed);
blockRule.Type = FirewallType::HyperV;
// Add hyper-v block rule, which is expected to be enforced
AddFirewallRuleAndValidateTraffic(blockRule, FirewallTestConnectivity::Allowed);
hostDisabledCleanup.reset();
// Configure Hyper-V firewall disabled
auto hyperVDisabledCleanup = ConfigureFirewallEnabled(FirewallType::HyperV, false, c_wslVmCreatorId);
// Add host block rule, which is expected to be enforced
blockRule.Type = FirewallType::Host;
AddFirewallRuleAndValidateTraffic(blockRule, FirewallTestConnectivity::Allowed);
// Add hyper-v block rule, which is expected to be enforced
blockRule.Type = FirewallType::HyperV;
AddFirewallRuleAndValidateTraffic(blockRule, FirewallTestConnectivity::Allowed);
hyperVDisabledCleanup.reset();
// host rules are propagated only if Hyper-V Firewall is enabled
// Configure conflicting policy for host and hyper-v (hyper-v policy takes precedence)
auto conflictingHostEnabledCleanup = ConfigureFirewallEnabled(FirewallType::Host, true);
// Add host block rule, which is expected to be enforced
blockRule.Type = FirewallType::Host;
AddFirewallRuleAndValidateTraffic(
blockRule, isHyperVFirewallEnabled ? FirewallTestConnectivity::Blocked : FirewallTestConnectivity::Allowed);
// Add hyper-v block rule, which is expected to be enforced
blockRule.Type = FirewallType::HyperV;
AddFirewallRuleAndValidateTraffic(
blockRule, isHyperVFirewallEnabled ? FirewallTestConnectivity::Blocked : FirewallTestConnectivity::Allowed);
// Configure hyper-v disabled
auto conflictingHyperVDisabledCleanup = ConfigureFirewallEnabled(FirewallType::HyperV, false, c_wslVmCreatorId);
// Add host block rule, which is expected to be NOT enforced (firewall is disabled)
blockRule.Type = FirewallType::Host;
AddFirewallRuleAndValidateTraffic(blockRule, FirewallTestConnectivity::Allowed);
// Add hyper-v block rule, which is expected to be NOT enforced (firewall is disabled)
blockRule.Type = FirewallType::HyperV;
AddFirewallRuleAndValidateTraffic(blockRule, FirewallTestConnectivity::Allowed);
conflictingHostEnabledCleanup.reset();
conflictingHyperVDisabledCleanup.reset();
// Configure conflicting policy for host and hyper-v (hyper-v policy takes precedence)
auto conflictingHyperVEnabledCleanup = ConfigureFirewallEnabled(FirewallType::HyperV, true, c_wslVmCreatorId);
// Add host block rule, which is expected to be enforced
blockRule.Type = FirewallType::Host;
AddFirewallRuleAndValidateTraffic(
blockRule, isHyperVFirewallEnabled ? FirewallTestConnectivity::Blocked : FirewallTestConnectivity::Allowed);
// Add hyper-v block rule, which is expected to be enforced
blockRule.Type = FirewallType::HyperV;
AddFirewallRuleAndValidateTraffic(
blockRule, isHyperVFirewallEnabled ? FirewallTestConnectivity::Blocked : FirewallTestConnectivity::Allowed);
// Configure host firewall disabled. Hyper-V firewall is still expected to be enforced, but host firewall rules will not be
auto conflictingHostDisabledCleanup = ConfigureFirewallEnabled(FirewallType::Host, false);
// Add host block rule, which is NOT expected to be enforced (host firewall disabled)
blockRule.Type = FirewallType::Host;
AddFirewallRuleAndValidateTraffic(blockRule, FirewallTestConnectivity::Allowed);
// Add hyper-v block rule, which is expected to be enforced (hyper-v firewall still enabled)
blockRule.Type = FirewallType::HyperV;
AddFirewallRuleAndValidateTraffic(
blockRule, isHyperVFirewallEnabled ? FirewallTestConnectivity::Blocked : FirewallTestConnectivity::Allowed);
}
TEST_METHOD(NatFirewallRulesEnabledSetting)
{
HYPERV_FIREWALL_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.firewall = true}));
ValidateInitialFirewallState(FirewallObjects::Required);
FirewallSettingEnabledTests(true);
}
TEST_METHOD(NatFirewallRulesEnabledSettingFirewallDisabled)
{
HYPERV_FIREWALL_TEST_ONLY();
SKIP_TEST_UNSTABLE();
WslConfigChange config(LxssGenerateTestConfig({.firewall = false}));
ValidateInitialFirewallState(FirewallObjects::NotRequired);
FirewallSettingEnabledTests(false);
}
/* Network Tests Helper Methods */
static GUID QueryAdapterId()
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(
L"readlink /sys/class/net/eth0 | grep -o -E '[[:xdigit:]]{8}(-[[:xdigit:]]{4}){3}-[[:xdigit:]]{12}'", 0);
out.pop_back();
const auto guid = wsl::shared::string::ToGuid(out);
VERIFY_IS_TRUE(guid.has_value());
return guid.value();
}
static void RunGns(const std::string& input, const std::optional<GUID>& adapter = {}, const std::optional<LX_MESSAGE_TYPE>& messageType = {}, int expectedErrorCode = 0)
{
constexpr auto InheritOnReadHandle = true;
constexpr auto DoNotEnableInheritOnWriteHandle = false;
SECURITY_ATTRIBUTES attributes = {sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
auto [read, write] =
CreateSubprocessPipe(InheritOnReadHandle, DoNotEnableInheritOnWriteHandle, static_cast<DWORD>(input.size()), &attributes);
THROW_IF_WIN32_BOOL_FALSE(WriteFile(write.get(), input.data(), static_cast<DWORD>(input.size()), nullptr, nullptr));
write.reset();
LogInfo("GNS Input: '%S'", input.c_str());
const auto adapterArg =
adapter.has_value() ? L"--adapter " + wsl::shared::string::GuidToString<wchar_t>(adapter.value()) + std::wstring(L" ") : L"";
const auto messageTypeArg =
messageType.has_value() ? L"--msg_type " + std::to_wstring(static_cast<int>(messageType.value())) + std::wstring(L" ") : L"";
LxsstuLaunchWslAndCaptureOutput(L"/gns " + adapterArg + messageTypeArg, expectedErrorCode, read.get());
}
template <typename T>
void RunGns(T& input, ModifyRequestType action, GuestEndpointResourceType type)
{
ModifyGuestEndpointSettingRequest<T> request;
request.RequestType = action;
request.ResourceType = type;
request.Settings = input;
RunGns(wsl::shared::ToJson(request), AdapterId, LxGnsMessageNotification);
}
template <typename T>
void RunGns(T& input, const LX_MESSAGE_TYPE messageType)
{
RunGns(wsl::shared::ToJson(input), AdapterId, messageType);
}
template <typename T>
void SendDeviceSettingsRequest(std::wstring targetDevice, T& input, ModifyRequestType action, GuestEndpointResourceType type)
{
wsl::shared::hns::ModifyGuestEndpointSettingRequest<T> request;
request.targetDeviceName = targetDevice;
request.RequestType = action;
request.ResourceType = type;
request.Settings = input;
RunGns(request, LxGnsMessageDeviceSettingRequest);
}
// Convert Unix line endings (\n) to Windows line endings (\r\n) for proper console display
static std::wstring FixLineEndings(const std::wstring& input)
{
std::wstring output;
for (size_t i = 0; i < input.length(); ++i)
{
if (input[i] == L'\n')
{
output += L"\r\n";
}
else if (input[i] != L'\r')
{
output += input[i];
}
}
return output;
}
static RoutingTableState GetRoutingTableState(std::wstring& out, std::wregex& defaultRoutePattern, std::wregex& routePattern)
{
RoutingTableState state;
std::wsmatch match;
std::wistringstream input(out);
std::wstring line;
while (std::getline(input, line) && !line.empty())
{
if (std::regex_search(line, match, defaultRoutePattern) && match.size() >= 3)
{
if (state.DefaultRoute.has_value())
{
continue;
}
state.DefaultRoute = {{match.str(1), match.str(2), {}, match.size() > 4 && match[4].matched ? std::stoi(match.str(4)) : 0}};
}
else if (std::regex_search(line, match, routePattern) && match.size() >= 4)
{
state.Routes.emplace_back(Route{
match.str(2), match.str(3), {match.str(1)}, match.size() > 5 && match[5].matched ? std::stoi(match.str(5)) : 0});
}
}
return state;
}
static RoutingTableState GetIpv4RoutingTableState()
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"ip route show");
LogInfo("Ip route output:\r\n%ls", FixLineEndings(out).c_str());
std::wregex defaultRoutePattern(L"default via ([0-9,.]+) dev ([a-zA-Z0-9]*) *(metric ([0-9]+))?");
std::wregex routePattern(L"([0-9,.,/]+) via ([0-9,.]+) dev ([a-zA-Z0-9]*) *(metric ([0-9]+))?");
return GetRoutingTableState(out, defaultRoutePattern, routePattern);
}
static void WaitForIpv6DefaultRoute()
{
const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(30);
while (std::chrono::steady_clock::now() < timeout)
{
auto state = GetIpv6RoutingTableState();
if (state.DefaultRoute.has_value())
{
return;
}
LogInfo("Waiting for IPv6 default route...");
std::this_thread::sleep_for(std::chrono::seconds(1));
}
VERIFY_FAIL(L"Timed out waiting for IPv6 default route");
}
static RoutingTableState GetIpv6RoutingTableState()
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"ip -6 route show");
LogInfo("Ip -6 route output:\r\n%ls", FixLineEndings(out).c_str());
RoutingTableState state;
std::wregex defaultRoutePattern(L"default via ([a-f,A-F,0-9,:]+) dev ([a-zA-Z0-9]*) *(metric ([0-9]+))?");
std::wregex routePattern(L"([a-f,A-F,0-9,:,/]+) via ([a-f,A-F,0-9,:]+) dev ([a-zA-Z0-9]*) *(metric ([0-9]+))?");
return GetRoutingTableState(out, defaultRoutePattern, routePattern);
}
static InterfaceState GetInterfaceState(const std::wstring& name, const std::wstring& expectedWarnings = L"")
{
// Sample output from "ip addr show":
// 4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
// link/ether 00:12:34:56:78:9A brd ff:ff:ff:ff:ff:ff
// inet 172.17.123.249/20 brd 172.17.127.255 scope global eth0
// valid_lft forever preferred_lft forever
// inet6 2001::1:2:3:4/64 scope global
// valid_lft forever preferred_lft 0sec
auto [out, warnings] = LxsstuLaunchWslAndCaptureOutput(L"ip addr show " + name);
LogInfo("ip addr show output:\r\n%ls", FixLineEndings(out).c_str());
if (expectedWarnings.empty())
{
VERIFY_IS_TRUE(warnings.empty());
}
else
{
if (!PathMatchSpec(warnings.c_str(), expectedWarnings.c_str()))
{
LogError("Warning '%ls' didn't match pattern '%ls'", warnings.c_str(), expectedWarnings.c_str());
VERIFY_FAIL();
}
}
std::wistringstream input(out);
std::wstring line;
InterfaceState state = {name};
// Drop first two lines
VERIFY_IS_TRUE(std::getline(input, line).good());
VERIFY_IS_TRUE(std::getline(input, line).good());
// Read the address lines
while (std::getline(input, line).good())
{
std::wregex v4Pattern(L"inet ([0-9,.]+)\\/([0-9]+) brd ([0-9,.]+) scope global .*" + name);
std::wregex v6Pattern(L"inet6 ([a-f,A-F,0-9,:]+)\\/([0-9]+) scope global");
std::wregex v4LocalPattern(L"inet 169.254.([0-9,.]+)\\/([0-9]+) brd 169.254.255.255 scope link");
std::wregex v6LocalPattern(L"inet6 ([a-f,A-F,0-9,:]+)\\/([0-9]+) scope link");
std::wregex v4LoopbackPattern(L"inet 127.0.0.1/8 scope host");
std::wregex v6LoopbackPattern(L"inet6 ::1/128 scope host");
std::wregex deprecatedPattern(L"deprecated");
std::wsmatch match, preferredStateMatch;
if (std::regex_search(line, match, v4Pattern) && match.size() == 4)
{
bool preferred = !std::regex_search(line, preferredStateMatch, deprecatedPattern);
state.V4Addresses.emplace_back(IpAddress{match.str(1), (uint8_t)std::stoul(match.str(2)), preferred});
}
else if (std::regex_search(line, match, v6Pattern) && match.size() == 3)
{
bool preferred = !std::regex_search(line, preferredStateMatch, deprecatedPattern);
state.V6Addresses.emplace_back(IpAddress{match.str(1), (uint8_t)std::stoul(match.str(2)), preferred});
}
else if (std::regex_search(line, match, v4LocalPattern) && match.size() == 3)
{
LogInfo("Skipping ipv4 link local address");
}
else if (std::regex_search(line, match, v6LocalPattern) && match.size() == 3)
{
LogInfo("Skipping ipv6 link local address");
}
else if (std::regex_search(line, match, v4LoopbackPattern) && match.size() == 1)
{
LogInfo("Skipping ipv4 loopback");
}
else if (std::regex_search(line, match, v6LoopbackPattern) && match.size() == 1)
{
LogInfo("Skipping ipv6 loopback");
}
else
{
LogInfo("Ip addr output:\r\n%ls", FixLineEndings(out).c_str());
LogInfo("Current line: \"%ls\"", line.c_str());
VERIFY_FAIL(L"Failed to extract interface state");
}
// Skip the lifetimes line
VERIFY_IS_TRUE(std::getline(input, line).good());
}
out = LxsstuLaunchWslAndCaptureOutput(L"cat /sys/class/net/" + name + L"/operstate").first;
state.Up = false;
if (out == L"up\n")
{
state.Up = true;
}
else if ((out != L"down\n") && ((name.substr(0, 4).compare(L"wlan") != 0) && (name != L"lo")))
{
LogInfo("Unexpected operstate: '%s'", out.c_str());
VERIFY_FAIL();
}
out = LxsstuLaunchWslAndCaptureOutput(L"cat /sys/class/net/" + name + L"/mtu").first;
state.Mtu = std::stoi(out);
auto routingTableState = GetIpv4RoutingTableState();
if (routingTableState.DefaultRoute.has_value())
{
state.Gateway = routingTableState.DefaultRoute->Via;
}
auto v6RoutingTableState = GetIpv6RoutingTableState();
if (v6RoutingTableState.DefaultRoute.has_value())
{
state.V6Gateway = v6RoutingTableState.DefaultRoute->Via;
}
return state;
}
static std::vector<InterfaceState> GetAllInterfaceStates()
{
// Result output is a list of interface names with newline as the delimiter
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"ip -brief link show | awk -F '[@ ]' '{print $1}'");
LogInfo("parsed ip link output:\r\n%ls", FixLineEndings(out).c_str());
std::wistringstream input(out);
std::vector<InterfaceState> interfaceStates;
std::wstring line;
while (std::getline(input, line).good())
{
interfaceStates.push_back(GetInterfaceState(line));
}
return interfaceStates;
}
void TestCase(const std::vector<InterfaceState>& interfaceStates)
{
WSL2_TEST_ONLY();
for (const auto& state : interfaceStates)
{
if (state.Rename)
{
wsl::shared::hns::HNSEndpoint endpoint;
endpoint.ID = AdapterId;
endpoint.PortFriendlyName = state.Name;
RunGns(wsl::shared::ToJson(endpoint));
}
// Remove existing addresses not in goal state
auto currentInterfaceState = GetInterfaceState(state.Name);
for (auto it = currentInterfaceState.V4Addresses.begin(); it != currentInterfaceState.V4Addresses.end(); ++it)
{
if (std::find(state.V4Addresses.begin(), state.V4Addresses.end(), *it) == state.V4Addresses.end())
{
wsl::shared::hns::IPAddress address;
address.Address = it->Address;
address.OnLinkPrefixLength = it->PrefixLength;
address.Family = AF_INET;
SendDeviceSettingsRequest(state.Name, address, ModifyRequestType::Remove, GuestEndpointResourceType::IPAddress);
}
}
for (auto it = currentInterfaceState.V6Addresses.begin(); it != currentInterfaceState.V6Addresses.end(); ++it)
{
if (std::find(state.V4Addresses.begin(), state.V4Addresses.end(), *it) == state.V4Addresses.end())
{
wsl::shared::hns::IPAddress address;
address.Address = it->Address;
address.OnLinkPrefixLength = it->PrefixLength;
address.Family = AF_INET6;
SendDeviceSettingsRequest(state.Name, address, ModifyRequestType::Remove, GuestEndpointResourceType::IPAddress);
}
}
// Add or update addresses
for (auto it = state.V4Addresses.begin(); it != state.V4Addresses.end(); ++it)
{
wsl::shared::hns::IPAddress address;
address.Address = it->Address;
address.OnLinkPrefixLength = it->PrefixLength;
address.Family = AF_INET;
address.PreferredLifetime = 0xFFFFFFFF;
bool updateAddress =
(std::find(currentInterfaceState.V4Addresses.begin(), currentInterfaceState.V4Addresses.end(), *it) !=
currentInterfaceState.V4Addresses.end());
SendDeviceSettingsRequest(
state.Name, address, updateAddress ? ModifyRequestType::Update : ModifyRequestType::Add, GuestEndpointResourceType::IPAddress);
Route prefixRoute{LX_INIT_UNSPECIFIED_ADDRESS, L"eth0", it->GetPrefix()};
if (!RouteExists(prefixRoute))
{
// Add the prefix route for the newly added/updated address
wsl::shared::hns::Route route;
route.NextHop = prefixRoute.Via;
route.DestinationPrefix = prefixRoute.Prefix.value();
route.Family = AF_INET;
SendDeviceSettingsRequest(state.Name, route, ModifyRequestType::Add, GuestEndpointResourceType::Route);
}
}
for (auto it = state.V6Addresses.begin(); it != state.V6Addresses.end(); ++it)
{
wsl::shared::hns::IPAddress address;
address.Address = it->Address;
address.OnLinkPrefixLength = it->PrefixLength;
address.Family = AF_INET6;
address.PreferredLifetime = 0xFFFFFFFF;
bool updateAddress =
(std::find(currentInterfaceState.V6Addresses.begin(), currentInterfaceState.V6Addresses.end(), *it) !=
currentInterfaceState.V6Addresses.end());
SendDeviceSettingsRequest(
state.Name, address, updateAddress ? ModifyRequestType::Update : ModifyRequestType::Add, GuestEndpointResourceType::IPAddress);
Route prefixRoute{LX_INIT_UNSPECIFIED_V6_ADDRESS, L"eth0", it->GetPrefix()};
if (!RouteExists(prefixRoute))
{
// Add the prefix route for the newly added/updated address
wsl::shared::hns::Route route;
route.NextHop = prefixRoute.Via;
route.DestinationPrefix = prefixRoute.Prefix.value();
route.Family = AF_INET6;
SendDeviceSettingsRequest(state.Name, route, ModifyRequestType::Add, GuestEndpointResourceType::Route);
}
}
if (state.Gateway.has_value())
{
wsl::shared::hns::Route route;
route.NextHop = state.Gateway.value();
route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX;
route.Family = AF_INET;
bool updateGw = currentInterfaceState.Gateway.has_value();
SendDeviceSettingsRequest(
state.Name, route, updateGw ? ModifyRequestType::Update : ModifyRequestType::Add, GuestEndpointResourceType::Route);
}
if (state.V6Gateway.has_value())
{
wsl::shared::hns::Route route;
route.NextHop = state.V6Gateway.value();
route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_V6_PREFIX;
route.Family = AF_INET6;
bool updateGw = currentInterfaceState.V6Gateway.has_value();
SendDeviceSettingsRequest(
state.Name, route, updateGw ? ModifyRequestType::Update : ModifyRequestType::Add, GuestEndpointResourceType::Route);
}
}
// Validate that the addresses and routes are in the final goal state
const auto& expectedInterfaceState = interfaceStates.back();
auto interfaceState = GetInterfaceState(expectedInterfaceState.Name);
for (auto it = expectedInterfaceState.V4Addresses.begin(); it != expectedInterfaceState.V4Addresses.end(); ++it)
{
VERIFY_IS_TRUE(
std::find(interfaceState.V4Addresses.begin(), interfaceState.V4Addresses.end(), *it) != interfaceState.V4Addresses.end());
}
if (expectedInterfaceState.Gateway.has_value())
{
VERIFY_ARE_EQUAL(expectedInterfaceState.Gateway, interfaceState.Gateway);
}
for (auto it = expectedInterfaceState.V6Addresses.begin(); it != expectedInterfaceState.V6Addresses.end(); ++it)
{
VERIFY_IS_TRUE(
std::find(interfaceState.V6Addresses.begin(), interfaceState.V6Addresses.end(), *it) != interfaceState.V6Addresses.end());
}
if (expectedInterfaceState.V6Gateway.has_value())
{
VERIFY_ARE_EQUAL(expectedInterfaceState.V6Gateway, interfaceState.V6Gateway);
}
}
static bool RouteExists(const Route& route)
{
auto v4State = GetIpv4RoutingTableState();
if (std::find(v4State.Routes.begin(), v4State.Routes.end(), route) != v4State.Routes.end())
{
return true;
}
auto v6State = GetIpv6RoutingTableState();
return std::find(v6State.Routes.begin(), v6State.Routes.end(), route) != v6State.Routes.end();
}
// Reads from the file until the substring is found, a timeout is reached, or ReadFile returns an error
// Returns true on success, false otherwise
static bool FindSubstring(wil::unique_handle& file, const std::string& substr, std::string& output)
{
char buffer[256];
DWORD bytesRead;
const HANDLE readFileThread = OpenThread(THREAD_ALL_ACCESS, false, GetCurrentThreadId());
const wil::unique_handle event(CreateEvent(nullptr, FALSE, FALSE, nullptr));
VERIFY_ARE_NOT_EQUAL(event.get(), INVALID_HANDLE_VALUE);
// ReadFile will block, so cancel the syscall if it is taking too long
const auto watchdogThread = std::async(std::launch::async, [&] {
if (WaitForSingleObject(event.get(), 30000) == WAIT_TIMEOUT)
{
LogInfo("Canceling synchronous IO", GetTickCount());
CancelSynchronousIo(readFileThread);
}
});
do
{
if (!ReadFile(file.get(), buffer, sizeof(buffer) - 1, &bytesRead, nullptr))
{
LogInfo("ReadFile failed with %d", GetLastError());
break;
}
buffer[bytesRead] = '\0';
output += std::string(buffer);
if (output.find(substr) != std::string::npos)
{
break;
}
} while (true);
SetEvent(event.get());
watchdogThread.wait();
// Convert narrow string output to wide string for logging, and fix line endings
std::wstring wideOutput;
wideOutput.reserve(output.length());
for (char c : output)
{
if (c == '\n')
{
wideOutput += L'\r';
wideOutput += L'\n';
}
else if (c != '\r')
{
wideOutput += static_cast<wchar_t>(static_cast<unsigned char>(c));
}
}
LogInfo("output=\r\n%ls", wideOutput.c_str());
return (output.find(substr) != std::string::npos);
}
static std::wstring CreateSocatString(const SOCKADDR_INET& si, int protocol, bool listen)
{
return std::wstring(((protocol == IPPROTO_TCP) ? L"TCP" : L"UDP")) + std::wstring(((si.si_family == AF_INET) ? L"4" : L"6")) +
std::wstring(L"-") + std::wstring((listen) ? L"LISTEN:" : ((IPPROTO_TCP) ? L"CONNECT:" : L"SENDTO:")) +
std::wstring(
(listen) ? std::to_wstring(ntohs(SS_PORT(&si))) + std::wstring(L",bind=") +
wsl::windows::common::string::SockAddrInetToWstring(si)
: wsl::windows::common::string::SockAddrInetToWstring(si) + std::wstring(L":") +
std::to_wstring(ntohs(SS_PORT(&si))));
}
struct GuestListener
{
GuestListener(const SOCKADDR_INET& addr, int protocol)
{
THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &writePipe, nullptr, 0));
THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(writePipe.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
const auto wslCmd = L"socat -dd " + CreateSocatString(addr, protocol, true) + L" STDOUT";
auto cmd = LxssGenerateWslCommandLine(wslCmd.data());
process = unique_kill_process(LxsstuStartProcess(cmd.data(), nullptr, nullptr, writePipe.get()));
writePipe.reset();
std::string output;
THROW_HR_IF(E_FAIL, !NetworkTests::FindSubstring(readPipe, "listening on", output));
}
// Start a listener in a different network namespace
GuestListener(const SOCKADDR_INET& addr, int protocol, const std::wstring& namespaceName)
{
THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &writePipe, nullptr, 0));
THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(writePipe.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
const auto wslCmd =
L"ip netns exec " + namespaceName + L" socat -dd " + CreateSocatString(addr, protocol, true) + L" STDOUT";
auto cmd = LxssGenerateWslCommandLine(wslCmd.data());
process = unique_kill_process(LxsstuStartProcess(cmd.data(), nullptr, nullptr, writePipe.get()));
writePipe.reset();
std::string output;
THROW_HR_IF(E_FAIL, !NetworkTests::FindSubstring(readPipe, "listening on", output));
}
void AcceptConnection()
{
std::string output;
VERIFY_IS_TRUE(NetworkTests::FindSubstring(readPipe, "starting data transfer loop", output));
}
wil::unique_handle dmesgFile;
unique_kill_process dmesg;
unique_kill_process process;
wil::unique_handle readPipe;
wil::unique_handle writePipe;
};
struct GuestClient
{
GuestClient(const SOCKADDR_INET& addr, int protocol) : GuestClient(CreateSocatString(addr, protocol, false))
{
}
GuestClient(const std::wstring& socatString, FirewallTestConnectivity expectedSuccess = FirewallTestConnectivity::Allowed)
{
const auto expectSuccess = expectedSuccess == FirewallTestConnectivity::Allowed;
const auto wslCmd = L"echo A | socat -dd " + socatString + L" STDIN";
auto cmd = LxssGenerateWslCommandLine(wslCmd.data());
const auto* connectionString = expectSuccess ? "starting data transfer loop" : "Connection timed out";
bool valueFound = false;
for (int i = 0; i < 3; ++i)
{
wil::unique_handle readPipe;
wil::unique_handle writePipe;
THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &writePipe, nullptr, 0));
THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(writePipe.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
unique_kill_process process = unique_kill_process(LxsstuStartProcess(cmd.data(), nullptr, nullptr, writePipe.get()));
writePipe.reset();
std::string output;
valueFound = FindSubstring(readPipe, connectionString, output);
if (expectSuccess && !valueFound && (output.find("Temporary failure") != std::string::npos))
{
LogWarning("Temporary failure - retrying up to 3 times");
continue;
}
break;
}
VERIFY_IS_TRUE(valueFound, (expectSuccess) ? "Verifying connection succeeded" : "Verifying connection failed");
}
};
static std::wstring GetGelNicDeviceName()
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"ip route get from 127.0.0.1 127.0.0.1 | awk 'FNR <= 1 {print $7}'");
out.pop_back();
return out;
}
static bool HostHasInternetConnectivity(ADDRESS_FAMILY family)
{
using ABI::Windows::Foundation::Collections::IVectorView;
using ABI::Windows::Networking::Connectivity::ConnectionProfile;
using ABI::Windows::Networking::Connectivity::INetworkAdapter;
using ABI::Windows::Networking::Connectivity::INetworkInformationStatics;
using ABI::Windows::Networking::Connectivity::NetworkConnectivityLevel;
// Get adapter addresses info.
const auto adapterAddresses = GetAdapterAddresses(family);
// Get connection profile info.
const auto roInit = wil::RoInitialize();
const auto networkInformationStatics =
wil::GetActivationFactory<INetworkInformationStatics>(RuntimeClass_Windows_Networking_Connectivity_NetworkInformation);
THROW_HR_IF_NULL_MSG(E_OUTOFMEMORY, networkInformationStatics.get(), "null INetworkInformationStatics");
wil::com_ptr<IVectorView<ConnectionProfile*>> connectionList;
THROW_IF_FAILED(networkInformationStatics->GetConnectionProfiles(&connectionList));
// If we find a connection profile marked as having internet access and the associated
// adapter has a <family> unicast address and a <family> default gateway, then conclude the
// host has <family> internet connectivity.
for (const auto& connectionProfile : wil::get_range(connectionList.get()))
{
NetworkConnectivityLevel connectivityLevel{};
CONTINUE_IF_FAILED(connectionProfile->GetNetworkConnectivityLevel(&connectivityLevel));
if (connectivityLevel != NetworkConnectivityLevel::NetworkConnectivityLevel_InternetAccess)
{
continue;
}
wil::com_ptr<INetworkAdapter> networkAdapter;
CONTINUE_IF_FAILED(connectionProfile->get_NetworkAdapter(&networkAdapter));
GUID interfaceGuid{};
CONTINUE_IF_FAILED(networkAdapter->get_NetworkAdapterId(&interfaceGuid));
NET_LUID interfaceLuid{};
CONTINUE_IF_FAILED_WIN32(ConvertInterfaceGuidToLuid(&interfaceGuid, &interfaceLuid));
for (auto* adapter = reinterpret_cast<const IP_ADAPTER_ADDRESSES*>(adapterAddresses.data()); adapter != nullptr;
adapter = adapter->Next)
{
if (interfaceLuid.Value == adapter->Luid.Value && adapter->FirstUnicastAddress != nullptr && adapter->FirstGatewayAddress != nullptr)
{
return true;
}
}
}
return false;
}
static bool HostHasIpv6DnsServers()
{
ULONG bufferSize = 0;
constexpr ULONG flags = GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS;
std::vector<BYTE> buffer;
ULONG result = GetAdaptersAddresses(AF_INET6, flags, nullptr, nullptr, &bufferSize);
while (result == ERROR_BUFFER_OVERFLOW)
{
buffer.resize(bufferSize);
result = GetAdaptersAddresses(AF_INET6, flags, nullptr, reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data()), &bufferSize);
}
if (result != NO_ERROR)
{
return false;
}
DWORD bestIndex = 0;
SOCKADDR_IN6 dest{};
dest.sin6_family = AF_INET6;
InetPtonW(AF_INET6, L"2001:4860:4860::8888", &dest.sin6_addr);
if (GetBestInterfaceEx(reinterpret_cast<SOCKADDR*>(&dest), &bestIndex) != NO_ERROR)
{
return false;
}
for (auto* adapter = reinterpret_cast<const IP_ADAPTER_ADDRESSES*>(buffer.data()); adapter != nullptr; adapter = adapter->Next)
{
if (adapter->IfIndex != bestIndex)
{
continue;
}
for (auto* dns = adapter->FirstDnsServerAddress; dns != nullptr; dns = dns->Next)
{
if (dns->Address.lpSockaddr->sa_family == AF_INET6)
{
return true;
}
}
}
return false;
}
static std::vector<BYTE> GetAdapterAddresses(ADDRESS_FAMILY family)
{
constexpr ULONG flags =
(GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_GATEWAYS);
ULONG bufferSize = 0;
std::vector<BYTE> buffer;
ULONG result = GetAdaptersAddresses(family, flags, nullptr, nullptr, &bufferSize);
while (result == ERROR_BUFFER_OVERFLOW)
{
buffer.resize(bufferSize);
result = GetAdaptersAddresses(family, flags, nullptr, reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.data()), &bufferSize);
}
VERIFY_WIN32_SUCCEEDED(result);
return buffer;
}
static void WaitForNATStateInLinux()
{
Stopwatch<std::chrono::seconds> Watchdog(std::chrono::seconds(30));
// NAT only supports IPv4 connectivity
// wait for the host to have v4 connectivity
do
{
if (HostHasInternetConnectivity(AF_INET))
{
break;
}
LogInfo("Waiting for Windows network connectivity...");
} while (Sleep(1000), !Watchdog.IsExpired());
VERIFY_IS_FALSE(Watchdog.IsExpired());
// reset the watchdog
Watchdog = Stopwatch{std::chrono::seconds(30)};
do
{
// Count how many interfaces have v4 connectivity, as defined by having a gateway and at least 1 preferred address.
int interfacesWithV4Connectivity = 0;
// Get all interface info from the VM.
for (const auto& i : GetAllInterfaceStates())
{
if (i.Gateway.has_value())
{
for (const auto& j : i.V4Addresses)
{
if (j.Preferred)
{
interfacesWithV4Connectivity++;
break;
}
}
}
}
// Consider mirroring to be complete if we have the same v4 connectivity in the VM as the host.
if (interfacesWithV4Connectivity > 0)
{
break;
}
LogInfo("Waiting for NAT state...");
} while (Sleep(1000), !Watchdog.IsExpired());
VERIFY_IS_FALSE(Watchdog.IsExpired());
}
TEST_METHOD(ConnectivityCheckTestNATDefaultSuccess)
{
WSL2_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig());
WaitForNATStateInLinux();
const auto coInit = wil::CoInitializeEx();
const wil::com_ptr<INetworkListManager> networkListManager = wil::CoCreateInstance<NetworkListManager, INetworkListManager>();
VERIFY_IS_NOT_NULL(networkListManager.get());
NLM_CONNECTIVITY hostConnectivity{};
VERIFY_SUCCEEDED(networkListManager->GetConnectivity(&hostConnectivity));
// Windows
const wsl::shared::conncheck::ConnCheckResult hostResult =
wsl::shared::conncheck::CheckConnection("www.msftconnecttest.com", "ipv6.msftconnecttest.com", "80");
if (hostConnectivity & NLM_CONNECTIVITY_IPV4_INTERNET)
{
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::Success, hostResult.Ipv4Status);
}
else
{
// one of the 2 expected runtime failures
VERIFY_IS_TRUE(
hostResult.Ipv4Status == wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo ||
hostResult.Ipv4Status == wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect);
}
if (hostConnectivity & NLM_CONNECTIVITY_IPV6_INTERNET)
{
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::Success, hostResult.Ipv4Status);
}
else
{
// one of the 2 expected runtime failures (sometimes v6 name resolution will fail, depending on the configuration)
VERIFY_IS_TRUE(
hostResult.Ipv6Status == wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo ||
hostResult.Ipv6Status == wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect);
}
// www.msftconnecttest.com will always fail IPv6 name resolution - it doesn't have any AAAA records registered for it
const int expectedErrorCode = static_cast<int>(hostResult.Ipv4Status) |
(static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo) << 16);
LogInfo("RunGns(www.msftconnecttest.com, 0x%x)", expectedErrorCode);
// TODO: pass 'expectedErrorCode' instead of 1, once the pipeline is fixed from running Init back to wsl.exe
// it returns 1 (static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::Success)
// as that's the lowest 16 bit value (unknown where the upper 16 bits are trimmed)
// if ManualConnectivityValidation is set true, one can confirm from the stdout captured that the correct result was determined and returned by init.
constexpr auto testErrorCode =
ManualConnectivityValidation ? expectedErrorCode : static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::Success);
RunGns("www.msftconnecttest.com", AdapterId, LxGnsMessageConnectTestRequest, testErrorCode);
}
TEST_METHOD(ConnectivityCheckTestNATNameResolutionFailure)
{
WSL2_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig());
WaitForNATStateInLinux();
// Windows
const wsl::shared::conncheck::ConnCheckResult result =
wsl::shared::conncheck::CheckConnection("asdlkfadsf.bbcxzncvb", nullptr, "80");
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo, result.Ipv4Status);
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo, result.Ipv6Status);
constexpr int expectedErrorCode = static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo) |
(static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo) << 16);
LogInfo("RunGns(asdlkfadsf.bbcxzncvb, 0x%x)", expectedErrorCode);
// TODO: pass 'expectedErrorCode' instead of 1, once the pipeline is fixed from running Init back to wsl.exe
// it returns 2 (static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo))
// as that's the lowest 16 bit value (unknown where the upper 16 bits are trimmed)
// if temporarily change this back to expectedErrorCode, one can confirm from the stdout captured that the correct result was determined and returned by init.
constexpr auto testErrorCode = ManualConnectivityValidation
? expectedErrorCode
: static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo);
RunGns("asdlkfadsf.bbcxzncvb", AdapterId, LxGnsMessageConnectTestRequest, testErrorCode);
}
TEST_METHOD(ConnectivityCheckTestNATNameResolvesButConnectivityFails)
{
WSL2_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig());
WaitForNATStateInLinux();
const auto* ncsiDnsOnlyName = "dns.msftncsi.com";
// v4 and v6 should succeed to resolve the name, but fail to connect,
// as this NCSI name is registered in global DNS, but there's not HTTP endpoint for it
// Windows
const wsl::shared::conncheck::ConnCheckResult result =
wsl::shared::conncheck::CheckConnection(ncsiDnsOnlyName, nullptr, "80");
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect, result.Ipv4Status);
// v6 name resolution might fail, depending on the configuration
VERIFY_IS_TRUE(
(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo == result.Ipv6Status) ||
(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect == result.Ipv6Status));
constexpr int expectedErrorCode = static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect) |
(static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect) << 16);
LogInfo("RunGns(%hs, 0x%x)", ncsiDnsOnlyName, expectedErrorCode);
// TODO: pass 'expectedErrorCode' instead of 1, once the pipeline is fixed from running Init back to wsl.exe
// it returns 4 (static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect))
// as that's the lowest 16 bit value (unknown where the upper 16 bits are trimmed)
// if ManualConnectivityValidation is set true, one can confirm from the stdout captured that the correct result was determined and returned by init.
constexpr auto testErrorCode = ManualConnectivityValidation
? expectedErrorCode
: static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect);
RunGns(ncsiDnsOnlyName, AdapterId, LxGnsMessageConnectTestRequest, testErrorCode);
}
};
class MirroredTests
{
WSL_TEST_CLASS(MirroredTests)
std::optional<WslConfigChange> m_config;
GUID AdapterId;
TEST_CLASS_SETUP(TestClassSetup)
{
VERIFY_ARE_EQUAL(LxsstuInitialize(false), TRUE);
if (LxsstuVmMode())
{
m_config.emplace(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
AdapterId = NetworkTests::QueryAdapterId();
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ln -f -s /init /gns"), (DWORD)0);
}
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
m_config.reset();
VERIFY_NO_THROW(LxsstuUninitialize(false));
return true;
}
TEST_METHOD(DnsTunneling)
{
DNS_TUNNELING_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = true}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyDnsTunneling(c_dnsTunnelingDefaultIp);
}
TEST_METHOD(DnsTunnelingWithSpecificIp)
{
DNS_TUNNELING_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig(
{.networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = true, .dnsTunnelingIpAddress = L"10.255.255.1"}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyDnsTunneling(L"10.255.255.1");
}
TEST_METHOD(DnsTunnelingVerifySuffixes)
{
DNS_TUNNELING_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = true}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyDnsSuffixes();
}
TEST_METHOD(WithoutTunnelingVerifySuffixes)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = false}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyDnsSuffixes();
}
TEST_METHOD(HttpProxyVerifyConfigDisabled)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = false}));
WaitForMirroredStateInLinux();
auto restoreProxySettings = wil::scope_exit([&] { NetworkTests::ClearHttpProxySettings(true); });
NetworkTests::SetHttpProxySettings(NetworkTests::c_httpProxyString, L"", L"", true);
NetworkTests::VerifyHttpProxyEnvVariables(L"", L"", L"");
}
TEST_METHOD(HttpProxySimple)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyHttpProxySimple();
}
TEST_METHOD(HttpProxySimpleMachineScope)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
// verify with machine scope
NetworkTests::VerifyHttpProxySimple(false);
}
TEST_METHOD(NoHttpProxyConfigured)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyNoHttpProxyConfigured();
}
TEST_METHOD(HttpProxyWithBypassesConfigured)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyHttpProxyWithBypassesConfigured();
}
TEST_METHOD(HttpProxyChange)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyHttpProxyChange();
}
TEST_METHOD(HttpProxyAndWslEnv)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyHttpProxyAndWslEnv();
}
TEST_METHOD(HttpProxyFilterByNetworkConfiguration)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
NetworkTests::VerifyHttpProxyFilterByNetworkConfigurationMirrored();
}
TEST_METHOD(SmokeTest)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Verify that we have a working connection
NetworkTests::GuestClient(L"tcp-connect:bing.com:80");
}
TEST_METHOD(InternetConnectivityV4)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
if (!NetworkTests::HostHasInternetConnectivity(AF_INET))
{
LogSkipped("Host does not have IPv4 internet connectivity. Skipping...");
return;
}
NetworkTests::GuestClient(L"tcp4-connect:bing.com:80");
}
TEST_METHOD(InternetConnectivityV6)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
if (!NetworkTests::HostHasInternetConnectivity(AF_INET6))
{
LogSkipped("Host does not have IPv6 internet connectivity. Skipping...");
return;
}
NetworkTests::GuestClient(L"tcp6-connect:bing.com:80");
}
TEST_METHOD(LoopbackLocal)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .hostAddressLoopback = true}));
WaitForMirroredStateInLinux();
std::vector<NetworkTests::InterfaceState> interfaceStates = NetworkTests::GetAllInterfaceStates();
// Verify loopback connectivity on assigned unicast addresses
for (auto i = interfaceStates.begin(); i != interfaceStates.end(); ++i)
{
for (auto j = i->V4Addresses.begin(); j != i->V4Addresses.end(); ++j)
{
// The IP used for DNS tunneling is not intended for guest<->host communication
if (j->Address != c_dnsTunnelingDefaultIp)
{
NetworkTests::VerifyLoopbackConnectivity(j->Address);
}
}
for (auto j = i->V6Addresses.begin(); j != i->V6Addresses.end(); ++j)
{
// TODO: enable when v6 loopback is supported
// VerifyLoopbackConnectivity(j->Address);
}
}
}
TEST_METHOD(LoopbackExplicit)
{
// TODO: re-enable once OS build 29555 loopback regression is resolved.
SKIP_TEST_UNSTABLE();
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Verify loopback connectivity on loopback addresses
NetworkTests::VerifyLoopbackConnectivity(L"127.0.0.1");
// TODO: enable when v6 loopback is supported
// VerifyLoopbackConnectivity(L"::1");
}
TEST_METHOD(LoopbackSystemd)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Write a .conf file to conflict with loopback settings.
#define CONFIG_FILE_PATH L"/etc/sysctl.d/MirroredLoopbackSystemd.conf"
auto revertConfigFile = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] {
const std::wstring deleteConfigFileCmd(L"-u root -e rm " CONFIG_FILE_PATH);
LxsstuLaunchWsl(deleteConfigFileCmd.data());
});
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"echo \"net.ipv4.conf.*.rp_filter=2\" > " CONFIG_FILE_PATH), static_cast<DWORD>(0));
// Enable systemd which will apply the .conf file.
auto revertSystemd = EnableSystemd();
// Verify the settings configured in the systemd hardening logic.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sysctl net.ipv4.conf.all.rp_filter | grep -w 0"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sysctl net.ipv4.conf." TEXT(LX_INIT_LOOPBACK_DEVICE_NAME) L".rp_filter | grep -w 0"), 0);
// Verify an E2E loopback scenario.
NetworkTests::VerifyLoopbackGuestToHost(L"127.0.0.1", IPPROTO_TCP);
}
TEST_METHOD(GuestPortCantBeBoundByHost)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
{
auto guestProcess = NetworkTests::BindGuestPort(L"TCP4-LISTEN:1234", true);
NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false);
}
{
auto guestProcess = NetworkTests::BindGuestPort(L"UDP4-LISTEN:1234", true);
NetworkTests::BindHostPort(1234, SOCK_DGRAM, IPPROTO_UDP, false);
}
}
TEST_METHOD(GuestPortIsReleased)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Make sure the VM doesn't time out
WslKeepAlive keepAlive;
{
auto guestProcess = NetworkTests::BindGuestPort(L"TCP4-LISTEN:1234", true);
NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false);
}
const wil::unique_socket listenSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
VERIFY_IS_TRUE(!!listenSocket);
SOCKADDR_IN Address{};
Address.sin_family = AF_INET;
Address.sin_port = htons(1234);
const auto timeout = std::chrono::steady_clock::now() + std::chrono::minutes(2);
bool bound = false;
while (!bound && std::chrono::steady_clock::now() < timeout)
{
bound = bind(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&Address), sizeof(Address)) != SOCKET_ERROR;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
VERIFY_IS_TRUE(bound);
}
TEST_METHOD(HostPortCantBeBoundByGuest)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
{
auto hostPort = NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, true);
NetworkTests::BindGuestPort(L"TCP4-LISTEN:1234", false);
}
{
auto hostPort = NetworkTests::BindHostPort(1234, SOCK_DGRAM, IPPROTO_UDP, true);
NetworkTests::BindGuestPort(L"UDP4-LISTEN:1234", false);
}
}
TEST_METHOD(UdpBindDoesNotPreventTcpBind)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
auto tcpPort = NetworkTests::BindGuestPort(L"TCP4-LISTEN:1234", true);
auto udpPort = NetworkTests::BindGuestPort(L"UDP4-LISTEN:1234", true);
}
TEST_METHOD(HostUdpBindDoesNotPreventGuestTcpBind)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
auto udpPort = NetworkTests::BindHostPort(2345, SOCK_DGRAM, IPPROTO_UDP, true);
auto tcpPort = NetworkTests::BindGuestPort(L"TCP4-LISTEN:2345", true);
}
TEST_METHOD(MultipleGuestBindOnSameTuple)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
auto bind1 = NetworkTests::BindGuestPort(L"TCP4-LISTEN:1234,bind=127.0.0.1", true);
{
auto bind2 = NetworkTests::BindGuestPort(L"TCP6-LISTEN:1234,bind=::1", true);
// Allow time for this second bind to be viewed as "in use" by the init port tracker
// before closing the socket. If the socket is closed before the init port tracker sees
// that the port allocation was in use, then the init port tracker will hold onto the
// allocation for a considerable amount of time (through the duration of this test case)
// before releasing it.
std::this_thread::sleep_for(std::chrono::seconds(3));
}
// Allow time for the init port tracker to detect the second port allocation as no longer in
// use and perform its cleanup of the second port allocation.
const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (std::chrono::steady_clock::now() < timeout)
{
// {TCP, 1234} should still be reserved for the guest from the first bind.
auto hostPort = NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
TEST_METHOD(EphemeralBind)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
auto tcpPort = NetworkTests::BindGuestPort(L"TCP4-LISTEN:0", true);
auto udpPort = NetworkTests::BindGuestPort(L"UDP4-LISTEN:0", true);
}
TEST_METHOD(PortZeroBindIsTracked)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Skip port-release verification in mirrored mode. The host reserves a contiguous
// ephemeral port range via HcnReserveGuestNetworkServicePortRange that no Windows
// process can bind for the lifetime of the VM. Port-0 binds resolve to ports within
// this range, so even after the guest releases the port the host still cannot bind
// it — the range-level reservation remains, making release unverifiable.
NetworkTests::VerifyPortZeroBindIsTracked(false);
}
TEST_METHOD(ExplicitEphemeralBind)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Get ephemeral port range
auto [start, err1] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv4/ip_local_port_range | cut -f1", 0);
start.pop_back();
const auto ephemeralRangeStart = std::stoi(start);
auto [end, err2] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv4/ip_local_port_range | cut -f2", 0);
end.pop_back();
const auto ephemeralRangeEnd = std::stoi(end);
// Walk the ephemeral port range and verify we can bind to at least one port (some might be already taken, but the test
// assumes there should be at least one free).
bool canBindTcp = false;
bool canBindUdp = false;
for (int port = ephemeralRangeStart; port <= ephemeralRangeEnd; port++)
{
auto [tcpListener, tcpSuccess, read] = NetworkTests::BindGuestPortHelper(L"TCP4-LISTEN:" + std::to_wstring(port));
if (tcpSuccess)
{
canBindTcp = true;
break;
}
}
for (int port = ephemeralRangeStart; port <= ephemeralRangeEnd; port++)
{
auto [udpListener, udpSuccess, read] = NetworkTests::BindGuestPortHelper(L"UDP4-LISTEN:" + std::to_wstring(port));
if (udpSuccess)
{
canBindUdp = true;
break;
}
}
VERIFY_IS_TRUE(canBindTcp);
VERIFY_IS_TRUE(canBindUdp);
}
TEST_METHOD(NonRootNamespaceEphemeralBind)
{
MIRRORED_NETWORKING_TEST_ONLY();
// Because the test creates a new network namespace, the resolv.conf from the root network namespace
// is copied in the resolv.conf of the new network namespace. The DNS tunneling listener running in the root namespace
// needs to be accessible from the new namespace, so it can't use a 127* IP
m_config->Update(LxssGenerateTestConfig(
{.guiApplications = true, .networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = true, .dnsTunnelingIpAddress = L"10.255.255.254"}));
WaitForMirroredStateInLinux();
NetworkTests::TestNonRootNamespaceEphemeralBind();
}
// Verifies that in mirrored mode, Windows can connect to a listener running in a Linux network namespace different from
// the Linux root network namespace.
TEST_METHOD(PortForwardingToNonRootNamespace)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig(
{.guiApplications = true, .networkingMode = wsl::core::NetworkingMode::Mirrored, .hostAddressLoopback = true}));
WaitForMirroredStateInLinux();
// We list the IPv4 addresses mirrored in Linux and use the first one we find in the test
std::vector<NetworkTests::InterfaceState> interfaceStates = NetworkTests::GetAllInterfaceStates();
std::wstring ipAddress;
for (auto i = interfaceStates.begin(); i != interfaceStates.end(); ++i)
{
for (auto j = i->V4Addresses.begin(); j != i->V4Addresses.end(); ++j)
{
// The IP used for DNS tunneling is not intended for guest<->host communication
if (j->Address != c_dnsTunnelingDefaultIp)
{
ipAddress = j->Address;
break;
}
}
}
// Get the forwarding state.
auto [oldIpForwardState, _1] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv4/ip_forward", 0);
std::wstring restoreIpForwardCommand = std::format(L"sysctl -w net.ipv4.ip_forward={}", oldIpForwardState.c_str());
// Clean up the below configurations.
auto revertConfig = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&restoreIpForwardCommand] {
LxsstuLaunchWsl(restoreIpForwardCommand.c_str());
LxsstuLaunchWsl(L"--system --user root nft flush chain nat POSTROUTING");
LxsstuLaunchWsl(L"--system --user root nft flush chain nat PREROUTING");
LxsstuLaunchWsl(L"ip link delete veth-test-br");
LxsstuLaunchWsl(L"ip link delete testbridge");
LxsstuLaunchWsl(L"ip netns delete testns");
});
// Set up a networking namespace and provide it external network access via a bridge, veth
// pair, SRCNAT iptables rule and forwarding.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip netns add testns"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link add testbridge type bridge"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link add veth-test type veth peer name veth-test-br"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set veth-test netns testns"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set veth-test-br master testbridge"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip -n testns link set veth-test up"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set veth-test-br up"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set testbridge up"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip -n testns addr add 192.168.15.2/24 dev veth-test"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip addr add 192.168.15.1/24 dev testbridge"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip -n testns route add default via 192.168.15.1 dev veth-test"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft add table nat"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft \"add chain nat POSTROUTING { type nat hook postrouting priority srcnat; }\""), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft add rule nat POSTROUTING ip saddr 192.168.15.0/24 oif != testbridge masquerade"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sysctl -w net.ipv4.ip_forward=1"), 0);
// Add rule for port forwarding traffic with destination port 8080 to port 80 in the new namespace
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft \"add chain nat PREROUTING { type nat hook prerouting priority dstnat; }\""), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft add rule nat PREROUTING tcp dport 8080 dnat to 192.168.15.2:80"), 0);
// Start listeners in root namespace on port 8080 and new namespace on port 80
SOCKADDR_INET rootListenerAddr = wsl::windows::common::string::StringToSockAddrInet(L"0.0.0.0");
SS_PORT(&rootListenerAddr) = htons(8080);
NetworkTests::GuestListener rootListener(rootListenerAddr, IPPROTO_TCP);
SOCKADDR_INET namespaceListenerAddr = wsl::windows::common::string::StringToSockAddrInet(L"0.0.0.0");
SS_PORT(&namespaceListenerAddr) = htons(80);
NetworkTests::GuestListener namespaceListener(namespaceListenerAddr, IPPROTO_TCP, L"testns");
// Verify Windows can connect to port 8080
SOCKADDR_INET serverAddr = wsl::windows::common::string::StringToSockAddrInet(ipAddress);
SS_PORT(&serverAddr) = htons(8080);
wil::unique_socket clientSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
VERIFY_ARE_NOT_EQUAL(clientSocket.get(), INVALID_SOCKET);
VERIFY_ARE_EQUAL(connect(clientSocket.get(), reinterpret_cast<SOCKADDR*>(&serverAddr), sizeof(serverAddr)), 0);
}
TEST_METHOD(LinuxNonRootNamespaceConnectToWindowsHost)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig(
{.guiApplications = true, .networkingMode = wsl::core::NetworkingMode::Mirrored, .hostAddressLoopback = true}));
WaitForMirroredStateInLinux();
// We list the IPv4 addresses mirrored in Linux and use the first one we find in the test
std::vector<NetworkTests::InterfaceState> interfaceStates = NetworkTests::GetAllInterfaceStates();
std::wstring ipAddress;
for (auto i = interfaceStates.begin(); i != interfaceStates.end(); ++i)
{
for (auto j = i->V4Addresses.begin(); j != i->V4Addresses.end(); ++j)
{
// The IP used for DNS tunneling is not intended for guest<->host communication
if (j->Address != c_dnsTunnelingDefaultIp)
{
ipAddress = j->Address;
break;
}
}
}
// Get the forwarding state.
auto [oldIpForwardState, _1] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv4/ip_forward", 0);
std::wstring restoreIpForwardCommand = std::format(L"sysctl -w net.ipv4.ip_forward={}", oldIpForwardState.c_str());
// Clean up the below configurations.
auto revertConfig = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&restoreIpForwardCommand] {
LxsstuLaunchWsl(restoreIpForwardCommand.c_str());
LxsstuLaunchWsl(L"--system --user root nft flush chain nat POSTROUTING");
LxsstuLaunchWsl(L"ip link delete veth-test-br");
LxsstuLaunchWsl(L"ip link delete testbridge");
LxsstuLaunchWsl(L"ip netns delete testns");
});
// Set up a networking namespace and provide it external network access via a bridge, veth
// pair, SRCNAT iptables rule and forwarding.
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip netns add testns"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link add testbridge type bridge"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link add veth-test type veth peer name veth-test-br"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set veth-test netns testns"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set veth-test-br master testbridge"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip -n testns link set veth-test up"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set veth-test-br up"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip link set testbridge up"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip -n testns addr add 192.168.15.2/24 dev veth-test"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip addr add 192.168.15.1/24 dev testbridge"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"ip -n testns route add default via 192.168.15.1 dev veth-test"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft add table nat"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft \"add chain nat POSTROUTING { type nat hook postrouting priority srcnat; }\""), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system --user root nft add rule nat POSTROUTING ip saddr 192.168.15.0/24 oif != testbridge masquerade"), 0);
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"sysctl -w net.ipv4.ip_forward=1"), 0);
// Create a listener on the Windows host on port 1234
SOCKADDR_INET addr = wsl::windows::common::string::StringToSockAddrInet(ipAddress);
SS_PORT(&addr) = htons(1234);
const wil::unique_socket listenSocket(socket(addr.si_family, SOCK_STREAM, IPPROTO_TCP));
VERIFY_ARE_NOT_EQUAL(listenSocket.get(), INVALID_SOCKET);
VERIFY_ARE_NOT_EQUAL(bind(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)), SOCKET_ERROR);
VERIFY_ARE_NOT_EQUAL(listen(listenSocket.get(), SOMAXCONN), SOCKET_ERROR);
// Verify the new network namespace can connect to the Windows host listener
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(
L"ip netns exec testns socat -dd tcp-connect:" + ipAddress + L":1234 create:/tmp/nonexistent", 1);
LogInfo("output %s", output.c_str());
LogInfo("warnings %s", warnings.c_str());
VERIFY_ARE_NOT_EQUAL(warnings.find(L"starting data transfer loop"), std::string::npos);
}
TEST_METHOD(ResolvConf)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/resolv.conf", 0);
const std::wregex pattern(L"(.|\n)*nameserver [0-9\\. ]+(.|\n)*", std::regex::extended);
VERIFY_IS_TRUE(std::regex_match(out, pattern));
}
TEST_METHOD(NetworkSettings)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
struct NetworkSetting
{
const std::wstring Path;
const std::wstring ExpectedValue;
};
std::vector<NetworkSetting> settings{
{L"/proc/sys/net/ipv6/conf/all/accept_ra", L"0\n"},
{L"/proc/sys/net/ipv6/conf/default/accept_ra", L"0\n"},
{L"/proc/sys/net/ipv6/conf/all/dad_transmits", L"0\n"},
{L"/proc/sys/net/ipv6/conf/default/dad_transmits", L"0\n"},
{L"/proc/sys/net/ipv6/conf/all/autoconf", L"0\n"},
{L"/proc/sys/net/ipv6/conf/default/autoconf", L"0\n"},
{L"/proc/sys/net/ipv6/conf/all/addr_gen_mode", L"1\n"},
{L"/proc/sys/net/ipv6/conf/default/addr_gen_mode", L"1\n"},
{L"/proc/sys/net/ipv6/conf/all/use_tempaddr", L"0\n"},
{L"/proc/sys/net/ipv6/conf/default/use_tempaddr", L"0\n"},
{L"/proc/sys/net/ipv4/conf/all/arp_filter", L"1\n"},
{L"/proc/sys/net/ipv4/conf/all/rp_filter", L"0\n"},
};
settings.push_back({L"/proc/sys/net/ipv4/conf/" + NetworkTests::GetGelNicDeviceName() + L"/rp_filter", L"0\n"});
for (const auto& setting : settings)
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat " + setting.Path);
LogInfo("%ls", (setting.Path + L" : " + out).c_str());
VERIFY_ARE_EQUAL(setting.ExpectedValue, out);
}
}
TEST_METHOD(FirewallRulesExpectedBlock)
{
HYPERV_FIREWALL_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
NetworkTests::ValidateInitialFirewallState(NetworkTests::FirewallObjects::Required);
NetworkTests::FirewallRuleBlockedTests(NetworkTests::FirewallTestConnectivity::Blocked);
}
TEST_METHOD(FirewallRulesExpectedAllow)
{
HYPERV_FIREWALL_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
NetworkTests::ValidateInitialFirewallState(NetworkTests::FirewallObjects::Required);
NetworkTests::FirewallRuleAllowedTests(NetworkTests::FirewallTestConnectivity::Allowed);
}
TEST_METHOD(FirewallRulesEnabledSetting)
{
HYPERV_FIREWALL_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
NetworkTests::ValidateInitialFirewallState(NetworkTests::FirewallObjects::Required);
NetworkTests::FirewallSettingEnabledTests(true);
}
TEST_METHOD(ConnectivityCheckTestDefaultSuccess)
{
WSL2_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
const auto coInit = wil::CoInitializeEx();
const wil::com_ptr<INetworkListManager> networkListManager = wil::CoCreateInstance<NetworkListManager, INetworkListManager>();
VERIFY_IS_NOT_NULL(networkListManager.get());
NLM_CONNECTIVITY hostConnectivity{};
VERIFY_SUCCEEDED(networkListManager->GetConnectivity(&hostConnectivity));
// Windows
const wsl::shared::conncheck::ConnCheckResult hostResult =
wsl::shared::conncheck::CheckConnection("www.msftconnecttest.com", "ipv6.msftconnecttest.com", "80");
if (hostConnectivity & NLM_CONNECTIVITY_IPV4_INTERNET)
{
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::Success, hostResult.Ipv4Status);
}
else
{
// one of the 2 expected runtime failures
VERIFY_IS_TRUE(
hostResult.Ipv4Status == wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo ||
hostResult.Ipv4Status == wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect);
}
if (hostConnectivity & NLM_CONNECTIVITY_IPV6_INTERNET)
{
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::Success, hostResult.Ipv4Status);
}
else
{
// one of the 2 expected runtime failures
VERIFY_IS_TRUE(
hostResult.Ipv6Status == wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo ||
hostResult.Ipv6Status == wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect);
}
// www.msftconnecttest.com will always fail IPv6 name resolution - it doesn't have any AAAA records registered for it
const int expectedErrorCode = static_cast<int>(hostResult.Ipv4Status) |
(static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo) << 16);
LogInfo("RunGns(www.msftconnecttest.com, 0x%x)", expectedErrorCode);
// TODO: pass 'expectedErrorCode' instead of 1, once the pipeline is fixed from running Init back to wsl.exe
// it returns 1 as that's the lowest 16 bit value (unknown where the upper 16 bits are trimmed)
// if ManualConnectivityValidation is set true, one can confirm from the stdout captured that the correct result was determined and returned by init.
constexpr auto testErrorCode = ManualConnectivityValidation ? expectedErrorCode : 1;
NetworkTests::RunGns("www.msftconnecttest.com", AdapterId, LxGnsMessageConnectTestRequest, testErrorCode);
}
TEST_METHOD(ConnectivityCheckTestNameResolutionFailure)
{
WSL2_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Windows
const wsl::shared::conncheck::ConnCheckResult result =
wsl::shared::conncheck::CheckConnection("asdlkfadsf.bbcxzncvb", nullptr, "80");
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo, result.Ipv4Status);
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo, result.Ipv6Status);
constexpr int expectedErrorCode = static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo) |
(static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo) << 16);
LogInfo("RunGns(asdlkfadsf.bbcxzncvb, 0x%x)", expectedErrorCode);
// TODO: pass 'expectedErrorCode' instead of 1, once the pipeline is fixed from running Init back to wsl.exe
// it returns 2 (static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo))
// as that's the lowest 16 bit value (unknown where the upper 16 bits are trimmed)
// if temporarily change this back to expectedErrorCode, one can confirm from the stdout captured that the correct result was determined and returned by init.
constexpr auto testErrorCode = ManualConnectivityValidation
? expectedErrorCode
: static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo);
NetworkTests::RunGns("asdlkfadsf.bbcxzncvb", AdapterId, LxGnsMessageConnectTestRequest, testErrorCode);
}
TEST_METHOD(ConnectivityCheckTestNameResolvesButConnectivityFails)
{
WSL2_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
const auto* ncsiDnsOnlyName = "dns.msftncsi.com";
// v4 and v6 should succeed to resolve the name, but fail to connect,
// as this NCSI name is registered in global DNS, but there's not HTTP endpoint for it
// Windows
const wsl::shared::conncheck::ConnCheckResult result =
wsl::shared::conncheck::CheckConnection(ncsiDnsOnlyName, nullptr, "80");
VERIFY_ARE_EQUAL(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect, result.Ipv4Status);
// v6 name resolution might fail, depending on the configuration
VERIFY_IS_TRUE(
(wsl::shared::conncheck::ConnCheckStatus::FailureGetAddrInfo == result.Ipv6Status) ||
(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect == result.Ipv6Status));
constexpr int expectedErrorCode = static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect) |
(static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect) << 16);
LogInfo("RunGns(%hs, 0x%x)", ncsiDnsOnlyName, expectedErrorCode);
// TODO: pass 'expectedErrorCode' instead of 1, once the pipeline is fixed from running Init back to wsl.exe
// it returns 4 (static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect))
// as that's the lowest 16 bit value (unknown where the upper 16 bits are trimmed)
// if ManualConnectivityValidation is set true, one can confirm from the stdout captured that the correct result was determined and returned by init.
constexpr auto testErrorCode = ManualConnectivityValidation
? expectedErrorCode
: static_cast<int>(wsl::shared::conncheck::ConnCheckStatus::FailureSocketConnect);
NetworkTests::RunGns(ncsiDnsOnlyName, AdapterId, LxGnsMessageConnectTestRequest, testErrorCode);
}
// Due to VM creation performance requirements, VM creation is allowed to finish even if all
// networking state has not been mirrored yet. This introduces a race condition between the
// mirroring of networking state and mirrored mode test case execution that relies on the
// networking state being mirrored.
//
// This routine resolves the race condition by waiting for networking state to be mirrored into
// the VM. Tracking all mirrored networking state is complicated, so we use a heuristic to
// simplify: default routes have been observed to be mirrored last, so if they are present in
// the VM then we consider mirroring to be completed.
static void WaitForMirroredStateInLinux()
{
const bool hostConnectivityV4 = NetworkTests::HostHasInternetConnectivity(AF_INET);
const bool hostConnectivityV6 = NetworkTests::HostHasInternetConnectivity(AF_INET6);
Stopwatch<std::chrono::seconds> Watchdog(std::chrono::seconds(30));
do
{
// Count how many interfaces have v4/v6 connectivity, as defined by having a gateway and at least 1 preferred address.
int interfacesWithV4Connectivity = 0;
int interfacesWithV6Connectivity = 0;
// Get all interface info from the VM.
for (const auto& i : NetworkTests::GetAllInterfaceStates())
{
if (i.Gateway.has_value())
{
for (const auto& j : i.V4Addresses)
{
if (j.Preferred)
{
interfacesWithV4Connectivity++;
break;
}
}
}
if (i.V6Gateway.has_value())
{
for (const auto& j : i.V6Addresses)
{
if (j.Preferred)
{
interfacesWithV6Connectivity++;
break;
}
}
}
}
// Consider mirroring to be complete if we have the same v4/v6 connectivity in the VM as the host.
if ((!hostConnectivityV4 || interfacesWithV4Connectivity > 0) && (!hostConnectivityV6 || interfacesWithV6Connectivity > 0))
{
break;
}
LogInfo("Waiting for mirrored state...");
} while (Sleep(1000), !Watchdog.IsExpired());
VERIFY_IS_FALSE(Watchdog.IsExpired());
}
TEST_METHOD(DnsResolutionBasic)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyDnsResolutionBasic();
}
TEST_METHOD(DnsResolutionDig)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyDnsResolutionDig();
}
TEST_METHOD(DnsResolutionRecordTypes)
{
MIRRORED_NETWORKING_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
NetworkTests::VerifyDnsResolutionRecordTypes();
}
};
class BridgedTests
{
WSL_TEST_CLASS(BridgedTests)
std::optional<WslConfigChange> m_config;
TEST_CLASS_SETUP(TestClassSetup)
{
VERIFY_ARE_EQUAL(LxsstuInitialize(false), TRUE);
if (LxsstuVmMode())
{
m_config.emplace(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Bridged, .vmSwitch = L"Default Switch"}));
}
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
m_config.reset();
VERIFY_NO_THROW(LxsstuUninitialize(false));
return true;
}
TEST_METHOD(Basic)
{
WSL2_TEST_ONLY();
WINDOWS_11_TEST_ONLY();
// There's no way to guarantee that an external switch will work in the test environment
// So this test just validates that the VM successfully starts.
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Bridged, .vmSwitch = L"Default Switch"}));
// Verify that ipv6 is disabled by default.
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv6/conf/all/disable_ipv6");
VERIFY_ARE_EQUAL(L"1\n", out);
}
TEST_METHOD(CustomMac)
{
WSL2_TEST_ONLY();
WINDOWS_11_TEST_ONLY();
constexpr auto mac = L"aa:bb:cc:dd:ee:ff";
m_config->Update(LxssGenerateTestConfig(
{.networkingMode = wsl::core::NetworkingMode::Bridged, .vmSwitch = L"Default Switch", .macAddress = mac}));
VERIFY_ARE_EQUAL(mac, GetMacAddress());
}
TEST_METHOD(CustomMacDashes)
{
WSL2_TEST_ONLY();
WINDOWS_11_TEST_ONLY();
// Note: The SynthNic fails to start if the first byte of the mac address is 0xff.
std::wstring mac = L"ee-ee-dd-cc-bb-aa";
m_config->Update(LxssGenerateTestConfig(
{.networkingMode = wsl::core::NetworkingMode::Bridged, .vmSwitch = L"Default Switch", .macAddress = mac}));
std::replace(mac.begin(), mac.end(), L'-', L':');
VERIFY_ARE_EQUAL(mac, GetMacAddress());
}
TEST_METHOD(Ipv6)
{
WSL2_TEST_ONLY();
WINDOWS_11_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig(
{.networkingMode = wsl::core::NetworkingMode::Bridged, .vmSwitch = L"Default Switch", .ipv6 = true}));
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /proc/sys/net/ipv6/conf/all/disable_ipv6");
VERIFY_ARE_EQUAL(L"0\n", out);
}
TEST_METHOD(SmokeTest)
{
WSL2_TEST_ONLY();
WINDOWS_11_TEST_ONLY();
if (!NetworkTests::HostHasInternetConnectivity(AF_INET) && !NetworkTests::HostHasInternetConnectivity(AF_INET6))
{
LogSkipped("Host does not have internet connectivity. Skipping...");
return;
}
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Bridged, .vmSwitch = L"Default Switch"}));
// Verify that we have a working connection
NetworkTests::GuestClient(L"tcp-connect:bing.com:80");
}
TEST_METHOD(InternetConnectivityV4)
{
WSL2_TEST_ONLY();
WINDOWS_11_TEST_ONLY();
if (!NetworkTests::HostHasInternetConnectivity(AF_INET))
{
LogSkipped("Host does not have IPv4 internet connectivity. Skipping...");
return;
}
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Bridged, .vmSwitch = L"Default Switch"}));
NetworkTests::GuestClient(L"tcp4-connect:bing.com:80");
}
TEST_METHOD(InternetConnectivityV6)
{
WSL2_TEST_ONLY();
WINDOWS_11_TEST_ONLY();
if (!NetworkTests::HostHasInternetConnectivity(AF_INET6))
{
LogSkipped("Host does not have IPv6 internet connectivity. Skipping...");
return;
}
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Bridged, .vmSwitch = L"Default Switch"}));
NetworkTests::GuestClient(L"tcp6-connect:bing.com:80");
}
};
class VirtioProxyTests
{
WSL_TEST_CLASS(VirtioProxyTests)
std::optional<WslConfigChange> m_config;
TEST_CLASS_SETUP(TestClassSetup)
{
VERIFY_ARE_EQUAL(LxsstuInitialize(false), TRUE);
if (LxsstuVmMode())
{
m_config.emplace(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
}
return true;
}
TEST_CLASS_CLEANUP(TestClassCleanup)
{
m_config.reset();
VERIFY_NO_THROW(LxsstuUninitialize(false));
return true;
}
TEST_METHOD(SmokeTest)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
// Verify that we have a working connection
NetworkTests::GuestClient(L"tcp-connect:bing.com:80");
}
TEST_METHOD(InternetConnectivityV4)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
if (!NetworkTests::HostHasInternetConnectivity(AF_INET))
{
LogSkipped("Host does not have IPv4 internet connectivity. Skipping...");
return;
}
NetworkTests::GuestClient(L"tcp4-connect:bing.com:80");
}
TEST_METHOD(InternetConnectivityV6)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
if (!NetworkTests::HostHasInternetConnectivity(AF_INET6))
{
LogSkipped("Host does not have IPv6 internet connectivity. Skipping...");
return;
}
NetworkTests::WaitForIpv6DefaultRoute();
NetworkTests::GuestClient(L"tcp6-connect:bing.com:80");
}
TEST_METHOD(Configuration)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
const auto state = NetworkTests::GetInterfaceState(L"eth0");
VERIFY_IS_FALSE(state.V4Addresses.empty());
VERIFY_IS_TRUE(state.Gateway.has_value());
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/resolv.conf", 0);
const std::wregex pattern(L"(.|\\n)*nameserver [0-9. ]+(.|\\n)*");
VERIFY_IS_TRUE(std::regex_match(out, pattern));
// Verify that /etc/resolv.conf contains a 'search' line if the host has DNS suffixes
auto [suffixOut, suffixErr] = LxsstuLaunchPowershellAndCaptureOutput(
L"(@((Get-DnsClientGlobalSetting).SuffixSearchList) + @((Get-DnsClient).ConnectionSpecificSuffix) | Where-Object "
L"{$_}).Count");
if (_wtoi(suffixOut.c_str()) > 0)
{
VERIFY_IS_TRUE(out.find(L"search ") != std::wstring::npos);
}
}
TEST_METHOD(GuestPortIsReleased)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
// Make sure the VM doesn't time out
WslKeepAlive keepAlive;
{
auto guestProcess = NetworkTests::BindGuestPort(L"TCP4-LISTEN:1234", true);
NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false);
}
wsl::shared::retry::RetryWithTimeout<void>(
[&]() {
const wil::unique_socket listenSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
THROW_HR_IF(E_ABORT, !listenSocket);
SOCKADDR_IN Address{};
Address.sin_family = AF_INET;
Address.sin_port = htons(1234);
THROW_HR_IF(E_FAIL, bind(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&Address), sizeof(Address)) == SOCKET_ERROR);
},
std::chrono::seconds(1),
std::chrono::minutes(2));
}
TEST_METHOD(LoopbackGuestToHost)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
// Verify guest can connect to host on loopback (TCP only, UDP not supported)
NetworkTests::VerifyLoopbackGuestToHost(L"127.0.0.1", IPPROTO_TCP);
NetworkTests::VerifyLoopbackGuestToHost(L"0.0.0.0", IPPROTO_TCP);
// TODO: enable when v6 loopback is supported
// NetworkTests::VerifyLoopbackGuestToHost(L"::1", IPPROTO_TCP);
// NetworkTests::VerifyLoopbackGuestToHost(L"::", IPPROTO_TCP);
}
TEST_METHOD(UdpBindDoesNotPreventTcpBind)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
auto tcpPort = NetworkTests::BindGuestPort(L"TCP4-LISTEN:1234", true);
auto udpPort = NetworkTests::BindGuestPort(L"UDP4-LISTEN:1234", true);
}
TEST_METHOD(HostUdpBindDoesNotPreventGuestTcpBind)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
auto udpPort = NetworkTests::BindHostPort(2345, SOCK_DGRAM, IPPROTO_UDP, true);
auto tcpPort = NetworkTests::BindGuestPort(L"TCP4-LISTEN:2345", true);
}
TEST_METHOD(PortZeroBindIsTracked)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
NetworkTests::VerifyPortZeroBindIsTracked();
}
TEST_METHOD(HttpProxySimple)
{
VIRTIOPROXY_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .autoProxy = true}));
NetworkTests::VerifyHttpProxySimple();
}
TEST_METHOD(ConfigurationV6)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
if (!NetworkTests::HostHasInternetConnectivity(AF_INET6))
{
LogSkipped("Host does not have IPv6 internet connectivity. Skipping...");
return;
}
// Wait for the device host to send an IPv6 Router Advertisement
// before querying the interface state.
NetworkTests::WaitForIpv6DefaultRoute();
const auto state = NetworkTests::GetInterfaceState(L"eth0");
// Verify that the guest has a global IPv6 address assigned
VERIFY_IS_FALSE(state.V6Addresses.empty());
// Verify that the guest has an IPv6 default gateway
VERIFY_IS_TRUE(state.V6Gateway.has_value());
// Verify the guest IPv6 address matches the host global IPv6 address
const auto adapterAddresses = NetworkTests::GetAdapterAddresses(AF_INET6);
DWORD bestIndex = 0;
SOCKADDR_IN6 dest{};
dest.sin6_family = AF_INET6;
InetPtonW(AF_INET6, L"2001:4860:4860::8888", &dest.sin6_addr);
VERIFY_ARE_EQUAL(NO_ERROR, GetBestInterfaceEx(reinterpret_cast<SOCKADDR*>(&dest), &bestIndex));
for (auto* adapter = reinterpret_cast<const IP_ADAPTER_ADDRESSES*>(adapterAddresses.data()); adapter != nullptr;
adapter = adapter->Next)
{
if (adapter->IfIndex != bestIndex)
{
continue;
}
for (auto* unicast = adapter->FirstUnicastAddress; unicast != nullptr; unicast = unicast->Next)
{
if (unicast->Address.lpSockaddr->sa_family != AF_INET6)
{
continue;
}
const auto& sin6 = *reinterpret_cast<SOCKADDR_IN6*>(unicast->Address.lpSockaddr);
if (IN6_IS_ADDR_LINKLOCAL(&sin6.sin6_addr) || IN6_IS_ADDR_LOOPBACK(&sin6.sin6_addr))
{
continue;
}
SOCKADDR_INET hostAddr{};
hostAddr.Ipv6 = sin6;
const auto hostAddrString = wsl::windows::common::string::SockAddrInetToWstring(hostAddr);
// The host address may not be at index 0 due to SLAAC addresses from RA
bool addressFound = false;
for (const auto& v6Addr : state.V6Addresses)
{
if (v6Addr.Address == hostAddrString)
{
addressFound = true;
break;
}
}
VERIFY_IS_TRUE(addressFound);
break;
}
break;
}
}
TEST_METHOD(GuestPortIsReleasedV6)
{
VIRTIOPROXY_TEST_ONLY();
WINDOWS_11_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
// Make sure the VM doesn't time out
WslKeepAlive keepAlive;
{
auto guestProcess = NetworkTests::BindGuestPort(L"TCP6-LISTEN:1234,bind=::", true);
NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false, true);
}
wsl::shared::retry::RetryWithTimeout<void>(
[&]() {
const wil::unique_socket listenSocket(socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP));
THROW_HR_IF(E_ABORT, !listenSocket);
SOCKADDR_IN6 Address{};
Address.sin6_family = AF_INET6;
Address.sin6_port = htons(1234);
THROW_HR_IF(E_FAIL, bind(listenSocket.get(), reinterpret_cast<SOCKADDR*>(&Address), sizeof(Address)) == SOCKET_ERROR);
},
std::chrono::seconds(1),
std::chrono::minutes(2));
}
TEST_METHOD(ConfigurationV6DnsServers)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy}));
if (!NetworkTests::HostHasInternetConnectivity(AF_INET6))
{
LogSkipped("Host does not have IPv6 internet connectivity. Skipping...");
return;
}
if (!NetworkTests::HostHasIpv6DnsServers())
{
LogSkipped("Host does not have IPv6 DNS servers configured. Skipping...");
return;
}
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/resolv.conf", 0);
// Verify that /etc/resolv.conf contains at least one IPv6 nameserver
const std::wregex v6Pattern(L"(.|\\n)*nameserver [0-9a-fA-F:]+\\n(.|\\n)*");
VERIFY_IS_TRUE(std::regex_match(out, v6Pattern));
}
TEST_METHOD(DnsResolutionBasic)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false}));
NetworkTests::VerifyDnsResolutionBasic();
}
TEST_METHOD(DnsResolutionDig)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false}));
NetworkTests::VerifyDnsResolutionDig();
}
TEST_METHOD(DnsResolutionRecordTypes)
{
VIRTIOPROXY_TEST_ONLY();
m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false}));
NetworkTests::VerifyDnsResolutionRecordTypes();
}
};
} // namespace NetworkTests