blob: ded6b2f0c7e7b5109657735cca090c1cc4e72ca2 [file] [log] [blame]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <arpa/inet.h>
#include <fcntl.h>
#include <net/if.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/ioctl.h>
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <format>
#include <thread>
#include <fbl/unique_fd.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <linux/capability.h>
#include <linux/if_tun.h>
#include "src/lib/files/directory.h"
#include "src/lib/files/file.h"
#include "src/starnix/tests/syscalls/cpp/capabilities_helper.h"
#include "src/starnix/tests/syscalls/cpp/test_helper.h"
namespace {
uint32_t GetLoopbackIndex() { return 1; }
// Waits for the address on the loopback device to be added or removed.
bool HasLoopbackAddress(int family, const char *address_str) {
fbl::unique_fd nl_sock(socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE));
EXPECT_TRUE(nl_sock.is_valid());
test_helper::NetlinkEncoder encoder(RTM_GETADDR, NLM_F_REQUEST | NLM_F_DUMP);
ifaddrmsg ifa_msg = {
.ifa_family = AF_UNSPEC,
.ifa_index = GetLoopbackIndex(),
};
encoder.Write(ifa_msg);
iovec iov = {};
encoder.Finalize(iov);
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
};
EXPECT_GE(sendmsg(nl_sock.get(), &msg, 0), 0) << strerror(errno);
uint8_t addr[16];
EXPECT_EQ(inet_pton(family, address_str, &addr), 1) << strerror(errno);
char buf[8192];
while (true) {
ssize_t len = recv(nl_sock.get(), buf, sizeof(buf), 0);
if (errno == EINTR) {
continue;
}
EXPECT_GE(len, 0);
for (nlmsghdr *nh = reinterpret_cast<nlmsghdr *>(buf); MY_NLMSG_OK(nh, len);
nh = NLMSG_NEXT(nh, len)) {
if (nh->nlmsg_type == NLMSG_DONE) {
return false;
}
if (nh->nlmsg_type != RTM_NEWADDR) {
continue;
}
ifaddrmsg *ifa = reinterpret_cast<ifaddrmsg *>(NLMSG_DATA(nh));
if (ifa->ifa_family != family) {
continue;
}
rtattr *rta = IFA_RTA(ifa);
int rta_len = IFA_PAYLOAD(nh);
for (; RTA_OK(rta, rta_len); rta = RTA_NEXT(rta, rta_len)) {
if (rta->rta_type != IFA_ADDRESS) {
continue;
}
if (memcmp(addr, RTA_DATA(rta), RTA_PAYLOAD(rta)) != 0) {
continue;
}
// TODO(https://issues.fuchsia.dev/472336920): Netstack currently
// only marks the address as unavailable which gets later translated
// to tentative by netlink. We should report RTM_DELADDR if we find
// it to be load bearing.
if (test_helper::IsStarnix() && (ifa->ifa_flags & IFA_F_TENTATIVE)) {
continue;
}
return true;
}
}
}
}
// Creates a new TUN device with the given name.
fbl::unique_fd NewTunDevice(const char *name) {
int tun = open("/dev/tun", O_RDWR);
if (tun == -1 && errno == ENOENT) {
tun = open("/dev/net/tun", O_RDWR);
}
EXPECT_GT(tun, 0) << strerror(errno);
ifreq ifr{};
ifr.ifr_flags = IFF_NO_PI | IFF_TUN;
strncpy(ifr.ifr_name, name, IFNAMSIZ);
EXPECT_EQ(ioctl(tun, TUNSETIFF, &ifr), 0) << strerror(errno);
return fbl::unique_fd(tun);
}
class SysctlTest : public ::testing::Test {};
class SysctlTestWithParam
: public SysctlTest,
public ::testing::WithParamInterface<std::tuple<std::string, std::string>> {};
TEST_P(SysctlTestWithParam, DirectoryContainsInterfaces) {
auto const &[version, conf_or_neigh] = GetParam();
std::vector<std::string> files;
EXPECT_TRUE(
files::ReadDirContents(std::format("/proc/sys/net/{}/{}", version, conf_or_neigh), &files));
EXPECT_THAT(files, testing::IsSupersetOf({"default", "lo"}));
}
INSTANTIATE_TEST_SUITE_P(SysctlTest, SysctlTestWithParam,
::testing::Combine(::testing::Values("ipv4", "ipv6"),
::testing::Values("conf", "neigh")),
[](const ::testing::TestParamInfo<SysctlTestWithParam::ParamType> &info) {
return std::format("{}_{}", std::get<0>(info.param),
std::get<1>(info.param));
});
TEST_F(SysctlTest, AcceptRaRtTable) {
if (!test_helper::HasCapability(CAP_NET_ADMIN)) {
GTEST_SKIP() << "Need CAP_NET_ADMIN to run SysctlTest";
}
std::string accept_ra_rt_table_str;
constexpr const char *kAcceptRaRtTable = "/proc/sys/net/ipv6/conf/{}/accept_ra_rt_table";
const std::string kDefault = std::format(kAcceptRaRtTable, "default");
const std::string kLo = std::format(kAcceptRaRtTable, "lo");
if (!test_helper::IsStarnix() && access(kDefault.c_str(), F_OK) == -1) {
GTEST_SKIP() << "The kernel is not compiled with this sysctl";
}
const char *kVal1 = "-100\n";
const char *kVal2 = "-200\n";
for (auto const &path : {kDefault, kLo}) {
EXPECT_TRUE(files::ReadFileToString(path, &accept_ra_rt_table_str));
EXPECT_STREQ(accept_ra_rt_table_str.c_str(), "0\n");
}
// Write then read back value for interface lo.
EXPECT_TRUE(files::WriteFile(kLo, kVal1));
EXPECT_TRUE(files::ReadFileToString(kLo, &accept_ra_rt_table_str));
EXPECT_STREQ(accept_ra_rt_table_str.c_str(), kVal1);
// Write then read back value for special file `default`.
EXPECT_TRUE(files::WriteFile(kDefault, kVal2));
EXPECT_TRUE(files::ReadFileToString(kDefault, &accept_ra_rt_table_str));
EXPECT_STREQ(accept_ra_rt_table_str.c_str(), kVal2);
const char *kTunName = "tun0";
auto tun = NewTunDevice(kTunName);
const std::string kTunPath = std::format(kAcceptRaRtTable, kTunName);
int trial = 0;
while (!files::ReadFileToString(kTunPath, &accept_ra_rt_table_str)) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
ASSERT_LE(++trial, 100);
}
// The new device will have the `default` value.
EXPECT_STREQ(accept_ra_rt_table_str.c_str(), kVal2);
}
TEST_F(SysctlTest, DisableIpv6) {
if (!test_helper::HasCapability(CAP_NET_ADMIN)) {
GTEST_SKIP() << "Need CAP_NET_ADMIN to run SysctlTest";
}
ASSERT_TRUE(HasLoopbackAddress(AF_INET6, "::1"));
const char kDisableIpv6[] = "/proc/sys/net/ipv6/conf/lo/disable_ipv6";
ASSERT_TRUE(files::WriteFile(kDisableIpv6, "1"));
// IP configurations are applied synchronously at netstack, but netlink
// watches for changes and updates addresses asynchronously. So we add some
// retry to avoid flakiness. The maximum delay is 100 * 100ms = 10s.
constexpr int kMaxAttempts = 100;
constexpr std::chrono::milliseconds kRetryTimeout(100);
// Verify ::1 is gone.
bool removed = false;
for (int trial = 0; trial < kMaxAttempts; trial++) {
if (!HasLoopbackAddress(AF_INET6, "::1")) {
removed = true;
break;
}
std::this_thread::sleep_for(kRetryTimeout);
}
ASSERT_TRUE(removed) << "::1 is not gone after "
<< kMaxAttempts * kRetryTimeout / std::chrono::seconds(1) << "s";
// Re-enable it and assert ::1 is back.
ASSERT_TRUE(files::WriteFile(kDisableIpv6, "0"));
bool added = false;
for (int trial = 0; trial < kMaxAttempts; trial++) {
if (HasLoopbackAddress(AF_INET6, "::1")) {
added = true;
break;
}
std::this_thread::sleep_for(kRetryTimeout);
}
ASSERT_TRUE(added) << "::1 is not back after "
<< kMaxAttempts * kRetryTimeout / std::chrono::seconds(1) << "s";
}
TEST_F(SysctlTest, DisableIpv6Default) {
if (!test_helper::HasCapability(CAP_NET_ADMIN)) {
GTEST_SKIP() << "Need CAP_NET_ADMIN to run SysctlTest";
}
files::WriteFile("/proc/sys/net/ipv6/conf/default/disable_ipv6", "1");
auto tun = NewTunDevice("tun1");
std::string disable_ipv6_str;
int trial = 0;
while (!files::ReadFileToString("/proc/sys/net/ipv6/conf/tun1/disable_ipv6", &disable_ipv6_str)) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
ASSERT_LE(++trial, 100);
}
ASSERT_STREQ(disable_ipv6_str.c_str(), "1\n");
}
struct SysctlTestReadBackParam {
std::string path;
const char *value;
};
class SysctlTestReadBack : public SysctlTest,
public ::testing::WithParamInterface<SysctlTestReadBackParam> {};
TEST_P(SysctlTestReadBack, ReadBack) {
if (!test_helper::HasCapability(CAP_NET_ADMIN)) {
GTEST_SKIP() << "Need CAP_NET_ADMIN to run SysctlTestReadBack";
}
const auto &[path, value] = GetParam();
std::string to_write = std::format("{}\n", value);
ASSERT_TRUE(files::WriteFile(path, to_write)) << strerror(errno);
std::string to_read;
ASSERT_TRUE(files::ReadFileToString(path, &to_read)) << strerror(errno);
ASSERT_EQ(to_read, to_write);
}
INSTANTIATE_TEST_SUITE_P(
SysctlTest, SysctlTestReadBack,
::testing::Values(
SysctlTestReadBackParam{"/proc/sys/net/ipv6/neigh/default/ucast_solicit", "3"},
SysctlTestReadBackParam{"/proc/sys/net/ipv4/neigh/default/ucast_solicit", "3"},
SysctlTestReadBackParam{"/proc/sys/net/ipv6/neigh/default/mcast_resolicit", "3"},
SysctlTestReadBackParam{"/proc/sys/net/ipv4/neigh/default/mcast_resolicit", "3"},
SysctlTestReadBackParam{"/proc/sys/net/ipv6/conf/default/dad_transmits", "1"},
SysctlTestReadBackParam{"/proc/sys/net/ipv6/neigh/default/base_reachable_time_ms", "2000"},
SysctlTestReadBackParam{"/proc/sys/net/ipv4/neigh/default/base_reachable_time_ms", "2000"},
SysctlTestReadBackParam{"/proc/sys/net/ipv6/neigh/default/retrans_time_ms", "2000"},
SysctlTestReadBackParam{"/proc/sys/net/ipv4/neigh/default/retrans_time_ms", "2000"},
SysctlTestReadBackParam{"/proc/sys/net/ipv6/conf/default/use_tempaddr", "0"},
SysctlTestReadBackParam{"/proc/sys/net/ipv6/conf/default/use_tempaddr", "2"},
SysctlTestReadBackParam{"/proc/sys/net/ipv6/conf/default/accept_ra_defrtr", "0"},
SysctlTestReadBackParam{"/proc/sys/net/ipv6/conf/default/accept_ra_defrtr", "1"},
SysctlTestReadBackParam{"/proc/sys/net/core/rmem_max", "6291456"},
SysctlTestReadBackParam{"/proc/sys/net/core/wmem_max", "6291456"},
SysctlTestReadBackParam{"/proc/sys/net/ipv4/tcp_rmem", "4096\t87380\t6291456"}),
[](const testing::TestParamInfo<SysctlTestReadBackParam> &info) {
auto path = std::filesystem::path(info.param.path);
auto name = path.filename().string();
auto version = std::next(path.begin(), 4)->string();
std::string value = info.param.value;
std::ranges::replace(value, '\t', '_');
return std::format("{}_{}_{}", version, name, value);
});
} // namespace