blob: 05b06cbcbfb387554e16791ead28b7fcfae6dff5 [file] [log] [blame]
/*
* Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
*
* Written by Václav Kubernát <kubernat@cesnet.cz>
*
*/
#include <iostream>
#include <spdlog/spdlog.h>
#include <sstream>
#include "firewall/Firewall.h"
#include "utils/sysrepo.h"
using namespace std::string_literals;
namespace {
const auto ietf_acl_module = "ietf-access-control-list"s;
namespace nodepaths {
const auto ace_comment = "/ietf-access-control-list:acls/acl/aces/ace/name";
const auto ipv4_matches = "/ietf-access-control-list:acls/acl/aces/ace/matches/l3/ipv4/ipv4/source-network/source-ipv4-network/source-ipv4-network";
const auto ipv6_matches = "/ietf-access-control-list:acls/acl/aces/ace/matches/l3/ipv6/ipv6/source-network/source-ipv6-network/source-ipv6-network";
const auto action = "/ietf-access-control-list:acls/acl/aces/ace/actions/forwarding";
}
/**
* Gets a string value from a node.
*
* @param node A libyang data node. Mustn't be nullptr.
*
*/
namespace {
const char* getValueAsString(const libyang::S_Data_Node& node)
{
if (!node || node->schema()->nodetype() != LYS_LEAF) {
throw std::logic_error("retrieveString: invalid node");
}
return libyang::Data_Node_Leaf_List(node).value_str();
}
}
std::string generateNftConfig(velia::Log logger, const libyang::S_Data_Node& tree)
{
using namespace std::string_view_literals;
std::ostringstream ss;
ss << "flush ruleset" << "\n";
ss << "add table inet filter" << "\n";
ss << "add chain inet filter acls { type filter hook input priority 0; }\n";
ss << "add rule inet filter acls ct state established,related accept\n";
ss << "add rule inet filter acls iif lo accept comment \"Accept any localhost traffic\"\n";
constexpr std::array skippedNodes{
// Top-level container - don't care
"/ietf-access-control-list:acls"sv,
// ACL container
"/ietf-access-control-list:acls/acl"sv,
// ACL name - don't care, we always only have one ACL
"/ietf-access-control-list:acls/acl/name"sv,
// ACEs container - don't care
"/ietf-access-control-list:acls/acl/aces"sv,
// The type is either ipv4, ipv6, eth (which is disabled by a deviation) or a mix of these. The type is there
// only for YANG validation and doesn't matter to us, because we check for "ipv4" and "ipv6" container.
"/ietf-access-control-list:acls/acl/type"sv,
// These are ignored, because they do not give any meaningful information. They are mostly containers.
"/ietf-access-control-list:acls/acl/aces/ace"sv,
"/ietf-access-control-list:acls/acl/aces/ace/matches"sv,
"/ietf-access-control-list:acls/acl/aces/ace/matches/l3/ipv4/ipv4"sv,
"/ietf-access-control-list:acls/acl/aces/ace/matches/l3/ipv6/ipv6"sv,
"/ietf-access-control-list:acls/acl/aces/ace/actions"sv,
};
logger->trace("traversing the tree");
std::string comment;
std::string match;
for (auto node : tree->tree_dfs()) {
auto nodeSchemaPath = node->schema()->path(LYS_PATH_FIRST_PREFIX);
if (std::any_of(skippedNodes.begin(), skippedNodes.end(), [&nodeSchemaPath] (const auto& skippedNode) { return nodeSchemaPath == skippedNode; })) {
logger->trace("skipping: {}", node->path());
continue;
}
logger->trace("processing node: data {}", node->path());
logger->trace(" schema {}", nodeSchemaPath);
if (nodeSchemaPath == nodepaths::ace_comment) {
// We will use the ACE name as a comment inside the rule. However, the comment must be at the end, so we
// save it for later.
comment = getValueAsString(node);
} else if (nodeSchemaPath == nodepaths::ipv4_matches) {
// Here we save the ip we're matching against.
match = " ip saddr "s + getValueAsString(node);
} else if (nodeSchemaPath == nodepaths::ipv6_matches) {
// Here we save the ip we're matching against.
match = " ip6 saddr "s + getValueAsString(node);
} else if (nodeSchemaPath == nodepaths::action) {
// Action is the last statement we get, so this is where we create the actual rule.
ss << "add rule inet filter acls" << match;
auto action = getValueAsString(node);
if (action == "ietf-access-control-list:accept"sv) {
ss << " accept";
} else if (action == "ietf-access-control-list:drop"sv) {
ss << " drop";
} else if (action == "ietf-access-control-list:reject"sv) {
ss << " reject";
} else {
// This should theoretically never happen.
throw std::logic_error("unsupported ACE action: "s + action);
}
// After the action, we only add the comment. This is the end of the rule.
ss << " comment \"" << comment << "\"\n";
match = "";
comment = "";
} else {
throw std::logic_error("unsupported node: " + node->path());
}
}
return ss.str();
}
}
velia::firewall::SysrepoFirewall::SysrepoFirewall(sysrepo::S_Session srSess, NftConfigConsumer consumer)
: m_session(srSess)
, m_sub(std::make_shared<sysrepo::Subscribe>(srSess))
, m_log(spdlog::get("firewall"))
{
auto lyCtx = m_session->get_context();
utils::ensureModuleImplemented(srSess, "ietf-access-control-list", "2019-03-04");
utils::ensureModuleImplemented(srSess, "czechlight-firewall", "2021-01-25");
sysrepo::ModuleChangeCb cb = [logger = m_log, consumer = std::move(consumer)] (
sysrepo::S_Session session,
[[maybe_unused]] const char *module_name,
[[maybe_unused]] const char *xpath,
[[maybe_unused]] sr_event_t event,
[[maybe_unused]] uint32_t request_id) {
logger->debug("Applying new data from sysrepo");
auto data = session->get_data(("/" + (ietf_acl_module + ":*")).c_str());
// The data from sysrepo aren't guaranteed to be sorted according to the schema, but generateNftConfig depends
// on that order.
// FIXME: when libyang2 becomes available, remove this.
// https://github.com/sysrepo/sysrepo/issues/2292
data->schema_sort(1);
auto config = generateNftConfig(logger, data);
logger->trace("running the consumer...");
consumer(config);
logger->trace("consumer done.");
return SR_ERR_OK;
};
m_sub->module_change_subscribe(ietf_acl_module.c_str(), cb, nullptr, 0, SR_SUBSCR_DONE_ONLY | SR_SUBSCR_ENABLED);
}