mirror of
https://github.com/microsoft/WSL.git
synced 2025-07-03 07:23:20 +00:00
4302 lines
181 KiB
C++
4302 lines
181 KiB
C++
![]() |
/*++
|
||
|
|
||
|
Copyright (c) Microsoft. All rights reserved.
|
||
|
|
||
|
Module Name:
|
||
|
|
||
|
NetworkTests.cpp
|
||
|
|
||
|
Abstract:
|
||
|
|
||
|
This file contains test cases for the networking logic.
|
||
|
|
||
|
--*/
|
||
|
|
||
|
#include "precomp.h"
|
||
|
#include "computenetwork.h"
|
||
|
#include "Common.h"
|
||
|
#include "wslpolicies.h"
|
||
|
#include "hns_schema.h"
|
||
|
|
||
|
#include <mstcpip.h>
|
||
|
#include <winhttp.h>
|
||
|
#include <winsock2.h>
|
||
|
#include <netlistmgr.h>
|
||
|
|
||
|
using wsl::shared::hns::GuestEndpointResourceType;
|
||
|
using wsl::shared::hns::ModifyGuestEndpointSettingRequest;
|
||
|
using wsl::shared::hns::ModifyRequestType;
|
||
|
|
||
|
bool TryLoadWinhttpProxyMethods() noexcept
|
||
|
{
|
||
|
constexpr auto winhttpModuleName = L"Winhttp.dll";
|
||
|
const wil::shared_hmodule winhttpModule{LoadLibraryEx(winhttpModuleName, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)};
|
||
|
if (!winhttpModule)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
try
|
||
|
{
|
||
|
// attempt to find the functions for the Winhttp proxy APIs.
|
||
|
static LxssDynamicFunction<decltype(WinHttpRegisterProxyChangeNotification)> WinHttpRegisterProxyChangeNotification{
|
||
|
winhttpModule, "WinHttpRegisterProxyChangeNotification"};
|
||
|
static LxssDynamicFunction<decltype(WinHttpUnregisterProxyChangeNotification)> WinHttpUnregisterProxyChangeNotification{
|
||
|
winhttpModule, "WinHttpUnregisterProxyChangeNotification"};
|
||
|
static LxssDynamicFunction<decltype(WinHttpGetProxySettingsEx)> WinHttpGetProxySettingsEx{
|
||
|
winhttpModule, "WinHttpGetProxySettingsEx"};
|
||
|
static LxssDynamicFunction<decltype(WinHttpGetProxySettingsResultEx)> WinHttpGetProxySettingsResultEx{
|
||
|
winhttpModule, "WinHttpGetProxySettingsResultEx"};
|
||
|
static LxssDynamicFunction<decltype(WinHttpFreeProxySettingsEx)> WinHttpFreeProxySettingsEx{
|
||
|
winhttpModule, "WinHttpFreeProxySettingsEx"};
|
||
|
}
|
||
|
catch (...)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
#define HYPERV_FIREWALL_TEST_ONLY() \
|
||
|
{ \
|
||
|
WSL2_TEST_ONLY(); \
|
||
|
WINDOWS_11_TEST_ONLY(); \
|
||
|
if (!AreExperimentalNetworkingFeaturesSupported() || !IsHyperVFirewallSupported()) \
|
||
|
{ \
|
||
|
LogSkipped("Hyper-V Firewall not supported on this OS. Skipping test..."); \
|
||
|
return; \
|
||
|
} \
|
||
|
}
|
||
|
|
||
|
#define MIRRORED_NETWORKING_TEST_ONLY() \
|
||
|
{ \
|
||
|
WSL2_TEST_ONLY(); \
|
||
|
WINDOWS_11_TEST_ONLY(); \
|
||
|
if (!AreExperimentalNetworkingFeaturesSupported() || !IsHyperVFirewallSupported()) \
|
||
|
{ \
|
||
|
LogSkipped("Mirrored networking not supported on this OS. Skipping test.."); \
|
||
|
return; \
|
||
|
} \
|
||
|
}
|
||
|
|
||
|
#define DNS_TUNNELING_TEST_ONLY() \
|
||
|
{ \
|
||
|
WSL2_TEST_ONLY(); \
|
||
|
WINDOWS_11_TEST_ONLY(); \
|
||
|
if (!AreExperimentalNetworkingFeaturesSupported()) \
|
||
|
{ \
|
||
|
LogSkipped("DNS tunneling not supported on this OS. Skipping test..."); \
|
||
|
return; \
|
||
|
} \
|
||
|
if (!TryLoadDnsResolverMethods()) \
|
||
|
{ \
|
||
|
LogSkipped("DNS tunneling APIs not present on this OS. Skipping test..."); \
|
||
|
return; \
|
||
|
} \
|
||
|
}
|
||
|
|
||
|
#define WINHTTP_PROXY_TEST_ONLY() \
|
||
|
{ \
|
||
|
WSL2_TEST_ONLY(); \
|
||
|
if (!TryLoadWinhttpProxyMethods()) \
|
||
|
{ \
|
||
|
LogSkipped("Winhttp proxy APIs not present on this OS. Skipping test..."); \
|
||
|
return; \
|
||
|
} \
|
||
|
}
|
||
|
|
||
|
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
|