Initial commit: project openvpn-monitor

This commit is contained in:
2025-08-21 22:40:47 +03:00
commit 3c45f22ebe
9 changed files with 456 additions and 0 deletions

0
.gitignore vendored Normal file
View File

View 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()]);
}

View 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);
});

View 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; }

View 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
View 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
View 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>

View 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;
}
}

View 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 [];
}
}