Initial commit: project openvpn-monitor
This commit is contained in:
20
openvpn-monitor/api/tls-errors.php
Normal file
20
openvpn-monitor/api/tls-errors.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
define('APP_INIT', true); // ✅ Добавлено
|
||||
|
||||
$path = __DIR__ . '/../src/OpenVPNMonitor.php';
|
||||
if (!file_exists($path)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'File not found: ' . $path]);
|
||||
exit;
|
||||
}
|
||||
require_once $path;
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$monitor = new OpenVPNMonitor();
|
||||
$errors = $monitor->getTlsErrors();
|
||||
echo json_encode(['tls_errors' => $errors]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
122
openvpn-monitor/assets/script.js
Normal file
122
openvpn-monitor/assets/script.js
Normal file
@@ -0,0 +1,122 @@
|
||||
function loadClients() {
|
||||
fetch('clients.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Загруженные клиенты:', data);
|
||||
renderClientTable(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Ошибка загрузки данных:', error);
|
||||
document.getElementById('client-table').innerHTML = '<tr><td colspan="12">Ошибка загрузки</td></tr>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderClientTable(clients) {
|
||||
const table = document.getElementById('client-table');
|
||||
table.innerHTML = '';
|
||||
|
||||
const stats = { LZO: 0, ADAPTIVE: 0, STUB: 0, '—': 0 };
|
||||
|
||||
clients.forEach(client => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// TLS ошибка
|
||||
if (client.tls_error) {
|
||||
row.classList.add('tls-error');
|
||||
row.title = client.tls_error;
|
||||
}
|
||||
|
||||
// Сжатие
|
||||
const compression = (client.compression || '—').toUpperCase();
|
||||
stats[compression] = (stats[compression] || 0) + 1;
|
||||
|
||||
let compressionClass = 'compression-none';
|
||||
if (compression === 'LZO') compressionClass = 'compression-lzo';
|
||||
else if (compression === 'ADAPTIVE') compressionClass = 'compression-adaptive';
|
||||
else if (compression === 'STUB') compressionClass = 'compression-stub';
|
||||
|
||||
// Потери
|
||||
let lossClass = '';
|
||||
if (client.loss_rate >= 10) lossClass = 'loss-high';
|
||||
else if (client.loss_rate >= 1) lossClass = 'loss-medium';
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${client.name}</td>
|
||||
<td>${client.real_ip}</td>
|
||||
<td>${client.virtual_ip}</td>
|
||||
<td>${client.connectedSince}</td>
|
||||
<td>${formatBytes(client.bytes_received)}</td>
|
||||
<td>${formatBytes(client.bytes_sent)}</td>
|
||||
<td>${client.statusLabel}</td>
|
||||
<td>${client.idleTime || '—'}</td>
|
||||
<td class="${compressionClass}">${compression}</td>
|
||||
<td>${client.packets_received ?? '—'}</td>
|
||||
<td>${client.packets_lost ?? '—'}</td>
|
||||
<td class="${lossClass}">${client.loss_rate ?? '—'}%</td>
|
||||
`;
|
||||
|
||||
table.appendChild(row);
|
||||
});
|
||||
|
||||
renderCompressionStats(stats);
|
||||
}
|
||||
|
||||
function renderCompressionStats(stats) {
|
||||
const container = document.getElementById('compression-stats');
|
||||
container.innerHTML = `
|
||||
<strong>Сжатие:</strong>
|
||||
LZO: ${stats.LZO || 0},
|
||||
Adaptive: ${stats.ADAPTIVE || 0},
|
||||
Stub: ${stats.STUB || 0},
|
||||
Нет: ${stats['—'] || 0}
|
||||
`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function loadTlsErrors() {
|
||||
fetch('api/tls-errors.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const container = document.getElementById('tls-errors');
|
||||
container.innerHTML = '';
|
||||
if (data.tls_errors && data.tls_errors.length > 0) {
|
||||
data.tls_errors.forEach(err => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = err;
|
||||
container.appendChild(div);
|
||||
});
|
||||
} else {
|
||||
container.textContent = 'Нет TLS ошибок.';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('tls-errors').textContent = 'Ошибка загрузки данных.';
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Вкладки
|
||||
const buttons = document.querySelectorAll('.tab-button');
|
||||
const tabs = document.querySelectorAll('.tab-content');
|
||||
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
buttons.forEach(b => b.classList.remove('active'));
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
|
||||
btn.classList.add('active');
|
||||
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Загрузка данных
|
||||
loadClients();
|
||||
loadTlsErrors();
|
||||
setInterval(loadClients, 5000);
|
||||
setInterval(loadTlsErrors, 10000);
|
||||
});
|
||||
80
openvpn-monitor/assets/style.css
Normal file
80
openvpn-monitor/assets/style.css
Normal file
@@ -0,0 +1,80 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
box-shadow: 0 0 5px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
tr.tls-error {
|
||||
background-color: #ffe0e0;
|
||||
color: #a00;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: var(--panel-color);
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background: none;
|
||||
border: 1px solid var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--bg-color);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #444;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr.tls-error {
|
||||
background-color: var(--error-bg);
|
||||
color: #ff9999;
|
||||
}
|
||||
td.compression-lzo { color: green; }
|
||||
td.compression-adaptive { color: blue; }
|
||||
td.compression-stub { color: orange; }
|
||||
td.compression-none { color: gray; }
|
||||
td.loss-high { color: red; font-weight: bold; }
|
||||
td.loss-medium { color: orange; }
|
||||
8
openvpn-monitor/clients.php
Normal file
8
openvpn-monitor/clients.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
define('APP_INIT', true);
|
||||
require_once __DIR__ . '/src/OpenVPNMonitor.php';
|
||||
|
||||
$monitor = new OpenVPNMonitor();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($monitor->getClientList(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
|
||||
13
openvpn-monitor/debug.php
Normal file
13
openvpn-monitor/debug.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
define('APP_INIT', true);
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/src/OpenVPNMonitor.php';
|
||||
use OpenVPN\OpenVPNMonitor;
|
||||
|
||||
$vpn = new OpenVPNMonitor();
|
||||
$clients = $vpn->getClientList();
|
||||
|
||||
echo '<pre>';
|
||||
print_r($clients);
|
||||
56
openvpn-monitor/index.php
Normal file
56
openvpn-monitor/index.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
date_default_timezone_set('Europe/Minsk');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OpenVPN Монитор</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<script src="assets/script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🔐 Мониторинг OpenVPN</h1>
|
||||
<nav class="tabs">
|
||||
<button class="tab-button active" data-tab="overview">Общие сведения</button>
|
||||
<button class="tab-button" data-tab="tls">Ошибки TLS</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="compression-stats" class="stats-block" style="margin-top: 10px;
|
||||
font-weight: bold;"></div>
|
||||
<section id="tab-overview" class="tab-content active">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th>
|
||||
<th>IP</th>
|
||||
<th>Вирт. IP</th>
|
||||
<th>Сессия</th>
|
||||
<th>RX</th>
|
||||
<th>TX</th>
|
||||
<th>Статус</th>
|
||||
<th>Простой</th>
|
||||
<th>Сжатие</th>
|
||||
<th>Пакеты приняты</th>
|
||||
<th>Пакеты потеряны</th>
|
||||
<th>Потери (%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="client-table"></tbody>
|
||||
</table>
|
||||
</section> </section>
|
||||
|
||||
<section id="tab-tls" class="tab-content">
|
||||
<h2>⚠️ Ошибки TLS от клиентов</h2>
|
||||
<div id="tls-errors" class="tls-error-box">Загрузка...</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© <?= date('Y') ?> OpenVPN Dashboard</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
41
openvpn-monitor/src/LogMonitor.php
Normal file
41
openvpn-monitor/src/LogMonitor.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
namespace OpenVPN;
|
||||
|
||||
class LogMonitor {
|
||||
private $host;
|
||||
private $port;
|
||||
private $socket;
|
||||
|
||||
public function __construct($host = '127.0.0.1', $port = 7505) {
|
||||
$this->host = $host;
|
||||
$this->port = $port;
|
||||
}
|
||||
|
||||
public function connect() {
|
||||
$this->socket = fsockopen($this->host, $this->port, $errno, $errstr, 5);
|
||||
if (!$this->socket) {
|
||||
throw new \Exception("Management connection failed: $errstr ($errno)");
|
||||
}
|
||||
stream_set_timeout($this->socket, 2);
|
||||
$this->send("log on");
|
||||
}
|
||||
|
||||
private function send($cmd) {
|
||||
fwrite($this->socket, "$cmd\n");
|
||||
}
|
||||
|
||||
public function getTlsErrors() {
|
||||
$this->connect();
|
||||
$errors = [];
|
||||
|
||||
while (!feof($this->socket)) {
|
||||
$line = fgets($this->socket);
|
||||
if (str_contains($line, 'TLS Error') || str_contains($line, 'VERIFY ERROR')) {
|
||||
$errors[] = trim($line);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($this->socket);
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
116
openvpn-monitor/src/OpenVPNMonitor.php
Normal file
116
openvpn-monitor/src/OpenVPNMonitor.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
date_default_timezone_set('Europe/Minsk');
|
||||
class OpenVPNMonitor
|
||||
{
|
||||
public $host = '127.0.0.1';
|
||||
public $port = 7505;
|
||||
|
||||
public function getClientList()
|
||||
{
|
||||
$clients = [];
|
||||
$tlsErrors = $this->getTlsErrors();
|
||||
$compressionStats = ['LZO' => 0, 'ADAPTIVE' => 0, 'STUB' => 0, '—' => 0];
|
||||
|
||||
$fp = @fsockopen($this->host, $this->port, $errno, $errstr, 2);
|
||||
if (!$fp) {
|
||||
return [[
|
||||
'name' => 'Ошибка подключения',
|
||||
'real_ip' => "$errstr ($errno)",
|
||||
'virtual_ip' => '—',
|
||||
'connectedSince' => '—',
|
||||
'bytes_received' => 0,
|
||||
'bytes_sent' => 0,
|
||||
'status' => 'disconnected',
|
||||
'statusLabel' => 'Ошибка',
|
||||
'idleTime' => null,
|
||||
'tls_error' => null,
|
||||
'compression' => '—',
|
||||
'packets_received' => 0,
|
||||
'packets_lost' => 0,
|
||||
'loss_rate' => 0
|
||||
]];
|
||||
}
|
||||
|
||||
stream_set_timeout($fp, 2);
|
||||
fwrite($fp, "status\n");
|
||||
|
||||
$raw = '';
|
||||
while (!feof($fp)) {
|
||||
$line = fgets($fp, 1024);
|
||||
if (trim($line) === "END") break;
|
||||
$raw .= $line;
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
|
||||
foreach (explode("\n", $raw) as $line) {
|
||||
if (strpos($line, "CLIENT_LIST") === 0) {
|
||||
$fields = explode(",", $line);
|
||||
|
||||
$rx = (int)$fields[5];
|
||||
$tx = (int)$fields[6];
|
||||
|
||||
$status = 'connected';
|
||||
$statusLabel = 'Подключён';
|
||||
|
||||
if ($rx === 0 && $tx === 0) {
|
||||
$status = 'idle';
|
||||
$statusLabel = 'Нет активности';
|
||||
}
|
||||
|
||||
$lastActivity = strtotime($fields[7]);
|
||||
$now = time();
|
||||
$idleSeconds = $now - $lastActivity;
|
||||
$idleTime = gmdate("H:i:s", $idleSeconds);
|
||||
|
||||
$compressionRaw = isset($fields[9]) ? trim($fields[9]) : '';
|
||||
$compression = strtoupper($compressionRaw);
|
||||
if ($compression === 'UNDEF' || $compression === '') {
|
||||
$compression = '—';
|
||||
}
|
||||
if (isset($compressionStats[$compression])) {
|
||||
$compressionStats[$compression]++;
|
||||
}
|
||||
|
||||
$packetsReceived = isset($fields[10]) ? (int)$fields[10] : 0;
|
||||
$packetsLost = isset($fields[11]) ? (int)$fields[11] : 0;
|
||||
$lossRate = $packetsReceived > 0 ? round(($packetsLost / $packetsReceived) * 100, 2) : 0;
|
||||
|
||||
$clients[] = [
|
||||
'name' => $fields[1],
|
||||
'real_ip' => $fields[2],
|
||||
'virtual_ip' => $fields[3],
|
||||
'bytes_received' => $rx,
|
||||
'bytes_sent' => $tx,
|
||||
'connectedSince' => $fields[7],
|
||||
'status' => $status,
|
||||
'statusLabel' => $statusLabel,
|
||||
'idleTime' => $status === 'idle' ? $idleTime : null,
|
||||
'tls_error' => isset($tlsErrors[$fields[2]]) ? $tlsErrors[$fields[2]] : null,
|
||||
'compression' => $compression,
|
||||
'packets_received' => $packetsReceived,
|
||||
'packets_lost' => $packetsLost,
|
||||
'loss_rate' => $lossRate
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
usort($clients, function ($a, $b) {
|
||||
$order = ['connected' => 0, 'idle' => 1, 'disconnected' => 2];
|
||||
$aVal = isset($order[$a['status']]) ? $order[$a['status']] : 99;
|
||||
$bVal = isset($order[$b['status']]) ? $order[$b['status']] : 99;
|
||||
|
||||
if ($aVal === $bVal) return 0;
|
||||
return ($aVal < $bVal) ? -1 : 1;
|
||||
});
|
||||
|
||||
return $clients;
|
||||
}
|
||||
|
||||
public function getTlsErrors()
|
||||
{
|
||||
// Заглушка — можно подключить лог TLS-ошибок
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user