Skip to content

Instantly share code, notes, and snippets.

@PhrozenByte
Last active April 6, 2022 23:46
Show Gist options
  • Save PhrozenByte/dbe4091343cebe529a18 to your computer and use it in GitHub Desktop.
Save PhrozenByte/dbe4091343cebe529a18 to your computer and use it in GitHub Desktop.
Munin plugin for OpenVPN
#!/usr/bin/env php
<?php
/**
* OpenVPN munin plugin
* Version 1.4 (build 20160206)
*
* SHORT DESCRIPTION:
* Monitors the number of connected OpenVPN clients and their bandwidth
* usage (server mode) or the payload and raw bandwidth usage of a client.
*
* DEPENDENCIES:
* This plugin requires various helper functions. You can download the
* required plugin.php from http://daniel-rudolf.de/oss/munin-php-helper
*
* COPYRIGHT AND LICENSING:
* Copyright (C) 2014-2016 Daniel Rudolf <www.daniel-rudolf.de>
*
* 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, version 3 of the License only.
*
* 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.
*
* See <http://www.gnu.org/licenses/> to receive a full-text-copy of
* the GNU General Public License.
*/
if (!file_exists(__DIR__ . '/plugin.php')) {
print_stderr('Unable to include helper functions from \'' . __DIR__ . '/plugin.php' .'\': No such file or directory');
print_stderr('You will need to download PhrozenByte\'s PHP helper functions from http://daniel-rudolf.de/oss/munin-php-helper');
exit(1);
}
require_once(__DIR__ . '/plugin.php');
// read munin plugin config
define('STATUS_FILE', isset($_SERVER['statusfile']) ? $_SERVER['statusfile'] : '/var/log/openvpn.status');
define('PERSISTENCE', isset($_SERVER['persistence']) ? $_SERVER['persistence'] : '-1 month');
// check multigraph capability
if (!check_multigraph(true)) {
exit(0);
}
// perform autoconf
if (is_munin_autoconf()) {
if (!file_exists(STATUS_FILE)) {
print_stdout('no (status file not found)');
} elseif (!is_readable(STATUS_FILE)) {
print_stdout('no (status file not readable)');
} else {
print_stdout("yes");
}
exit(0);
}
// print suggestions
if (is_munin_suggest()) {
$configFiles = glob("/etc/openvpn/*.conf");
foreach ($configFiles as $configFile) {
$configFile = basename($configFile, '.conf');
$configFile = preg_replace('/[^a-zA-Z0-9]/', '_', $configFile);
print_stdout($configFile);
}
exit(0);
}
// set GRAPH_NAME
if (isset($_SERVER['argv'][0])) {
define('GRAPH_NAME', basename($_SERVER['argv'][0]));
} else {
define('GRAPH_NAME', 'openvpn');
}
// read state file
$stateData = read_statefile();
/**
* read status file
*/
$errorMessagePrefix = 'Unable to read status file \'' . STATUS_FILE .'\'';
// read status file
if (!file_exists(STATUS_FILE)) {
// a client config doesn't necessarily require a status file (= client isn't connected);
// if the status file doesn't exist, check for a existing state file and print zeros
if (!isset($stateData[GRAPH_NAME]['type']) || ($stateData[GRAPH_NAME]['type'] !== 'client')) {
print_stderr($errorMessagePrefix . ': No such file or directory');
exit(1);
}
define('OPENVPN_TYPE', 'client_disconnected');
$lastUpdate = TIME_NOW;
} else {
if (!is_readable(STATUS_FILE)) {
print_stderr($errorMessagePrefix . ': Permission denied');
exit(1);
}
$rawStatusFile = file(STATUS_FILE, FILE_IGNORE_NEW_LINES);
if (!is_array($rawStatusFile)) {
print_stderr($errorMessagePrefix . ': Unknown file() error');
exit(1);
}
if (empty($rawStatusFile) || ($rawStatusFile === array(''))) {
print_stderr($errorMessagePrefix . ': File is empty');
exit(1);
}
// validate status file type
$statusFileTitle = array_shift($rawStatusFile);
if ($statusFileTitle === 'OpenVPN CLIENT LIST') {
define('OPENVPN_TYPE', 'server');
} elseif ($statusFileTitle === 'OpenVPN STATISTICS') {
define('OPENVPN_TYPE', 'client');
} else {
print_stderr($errorMessagePrefix . ': Invalid status file title \'' . $statusFileTitle . '\'');
exit(1);
}
if (isset($stateData[GRAPH_NAME]['type']) && (OPENVPN_TYPE !== $stateData[GRAPH_NAME]['type'])) {
print_stderr($errorMessagePrefix . ': Expecting a ' . $stateData[GRAPH_NAME]['type'] . ' status file, ' . OPENVPN_TYPE . ' status file given');
exit(1);
}
// get and validate last update time
$lastUpdateLine = array_shift($rawStatusFile);
if (substr($lastUpdateLine, 0, 8) !== 'Updated,') {
print_stderr($errorMessagePrefix . ': Invalid last update line \'' . $lastUpdateLine . '\'');
exit(1);
}
$rawLastUpdate = substr($lastUpdateLine, 8);
$lastUpdate = strtotime($rawLastUpdate);
if ($lastUpdate === false) {
print_stderr($errorMessagePrefix . ': Invalid last update time \'' . $rawLastUpdate . '\'');
exit(1);
}
}
/**
* read data
*/
if (OPENVPN_TYPE === 'server') {
/**
* read server statistics
*/
// get and validate client list header
$clientListHeaderLine = array_shift($rawStatusFile);
$clientListHeader = explode(',', $clientListHeaderLine);
if (
!in_array('Common Name', $clientListHeader)
|| !in_array('Bytes Received', $clientListHeader)
|| !in_array('Bytes Sent', $clientListHeader)
|| !in_array('Connected Since', $clientListHeader)
) {
print_stderr($errorMessagePrefix . ': Invalid client list header \'' . $clientListHeaderLine . '\'');
exit(1);
}
// read client list
$clientList = array();
$clientListHeaderCount = count($clientListHeader);
foreach ($rawStatusFile as $clientListLine) {
// end of client list
if (($clientListLine === 'ROUTING TABLE') || ($clientListLine === 'GLOBAL STATS') || ($clientListLine === 'END')) {
break;
}
// breakup columns
$rawClientData = explode(',', $clientListLine);
// common names can contain commas
// evaluate the columns in reverse
$commonNameCorrection = (count($rawClientData) - $clientListHeaderCount);
if ($commonNameCorrection < 0) {
print_stderr($errorMessagePrefix . ': Invalid client line \'' . $clientListLine . '\'');
exit(1);
} else {
$commonName = array_shift($rawClientData);
for ($i = 1; $i <= $commonNameCorrection; $i++) {
$commonName += ',' . array_shift($rawClientData);
}
array_unshift($rawClientData, $commonName);
}
// combine header and values
$clientList[] = array_combine(
$clientListHeader,
$rawClientData
);
}
// prepare data of connected clients
$clientData = array();
foreach ($clientList as $rawClientData) {
$identifier = GRAPH_NAME . '_traffic_' . preg_replace('/(^[^A-Za-z_]|[^A-Za-z0-9_])/', '_', $rawClientData['Common Name']);
$connectedSince = strtotime($rawClientData['Connected Since']);
if ($connectedSince === false) {
print_stderr('Invalid value of \'Connected Since\' of client \'' . $rawClientData['Common Name'] . '\': ' . $rawClientData['Connected Since']);
exit(1);
}
$clientData[$identifier] = array(
'id' => $identifier,
'name' => $rawClientData['Common Name'],
'in' => $rawClientData['Bytes Received'],
'out' => $rawClientData['Bytes Sent'],
'lastSeen' => $lastUpdate,
'connectedSince' => $connectedSince
);
}
// add persistent clients
$lastSeenLimit = strtotime(PERSISTENCE, $lastUpdate);
if ($lastSeenLimit === false) {
print_stderr('Invalid \'persistence\' config value \'' . PERSISTENCE . '\'');
exit(1);
}
$persistentClientData = array();
foreach ($stateData as $identifier => $oldClientData) {
if (($identifier === GRAPH_NAME) || ($identifier === GRAPH_NAME . '_traffic')) {
continue;
}
if (!isset($clientData[$identifier]) && ($oldClientData['lastSeen'] >= $lastSeenLimit)) {
$persistentClientData[$identifier] = array(
'id' => $identifier,
'name' => $oldClientData['name'],
'in' => $oldClientData['in'],
'out' => $oldClientData['out'],
'lastSeen' => $oldClientData['lastSeen'],
'connectedSince' => false
);
}
}
// merge data of connected and persistent clients
$allClientData = $clientData + $persistentClientData;
} elseif (OPENVPN_TYPE === 'client') {
/**
* read client statistics
*/
$clientData = array();
foreach ($rawStatusFile as $statsLine) {
// end of client statistics
if (($statsLine === 'END')) {
break;
}
// parse statistic lines
$rawValue = explode(',', $statsLine);
if (count($rawValue) !== 2) {
print_stderr($errorMessagePrefix . ': Invalid statistics line \'' . $statsLine . '\'');
exit(1);
}
switch ($rawValue[0]) {
case 'TUN/TAP read bytes':
// "outgoing" traffic is processed Kernel --> TUN/TAP --> OpenVPN --> TCP/UDP --> Network,
// thus for routing outgoing traffic, OpenVPN reads from TUN/TAP
$clientData['out'] = $rawValue[1];
break;
case 'TUN/TAP write bytes':
$clientData['in'] = $rawValue[1];
break;
case 'TCP/UDP read bytes':
$clientData['rawIn'] = $rawValue[1];
break;
case 'TCP/UDP write bytes':
$clientData['rawOut'] = $rawValue[1];
break;
}
}
if (!isset($clientData['in']) || !isset($clientData['out']) || !isset($clientData['rawIn']) || !isset($clientData['rawOut'])) {
print_stderr($errorMessagePrefix . ': Invalid client statistics');
exit(1);
}
}
/**
* print graph configs
*/
if (is_munin_config()) {
$graphTitleSuffix = '';
if (GRAPH_NAME !== 'openvpn') {
if (substr(GRAPH_NAME, 0, 8) === 'openvpn_') {
$graphTitleSuffix = ' (' . substr(GRAPH_NAME, 8) . ')';
} else {
$graphTitleSuffix = ' (' . GRAPH_NAME . ')';
}
$graphTitleSuffix = str_replace('_', ' ', $graphTitleSuffix);
$graphTitleSuffix = preg_replace('/[ ]{2,}/', ' ', $graphTitleSuffix);
}
if (OPENVPN_TYPE === 'server') {
/**
* print server graph config
*/
// build a graph showing the number of connected clients
print_stdout('multigraph ' . GRAPH_NAME);
print_stdout('graph_title OpenVPN clients' . $graphTitleSuffix);
print_stdout('graph_info This graph shows the number of connected OpenVPN clients.');
print_stdout('graph_args --base 1000 --lower-limit 0');
print_stdout('graph_scale no');
print_stdout('graph_vlabel clients');
print_stdout('graph_category openvpn');
print_stdout('clients.label users');
print_stdout('clients.info Number of clients connected to the OpenVPN server.');
print_stdout('clients.type GAUGE');
print_stdout('clients.min 0');
// sort clients
// sorting is important for munin config...
uasort($allClientData, function ($a, $b) {
return strnatcmp($a['name'], $b['name']);
});
// build a total traffic graph and one for every client (persistent or connected)
reset($allClientData);
$currentClient = null;
do {
// graph config
if ($currentClient === null) {
print_stdout('multigraph ' . GRAPH_NAME . '_traffic');
print_stdout('graph_title OpenVPN client traffic' . $graphTitleSuffix);
print_stdout('graph_info This graph shows the total traffic of all OpenVPN clients.');
} else {
print_stdout('multigraph ' . GRAPH_NAME . '_traffic.' . $currentClient['id']);
print_stdout('graph_title OpenVPN traffic of client "' . $currentClient['name'] . '"');
print_stdout('graph_info This graph shows the traffic of the OpenVPN client "' . $currentClient['name'] . '".');
}
print_stdout('graph_args --base 1000');
print_stdout('graph_vlabel bits in (-) / out (+) per ${graph_period}');
print_stdout('graph_category openvpn');
print_stdout('graph_order in out');
// received
print_stdout('in.label received');
print_stdout('in.type GAUGE');
print_stdout('in.graph no');
print_stdout('in.min 0');
print_stdout('in.cdef in,8,*');
// sent
print_stdout('out.label bps');
print_stdout('out.info Traffic of the OpenVPN user.');
print_stdout('out.type GAUGE');
print_stdout('out.negative in');
print_stdout('out.min 0');
print_stdout('out.cdef out,8,*');
// first iteration is the total graph,
// all following iterations are client graphs
$nextClientData = each($allClientData);
$currentClient = ($nextClientData !== false) ? $nextClientData['value'] : false;
} while ($currentClient !== false);
} else {
/**
* print client graph config
*/
// graph config
print_stdout('graph_title OpenVPN client traffic' . $graphTitleSuffix);
print_stdout('graph_info This graph shows the traffic of a OpenVPN client.');
print_stdout('graph_args --base 1000');
print_stdout('graph_vlabel bits in (-) / out (+) per ${graph_period}');
print_stdout('graph_category openvpn');
print_stdout('graph_order in out rawIn rawOut');
// received
print_stdout('in.label received');
print_stdout('in.type GAUGE');
print_stdout('in.graph no');
print_stdout('in.min 0');
print_stdout('in.cdef in,8,*');
// sent
print_stdout('out.label Payload');
print_stdout('out.info Payload traffic of the OpenVPN client.');
print_stdout('out.type GAUGE');
print_stdout('out.negative in');
print_stdout('out.min 0');
print_stdout('out.cdef out,8,*');
// received (raw)
print_stdout('rawIn.label received_raw');
print_stdout('rawIn.type GAUGE');
print_stdout('rawIn.graph no');
print_stdout('rawIn.min 0');
print_stdout('rawIn.cdef rawIn,8,*');
// sent (raw)
print_stdout('rawOut.label Raw');
print_stdout('rawOut.info Raw traffic of the OpenVPN client (after compression, including protocol overhead).');
print_stdout('rawOut.type GAUGE');
print_stdout('rawOut.negative rawIn');
print_stdout('rawOut.min 0');
print_stdout('rawOut.cdef rawOut,8,*');
}
exit(0);
}
/**
* print graph data
*/
$saveStateData = array();
$saveStateData[GRAPH_NAME]['lastUpdate'] = $lastUpdate;
$saveStateData[GRAPH_NAME]['type'] = (OPENVPN_TYPE === 'server') ? 'server' : 'client';
if (OPENVPN_TYPE === 'server') {
/**
* print graph data of a server
*/
// client count graph
print_stdout('multigraph '.GRAPH_NAME);
print_stdout('clients.value ' . $lastUpdate . ':' . count($clientData));
// traffic graphs
if (isset($stateData[GRAPH_NAME]['lastUpdate'])) {
// anything to do?
if ($stateData[GRAPH_NAME]['lastUpdate'] >= $lastUpdate) {
print_stdout('multigraph ' . GRAPH_NAME . '_traffic');
print_stdout('in.value U');
print_stdout('out.value U');
foreach ($allClientData as $identifier => $data) {
print_stdout('multigraph ' . GRAPH_NAME . '_traffic.' . $identifier);
print_stdout('in.value U');
print_stdout('out.value U');
}
exit(0);
}
// calculate total and print values of connected clients
bcscale(0);
$total = array('in' => 0, 'out' => 0);
foreach ($clientData as $identifier => $data) {
if (isset($stateData[$identifier]) && ($stateData[$identifier]['connectedSince'] == $data['connectedSince'])) {
$periodLength = ($lastUpdate - $stateData[$identifier]['lastSeen']);
$clientIn = bcdiv(bcsub($data['in'], $stateData[$identifier]['in']), $periodLength);
$clientOut = bcdiv(bcsub($data['out'], $stateData[$identifier]['out']), $periodLength);
} else {
$periodLength = ($lastUpdate - $data['connectedSince']);
$clientIn = bcdiv($data['in'], $periodLength);
$clientOut = bcdiv($data['out'], $periodLength);
}
$total['in'] = bcadd($total['in'], $clientIn);
$total['out'] = bcadd($total['out'], $clientOut);
print_stdout('multigraph ' . GRAPH_NAME . '_traffic.' . $identifier);
print_stdout('in.value ' . $lastUpdate . ':' . $clientIn);
print_stdout('out.value ' . $lastUpdate . ':' . $clientOut);
}
// print values of persistent clients
foreach ($persistentClientData as $identifier => $data) {
print_stdout('multigraph ' . GRAPH_NAME . '_traffic.' . $identifier);
print_stdout('in.value ' . $lastUpdate . ':0');
print_stdout('out.value ' . $lastUpdate . ':0');
}
// print values of total graph
print_stdout('multigraph ' . GRAPH_NAME . '_traffic');
print_stdout('in.value ' . $lastUpdate . ':' . $total['in']);
print_stdout('out.value ' . $lastUpdate . ':' . $total['out']);
} else {
// first run of the plugin
print_stdout('multigraph ' . GRAPH_NAME . '_traffic');
print_stdout('in.value ' . $lastUpdate . ':U');
print_stdout('out.value ' . $lastUpdate . ':U');
foreach ($allClientData as $identifier => $data) {
print_stdout('multigraph ' . GRAPH_NAME . '_traffic.' . $identifier);
print_stdout('in.value ' . $lastUpdate . ':U');
print_stdout('out.value ' . $lastUpdate . ':U');
}
}
// update statefile data
$saveStateData += $allClientData;
} elseif (OPENVPN_TYPE === 'client') {
/**
* print graph data of a connected client
*/
$clientTrafficUndefined = true;
if (isset($stateData[GRAPH_NAME]['lastUpdate'])) {
// anything to do?
if ($stateData[GRAPH_NAME]['lastUpdate'] >= $lastUpdate) {
print_stdout('in.value U');
print_stdout('out.value U');
print_stdout('rawIn.value U');
print_stdout('rawOut.value U');
exit(0);
}
// print values of the connected client
if (
isset($stateData[GRAPH_NAME]['in'])
&& ($clientData['in'] >= $stateData[GRAPH_NAME]['in'])
&& ($clientData['out'] >= $stateData[GRAPH_NAME]['out'])
&& ($clientData['rawIn'] >= $stateData[GRAPH_NAME]['rawIn'])
&& ($clientData['rawOut'] >= $stateData[GRAPH_NAME]['rawOut'])
) {
bcscale(0);
$periodLength = ($lastUpdate - $stateData[GRAPH_NAME]['lastUpdate']);
$clientIn = bcdiv(bcsub($clientData['in'], $stateData[GRAPH_NAME]['in']), $periodLength);
$clientOut = bcdiv(bcsub($clientData['out'], $stateData[GRAPH_NAME]['out']), $periodLength);
$clientRawIn = bcdiv(bcsub($clientData['rawIn'], $stateData[GRAPH_NAME]['rawIn']), $periodLength);
$clientRawOut = bcdiv(bcsub($clientData['rawOut'], $stateData[GRAPH_NAME]['rawOut']), $periodLength);
print_stdout('in.value ' . $lastUpdate . ':' . $clientIn);
print_stdout('out.value ' . $lastUpdate . ':' . $clientOut);
print_stdout('rawIn.value ' . $lastUpdate . ':' . $clientRawIn);
print_stdout('rawOut.value ' . $lastUpdate . ':' . $clientRawOut);
$clientTrafficUndefined = false;
}
}
// first run of the plugin or client reconnected in the meantime
if ($clientTrafficUndefined) {
print_stdout('in.value ' . $lastUpdate . ':U');
print_stdout('out.value ' . $lastUpdate . ':U');
print_stdout('rawIn.value ' . $lastUpdate . ':U');
print_stdout('rawOut.value ' . $lastUpdate . ':U');
}
// update statefile data
$saveStateData[GRAPH_NAME] += $clientData;
} else {
/**
* print graph data of a disconnected client
*/
print_stdout('in.value 0');
print_stdout('out.value 0');
print_stdout('rawIn.value 0');
print_stdout('rawOut.value 0');
// statefile is going to be reset
}
// write statefile
write_statefile($saveStateData);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment