WSL/test/windows/NetworkTests.cpp

4302 lines
181 KiB
C++
Raw Permalink Normal View History

/*++
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; \
} \
}
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";
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 NetworkTests
{
WSL_TEST_CLASS(NetworkTests)
static std::wstring SockaddrToString(const SOCKADDR_INET* sockAddr)
{
constexpr auto ipv4AddressStringLength = 16;
constexpr auto ipv6AddressStringLength = 48;
std::wstring address(std::max(ipv4AddressStringLength, ipv6AddressStringLength), L'\0');
switch (sockAddr->si_family)
{
case AF_INET:
{
RtlIpv4AddressToStringW(&sockAddr->Ipv4.sin_addr, address.data());
break;
}
case AF_INET6:
{
RtlIpv6AddressToStringW(&sockAddr->Ipv6.sin6_addr, address.data());
break;
}
default:
break;
}
address.resize(std::wcslen(address.data()));
return address;
}
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 SockaddrToString(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;
}
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());
AdapterId = guid.value();
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: '%ls'", 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 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(MirroredDnsTunneling)
{
DNS_TUNNELING_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = true}));
WaitForMirroredStateInLinux();
VerifyDnsTunneling(c_dnsTunnelingDefaultIp);
}
TEST_METHOD(MirroredDnsTunnelingWithSpecificIp)
{
DNS_TUNNELING_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig(
{.networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = true, .dnsTunnelingIpAddress = L"10.255.255.1"}));
WaitForMirroredStateInLinux();
VerifyDnsTunneling(L"10.255.255.1");
}
TEST_METHOD(MirroredDnsTunnelingVerifySuffixes)
{
DNS_TUNNELING_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = true}));
WaitForMirroredStateInLinux();
VerifyDnsSuffixes();
}
TEST_METHOD(MirroredWithoutTunnelingVerifySuffixes)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = false}));
WaitForMirroredStateInLinux();
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.Domain = L"microsoft.com";
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"domain microsoft.com\n"
L"search foo.microsoft.com bar.microsoft.com\n";
VERIFY_ARE_EQUAL(expected, out.c_str());
}
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 %ls", 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(MirroredHttpProxyVerifyConfigDisabled)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = false}));
WaitForMirroredStateInLinux();
auto restoreProxySettings = wil::scope_exit([&] { ClearHttpProxySettings(true); });
SetHttpProxySettings(c_httpProxyString, L"", L"", true);
VerifyHttpProxyEnvVariables(L"", L"", L"");
}
TEST_METHOD(MirroredHttpProxySimple)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
VerifyHttpProxySimple();
}
TEST_METHOD(MirroredHttpProxySimpleMachineScope)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
// verify with machine scope
VerifyHttpProxySimple(false);
}
TEST_METHOD(MirroredNoHttpProxyConfigured)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
VerifyNoHttpProxyConfigured();
}
TEST_METHOD(MirroredHttpProxyWithBypassesConfigured)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
VerifyHttpProxyWithBypassesConfigured();
}
TEST_METHOD(MirroredHttpProxyChange)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
VerifyHttpProxyChange();
}
TEST_METHOD(MirroredHttpProxyAndWslEnv)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
WaitForMirroredStateInLinux();
VerifyHttpProxyAndWslEnv();
}
TEST_METHOD(MirroredHttpProxyFilterByNetworkConfiguration)
{
MIRRORED_NETWORKING_TEST_ONLY();
WINHTTP_PROXY_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .autoProxy = true}));
VerifyHttpProxyFilterByNetworkConfigurationMirrored();
}
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);
}
TEST_METHOD(MirroredSmokeTest)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Verify that we have a working connection
GuestClient(L"tcp-connect:bing.com:80");
}
TEST_METHOD(MirroredInternetConnectivityV4)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
if (!HostHasInternetConnectivity(AF_INET))
{
LogSkipped("Host does not have IPv4 internet connectivity. Skipping...");
return;
}
GuestClient(L"tcp4-connect:bing.com:80");
}
TEST_METHOD(MirroredInternetConnectivityV6)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
if (!HostHasInternetConnectivity(AF_INET6))
{
LogSkipped("Host does not have IPv6 internet connectivity. Skipping...");
return;
}
GuestClient(L"tcp6-connect:bing.com:80");
}
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);
}
TEST_METHOD(MirroredLoopbackLocal)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored, .hostAddressLoopback = true}));
WaitForMirroredStateInLinux();
std::vector<InterfaceState> interfaceStates = 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)
{
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(MirroredLoopbackExplicit)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Verify loopback connectivity on loopback addresses
VerifyLoopbackConnectivity(L"127.0.0.1");
// TODO: enable when v6 loopback is supported
// VerifyLoopbackConnectivity(L"::1");
}
TEST_METHOD(MirroredLoopbackSystemd)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(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.
VerifyLoopbackGuestToHost(L"127.0.0.1", 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));
}
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(bool ipv6)
{
// Bind a port in the guest.
auto [guestProcess, read] = BindGuestPort(ipv6 ? 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 = ipv6 ? AF_INET6 : AF_INET;
INETADDR_SETLOOPBACK((PSOCKADDR)&addr);
SS_PORT(&addr) = htons(1234);
auto pred = [&]() {
hostSocket.reset(socket(ipv6 ? AF_INET6 : AF_INET, 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(false);
ValidateLocalhostRelayTraffic(true);
}
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(false);
}
TEST_METHOD(MirroredGuestPortCantBeBoundByHost)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
{
auto guestProcess = BindGuestPort(L"TCP4-LISTEN:1234", true);
BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false);
}
{
auto guestProcess = BindGuestPort(L"UDP4-LISTEN:1234", true);
BindHostPort(1234, SOCK_DGRAM, IPPROTO_UDP, false);
}
}
TEST_METHOD(MirroredGuestPortIsReleased)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
// Make sure the VM doesn't time out
WslKeepAlive keepAlive;
{
auto guestProcess = BindGuestPort(L"TCP4-LISTEN:1234", true);
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(MirroredHostPortCantBeBoundByGuest)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
{
auto hostPort = BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, true);
BindGuestPort(L"TCP4-LISTEN:1234", false);
}
{
auto hostPort = BindHostPort(1234, SOCK_DGRAM, IPPROTO_UDP, true);
BindGuestPort(L"UDP4-LISTEN:1234", false);
}
}
TEST_METHOD(MirroredUdpBindDoesNotPreventTcpBind)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
auto tcpPort = BindGuestPort(L"TCP4-LISTEN:1234", true);
auto udpPort = BindGuestPort(L"UDP4-LISTEN:1234", true);
}
TEST_METHOD(MirroredHostUdpBindDoesNotPreventGuestTcpBind)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
auto tcpPort = BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, true);
auto udpPort = BindGuestPort(L"UDP4-LISTEN:1234", true);
}
TEST_METHOD(MirroredMultipleGuestBindOnSameTuple)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
auto bind1 = BindGuestPort(L"TCP4-LISTEN:1234,bind=127.0.0.1", true);
{
auto bind2 = 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 = BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
TEST_METHOD(MirroredEphemeralBind)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
auto tcpPort = BindGuestPort(L"TCP4-LISTEN:0", true);
auto udpPort = BindGuestPort(L"UDP4-LISTEN:0", true);
}
TEST_METHOD(MirroredExplicitEphemeralBind)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(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] = BindGuestPortHelper(L"TCP4-LISTEN:" + std::to_wstring(port));
if (tcpSuccess)
{
canBindTcp = true;
break;
}
}
for (int port = ephemeralRangeStart; port <= ephemeralRangeEnd; port++)
{
auto [udpListener, udpSuccess, read] = BindGuestPortHelper(L"UDP4-LISTEN:" + std::to_wstring(port));
if (udpSuccess)
{
canBindUdp = true;
break;
}
}
VERIFY_IS_TRUE(canBindTcp);
VERIFY_IS_TRUE(canBindUdp);
}
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 accesible 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();
}
TEST_METHOD(MirroredNonRootNamespaceEphemeralBind)
{
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 accesible from the new namespace, so it can't use a 127* IP
WslConfigChange config(LxssGenerateTestConfig(
{.guiApplications = true, .networkingMode = wsl::core::NetworkingMode::Mirrored, .dnsTunneling = true, .dnsTunnelingIpAddress = L"10.255.255.254"}));
WaitForMirroredStateInLinux();
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(MirroredPortForwardingToNonRootNamespace)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig(
{.guiApplications = true, .networkingMode = wsl::core::NetworkingMode::Mirrored, .hostAddressLoopback = true}));
WaitForMirroredStateInLinux();
// We list the IPv4 addresses mirrored in Linux and use in the test the first one we find
std::vector<InterfaceState> interfaceStates = 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);
GuestListener rootListener(rootListenerAddr, IPPROTO_TCP);
SOCKADDR_INET namespaceListenerAddr = wsl::windows::common::string::StringToSockAddrInet(L"0.0.0.0");
SS_PORT(&namespaceListenerAddr) = htons(80);
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(MirroredLinuxNonRootNamespaceConnectToWindowsHost)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(LxssGenerateTestConfig(
{.guiApplications = true, .networkingMode = wsl::core::NetworkingMode::Mirrored, .hostAddressLoopback = true}));
WaitForMirroredStateInLinux();
// We list the IPv4 addresses mirrored in Linux and use in the test the first one we find
std::vector<InterfaceState> interfaceStates = 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(MirroredResolvConf)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(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(MirroredNetworkSettings)
{
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(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/" + 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);
}
}
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
// valdiating 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:[\n %ls]", 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:[\n %ls]", 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(
"Validing 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);
}
TEST_METHOD(MirroredFirewallRulesExpectedBlock)
{
HYPERV_FIREWALL_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
ValidateInitialFirewallState(FirewallObjects::Required);
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);
}
TEST_METHOD(MirroredFirewallRulesExpectedAllow)
{
HYPERV_FIREWALL_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
ValidateInitialFirewallState(FirewallObjects::Required);
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);
}
TEST_METHOD(MirroredFirewallRulesEnabledSetting)
{
HYPERV_FIREWALL_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
WaitForMirroredStateInLinux();
ValidateInitialFirewallState(FirewallObjects::Required);
FirewallSettingEnabledTests(true);
}
/* Network Tests Helper Methods */
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);
}
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)
{
VERIFY_IS_FALSE(state.DefaultRoute.has_value());
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: '%ls'", 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 RoutingTableState GetIpv6RoutingTableState()
{
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"ip -6 route show");
LogInfo("Ip -6 route output: '%ls'", 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: '%ls'", 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: '%ls'", 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:'%ls'", 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();
LogInfo("output=\n %S", output.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 (const IP_ADAPTER_ADDRESSES* adapter = adapterAddresses.get(); adapter != nullptr; adapter = adapter->Next)
{
if (interfaceLuid.Value == adapter->Luid.Value && adapter->FirstUnicastAddress != nullptr && adapter->FirstGatewayAddress != nullptr)
{
return true;
}
}
}
return false;
}
static std::unique_ptr<IP_ADAPTER_ADDRESSES> GetAdapterAddresses(ADDRESS_FAMILY family)
{
ULONG result;
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::unique_ptr<IP_ADAPTER_ADDRESSES> buffer;
while ((result = GetAdaptersAddresses(family, flags, nullptr, buffer.get(), &bufferSize)) == ERROR_BUFFER_OVERFLOW)
{
buffer.reset(static_cast<IP_ADAPTER_ADDRESSES*>(malloc(bufferSize)));
VERIFY_IS_NOT_NULL(buffer.get());
}
VERIFY_WIN32_SUCCEEDED(result);
return buffer;
}
// 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 = HostHasInternetConnectivity(AF_INET);
const bool hostConnectivityV6 = 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 : 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());
}
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());
}
// 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;
TEST_METHOD(ConnectivityCheckTestMirroredDefaultSuccess)
{
WSL2_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
WslConfigChange config(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;
RunGns("www.msftconnecttest.com", AdapterId, LxGnsMessageConnectTestRequest, testErrorCode);
}
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(ConnectivityCheckTestMirroredNameResolutionFailure)
{
WSL2_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
WslConfigChange config(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);
RunGns("asdlkfadsf.bbcxzncvb", 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(ConnectivityCheckTestMirroredNameResolvesButConnectivityFails)
{
WSL2_TEST_ONLY();
MIRRORED_NETWORKING_TEST_ONLY();
SKIP_TEST_UNSTABLE();
WslConfigChange config(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);
RunGns(ncsiDnsOnlyName, 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 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);
}
};
} // namespace NetworkTests