diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7525d02..6ff2fda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,9 @@ jobs: - name: Install dependencies run: | sudo apt-get update && sudo apt-get install -yq \ - clang + clang-15 + sudo ln -sf /usr/bin/clang-15 /usr/bin/clang + sudo ln -sf /usr/bin/clang++-15 /usr/bin/clang++ - name: Mount bazel cache uses: actions/cache/restore@v4 @@ -50,8 +52,14 @@ jobs: format: X - name: Build + env: + CC: clang-15 + CXX: clang++-15 run: bazel build --test_output=errors //... - name: Test + env: + CC: clang-15 + CXX: clang++-15 run: bazel test --test_output=errors //... - name: Save end time diff --git a/netkat/BUILD.bazel b/netkat/BUILD.bazel index 9121fef..e939af6 100644 --- a/netkat/BUILD.bazel +++ b/netkat/BUILD.bazel @@ -107,6 +107,34 @@ cc_test( ], ) +cc_library( + name = "table_builder", + srcs = ["table_builder.cc"], + hdrs = ["table_builder.h"], + deps = [ + ":table", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_gutil//gutil:proto", + ], +) + +cc_test( + name = "table_builder_test", + srcs = ["table_builder_test.cc"], + deps = [ + ":analysis_engine", + ":frontend", + ":table", + ":table_builder", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_googletest//:gtest_main", + ], +) + cc_test( name = "netkat_test", srcs = ["netkat_test.cc"], diff --git a/netkat/manager_handle_pattern.md b/netkat/manager_handle_pattern.md index e2c81b4..98b8437 100644 --- a/netkat/manager_handle_pattern.md +++ b/netkat/manager_handle_pattern.md @@ -26,19 +26,28 @@ combining them, or inspecting the underlying sets, one must call methods on the manager class, which acts as an arena allocator that owns all memory associated with the handles. -For example: ``` // We need a manager to construct handles. PacketSetManager -manager; PacketSetHandle a = manager.EmptySet() PacketSetHandle b = -manager.Match("src_mac", 0xFF'FF'FF'FF); +For example: -// Handles can be compared and hashed without the help of the manager, // but -that's about it. CHECK(a != b); absl::flat_hash_map ab_set{a, -b}; +``` +// We need a manager to construct handles. +PacketSetManager manager; +PacketSetHandle a = manager.EmptySet() +PacketSetHandle b = manager.Match("src_mac", 0xFF'FF'FF'FF); + +// Handles can be compared and hashed without the help of the manager, +// but that's about it. +CHECK(a != b); +absl::flat_hash_map ab_set{a, b}; // To do interesting things with the handles, we need the manager. -PacketSetHandle c = manager.And(a, b); // The set union of `a` and `b`. -PacketSetHandle not_c = manager.Not(c); // The set complement of `c`. if -(manager.Contains(c, packet)) { CHECK(!manager.Contains(not_c, packet)); } else -{ CHECK(manager.Contains(not_c, packet)); } ``` +PacketSetHandle c = manager.And(a, b); // The set union of `a` and `b`. +PacketSetHandle not_c = manager.Not(c); // The set complement of `c`. +if (manager.Contains(c, packet)) { + CHECK(!manager.Contains(not_c, packet)); +} else { + CHECK(manager.Contains(not_c, packet)); +} +``` ## Motivation for Using the Pattern diff --git a/netkat/table.cc b/netkat/table.cc index 4b19b4b..b2369cf 100644 --- a/netkat/table.cc +++ b/netkat/table.cc @@ -206,6 +206,10 @@ absl::Status NetkatTable::AddRule(int priority, Predicate match, return absl::OkStatus(); } +absl::Status NetkatTable::AddRule(Rule rule) { + return AddRule(rule.priority, std::move(rule.match), std::move(rule.action)); +} + Policy NetkatTable::GetPolicy() const& { return GetPolicyInternal(rules_, accept_default_ ? Policy::Accept() : Policy::Deny()); diff --git a/netkat/table.h b/netkat/table.h index 6f7ad33..7804510 100644 --- a/netkat/table.h +++ b/netkat/table.h @@ -70,6 +70,16 @@ class NetkatTable { const Policy* existing_policy; }; + // A rule to be added to the table. + struct Rule { + // The priority the rule is being added for. + int priority; + // The predicate that the rule will match against. + Predicate match; + // The policy that the rule will apply to any matching packets. + Policy action; + }; + // A functor that represents some constraint to be applied or evaluated // against a pending rule of the table. The constraint must return either an // error detailing the cause of the violation or OK if there is none. @@ -131,6 +141,7 @@ class NetkatTable { // TODO(anthonyroy): Consider a more API-friendly way to disallow `action` // from being more restrictive than `match`. absl::Status AddRule(int priority, Predicate match, Policy action); + absl::Status AddRule(Rule rule); // Returns a unified policy representing all rules in the NetkatTable. The // returned policy will emulate a priority based match-action table of a diff --git a/netkat/table_builder.cc b/netkat/table_builder.cc new file mode 100644 index 0000000..4ea5b04 --- /dev/null +++ b/netkat/table_builder.cc @@ -0,0 +1,73 @@ +// Copyright 2026 The NetKAT authors +// +// 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 +// +// https://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 "netkat/table_builder.h" + +#include // NOLINT - absl::SourceLocation not available yet. +#include +#include + +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "gutil/proto.h" +#include "netkat/table.h" + +namespace netkat { + +NetkatTableBuilder::NetkatTableBuilder(NetkatTable& table) : table_(table) {} + +NetkatTableBuilder& NetkatTableBuilder::LogRules() { + LOG(INFO) << "Rules in the table builder: \n" + << absl::StrJoin( + located_rules_, "\n", + [](std::string* out, const LocatedRule& located_rule) { + const NetkatTable::Rule& rule = located_rule.rule; + absl::StrAppend( + out, "priority: ", rule.priority, "\nmatch: ", + gutil::PrintTextProto(rule.match.GetProto()), + "action: ", + gutil::PrintTextProto(rule.action.GetProto())); + }); + return *this; +} + +NetkatTableBuilder& NetkatTableBuilder::AddRule(NetkatTable::Rule rule, + std::source_location loc) { + located_rules_.push_back({std::move(rule), loc}); + return *this; +} + +absl::Status NetkatTableBuilder::InstallRules() { + NetkatTable temp_table(table_); + for (LocatedRule& located_rule : located_rules_) { + std::source_location location = located_rule.location; + absl::Status status = temp_table.AddRule(std::move(located_rule.rule)); + if (!status.ok()) { + // Clear all the located rules in the table builder to avoid installing an + // invalid table entry accidentally in a later call to `InstallRules`. + located_rules_.clear(); + return absl::Status( + status.code(), + absl::StrCat(location.file_name(), ":", location.line(), + ": Failed to install table rule: ", status.message())); + } + } + table_ = std::move(temp_table); + return absl::OkStatus(); +} + +} // namespace netkat diff --git a/netkat/table_builder.h b/netkat/table_builder.h new file mode 100644 index 0000000..2479a5a --- /dev/null +++ b/netkat/table_builder.h @@ -0,0 +1,69 @@ +// Copyright 2026 The NetKAT authors +// +// 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 +// +// https://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 GOOGLE_NETKAT_NETKAT_TABLE_BUILDER_H_ +#define GOOGLE_NETKAT_NETKAT_TABLE_BUILDER_H_ + +#include // NOLINT - absl::SourceLocation not available yet. +#include + +#include "absl/status/status.h" +#include "netkat/table.h" + +namespace netkat { + +// A builder class for modifying an existing NetkatTable using a fluent +// interface. Modifications are applied atomically: if any rule fails to be +// installed (e.g., due to a constraint violation), the underlying table remains +// completely unmodified. +class NetkatTableBuilder { + public: + // Creates a NetkatTableBuilder that modifies the given `table`. Note that + // NetkatTable must outlive the NetkatTableBuilder. + explicit NetkatTableBuilder(NetkatTable& table); + + // Logs the current rules in the NetkatTableBuilder to LOG(INFO). + NetkatTableBuilder& LogRules(); + + // Adds a rule to be inserted into the table at the given priority. + NetkatTableBuilder& AddRule( + NetkatTable::Rule rule, + std::source_location loc = std::source_location::current()); + + // Moves all buffered rules into the table atomically. Returns an error + // with the source location of the caller if any rule cannot be successfully + // added. If any rule fails to be installed (e.g., due to a constraint + // violation), the underlying table remains completely unmodified, + // NetkatTableBuilder dumps all the rules that were added to it, and an error + // is returned. + absl::Status InstallRules(); + + private: + friend class NetkatTable; + + // A rule with its source location, for error reporting. + struct LocatedRule { + // A rule to be installed in the table. + NetkatTable::Rule rule; + // The source location of the rule, for error reporting. + std::source_location location; + }; + + std::vector located_rules_; + NetkatTable& table_; +}; + +} // namespace netkat + +#endif // GOOGLE_NETKAT_NETKAT_TABLE_BUILDER_H_ diff --git a/netkat/table_builder_test.cc b/netkat/table_builder_test.cc new file mode 100644 index 0000000..7a0f82a --- /dev/null +++ b/netkat/table_builder_test.cc @@ -0,0 +1,156 @@ +// Copyright 2026 The NetKAT authors +// +// 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 +// +// https://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 "netkat/table_builder.h" + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "netkat/analysis_engine.h" +#include "netkat/frontend.h" +#include "netkat/table.h" + +namespace netkat { +namespace { + +using ::absl_testing::StatusIs; + +TEST(NetkatTableBuilderTest, BuildEmptyTable) { + NetkatTable table; + ASSERT_OK(NetkatTableBuilder(table).LogRules().InstallRules()); + + AnalysisEngine engine; + EXPECT_TRUE( + engine.CheckEquivalent(table.GetPolicy(), Policy::Deny()).IsSuccess()); +} + +TEST(NetkatTableBuilderTest, BuildTableWithDefaultPolicy) { + NetkatTable table({}, /*accept_default=*/true); + ASSERT_OK(NetkatTableBuilder(table).LogRules().InstallRules()); + + AnalysisEngine engine; + EXPECT_TRUE( + engine.CheckEquivalent(table.GetPolicy(), Policy::Accept()).IsSuccess()); +} + +TEST(NetkatTableBuilderTest, BuildTableWithRulesAndDefaultPolicy) { + NetkatTable table({}, /*accept_default=*/true); + + NetkatTable::Rule rule1 = { + .priority = 0, + .match = Match("port", 0), + .action = Modify("vrf", 1), + }; + NetkatTable::Rule rule2 = { + .priority = 0, + .match = Match("port", 1), + .action = Modify("vrf", 2), + }; + + ASSERT_OK(NetkatTableBuilder(table) + .AddRule(rule1) + .AddRule(rule2) + .LogRules() + .InstallRules()); + + NetkatTable expected_table({}, /*accept_default=*/true); + ASSERT_OK(expected_table.AddRule(rule1)); + ASSERT_OK(expected_table.AddRule(rule2)); + + AnalysisEngine engine; + EXPECT_TRUE( + engine.CheckEquivalent(table.GetPolicy(), expected_table.GetPolicy()) + .IsSuccess()); +} + +TEST(NetkatTableBuilderTest, CustomConstraintPropagatesError) { + NetkatTable table({[](const NetkatTable::PendingRuleInfo& info) { + if (info.priority > 10) return absl::InvalidArgumentError("Bad priority."); + return absl::OkStatus(); + }}); + EXPECT_THAT(NetkatTableBuilder(table) + .AddRule({ + .priority = 11, + .match = Match("port", 2), + .action = Modify("vrf", 3), + }) + .LogRules() + .InstallRules(), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(NetkatTableBuilderTest, TableRemainsUnchangedWhenTableBuilderFails) { + NetkatTable table({[](const NetkatTable::PendingRuleInfo& info) { + if (info.priority > 10) return absl::InvalidArgumentError("Bad priority."); + return absl::OkStatus(); + }}); + ASSERT_OK(NetkatTableBuilder(table) + .AddRule({ + .priority = 0, + .match = Match("port", 1), + .action = Modify("vrf", 2), + }) + .InstallRules()); + + NetkatTable expected_table = table; + ASSERT_THAT(NetkatTableBuilder(table) + .AddRule({ + .priority = 11, + .match = Match("port", 2), + .action = Modify("vrf", 3), + }) + .LogRules() + .InstallRules(), + StatusIs(absl::StatusCode::kInvalidArgument)); + + AnalysisEngine engine; + EXPECT_TRUE( + engine.CheckEquivalent(table.GetPolicy(), expected_table.GetPolicy()) + .IsSuccess()); +} + +TEST(NetkatTableBuilderTest, TableBuilderFromExistingTable) { + NetkatTable table; + NetkatTable::Rule rule1 = { + .priority = 0, + .match = Match("port", 0), + .action = Modify("vrf", 1), + }; + ASSERT_OK(table.AddRule(rule1)); + + NetkatTable::Rule rule2 = { + .priority = 2, + .match = Match("vrf", 10), + .action = Modify("vlan_id", 5), + }; + ASSERT_OK(table.AddRule(rule2)); + + NetkatTable expected_table = table; + NetkatTable::Rule rule3 = { + .priority = 5, + .match = Match("vlan_id", 1), + .action = Modify("out_port", 2), + }; + ASSERT_OK(expected_table.AddRule(rule3)); + ASSERT_OK(NetkatTableBuilder(table).AddRule(rule3).LogRules().InstallRules()); + + AnalysisEngine engine; + EXPECT_TRUE( + engine.CheckEquivalent(table.GetPolicy(), expected_table.GetPolicy()) + .IsSuccess()); +} + +} // namespace +} // namespace netkat