www/vpn.php

229 lines
9.9 KiB
PHP
Raw Permalink Normal View History

2026-06-27 21:44:44 +00:00
<?php
require_once __DIR__.'/inc/common.php';
$SSH_KEY = '/var/www/.ssh/nordvpn';
$WIREGUARD = ['host' => '192.168.10.100', 'user' => 'root', 'key' => $SSH_KEY];
$NORDVPNS = [
'lxc104' => ['label' => 'YouTube', 'icon' => '📺', 'host' => '192.168.10.104', 'desc' => 'WireGuard'],
'lxc110' => ['label' => 'Torrents', 'icon' => '🧲', 'host' => '192.168.10.110', 'desc' => 'Transmission'],
];
$COUNTRIES = [
'Albania' => '🇦🇱', 'Australia' => '🇦🇺', 'Austria' => '🇦🇹',
'Belgium' => '🇧🇪', 'Brazil' => '🇧🇷', 'Bulgaria' => '🇧🇬',
'Canada' => '🇨🇦', 'Chile' => '🇨🇱', 'Colombia' => '🇨🇴',
'Croatia' => '🇭🇷', 'Czech Republic' => '🇨🇿', 'Denmark' => '🇩🇰',
'Estonia' => '🇪🇪', 'Finland' => '🇫🇮', 'France' => '🇫🇷',
'Germany' => '🇩🇪', 'Greece' => '🇬🇷', 'Hong Kong' => '🇭🇰',
'Hungary' => '🇭🇺', 'Iceland' => '🇮🇸', 'India' => '🇮🇳',
'Ireland' => '🇮🇪', 'Israel' => '🇮🇱', 'Italy' => '🇮🇹',
'Japan' => '🇯🇵', 'Latvia' => '🇱🇻', 'Lithuania' => '🇱🇹',
'Luxembourg' => '🇱🇺', 'Malaysia' => '🇲🇾', 'Mexico' => '🇲🇽',
'Moldova' => '🇲🇩', 'Netherlands' => '🇳🇱', 'New Zealand' => '🇳🇿',
'Nigeria' => '🇳🇬', 'Norway' => '🇳🇴', 'Philippines' => '🇵🇭',
'Poland' => '🇵🇱', 'Portugal' => '🇵🇹', 'Romania' => '🇷🇴',
'Serbia' => '🇷🇸', 'Singapore' => '🇸🇬', 'Slovakia' => '🇸🇰',
'Slovenia' => '🇸🇮', 'South Africa' => '🇿🇦', 'South Korea' => '🇰🇷',
'Spain' => '🇪🇸', 'Sweden' => '🇸🇪', 'Switzerland' => '🇨🇭',
'Taiwan' => '🇹🇼', 'Thailand' => '🇹🇭', 'Turkey' => '🇹🇷',
'Ukraine' => '🇺🇦', 'United Kingdom' => '🇬🇧', 'United States' => '🇺🇸',
];
function do_ssh(string $host, string $user, string $key, string $cmd): string {
$opts = "-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8";
$ssh = "/usr/bin/ssh -i ".escapeshellarg($key)." $opts "
.escapeshellarg("$user@$host")." ".escapeshellarg($cmd)." 2>&1";
return trim((string)shell_exec($ssh));
}
function parse_nordvpn_status(string $raw): array {
$connected = stripos($raw, 'Connected') !== false;
$country = '—'; $ip = '—';
if (preg_match('/Country:\s*(.+)/i', $raw, $m)) $country = trim($m[1]);
if (preg_match('/IP:\s*(\S+)/i', $raw, $m)) $ip = trim($m[1]);
return compact('connected', 'country', 'ip');
}
function parse_wg_status(string $raw): array {
$parts = preg_split('/=== (wg\d+) ===/i', $raw, -1, PREG_SPLIT_DELIM_CAPTURE);
$ifaces = [];
for ($i = 1; $i < count($parts) - 1; $i += 2) {
$name = $parts[$i];
$block = $parts[$i + 1];
$peers = substr_count($block, 'peer:');
$active = preg_match_all('/latest handshake: (\d+) (second|minute|hour)/i', $block);
$ip = '—';
$ext = '—';
if (preg_match('/addr:\s*(\S+)/i', $block, $m)) $ip = $m[1];
if (preg_match('/ext:\s*(\S+)/i', $block, $m)) $ext = $m[1];
$ifaces[$name] = ['peers' => $peers, 'active' => $active, 'ip' => $ip, 'ext' => $ext];
}
return $ifaces;
}
// AJAX
if (isset($_GET['ajax'])) {
header('Content-Type: application/json');
$id = $_GET['ajax'];
if ($id === 'wg') {
$raw = do_ssh($WIREGUARD['host'], $WIREGUARD['user'], $WIREGUARD['key'], 'status');
echo json_encode(['ifaces' => parse_wg_status($raw)]);
} elseif (isset($NORDVPNS[$id])) {
$raw = do_ssh($NORDVPNS[$id]['host'], 'root', $SSH_KEY, 'status');
echo json_encode(parse_nordvpn_status($raw));
}
exit;
}
$result = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$section = $_POST['section'] ?? '';
$action = $_POST['action'] ?? '';
if (isset($NORDVPNS[$section])) {
$vpn = $NORDVPNS[$section];
if ($action === 'connect') {
$country = $_POST['country'] ?? '';
if (isset($COUNTRIES[$country])) {
$raw = do_ssh($vpn['host'], 'root', $SSH_KEY, "connect $country");
$result = ['id' => $section, 'ok' => stripos($raw, 'connected') !== false, 'msg' => $raw];
}
} elseif ($action === 'disconnect') {
$raw = do_ssh($vpn['host'], 'root', $SSH_KEY, 'disconnect');
$result = ['id' => $section, 'ok' => true, 'msg' => $raw];
}
}
}
$title = 'VPN';
ob_start();
?>
<div class="page-header">
<div>
<div class="breadcrumb"><a href="/painel.php">Painel</a><span class="sep"></span>VPN</div>
<h1>VPN</h1>
</div>
</div>
<?php if ($result): ?>
<div class="alert <?= $result['ok'] ? 'alert-success' : 'alert-error' ?>" style="margin-bottom:16px">
<?= $result['ok'] ? '✓' : '⚠' ?> <?= h($result['msg']) ?>
</div>
<?php endif; ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px">
<!-- NordVPN cards -->
<?php foreach ($NORDVPNS as $id => $vpn): ?>
<div class="card">
<div class="card-header">
<h2><?= $vpn['icon'] ?> <?= h($vpn['label']) ?></h2>
<span style="font-size:11px;color:var(--muted)"><?= h($vpn['desc']) ?></span>
</div>
<div class="card-body" style="display:flex;flex-direction:column;gap:14px">
<!-- Estado resumido -->
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<div id="flag-<?= $id ?>" style="font-size:22px;line-height:1"></div>
<div id="country-<?= $id ?>" style="font-size:13px;font-weight:500;margin-top:4px">A carregar…</div>
<div id="ip-<?= $id ?>" style="font-size:11px;color:var(--muted);font-family:monospace"></div>
</div>
<div id="dot-<?= $id ?>" style="width:10px;height:10px;border-radius:50%;background:var(--muted);flex-shrink:0"></div>
</div>
<!-- Dropdown + ações -->
<form method="post" style="display:flex;gap:8px">
<input type="hidden" name="section" value="<?= $id ?>">
<input type="hidden" name="action" value="connect">
<select name="country" required style="flex:1;padding:7px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none">
<option value="" disabled selected>Mudar país…</option>
<?php foreach ($COUNTRIES as $name => $flag): ?>
<option value="<?= h($name) ?>"><?= $flag ?> <?= h($name) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-primary btn-sm">Ligar</button>
</form>
<form method="post">
<input type="hidden" name="section" value="<?= $id ?>">
<input type="hidden" name="action" value="disconnect">
<button type="submit" class="btn btn-danger btn-sm" style="width:100%">Desligar</button>
</form>
</div>
</div>
<?php endforeach; ?>
<!-- WireGuard -->
<div class="card">
<div class="card-header">
<h2>🔒 WireGuard</h2>
<span style="font-size:11px;color:var(--muted)">LXC 100</span>
</div>
<div class="card-body" style="display:flex;flex-direction:column;gap:12px">
<?php foreach (['wg0' => 'Direto', 'wg1' => 'NordVPN'] as $iface => $desc): ?>
<div style="display:flex;align-items:center;justify-content:space-between">
<div style="display:flex;align-items:center;gap:10px">
<div id="dot-<?= $iface ?>" style="width:9px;height:9px;border-radius:50%;background:var(--muted);flex-shrink:0"></div>
<div>
<div style="font-size:13px;font-weight:600"><?= $iface ?> <span style="font-weight:400;color:var(--muted);font-size:12px">— <?= $desc ?></span></div>
<div id="<?= $iface ?>-info" style="font-size:11px;color:var(--muted)">A carregar…</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<script>
const FLAGS = <?= json_encode(array_map(fn($f) => $f, $COUNTRIES), JSON_UNESCAPED_UNICODE) ?>;
const NAMES = <?= json_encode(array_keys($COUNTRIES)) ?>;
function getFlag(country) {
const i = NAMES.indexOf(country);
return i >= 0 ? Object.values(FLAGS)[i] : '🌐';
}
function loadNord(id) {
fetch('/vpn.php?ajax=' + id).then(r => r.json()).then(d => {
document.getElementById('flag-' + id).textContent = d.connected ? getFlag(d.country) : '—';
document.getElementById('country-' + id).textContent = d.connected ? d.country : 'Desligado';
document.getElementById('ip-' + id).textContent = d.connected ? d.ip : '';
document.getElementById('dot-' + id).style.background = d.connected ? '#3fb950' : '#6e2120';
}).catch(() => {
document.getElementById('country-' + id).textContent = 'Erro';
document.getElementById('dot-' + id).style.background = '#6e2120';
});
}
function loadWG() {
fetch('/vpn.php?ajax=wg').then(r => r.json()).then(d => {
const ifaces = d.ifaces || {};
['wg0','wg1'].forEach(name => {
const wg = ifaces[name] || {peers:0, active:0};
const info = document.getElementById(name + '-info');
const dot = document.getElementById('dot-' + name);
if (info) info.textContent = (wg.ext || '—') + ' · ' + wg.peers + ' peers · ' + wg.active + ' activo' + (wg.active !== 1 ? 's' : '');
if (dot) dot.style.background = wg.active > 0 ? '#3fb950' : '#7d8590';
});
}).catch(() => {
['wg0','wg1'].forEach(name => {
const info = document.getElementById(name + '-info');
const dot = document.getElementById('dot-' + name);
if (info) info.textContent = 'Erro SSH';
if (dot) dot.style.background = '#6e2120';
});
});
}
<?php foreach (array_keys($NORDVPNS) as $id): ?>loadNord('<?= $id ?>');<?php endforeach; ?>
loadWG();
</script>
<?php $content = ob_get_clean(); require __DIR__.'/inc/layout.php';