Last active
April 6, 2022 23:46
-
-
Save PhrozenByte/dbe4091343cebe529a18 to your computer and use it in GitHub Desktop.
Munin plugin for OpenVPN
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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