]> git.ipfire.org Git - people/ms/telemetry.git/commitdiff
sources: Collect nftables counters
authorMichael Tremer <michael.tremer@ipfire.org>
Fri, 28 Nov 2025 12:16:33 +0000 (12:16 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Fri, 28 Nov 2025 12:17:36 +0000 (12:17 +0000)
This is basically the same as iptables counters.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/daemon/sources.c
src/daemon/sources/nftables.c [new file with mode: 0644]
src/daemon/sources/nftables.h [new file with mode: 0644]

index 776eff02b4ab9b44b717123dc552ce2835f00168..7530b41851a14b5b2a52f3c9b64e353eb2d3e0e3 100644 (file)
@@ -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 \
index 9accd2630936f87974b1a274a445e819da692bcb..f565787b336d3c7033ef258af9a9b52faef5bf1a 100644 (file)
 #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 (file)
index 0000000..765d9d7
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+#include <linux/netfilter/nf_tables.h>
+
+#include <libmnl/libmnl.h>
+
+#include <libnftnl/common.h>
+#include <libnftnl/expr.h>
+#include <libnftnl/rule.h>
+#include <libnftnl/udata.h>
+
+#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 (file)
index 0000000..2555fc7
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+#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 */