From f784d5c07d9597aedabd97b248bcbc0831fd8ee6 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Fri, 28 Nov 2025 12:16:33 +0000 Subject: [PATCH] sources: Collect nftables counters This is basically the same as iptables counters. Signed-off-by: Michael Tremer --- Makefile.am | 2 + src/daemon/sources.c | 14 ++ src/daemon/sources/nftables.c | 387 ++++++++++++++++++++++++++++++++++ src/daemon/sources/nftables.h | 28 +++ 4 files changed, 431 insertions(+) create mode 100644 src/daemon/sources/nftables.c create mode 100644 src/daemon/sources/nftables.h diff --git a/Makefile.am b/Makefile.am index 776eff0..7530b41 100644 --- a/Makefile.am +++ b/Makefile.am @@ -182,6 +182,8 @@ dist_telemetryd_SOURCES = \ src/daemon/sources/loadavg.h \ src/daemon/sources/memory.c \ src/daemon/sources/memory.h \ + src/daemon/sources/nftables.c \ + src/daemon/sources/nftables.h \ src/daemon/sources/pressure-cpu.c \ src/daemon/sources/pressure-cpu.h \ src/daemon/sources/pressure-io.c \ diff --git a/src/daemon/sources.c b/src/daemon/sources.c index 9accd26..f565787 100644 --- a/src/daemon/sources.c +++ b/src/daemon/sources.c @@ -59,6 +59,13 @@ #endif /* HAVE_LIBNL3_ROUTE */ #endif /* HAVE_LIBNL3 */ +// nftables +#ifdef HAVE_LIBMNL +# ifdef HAVE_LIBNFTNL +# include "sources/nftables.h" +# endif /* HAVE_LIBNFTNL */ +#endif /* HAVE_LIBMNL */ + // sensors #ifdef HAVE_SENSORS # include "sources/sensors.h" @@ -103,6 +110,13 @@ static const td_source_impl* source_impls[] = { #endif /* HAVE_LIBNL3_ROUTE */ #endif /* HAVE_LIBNL3 */ + // nftables +#ifdef HAVE_LIBMNL +# ifdef HAVE_LIBNFTNL + &nftables_source, +# endif /* HAVE_LIBNFTNL */ +#endif /* HAVE_LIBMNL */ + // sensors #ifdef HAVE_SENSORS &sensors_input_source, diff --git a/src/daemon/sources/nftables.c b/src/daemon/sources/nftables.c new file mode 100644 index 0000000..765d9d7 --- /dev/null +++ b/src/daemon/sources/nftables.c @@ -0,0 +1,387 @@ +/*############################################################################# +# # +# telemetryd - The IPFire Telemetry Collection Service # +# Copyright (C) 2025 IPFire Development Team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +#############################################################################*/ + +#include + +#include + +#include +#include +#include +#include + +#include "../ctx.h" +#include "../source.h" +#include "../string.h" +#include "nftables.h" + +// Maximum length of the comment +#define MAX_COMMENT 2048 + +// Netfilter Socket +static struct mnl_socket* nl = NULL; + +// Sequence Number +static uint32_t seq = 0; + +typedef struct nftables_comment { + // The comment string + char comment[MAX_COMMENT]; + + // Counters + uint64_t packets; + uint64_t bytes; +} nftables_comment; + +typedef struct nftables_comments { + nftables_comment* comments; + unsigned int num_comments; +} nftables_comments; + +static int nftables_init(td_ctx* ctx) { + int r; + + // Don't create another socket if one is already open + if (nl) + return 0; + + // Open a new netlink socket + nl = mnl_socket_open(NETLINK_NETFILTER); + if (!nl) { + ERROR(ctx, "Failed to open a Netfilter socket: %m\n"); + return -errno; + } + + // Bind the socket + r = mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID); + if (r < 0) { + ERROR(ctx, "Failed to bind the Netfilter socket: %m\n"); + return -errno; + } + + return 0; +} + +static int nftables_free(td_ctx* ctx) { + int r; + + // Close the socket + if (nl) { + r = mnl_socket_close(nl); + if (r < 0) { + ERROR(ctx, "Failed to close the Netfilter socket: %m\n"); + return -errno; + } + } + + return 0; +} + +static int mnl_talk(struct mnl_socket* socket, const void* data, unsigned int length, + int (*cb)(const struct nlmsghdr* nlh, void* data), void* cb_data) { + char buffer[MNL_SOCKET_BUFFER_SIZE]; + int r; + + uint32_t portid = mnl_socket_get_portid(socket); + + // Send the message + r = mnl_socket_sendto(socket, data, length); + if (r < 0) + return r; + + // Receive the entire response and call the callback + for (;;) { + // Receive the next chunk + r = mnl_socket_recvfrom(socket, buffer, sizeof(buffer)); + if (r < 0) + goto ERROR; + + // EOF + else if (r == 0) + break; + + // Call the callback + r = mnl_cb_run(buffer, r, seq, portid, cb, cb_data); + if (r <= 0) + goto ERROR; + } + +ERROR: + if (r < 0 && errno == EAGAIN) + return 0; + + return r; +} + +static int find_comment( + nftables_comment** ret, nftables_comments* comments, const char* s) { + nftables_comment* comment = NULL; + nftables_comment* p = NULL; + int r; + + // Return any matching comments + for (unsigned int i = 0; i < comments->num_comments; i++) { + if (td_string_equals(comments->comments[i].comment, s)) { + *ret = &comments->comments[i]; + return 0; + } + } + + // No comment found, let's increase the length of the array + p = reallocarray(comments->comments, comments->num_comments + 1, sizeof(*p)); + if (!p) + return -errno; + + // Update the pointer + comments->comments = p; + + // Pointer to the new comment + comment = &comments->comments[comments->num_comments]; + + // Store the comment string + r = td_string_set(comment->comment, s); + if (r < 0) + return r; + + // Initialize the counters + comment->packets = comment->bytes = 0; + + // The array is now longer + comments->num_comments++; + + // Return the comment + *ret = comment; + + return 0; +} + +static int skip_comment(const char* comment) { + // Skip empty comments + if (!*comment) + return 1; + + // Skip comments that contain anything else but uppercase characters, digits and _ + for (const char* p = comment; *p; p++) { + // Allow uppercase characters + if (isupper(*p)) + continue; + + // Allow digits + else if (isdigit(*p)) + continue; + + // Allow underscore + else if (*p == '_') + continue; + + // Invalid character found, skip! + return 1; + } + + return 0; +} + +// Parse any (legacy) xtables matches. This is needed when nftables is being +// fed rules by the iptables(-nft) command. +static int nftables_expr_find_comment_callback(struct nftnl_expr* expr, void* data) { + char* comment = data; + + // Fetch the name of the expression + const char* name = nftnl_expr_get_str(expr, NFTNL_EXPR_NAME); + + if (td_string_equals(name, "match")) { + const char* n = nftnl_expr_get_str(expr, NFTNL_EXPR_MT_NAME); + const char* i = nftnl_expr_get_str(expr, NFTNL_EXPR_MT_INFO); + + // Store the comment + if (td_string_equals(n, "comment")) + return __td_string_set(comment, MAX_COMMENT, i); + } + + return 0; +} + +static int nftables_expr_fetch_counters_callback(struct nftnl_expr* expr, void* data) { + nftables_comment* comment = data; + + // Fetch the name of the expression + const char* name = nftnl_expr_get_str(expr, NFTNL_EXPR_NAME); + + // Fetch the counter values + if (td_string_equals(name, "counter")) { + comment->packets += nftnl_expr_get_u64(expr, NFTNL_EXPR_CTR_PACKETS); + comment->bytes += nftnl_expr_get_u64(expr, NFTNL_EXPR_CTR_BYTES); + } + + return 0; +} + +static int nftables_udata_callback(const struct nftnl_udata* attr, void* data) { + nftables_comment* comment = data; + unsigned char* value = NULL; + uint8_t length = 0; + + switch (nftnl_udata_type(attr)) { + // Fetch the comment + case NFTNL_UDATA_RULE_COMMENT: + // Fetch the value + value = nftnl_udata_get(attr); + + // Fetch the length + length = nftnl_udata_len(attr); + + // Copy the comment + return td_string_setn(comment->comment, (const char*)value, length); + + // Ignore the rest + default: + break; + } + + return 0; +} + +static int nftables_rule_callback(const struct nlmsghdr* nlh, void* data) { + nftables_comments* comments = data; + nftables_comment* comment = NULL; + struct nftnl_rule* rule = NULL; + const void* udata = NULL; + uint32_t length = 0; + int r; + + // Comment string + char c[MAX_COMMENT] = ""; + + // Allocate a rule + rule = nftnl_rule_alloc(); + if (!rule) { + r = -errno; + goto ERROR; + } + + // Parse the rule + r = nftnl_rule_nlmsg_parse(nlh, rule); + if (r < 0) + goto ERROR; + + // Fetch the userdata of this rule + udata = nftnl_rule_get_data(rule, NFTNL_RULE_USERDATA, &length); + + // Iterate over all userdata + r = nftnl_udata_parse(udata, length, nftables_udata_callback, c); + if (r < 0) + goto ERROR; + + // Fall back to the legacy xtables comments + if (!*c) { + // Iterate over all expressions of this rule + r = nftnl_expr_foreach(rule, nftables_expr_find_comment_callback, c); + if (r < 0) + goto ERROR; + } + + // Skip any rules without or invalid comments + if (skip_comment(c)) + goto DONE; + + // Allocate a new comment + r = find_comment(&comment, comments, c); + if (r < 0) + goto ERROR; + + // Fetch the counters + r = nftnl_expr_foreach(rule, nftables_expr_fetch_counters_callback, &comment); + if (r < 0) + goto ERROR; + +DONE: + // Success + r = MNL_CB_OK; + +ERROR: + if (rule) + nftnl_rule_free(rule); + + return r; +} + +static int nftables_iterate_rules(td_ctx* ctx, td_source* source, int family, void* data) { + char buffer[MNL_SOCKET_BUFFER_SIZE]; + struct nlmsghdr* nlh = NULL; + int r; + + // Compose the message + nlh = nftnl_nlmsg_build_hdr(buffer, NFT_MSG_GETRULE, family, NLM_F_DUMP, seq); + + // Send the message + r = mnl_talk(nl, nlh, nlh->nlmsg_len, nftables_rule_callback, data); + if (r < 0) + ERROR(ctx, "Failed to iterate over nftables rules: %m\n"); + + // Increment the sequence number + seq++; + + return r; +} + +static int nftables_heartbeat(td_ctx* ctx, td_source* source) { + nftables_comments comments = {}; + nftables_comment* comment = NULL; + int r; + + // Iterate over all rules + r = nftables_iterate_rules(ctx, source, 0, &comments); + if (r < 0) + goto ERROR; + + // Submit all comments + for (unsigned int i = 0; i < comments.num_comments; i++) { + comment = &comments.comments[i]; + + r = td_source_submit_values(source, comment->comment, VALUES( + VALUE_UINT64("packets", &comment->packets), + VALUE_UINT64("bytes", &comment->bytes) + )); + if (r < 0) + goto ERROR; + } + +ERROR: + if (comments.comments) + free(comments.comments); + + return r; +} + +const td_source_impl nftables_source = { + .name = "nftables", + + // RRD Data Sources + .rrd_dss = { + { "packets", "DERIVE", 0, -1, }, + { "bytes", "DERIVE", 0, -1, }, + { NULL }, + }, + + // Methods + .init = nftables_init, + .free = nftables_free, + .heartbeat = nftables_heartbeat, +}; diff --git a/src/daemon/sources/nftables.h b/src/daemon/sources/nftables.h new file mode 100644 index 0000000..2555fc7 --- /dev/null +++ b/src/daemon/sources/nftables.h @@ -0,0 +1,28 @@ +/*############################################################################# +# # +# telemetryd - The IPFire Telemetry Collection Service # +# Copyright (C) 2025 IPFire Development Team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +#############################################################################*/ + +#ifndef TELEMETRY_SOURCE_NFTABLES_H +#define TELEMETRY_SOURCE_NFTABLES_H + +#include "../source.h" + +extern const td_source_impl nftables_source; + +#endif /* TELEMETRY_SOURCE_NFTABLES_H */ -- 2.47.3