--- /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/>. #
+# #
+#############################################################################*/
+
+#ifdef BUILD_SOURCE_OPENVPN
+
+#include "../ctx.h"
+#include "../source.h"
+#include "../string.h"
+#include "openvpn.h"
+
+#define OPENVPN_LOG_PATH "/var/run/openvpn-rw.log"
+
+typedef struct openvpn_fields {
+ td_source* source;
+ unsigned int common_name;
+ unsigned int rx_bytes;
+ unsigned int tx_bytes;
+} openvpn_fields;
+
+typedef struct openvpn_client {
+ char* common_name;
+ size_t rx_bytes;
+ size_t tx_bytes;
+} openvpn_client;
+
+static void openvpn_escape_name(char* name) {
+ for (char* p = name; *p; p++) {
+ switch (*p) {
+ case ' ':
+ *p = '-';
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+/*
+ This function splits the line by tabs and will call the callback function for each token.
+*/
+static int split_line(td_ctx* ctx, char* line, openvpn_fields* fields,
+ int (*callback)(td_ctx* ctx, openvpn_fields* fields, unsigned int idx,
+ char* token, void* data), void* data) {
+ char* p = line;
+ char* d = NULL;
+ int r;
+
+ unsigned int idx = 0;
+
+ while (p && *p) {
+ // Find the next delimiter
+ d = strchr(p, '\t');
+
+ // Handle the last token
+ if (!d)
+ return callback(ctx, fields, idx++, p, data);
+
+ // Terminate the token
+ *d = '\0';
+
+ // Call the callback
+ r = callback(ctx, fields, idx++, p, data);
+ if (r < 0)
+ return r;
+
+ // Advance p
+ p = d + 1;
+ }
+
+ return 0;
+}
+
+static int openvpn_parse_header(td_ctx* ctx, openvpn_fields* fields,
+ unsigned int idx, char* token, void* data) {
+ switch (idx) {
+ // The first field must be "HEADER"
+ case 0:
+ if (!td_string_equals(token, "HEADER"))
+ return 0;
+ break;
+
+ // The second field must be "CLIENT_LIST"
+ case 1:
+ if (!td_string_equals(token, "CLIENT_LIST"))
+ return 0;
+ break;
+
+ // Store the indexes of all required fields
+ default:
+ if (td_string_equals(token, "Common Name"))
+ fields->common_name = idx - 1;
+
+ else if (td_string_equals(token, "Bytes Received"))
+ fields->rx_bytes = idx - 1;
+
+ else if (td_string_equals(token, "Bytes Sent"))
+ fields->tx_bytes = idx - 1;
+
+ break;
+ }
+
+ return 0;
+}
+
+static int openvpn_parse_client_list(td_ctx* ctx, openvpn_fields* fields,
+ unsigned int idx, char* token, void* data) {
+ openvpn_client* client = data;
+ char* e = NULL;
+
+ // All fields must be found
+ if (!fields->common_name || !fields->rx_bytes || !fields->tx_bytes)
+ return -ENOTSUP;
+
+ // Store the common name
+ if (fields->common_name == idx)
+ client->common_name = token;
+
+ // Store RX bytes
+ else if (fields->rx_bytes == idx)
+ client->rx_bytes = strtoul(token, &e, 10);
+
+ // Store TX bytes
+ else if (fields->tx_bytes == idx)
+ client->tx_bytes = strtoul(token, &e, 10);
+
+ // If e is set, we could not parse the entire token
+ if (e && *e) {
+ ERROR(ctx, "Failed to parse token: %s\n", token);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int openvpn_parse(td_ctx* ctx, td_file* file, unsigned long lineno,
+ char* line, size_t length, void* data) {
+ openvpn_fields* fields = data;
+ openvpn_client client = {};
+ int r;
+
+ // Parse any headers
+ if (td_string_startswith(line, "HEADER"))
+ return split_line(ctx, line, fields, openvpn_parse_header, NULL);
+
+ // Parse the client list
+ else if (td_string_startswith(line, "CLIENT_LIST")) {
+ r = split_line(ctx, line, fields, openvpn_parse_client_list, &client);
+ if (r < 0)
+ return r;
+
+ // Don't submit anything if we could not parse the line
+ if (!client.common_name)
+ return 0;
+
+ // Escape the name
+ openvpn_escape_name(client.common_name);
+
+ // Submit the client
+ return td_source_submit_values(fields->source,
+ client.common_name, VALUES(
+ VALUE_UINT64("rx_bytes", &client.rx_bytes),
+ VALUE_UINT64("tx_bytes", &client.tx_bytes)
+ )
+ );
+ }
+
+ return 0;
+}
+
+static int openvpn_heartbeat(td_ctx* ctx, td_source* source) {
+ openvpn_fields fields = {
+ .source = source,
+ };
+ td_file* file = NULL;
+ int r;
+
+ // Open the OpenVPN log file
+ r = td_file_open_path(&file, ctx, OPENVPN_LOG_PATH);
+ if (r < 0)
+ goto ERROR;
+
+ // Walk through the entire file
+ r = td_file_walk(file, openvpn_parse, &fields);
+
+ERROR:
+ if (file)
+ td_file_unref(file);
+
+ return r;
+}
+
+const td_source_impl openvpn_source = {
+ .name = "openvpn",
+
+ // RRD Data Sources
+ .rrd_dss = {
+ // some
+ { "rx_bytes", "DERIVE", 0, -1, },
+ { "tx_bytes", "DERIVE", 0, -1, },
+ { NULL },
+ },
+
+ // Methods
+ .heartbeat = openvpn_heartbeat,
+};
+
+#endif /* BUILD_SOURCE_OPENVPN */
--- /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/>. #
+# #
+#############################################################################*/
+
+#ifndef TELEMETRY_SOURCE_OPENVPN_H
+#define TELEMETRY_SOURCE_OPENVPN_H
+#ifdef BUILD_SOURCE_OPENVPN
+
+#include "../source.h"
+
+extern const td_source_impl openvpn_source;
+
+#endif /* BUILD_SOURCE_OPENVPN */
+#endif /* TELEMETRY_SOURCE_OPENVPN_H */