--- /dev/null
+/*#############################################################################
+# #
+# 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,
+};