www/vpn.php
2026-06-27 21:44:44 +00:00

229 lines
9.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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';