From 3c45f22ebebccd81cb6bb96f378078e1ce790393 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 21 Aug 2025 22:40:47 +0300 Subject: [PATCH] Initial commit: project openvpn-monitor --- .gitignore | 0 openvpn-monitor/api/tls-errors.php | 20 ++++ openvpn-monitor/assets/script.js | 122 +++++++++++++++++++++++++ openvpn-monitor/assets/style.css | 80 ++++++++++++++++ openvpn-monitor/clients.php | 8 ++ openvpn-monitor/debug.php | 13 +++ openvpn-monitor/index.php | 56 ++++++++++++ openvpn-monitor/src/LogMonitor.php | 41 +++++++++ openvpn-monitor/src/OpenVPNMonitor.php | 116 +++++++++++++++++++++++ 9 files changed, 456 insertions(+) create mode 100644 .gitignore create mode 100644 openvpn-monitor/api/tls-errors.php create mode 100644 openvpn-monitor/assets/script.js create mode 100644 openvpn-monitor/assets/style.css create mode 100644 openvpn-monitor/clients.php create mode 100644 openvpn-monitor/debug.php create mode 100644 openvpn-monitor/index.php create mode 100644 openvpn-monitor/src/LogMonitor.php create mode 100644 openvpn-monitor/src/OpenVPNMonitor.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/openvpn-monitor/api/tls-errors.php b/openvpn-monitor/api/tls-errors.php new file mode 100644 index 0000000..41a714b --- /dev/null +++ b/openvpn-monitor/api/tls-errors.php @@ -0,0 +1,20 @@ + '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()]); +} diff --git a/openvpn-monitor/assets/script.js b/openvpn-monitor/assets/script.js new file mode 100644 index 0000000..6a94d15 --- /dev/null +++ b/openvpn-monitor/assets/script.js @@ -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 = 'Ошибка загрузки'; + }); +} + +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 = ` + ${client.name} + ${client.real_ip} + ${client.virtual_ip} + ${client.connectedSince} + ${formatBytes(client.bytes_received)} + ${formatBytes(client.bytes_sent)} + ${client.statusLabel} + ${client.idleTime || '—'} + ${compression} + ${client.packets_received ?? '—'} + ${client.packets_lost ?? '—'} + ${client.loss_rate ?? '—'}% + `; + + table.appendChild(row); + }); + + renderCompressionStats(stats); +} + +function renderCompressionStats(stats) { + const container = document.getElementById('compression-stats'); + container.innerHTML = ` + Сжатие: + 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); +}); diff --git a/openvpn-monitor/assets/style.css b/openvpn-monitor/assets/style.css new file mode 100644 index 0000000..604561a --- /dev/null +++ b/openvpn-monitor/assets/style.css @@ -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; } diff --git a/openvpn-monitor/clients.php b/openvpn-monitor/clients.php new file mode 100644 index 0000000..de5747f --- /dev/null +++ b/openvpn-monitor/clients.php @@ -0,0 +1,8 @@ +getClientList(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + diff --git a/openvpn-monitor/debug.php b/openvpn-monitor/debug.php new file mode 100644 index 0000000..00b22a3 --- /dev/null +++ b/openvpn-monitor/debug.php @@ -0,0 +1,13 @@ +getClientList(); + +echo '
';
+print_r($clients);
diff --git a/openvpn-monitor/index.php b/openvpn-monitor/index.php
new file mode 100644
index 0000000..d64ca88
--- /dev/null
+++ b/openvpn-monitor/index.php
@@ -0,0 +1,56 @@
+
+
+
+
+  
+  OpenVPN Монитор
+  
+  
+
+
+  
+

🔐 Мониторинг OpenVPN

+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + +
ИмяIPВирт. IPСессияRXTXСтатусПростойСжатиеПакеты принятыПакеты потеряныПотери (%)
+
+ +
+

⚠️ Ошибки TLS от клиентов

+
Загрузка...
+
+
+ +
+

© OpenVPN Dashboard

+
+ + diff --git a/openvpn-monitor/src/LogMonitor.php b/openvpn-monitor/src/LogMonitor.php new file mode 100644 index 0000000..03206d7 --- /dev/null +++ b/openvpn-monitor/src/LogMonitor.php @@ -0,0 +1,41 @@ +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; + } +} diff --git a/openvpn-monitor/src/OpenVPNMonitor.php b/openvpn-monitor/src/OpenVPNMonitor.php new file mode 100644 index 0000000..01eed00 --- /dev/null +++ b/openvpn-monitor/src/OpenVPNMonitor.php @@ -0,0 +1,116 @@ +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 []; + } +} +