diff --git a/base/cvd/allocd/net/BUILD.bazel b/base/cvd/allocd/net/BUILD.bazel index c189f7d9728..4038407b980 100644 --- a/base/cvd/allocd/net/BUILD.bazel +++ b/base/cvd/allocd/net/BUILD.bazel @@ -1,4 +1,4 @@ -load("//cuttlefish/bazel:rules.bzl", "cf_cc_library") +load("//cuttlefish/bazel:rules.bzl", "cf_cc_library", "cf_cc_test") package( default_visibility = ["//:android_cuttlefish"], @@ -19,3 +19,42 @@ cf_cc_library( "@abseil-cpp//absl/log", ], ) + +cf_cc_library( + name = "nftables", + srcs = [ + "nft_rule.cc", + "nftables_nft.cc", + ], + hdrs = [ + "nft_rule.h", + "nftables.h", + "nftables_nft.h", + ], + target_compatible_with = [ + "@platforms//os:linux", + ], + deps = [ + "//cuttlefish/common/libs/utils:files", + "//cuttlefish/common/libs/utils:json", + "//cuttlefish/process:command", + "//cuttlefish/process:managed_stdio", + "//cuttlefish/process:subprocess", + "//cuttlefish/result", + "@abseil-cpp//absl/base:no_destructor", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/strings", + "@jsoncpp", + ], +) + +cf_cc_test( + name = "nft_rule_test", + srcs = ["nft_rule_test.cc"], + deps = [ + ":nftables", + "//allocd/test:fake_nftables", + "//cuttlefish/result", + "//cuttlefish/result:result_matchers", + ], +) diff --git a/base/cvd/allocd/net/nft_rule.cc b/base/cvd/allocd/net/nft_rule.cc new file mode 100644 index 00000000000..e5292ec7918 --- /dev/null +++ b/base/cvd/allocd/net/nft_rule.cc @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "allocd/net/nft_rule.h" + +#include +#include +#include + +#include "absl/log/log.h" +#include "absl/strings/str_cat.h" + +#include "allocd/net/nftables.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { +namespace { + +// Namespace prefix applied to every dynamic cvdalloc rule comment. +constexpr std::string_view kCvdallocCommentPrefix = "cvdalloc-"; + +} // namespace + +Result NftRule::Create(Nftables& nft, std::string_view family, + std::string_view table, std::string_view chain, + std::string_view content, + std::string_view tag) { + std::string comment = absl::StrCat(kCvdallocCommentPrefix, tag); + CF_EXPECT(nft.AddRule(family, table, chain, content, comment)); + return NftRule(&nft, family, table, chain, std::move(comment)); +} + +NftRule::NftRule(Nftables* nft, std::string_view family, std::string_view table, + std::string_view chain, std::string comment) + : nft_(nft), + family_(family), + table_(table), + chain_(chain), + comment_(std::move(comment)) {} + +NftRule::NftRule(NftRule&& r) noexcept + : nft_(std::exchange(r.nft_, nullptr)), + family_(std::move(r.family_)), + table_(std::move(r.table_)), + chain_(std::move(r.chain_)), + comment_(std::move(r.comment_)) {} + +NftRule::~NftRule() { + // nft_ is nulled on move, so a moved-from rule is inert and never deletes. + if (nft_ != nullptr) { + auto res = nft_->DeleteRulesByComment(family_, table_, chain_, comment_); + if (!res.has_value()) { + LOG(ERROR) << "Failed to delete nft rule(s) in NftRule destructor: " + << res.error(); + } + } +} + +} // namespace cuttlefish diff --git a/base/cvd/allocd/net/nft_rule.h b/base/cvd/allocd/net/nft_rule.h new file mode 100644 index 00000000000..c2c5030ded7 --- /dev/null +++ b/base/cvd/allocd/net/nft_rule.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ALLOCD_NET_NFT_RULE_H_ +#define ALLOCD_NET_NFT_RULE_H_ + +#include +#include + +#include "allocd/net/nftables.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { + +class NftRule { + public: + // `tag` is a caller-supplied unique identifier (e.g. an interface name). + static Result Create(Nftables& nft, std::string_view family, + std::string_view table, std::string_view chain, + std::string_view content, std::string_view tag); + + ~NftRule(); + + NftRule(NftRule&& r) noexcept; + NftRule& operator=(NftRule&& r) = delete; + NftRule(const NftRule& r) = delete; + NftRule& operator=(const NftRule& r) = delete; + + private: + NftRule(Nftables* nft, std::string_view family, std::string_view table, + std::string_view chain, std::string comment); + + Nftables* nft_ = nullptr; + std::string family_; + std::string table_; + std::string chain_; + std::string comment_; +}; + +} // namespace cuttlefish + +#endif // ALLOCD_NET_NFT_RULE_H_ diff --git a/base/cvd/allocd/net/nft_rule_test.cc b/base/cvd/allocd/net/nft_rule_test.cc new file mode 100644 index 00000000000..cd3d5c8a75a --- /dev/null +++ b/base/cvd/allocd/net/nft_rule_test.cc @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "allocd/net/nft_rule.h" + +#include +#include + +#include + +#include "allocd/test/fake_nftables.h" +#include "cuttlefish/result/result.h" +#include "cuttlefish/result/result_matchers.h" + +namespace cuttlefish { +namespace { + +class NftRuleTest : public ::testing::Test { + protected: + void SetUp() override { + ASSERT_THAT(fake_.EnsureTable("ip", "table1"), IsOk()); + ASSERT_THAT(fake_.EnsureChain("ip", "table1", "chain1", ""), IsOk()); + } + + FakeNftables fake_; +}; + +TEST_F(NftRuleTest, CreateAddsRuleWithPrefixedComment) { + Result rule = + NftRule::Create(fake_, "ip", "table1", "chain1", "content1", "tag1"); + ASSERT_THAT(rule, IsOk()); + EXPECT_TRUE( + fake_.HasRuleWithComment("ip", "table1", "chain1", "cvdalloc-tag1")); +} + +TEST_F(NftRuleTest, DeletesRuleOnDestruction) { + { + Result rule = + NftRule::Create(fake_, "ip", "table1", "chain1", "content1", "tag1"); + ASSERT_THAT(rule, IsOk()); + EXPECT_TRUE( + fake_.HasRuleWithComment("ip", "table1", "chain1", "cvdalloc-tag1")); + } + EXPECT_FALSE( + fake_.HasRuleWithComment("ip", "table1", "chain1", "cvdalloc-tag1")); + EXPECT_EQ(fake_.RuleCount("ip", "table1", "chain1"), 0); +} + +TEST_F(NftRuleTest, MoveConstructorTransfersOwnership) { + { + Result rule1 = + NftRule::Create(fake_, "ip", "table1", "chain1", "content1", "tag1"); + ASSERT_THAT(rule1, IsOk()); + + NftRule rule2(std::move(*rule1)); + EXPECT_TRUE( + fake_.HasRuleWithComment("ip", "table1", "chain1", "cvdalloc-tag1")); + } + EXPECT_FALSE( + fake_.HasRuleWithComment("ip", "table1", "chain1", "cvdalloc-tag1")); + EXPECT_EQ(fake_.RuleCount("ip", "table1", "chain1"), 0); +} + +} // namespace +} // namespace cuttlefish diff --git a/base/cvd/allocd/net/nftables.h b/base/cvd/allocd/net/nftables.h new file mode 100644 index 00000000000..fc4384495f6 --- /dev/null +++ b/base/cvd/allocd/net/nftables.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ALLOCD_NET_NFTABLES_H_ +#define ALLOCD_NET_NFTABLES_H_ + +#include + +#include + +#include "cuttlefish/result/result.h" + +namespace cuttlefish { + +class Nftables { + public: + virtual ~Nftables() = default; + + virtual Result EnsureTable(std::string_view family, + std::string_view table) = 0; + virtual Result DeleteTable(std::string_view family, + std::string_view table) = 0; + virtual Result EnsureChain(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view content) = 0; + virtual Result AddRule(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view content, + std::string_view comment) = 0; + virtual Result DeleteRule(std::string_view family, + std::string_view table, + std::string_view chain, uint64_t handle) = 0; + virtual Result DeleteRulesByComment(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view comment) = 0; +}; + +} // namespace cuttlefish + +#endif // ALLOCD_NET_NFTABLES_H_ diff --git a/base/cvd/allocd/net/nftables_nft.cc b/base/cvd/allocd/net/nftables_nft.cc new file mode 100644 index 00000000000..03f0c8458d9 --- /dev/null +++ b/base/cvd/allocd/net/nftables_nft.cc @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "allocd/net/nftables_nft.h" + +#include + +#include +#include +#include +#include + +#include "absl/base/no_destructor.h" +#include "absl/log/log.h" +#include "absl/strings/str_cat.h" +#include "json/value.h" + +#include "cuttlefish/common/libs/utils/files.h" +#include "cuttlefish/common/libs/utils/json.h" +#include "cuttlefish/process/command.h" +#include "cuttlefish/process/managed_stdio.h" +#include "cuttlefish/process/subprocess.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { + +namespace { + +constexpr std::string_view kNftBinary = "nft"; + +// nft usually lives in an sbin directory, which is often not on PATH for +// non-root callers, so fall back to the usual sbin locations rather than +// bailing out if we don't find it at the PATH. +Result SearchForNft() { + Result p = Search(Path(), std::string(kNftBinary)); + if (p.has_value()) { + return p; + } + return CF_EXPECT(Search({"/usr/sbin", "/sbin"}, std::string(kNftBinary)), + "could not find nft binary"); +} + +} // namespace + +Result NftablesNft::BinaryPath() { + static const absl::NoDestructor path( + SearchForNft().value_or("")); + CF_EXPECT(!path->empty(), "could not find nft binary"); + return *path; +} + +Result NftablesNft::EnsureTable(std::string_view family, + std::string_view table) { + Command cmd = Command(CF_EXPECT(BinaryPath())) + .AddParameter("add") + .AddParameter("table") + .AddParameter(family) + .AddParameter(table); + + CF_EXPECTF(cmd.Start().Wait() == 0, + "Failed to ensure nft table: family={}, table={}", family, table); + return {}; +} + +Result NftablesNft::DeleteTable(std::string_view family, + std::string_view table) { + Command cmd = Command(CF_EXPECT(BinaryPath())) + .AddParameter("delete") + .AddParameter("table") + .AddParameter(family) + .AddParameter(table); + + CF_EXPECTF(cmd.Start().Wait() == 0, + "Failed to delete nft table: family={}, table={}", family, table); + return {}; +} + +Result NftablesNft::EnsureChain(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view content) { + Command cmd = Command(CF_EXPECT(BinaryPath())) + .AddParameter("add") + .AddParameter("chain") + .AddParameter(family) + .AddParameter(table) + .AddParameter(chain); + if (!content.empty()) { + cmd.AddParameter(content); + } + + CF_EXPECTF( + cmd.Start().Wait() == 0, + "Failed to ensure nft chain: family={}, table={}, chain={}, content={}", + family, table, chain, content); + return {}; +} + +Result NftablesNft::AddRule(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view content, + std::string_view comment) { + std::string rule = comment.empty() + ? std::string(content) + : absl::StrCat(content, " comment \"", comment, "\""); + + Command cmd = Command(CF_EXPECT(BinaryPath())) + .AddParameter("-j") + .AddParameter("-e") + .AddParameter("add") + .AddParameter("rule") + .AddParameter(family) + .AddParameter(table) + .AddParameter(chain) + .AddParameter(rule); + + std::string stdout_str = CF_EXPECT(RunAndCaptureStdout(std::move(cmd))); + Json::Value json = CF_EXPECT(ParseJson(stdout_str)); + + CF_EXPECTF(json.isMember("nftables") && json["nftables"].isArray(), + "Invalid JSON output from nft: {}", stdout_str); + + for (const auto& item : json["nftables"]) { + if (item.isMember("add") && item["add"].isMember("rule") && + item["add"]["rule"].isMember("handle")) { + return item["add"]["rule"]["handle"].asUInt64(); + } + } + + return CF_ERRF("No rule handle found in nft JSON output: {}", stdout_str); +} + +Result NftablesNft::DeleteRule(std::string_view family, + std::string_view table, + std::string_view chain, uint64_t handle) { + Command cmd = Command(CF_EXPECT(BinaryPath())) + .AddParameter("delete") + .AddParameter("rule") + .AddParameter(family) + .AddParameter(table) + .AddParameter(chain) + .AddParameter("handle") + .AddParameter(std::to_string(handle)); + + CF_EXPECTF(cmd.Start().Wait() == 0, + "Failed to delete nft rule: family={}, table={}, chain={}, " + "handle={}", + family, table, chain, handle); + return {}; +} + +Result NftablesNft::DeleteRulesByComment(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view comment) { + Command cmd = Command(CF_EXPECT(BinaryPath())) + .AddParameter("-j") + .AddParameter("list") + .AddParameter("chain") + .AddParameter(family) + .AddParameter(table) + .AddParameter(chain); + + // If the chain/table no longer exists (e.g. torn down already), there is + // nothing to delete; treat that as success so teardown stays idempotent. + Result stdout_str = RunAndCaptureStdout(std::move(cmd)); + if (!stdout_str.has_value()) { + LOG(INFO) << "nft list chain failed, treating as no-op: family=" << family + << ", table=" << table << ", chain=" << chain; + return {}; + } + + Json::Value json = CF_EXPECT(ParseJson(*stdout_str)); + CF_EXPECTF(json.isMember("nftables") && json["nftables"].isArray(), + "Invalid JSON output from nft: {}", *stdout_str); + + std::vector handles; + for (const auto& item : json["nftables"]) { + if (!item.isMember("rule")) { + continue; + } + const Json::Value& rule = item["rule"]; + if (rule.isMember("comment") && rule.isMember("handle") && + rule["comment"].asString() == comment) { + handles.push_back(rule["handle"].asUInt64()); + } + } + + Result deletion_result = {}; + for (uint64_t handle : handles) { + Result res = DeleteRule(family, table, chain, handle); + if (!res.has_value()) { + LOG(ERROR) << "Failed to delete nft rule, continuing: handle=" << handle + << ": " << res.error(); + if (deletion_result.has_value()) { + deletion_result = std::move(res); + } + } + } + + return deletion_result; +} + +} // namespace cuttlefish diff --git a/base/cvd/allocd/net/nftables_nft.h b/base/cvd/allocd/net/nftables_nft.h new file mode 100644 index 00000000000..166eb7a1275 --- /dev/null +++ b/base/cvd/allocd/net/nftables_nft.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ALLOCD_NET_NFTABLES_NFT_H_ +#define ALLOCD_NET_NFTABLES_NFT_H_ + +#include + +#include +#include + +#include "allocd/net/nftables.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { + +class NftablesNft : public Nftables { + public: + NftablesNft() = default; + ~NftablesNft() override = default; + + // Returns the resolved path to the `nft` binary, or an error if it is not + // available. Static so callers can probe for nft support without having to + // construct an instance. + static Result BinaryPath(); + + Result EnsureTable(std::string_view family, + std::string_view table) override; + Result DeleteTable(std::string_view family, + std::string_view table) override; + Result EnsureChain(std::string_view family, std::string_view table, + std::string_view chain, + std::string_view content) override; + Result AddRule(std::string_view family, std::string_view table, + std::string_view chain, std::string_view content, + std::string_view comment) override; + Result DeleteRule(std::string_view family, std::string_view table, + std::string_view chain, uint64_t handle) override; + Result DeleteRulesByComment(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view comment) override; +}; + +} // namespace cuttlefish + +#endif // ALLOCD_NET_NFTABLES_NFT_H_ diff --git a/base/cvd/allocd/test/BUILD.bazel b/base/cvd/allocd/test/BUILD.bazel new file mode 100644 index 00000000000..281da74f348 --- /dev/null +++ b/base/cvd/allocd/test/BUILD.bazel @@ -0,0 +1,26 @@ +load("//cuttlefish/bazel:rules.bzl", "cf_cc_library", "cf_cc_test") + +package( + default_visibility = ["//:android_cuttlefish"], +) + +cf_cc_library( + name = "fake_nftables", + testonly = True, + srcs = ["fake_nftables.cc"], + hdrs = ["fake_nftables.h"], + deps = [ + "//allocd/net:nftables", + "//cuttlefish/result", + ], +) + +cf_cc_test( + name = "fake_nftables_test", + srcs = ["fake_nftables_test.cc"], + deps = [ + ":fake_nftables", + "//cuttlefish/result", + "//cuttlefish/result:result_matchers", + ], +) diff --git a/base/cvd/allocd/test/fake_nftables.cc b/base/cvd/allocd/test/fake_nftables.cc new file mode 100644 index 00000000000..8f41b04167d --- /dev/null +++ b/base/cvd/allocd/test/fake_nftables.cc @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "allocd/test/fake_nftables.h" + +#include + +#include +#include +#include +#include + +#include "cuttlefish/result/result.h" + +namespace cuttlefish { + +const FakeNftables::Table* FakeNftables::FindTable( + std::string_view family, std::string_view table) const { + auto it = tables_.find({std::string(family), std::string(table)}); + return it == tables_.end() ? nullptr : &it->second; +} + +FakeNftables::Table* FakeNftables::FindTable(std::string_view family, + std::string_view table) { + auto it = tables_.find({std::string(family), std::string(table)}); + return it == tables_.end() ? nullptr : &it->second; +} + +Result FakeNftables::EnsureTable(std::string_view family, + std::string_view table) { + tables_.try_emplace({std::string(family), std::string(table)}); + return {}; +} + +Result FakeNftables::DeleteTable(std::string_view family, + std::string_view table) { + auto it = tables_.find({std::string(family), std::string(table)}); + CF_EXPECTF(it != tables_.end(), "no such table: family={}, table={}", family, + table); + // Erasing the table drops its chains/rules; a later EnsureTable starts a + // fresh handle counter, exactly as nft does when a table is recreated. + tables_.erase(it); + return {}; +} + +Result FakeNftables::EnsureChain(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view /*content*/) { + Table* t = FindTable(family, table); + CF_EXPECTF(t != nullptr, "no such table: family={}, table={}", family, table); + t->chains.try_emplace(std::string(chain)); + return {}; +} + +Result FakeNftables::AddRule(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view content, + std::string_view comment) { + Table* t = FindTable(family, table); + CF_EXPECTF(t != nullptr, "no such table: family={}, table={}", family, table); + auto chain_it = t->chains.find(chain); + CF_EXPECTF(chain_it != t->chains.end(), + "no such chain: family={}, table={}, chain={}", family, table, + chain); + uint64_t handle = t->next_handle++; + chain_it->second.push_back( + Rule{handle, std::string(content), std::string(comment)}); + return handle; +} + +Result FakeNftables::DeleteRule(std::string_view family, + std::string_view table, + std::string_view chain, uint64_t handle) { + Table* t = FindTable(family, table); + CF_EXPECTF(t != nullptr, "no such table: family={}, table={}", family, table); + auto chain_it = t->chains.find(chain); + CF_EXPECTF(chain_it != t->chains.end(), + "no such chain: family={}, table={}, chain={}", family, table, + chain); + std::vector::size_type removed = std::erase_if( + chain_it->second, [&](const Rule& r) { return r.handle == handle; }); + CF_EXPECTF(removed > 0, "no rule with handle {}", handle); + return {}; +} + +Result FakeNftables::DeleteRulesByComment(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view comment) { + // A missing table/chain or a comment that matches nothing is a no-op, + // matching the idempotent teardown semantics of the real implementation. + Table* t = FindTable(family, table); + if (t == nullptr) { + return {}; + } + auto chain_it = t->chains.find(chain); + if (chain_it == t->chains.end()) { + return {}; + } + std::erase_if(chain_it->second, + [&](const Rule& r) { return r.comment == comment; }); + return {}; +} + +bool FakeNftables::HasTable(std::string_view family, + std::string_view table) const { + return FindTable(family, table) != nullptr; +} + +bool FakeNftables::HasChain(std::string_view family, std::string_view table, + std::string_view chain) const { + const Table* t = FindTable(family, table); + return t != nullptr && t->chains.contains(chain); +} + +bool FakeNftables::HasRuleWithComment(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view comment) const { + const Table* t = FindTable(family, table); + if (t == nullptr) { + return false; + } + auto chain_it = t->chains.find(chain); + if (chain_it == t->chains.end()) { + return false; + } + return std::any_of(chain_it->second.begin(), chain_it->second.end(), + [&](const Rule& r) { return r.comment == comment; }); +} + +int FakeNftables::RuleCount(std::string_view family, std::string_view table, + std::string_view chain) const { + const Table* t = FindTable(family, table); + if (t == nullptr) { + return 0; + } + auto chain_it = t->chains.find(chain); + if (chain_it == t->chains.end()) { + return 0; + } + return static_cast(chain_it->second.size()); +} + +} // namespace cuttlefish diff --git a/base/cvd/allocd/test/fake_nftables.h b/base/cvd/allocd/test/fake_nftables.h new file mode 100644 index 00000000000..a79ffe6de7e --- /dev/null +++ b/base/cvd/allocd/test/fake_nftables.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ALLOCD_TEST_FAKE_NFTABLES_H_ +#define ALLOCD_TEST_FAKE_NFTABLES_H_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "allocd/net/nftables.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { + +// A stateful, in-memory Nftables test double. +// Statefulness is needed for testing behaviour around handles. +class FakeNftables : public Nftables { + public: + Result EnsureTable(std::string_view family, + std::string_view table) override; + Result DeleteTable(std::string_view family, + std::string_view table) override; + Result EnsureChain(std::string_view family, std::string_view table, + std::string_view chain, + std::string_view content) override; + Result AddRule(std::string_view family, std::string_view table, + std::string_view chain, std::string_view content, + std::string_view comment) override; + Result DeleteRule(std::string_view family, std::string_view table, + std::string_view chain, uint64_t handle) override; + Result DeleteRulesByComment(std::string_view family, + std::string_view table, + std::string_view chain, + std::string_view comment) override; + + bool HasTable(std::string_view family, std::string_view table) const; + bool HasChain(std::string_view family, std::string_view table, + std::string_view chain) const; + bool HasRuleWithComment(std::string_view family, std::string_view table, + std::string_view chain, + std::string_view comment) const; + int RuleCount(std::string_view family, std::string_view table, + std::string_view chain) const; + + private: + struct Rule { + uint64_t handle; + std::string content; + std::string comment; + }; + struct Table { + std::map, std::less<>> chains; + uint64_t next_handle = 1; + }; + using TableKey = std::pair; + + const Table* FindTable(std::string_view family, std::string_view table) const; + Table* FindTable(std::string_view family, std::string_view table); + + std::map tables_; +}; + +} // namespace cuttlefish + +#endif // ALLOCD_TEST_FAKE_NFTABLES_H_ diff --git a/base/cvd/allocd/test/fake_nftables_test.cc b/base/cvd/allocd/test/fake_nftables_test.cc new file mode 100644 index 00000000000..4295417714d --- /dev/null +++ b/base/cvd/allocd/test/fake_nftables_test.cc @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "allocd/test/fake_nftables.h" + +#include +#include +#include + +#include + +#include "cuttlefish/result/result.h" +#include "cuttlefish/result/result_matchers.h" + +namespace cuttlefish { +namespace { + +constexpr std::string_view kFamily = "ip"; +constexpr std::string_view kTable = "t"; +constexpr std::string_view kChain = "c"; + +class FakeNftablesTest : public ::testing::Test { + protected: + void CreateReadyChain() { + ASSERT_THAT(fake_.EnsureTable(kFamily, kTable), IsOk()); + ASSERT_THAT(fake_.EnsureChain(kFamily, kTable, kChain, ""), IsOk()); + } + + FakeNftables fake_; +}; + +TEST_F(FakeNftablesTest, EnsureTableIsIdempotent) { + ASSERT_THAT(fake_.EnsureTable(kFamily, kTable), IsOk()); + EXPECT_THAT(fake_.EnsureTable(kFamily, kTable), IsOk()); + EXPECT_TRUE(fake_.HasTable(kFamily, kTable)); +} + +TEST_F(FakeNftablesTest, EnsureChainIsIdempotent) { + ASSERT_THAT(fake_.EnsureTable(kFamily, kTable), IsOk()); + EXPECT_THAT(fake_.EnsureChain(kFamily, kTable, kChain, ""), IsOk()); + EXPECT_THAT(fake_.EnsureChain(kFamily, kTable, kChain, ""), IsOk()); + EXPECT_TRUE(fake_.HasChain(kFamily, kTable, kChain)); +} + +TEST_F(FakeNftablesTest, EnsureChainRequiresTable) { + EXPECT_THAT(fake_.EnsureChain(kFamily, "missing", kChain, ""), IsError()); +} + +TEST_F(FakeNftablesTest, AddRuleRequiresTableAndChain) { + CreateReadyChain(); + EXPECT_THAT(fake_.AddRule(kFamily, "missing", kChain, "content", "cmt"), + IsError()); + EXPECT_THAT(fake_.AddRule(kFamily, kTable, "missing", "content", "cmt"), + IsError()); +} + +TEST_F(FakeNftablesTest, HandlesAreUniqueAndMonotonic) { + CreateReadyChain(); + Result h1 = fake_.AddRule(kFamily, kTable, kChain, "content1", "a"); + Result h2 = fake_.AddRule(kFamily, kTable, kChain, "content2", "b"); + ASSERT_THAT(h1, IsOk()); + ASSERT_THAT(h2, IsOk()); + EXPECT_LT(*h1, *h2); +} + +TEST_F(FakeNftablesTest, DeleteRuleByHandleRemovesOnlyThatRule) { + CreateReadyChain(); + Result h1 = fake_.AddRule(kFamily, kTable, kChain, "content1", "a"); + ASSERT_THAT(h1, IsOk()); + ASSERT_THAT(fake_.AddRule(kFamily, kTable, kChain, "content2", "b"), IsOk()); + EXPECT_EQ(fake_.RuleCount(kFamily, kTable, kChain), 2); + + EXPECT_THAT(fake_.DeleteRule(kFamily, kTable, kChain, *h1), IsOk()); + EXPECT_EQ(fake_.RuleCount(kFamily, kTable, kChain), 1); + EXPECT_FALSE(fake_.HasRuleWithComment(kFamily, kTable, kChain, "a")); + EXPECT_TRUE(fake_.HasRuleWithComment(kFamily, kTable, kChain, "b")); +} + +TEST_F(FakeNftablesTest, DeleteRuleWithUnknownHandleFails) { + CreateReadyChain(); + EXPECT_THAT(fake_.DeleteRule(kFamily, kTable, kChain, 9999), IsError()); +} + +TEST_F(FakeNftablesTest, DeleteRulesByCommentRemovesAllMatching) { + CreateReadyChain(); + ASSERT_THAT(fake_.AddRule(kFamily, kTable, kChain, "content1", "shared"), + IsOk()); + ASSERT_THAT(fake_.AddRule(kFamily, kTable, kChain, "content2", "shared"), + IsOk()); + ASSERT_THAT(fake_.AddRule(kFamily, kTable, kChain, "content3", "other"), + IsOk()); + + EXPECT_THAT(fake_.DeleteRulesByComment(kFamily, kTable, kChain, "shared"), + IsOk()); + EXPECT_FALSE(fake_.HasRuleWithComment(kFamily, kTable, kChain, "shared")); + EXPECT_TRUE(fake_.HasRuleWithComment(kFamily, kTable, kChain, "other")); + EXPECT_EQ(fake_.RuleCount(kFamily, kTable, kChain), 1); +} + +TEST_F(FakeNftablesTest, DeleteRulesByCommentIsIdempotent) { + CreateReadyChain(); + // No matching comment, existing chain: no-op success. + EXPECT_THAT(fake_.DeleteRulesByComment(kFamily, kTable, kChain, "nope"), + IsOk()); + // Missing chain and missing table: still no-op success. + EXPECT_THAT(fake_.DeleteRulesByComment(kFamily, kTable, "missing", "x"), + IsOk()); + EXPECT_THAT(fake_.DeleteRulesByComment(kFamily, "missing", kChain, "x"), + IsOk()); +} + +TEST_F(FakeNftablesTest, DeleteTableFailsWhenMissing) { + EXPECT_THAT(fake_.DeleteTable(kFamily, "missing"), IsError()); +} + +TEST_F(FakeNftablesTest, RecreatingTableResetsHandleCounter) { + CreateReadyChain(); + Result first = + fake_.AddRule(kFamily, kTable, kChain, "content", "a"); + ASSERT_THAT(first, IsOk()); + + ASSERT_THAT(fake_.DeleteTable(kFamily, kTable), IsOk()); + EXPECT_FALSE(fake_.HasTable(kFamily, kTable)); + + CreateReadyChain(); + Result again = + fake_.AddRule(kFamily, kTable, kChain, "content", "a"); + EXPECT_THAT(again, IsOkAndValue(*first)); +} + +TEST_F(FakeNftablesTest, TablesAreIsolatedByFamilyAndName) { + CreateReadyChain(); + ASSERT_THAT(fake_.EnsureTable("bridge", kTable), IsOk()); + ASSERT_THAT(fake_.EnsureChain("bridge", kTable, kChain, ""), IsOk()); + ASSERT_THAT(fake_.AddRule(kFamily, kTable, kChain, "content", "a"), IsOk()); + + EXPECT_EQ(fake_.RuleCount(kFamily, kTable, kChain), 1); + EXPECT_EQ(fake_.RuleCount("bridge", kTable, kChain), 0); +} + +} // namespace +} // namespace cuttlefish