--- /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>
+#include <linux/if_arp.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 "legacy-gateway-latency4.h"
+
+static int fetch_default_gateway(td_ctx* ctx,
+ char* address, size_t length, unsigned int* type) {
+ struct nl_sock* sock = NULL;
+ struct nl_cache* routes = NULL;
+ struct rtnl_route* route = NULL;
+ struct nl_cache* links = NULL;
+ struct rtnl_link* link = NULL;
+ struct rtnl_nexthop* nh = NULL;
+ struct nl_addr* gw = NULL;
+ int ifindex = -1;
+ int r;
+
+ // 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;
+ }
+
+ // Fetch the route cache
+ r = rtnl_route_alloc_cache(sock, AF_INET, 0, &routes);
+ if (r < 0) {
+ ERROR(ctx, "Failed to fetch the route cache: %s\n", nl_geterror(r));
+ r = -ENOTSUP;
+ goto ERROR;
+ }
+
+ // 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 routes
+ for (struct nl_object* o = nl_cache_get_first(routes); o; o = nl_cache_get_next(o)) {
+ route = (struct rtnl_route*)o;
+
+ // Ignore anything that isn't IPv4
+ if (rtnl_route_get_family(route) != AF_INET)
+ continue;
+
+ // Ignore anything but the main routing table
+ if (rtnl_route_get_table(route) != RT_TABLE_MAIN)
+ continue;
+
+ // Ignore anything but the default route
+ if (!rtnl_route_get_dst(route))
+ continue;
+
+ // Fetch the nexthop
+ nh = rtnl_route_nexthop_n(route, 0);
+ if (!nh)
+ continue;
+
+ // Fetch the gateway
+ gw = rtnl_route_nh_get_gateway(nh);
+ if (!gw)
+ continue;
+
+ // Return the gateway address
+ if (!nl_addr2str(gw, address, length))
+ continue;
+
+ // Fetch the interface
+ ifindex = rtnl_route_nh_get_ifindex(nh);
+
+ // Fetch the link
+ link = rtnl_link_get(links, ifindex);
+ if (!link)
+ continue;
+
+ // Fetch the type of the interface
+ *type = rtnl_link_get_arptype(link);
+ }
+
+ERROR:
+ if (routes)
+ nl_cache_free(routes);
+ if (links)
+ nl_cache_free(links);
+ if (sock)
+ nl_socket_free(sock);
+
+ return r;
+}
+
+static int legacy_gateway_latency_on_success(td_ctx* ctx,
+ int rc, td_file* stdout, void* data) {
+ td_source* source = data;
+ double min = -1.0;
+ double avg = -1.0;
+ double max = -1.0;
+ double mdev = -1.0;
+ double loss = 0;
+ int r;
+
+ td_file_parser parser[] = {
+ // ping
+ PARSE4("rtt min/avg/max/mdev = %lf/%lf/%lf/%lf ms", &min, &avg, &max, &mdev),
+ PARSE1("%*d packets transmitted, %*d received, %lf%% packet loss", &loss),
+
+ // arping
+ PARSE4("rtt min/avg/max/std-dev = %lf/%lf/%lf/%lf ms", &min, &avg, &max, &mdev),
+ PARSE1("%*d packets transmitted, %*d packets received, %lf%% unanswered", &loss),
+ { NULL },
+ };
+
+ // Parse the output
+ r = td_file_parse(stdout, parser);
+ if (r < 0)
+ return r;
+
+ // Convert the loss
+ loss /= 100.0;
+
+ // Submit values
+ return td_source_submit_values(source, NULL, VALUES(
+ VALUE_FLOAT("latency", &avg),
+ VALUE_FLOAT("stddev", &mdev),
+ VALUE_FLOAT("loss", &loss)
+ ));
+}
+
+
+static int do_arping(td_ctx* ctx, td_source* source, const char* address) {
+ td_command* command = NULL;
+ int r;
+
+ // Run the ping command
+ const char* argv[] = { "arping", "-c3", address, NULL };
+
+ // Create a new command
+ r = td_source_create_command(source, &command);
+ if (r < 0)
+ goto ERROR;
+
+ // Register the success callback
+ td_command_on_success(command, legacy_gateway_latency_on_success, source);
+
+ // Execute the command
+ r = td_command_execute(command, argv);
+
+ERROR:
+ if (command)
+ td_command_unref(command);
+
+ return r;
+}
+
+static int do_ping(td_ctx* ctx, td_source* source, const char* address) {
+ td_command* command = NULL;
+ int r;
+
+ // Run the ping command
+ const char* argv[] = { "ping", "-c3", address, NULL };
+
+ // Create a new command
+ r = td_source_create_command(source, &command);
+ if (r < 0)
+ goto ERROR;
+
+ // Register the success callback
+ td_command_on_success(command, legacy_gateway_latency_on_success, source);
+
+ // Execute the command
+ r = td_command_execute(command, argv);
+
+ERROR:
+ if (command)
+ td_command_unref(command);
+
+ return r;
+}
+
+static int legacy_gateway_latency4_heartbeat(td_ctx* ctx, td_source* source) {
+ char address[NAME_MAX] = "";
+ unsigned int type = 0;
+ int r;
+
+ // Fetch the IP address of the default gateway
+ r = fetch_default_gateway(ctx, address, sizeof(address), &type);
+ if (r < 0)
+ return r;
+
+ // Bail if we don't have a gateway address
+ if (!*address) {
+ DEBUG(ctx, "Failed to fetch the gateway address\n");
+ return 0;
+ }
+
+ // Use ARP ping for Ethernet interfaces and otherwise fall back on ICMP ping
+ switch (type) {
+ case ARPHRD_ETHER:
+ return do_arping(ctx, source, address);
+
+ default:
+ return do_ping(ctx, source, address);
+ }
+}
+
+const td_source_impl legacy_gateway_latency4_source = {
+ .name = "legacy-gateway-latency4",
+
+ // RRD Data Sources
+ .rrd_dss = {
+ { "latency", "GAUGE", 0, -1, },
+ { "stddev", "GAUGE", 0, -1, },
+ { "loss", "GAUGE", 0, -1, },
+ { NULL },
+ },
+
+ // Methods
+ .heartbeat = legacy_gateway_latency4_heartbeat,
+};