--- /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 <errno.h>
+#include <limits.h>
+
+// libnl-3
+#include <netlink/netlink.h>
+#include <netlink/route/link.h>
+#include <netlink/route/route.h>
+#include <netlink/socket.h>
+
+#include "../command.h"
+#include "../ctx.h"
+#include "../source.h"
+#include "../time.h"
+#include "interfaces.h"
+
+// Netlink Socket
+static struct nl_sock* sock = NULL;
+
+typedef struct interfaces_stats {
+ const char* field;
+ rtnl_link_stat_id_t id;
+} interfaces_stats;
+
+static int interfaces_init(td_ctx* ctx) {
+ int r;
+
+ // Don't open the socket again
+ if (sock)
+ return 0;
+
+ // Create a new netlink socket
+ sock = nl_socket_alloc();
+ if (!sock) {
+ ERROR(ctx, "Failed to create a netlink socket: %m\n");
+ r = -errno;
+ goto ERROR;
+ }
+
+ // Select routing
+ r = nl_connect(sock, NETLINK_ROUTE);
+ if (r < 0) {
+ ERROR(ctx, "Failed to select routing: %s\n", nl_geterror(r));
+ r = -ENOTSUP;
+ goto ERROR;
+ }
+
+ // Success
+ return 0;
+
+ERROR:
+ if (sock) {
+ nl_socket_free(sock);
+ sock = NULL;
+ }
+
+ return r;
+}
+
+static int interfaces_free(td_ctx* ctx) {
+ if (sock) {
+ nl_socket_free(sock);
+ sock = NULL;
+ }
+
+ return 0;
+}
+
+static const interfaces_stats stats[] = {
+ // Packets Received/Sent
+ { "rx_packets", RTNL_LINK_RX_PACKETS },
+ { "tx_packets", RTNL_LINK_TX_PACKETS },
+
+ // Bytes Received/Sent
+ { "rx_bytes", RTNL_LINK_RX_BYTES },
+ { "tx_bytes", RTNL_LINK_TX_BYTES },
+
+ // RX/TX Errors
+ { "rx_errors", RTNL_LINK_RX_ERRORS },
+ { "tx_errors", RTNL_LINK_TX_ERRORS },
+
+ // Received Packets Dropped/Packets Dropped During Transmit
+ { "rx_dropped", RTNL_LINK_RX_DROPPED },
+ { "tx_dropped", RTNL_LINK_TX_DROPPED },
+
+ // Compressed Packets Received/Sent
+ { "rx_compressed", RTNL_LINK_RX_COMPRESSED },
+ { "tx_compressed", RTNL_LINK_TX_COMPRESSED },
+
+ // FIFO Errors
+ { "rx_fifo_errors", RTNL_LINK_RX_FIFO_ERR },
+ { "tx_fifo_errors", RTNL_LINK_TX_FIFO_ERR },
+
+ // RX Length Errors
+ { "rx_length_errors", RTNL_LINK_RX_LEN_ERR },
+
+ // RX Over Errors
+ { "rx_over_errors", RTNL_LINK_RX_OVER_ERR },
+
+ // RX CRC Errors
+ { "rx_crc_errors", RTNL_LINK_RX_CRC_ERR },
+
+ // RX Frame Errors
+ { "rx_frame_errors", RTNL_LINK_RX_FRAME_ERR },
+
+ // RX Missed Errors
+ { "rx_missed_errors", RTNL_LINK_RX_MISSED_ERR },
+
+ // TX Aborted Errors
+ { "tx_aborted_errors", RTNL_LINK_TX_ABORT_ERR },
+
+ // TX Carrier Errors
+ { "tx_carrier_errors", RTNL_LINK_TX_CARRIER_ERR },
+
+ // TX Heartbeat Errors
+ { "tx_heartbeat_errors", RTNL_LINK_TX_HBEAT_ERR },
+
+ // TX Window Errors
+ { "tx_window_errors", RTNL_LINK_TX_WIN_ERR },
+
+ // Collisions
+ { "collisions", RTNL_LINK_COLLISIONS },
+
+ // Multicast
+ { "multicast", RTNL_LINK_MULTICAST },
+
+ { NULL },
+};
+
+static void interfaces_link_callback(struct nl_object* object, void* data) {
+ struct rtnl_link* link = (struct rtnl_link*)object;
+ td_metrics* metrics = NULL;
+ td_source* source = data;
+ uint64_t value = 0;
+ int r;
+
+ // Fetch the link name
+ const char *name = rtnl_link_get_name(link);
+ if (!name)
+ goto ERROR;
+
+ // Create a new metrics object
+ r = td_source_create_metrics(source, &metrics, name);
+ if (r < 0)
+ goto ERROR;
+
+ // Collect all stats
+ for (const interfaces_stats* stat = stats; stat->field; stat++) {
+ // Fetch the value
+ value = rtnl_link_get_stat(link, stat->id);
+
+ // Submit the value
+ r = td_metrics_push_uint64(metrics, stat->field, value);
+ if (r < 0)
+ goto ERROR;
+ }
+
+ // Submit metrics
+ r = td_source_submit_metrics(source, metrics);
+ if (r < 0)
+ goto ERROR;
+
+ERROR:
+ if (metrics)
+ td_metrics_unref(metrics);
+}
+
+static int interfaces_heartbeat(td_ctx* ctx, td_source* source) {
+ struct nl_cache* links = NULL;
+ int r;
+
+ // Fetch the links
+ r = rtnl_link_alloc_cache(sock, AF_UNSPEC, &links);
+ if (r < 0) {
+ ERROR(ctx, "Failed to fetch the links: %s\n", nl_geterror(r));
+ r = -ENOTSUP;
+ goto ERROR;
+ }
+
+ // Walk through all interfaces
+ nl_cache_foreach(links, interfaces_link_callback, source);
+
+ERROR:
+ if (links)
+ nl_cache_free(links);
+
+ return r;
+}
+
+const td_source_impl interfaces_source = {
+ .name = "interfaces",
+
+ // RRD Data Sources
+ .rrd_dss = {
+ // Packets Received/Sent
+ { "rx_packets", "DERIVE", 0, -1, },
+ { "tx_packets", "DERIVE", 0, -1, },
+
+ // Bytes Received/Sent
+ { "rx_bytes", "DERIVE", 0, -1, },
+ { "tx_bytes", "DERIVE", 0, -1, },
+
+ // RX/TX Errors
+ { "rx_errors", "DERIVE", 0, -1, },
+ { "tx_errors", "DERIVE", 0, -1, },
+
+ // Received Packets Dropped/Packets Dropped During Transmit
+ { "rx_dropped", "DERIVE", 0, -1, },
+ { "tx_dropped", "DERIVE", 0, -1, },
+
+ // Compressed Packets Received/Sent
+ { "rx_compressed", "DERIVE", 0, -1, },
+ { "tx_compressed", "DERIVE", 0, -1, },
+
+ // FIFO Errors
+ { "rx_fifo_errors", "DERIVE", 0, -1, },
+ { "tx_fifo_errors", "DERIVE", 0, -1, },
+
+ // RX Length Errors
+ { "rx_length_errors", "DERIVE", 0, -1, },
+
+ // RX Over Errors
+ { "rx_over_errors", "DERIVE", 0, -1, },
+
+ // RX CRC Errors
+ { "rx_crc_errors", "DERIVE", 0, -1, },
+
+ // RX Frame Errors
+ { "rx_frame_errors", "DERIVE", 0, -1, },
+
+ // RX Missed Errors
+ { "rx_missed_errors", "DERIVE", 0, -1, },
+
+ // TX Aborted Errors
+ { "tx_aborted_errors", "DERIVE", 0, -1, },
+
+ // TX Carrier Errors
+ { "tx_carrier_errors", "DERIVE", 0, -1, },
+
+ // TX Heartbeat Errors
+ { "tx_heartbeat_errors", "DERIVE", 0, -1, },
+
+ // TX Window Errors
+ { "tx_window_errors", "DERIVE", 0, -1, },
+
+ // Collisions
+ { "collisions", "DERIVE", 0, -1, },
+
+ // Multicast
+ { "multicast", "DERIVE", 0, -1, },
+
+ { NULL },
+ },
+
+ // Methods
+ .init = interfaces_init,
+ .free = interfaces_free,
+ .heartbeat = interfaces_heartbeat,
+};