|
@@ -113,6 +113,7 @@ function navigate(tabName) {
|
|
|
'dynamic-routing': { title: 'BGP Routing (/routing bgp)', sub: 'BIRD 2 dynamic routing engine' },
|
|
'dynamic-routing': { title: 'BGP Routing (/routing bgp)', sub: 'BIRD 2 dynamic routing engine' },
|
|
|
ppp: { title: 'PPP Servers & Secrets', sub: 'Subscriber authentication, dynamic IP pools and VPN access concentrators' },
|
|
ppp: { title: 'PPP Servers & Secrets', sub: 'Subscriber authentication, dynamic IP pools and VPN access concentrators' },
|
|
|
eoip: { title: 'EoIP Tunnels', sub: 'Ethernet over IP (EoIP / GRETAP) Layer 2 encapsulation & transparent network bridging' },
|
|
eoip: { title: 'EoIP Tunnels', sub: 'Ethernet over IP (EoIP / GRETAP) Layer 2 encapsulation & transparent network bridging' },
|
|
|
|
|
+ aerotun: { title: 'AeroTUN VPN (/interface aerotun)', sub: 'Ultra-fast Layer 3 encrypted tunnel with ChaCha20-Poly1305 and live Adaptive LZ4 compression' },
|
|
|
pppoe: { title: 'PPPoE Client WAN', sub: 'Point-to-Point Protocol over Ethernet WAN access' },
|
|
pppoe: { title: 'PPPoE Client WAN', sub: 'Point-to-Point Protocol over Ethernet WAN access' },
|
|
|
wireguard: { title: 'WireGuard VPN', sub: 'High-performance cryptographic Layer 3 tunnels with ChaCha20-Poly1305 key exchange' },
|
|
wireguard: { title: 'WireGuard VPN', sub: 'High-performance cryptographic Layer 3 tunnels with ChaCha20-Poly1305 key exchange' },
|
|
|
ipsec: { title: 'IPSec (IKEv2) strongSwan', sub: 'Enterprise Site-to-Site & Roadwarrior IPSec controller' },
|
|
ipsec: { title: 'IPSec (IKEv2) strongSwan', sub: 'Enterprise Site-to-Site & Roadwarrior IPSec controller' },
|
|
@@ -152,6 +153,7 @@ function navigate(tabName) {
|
|
|
if (tabName === 'dynamic-routing') loadDynamicRouting();
|
|
if (tabName === 'dynamic-routing') loadDynamicRouting();
|
|
|
if (tabName === 'ppp') loadPPP();
|
|
if (tabName === 'ppp') loadPPP();
|
|
|
if (tabName === 'eoip') loadEoIP();
|
|
if (tabName === 'eoip') loadEoIP();
|
|
|
|
|
+ if (tabName === 'aerotun') loadAeroTUN();
|
|
|
if (tabName === 'persistence') loadDebianConfig();
|
|
if (tabName === 'persistence') loadDebianConfig();
|
|
|
if (tabName === 'pppoe') loadPPPoE();
|
|
if (tabName === 'pppoe') loadPPPoE();
|
|
|
if (tabName === 'wireguard') loadWireGuard();
|
|
if (tabName === 'wireguard') loadWireGuard();
|
|
@@ -232,6 +234,7 @@ async function fetchData(isManual = false) {
|
|
|
if (state.activeTab === 'dynamic-routing' || state.activeTab === 'bgp' || state.activeTab === 'ospf') loadDynamicRouting(false);
|
|
if (state.activeTab === 'dynamic-routing' || state.activeTab === 'bgp' || state.activeTab === 'ospf') loadDynamicRouting(false);
|
|
|
if (state.activeTab === 'ppp') loadPPP(false);
|
|
if (state.activeTab === 'ppp') loadPPP(false);
|
|
|
if (state.activeTab === 'eoip') loadEoIP(false);
|
|
if (state.activeTab === 'eoip') loadEoIP(false);
|
|
|
|
|
+ if (state.activeTab === 'aerotun') loadAeroTUN(false);
|
|
|
if (state.activeTab === 'pppoe') loadPPPoE(false);
|
|
if (state.activeTab === 'pppoe') loadPPPoE(false);
|
|
|
if (state.activeTab === 'wireguard') loadWireGuard(false);
|
|
if (state.activeTab === 'wireguard') loadWireGuard(false);
|
|
|
if (state.activeTab === 'ipsec') loadIPSec(false);
|
|
if (state.activeTab === 'ipsec') loadIPSec(false);
|
|
@@ -442,6 +445,8 @@ function updateBadges() {
|
|
|
const v = state.vpnStatus;
|
|
const v = state.vpnStatus;
|
|
|
const bEoIP = document.getElementById('badge-eoip-count');
|
|
const bEoIP = document.getElementById('badge-eoip-count');
|
|
|
if (bEoIP) bEoIP.innerText = v.total_eoip || 0;
|
|
if (bEoIP) bEoIP.innerText = v.total_eoip || 0;
|
|
|
|
|
+ const bAero = document.getElementById('badge-aerotun-count');
|
|
|
|
|
+ if (bAero) bAero.innerText = v.total_aerotun || 0;
|
|
|
const bPPPSess = document.getElementById('badge-ppp-sessions');
|
|
const bPPPSess = document.getElementById('badge-ppp-sessions');
|
|
|
if (bPPPSess) bPPPSess.innerText = v.total_ppp_sessions || 0;
|
|
if (bPPPSess) bPPPSess.innerText = v.total_ppp_sessions || 0;
|
|
|
setDotBadge('badge-pppoe-status', v.active_pppoe > 0 || v.pppoe_server_running);
|
|
setDotBadge('badge-pppoe-status', v.active_pppoe > 0 || v.pppoe_server_running);
|
|
@@ -6531,6 +6536,334 @@ async function deleteEoIP(id) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ======================== AeroTUN (Custom Encrypted VPN with Adaptive LZ4) ========================
|
|
|
|
|
+async function loadAeroTUN(populate = true) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [tunnelsRes, statsRes] = await Promise.all([
|
|
|
|
|
+ fetch(`${API_BASE}/vpn/aerotun/tunnels`),
|
|
|
|
|
+ fetch(`${API_BASE}/vpn/aerotun/stats`)
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ if (tunnelsRes.ok) {
|
|
|
|
|
+ const data = await tunnelsRes.json();
|
|
|
|
|
+ state.aerotunTunnels = Array.isArray(data) ? data : [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (statsRes.ok) {
|
|
|
|
|
+ state.aerotunStats = await statsRes.json();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const countBadge = document.getElementById('aerotun-count-badge');
|
|
|
|
|
+ if (countBadge) countBadge.innerText = `${(state.aerotunTunnels || []).length} Tunnels`;
|
|
|
|
|
+
|
|
|
|
|
+ const sideBadge = document.getElementById('badge-aerotun-count');
|
|
|
|
|
+ if (sideBadge) sideBadge.innerText = (state.aerotunTunnels || []).length;
|
|
|
|
|
+
|
|
|
|
|
+ renderAeroTUN();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to load AeroTUN:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderAeroTUN() {
|
|
|
|
|
+ const tbody = document.getElementById('aerotun-table-body');
|
|
|
|
|
+ const empty = document.getElementById('aerotun-empty');
|
|
|
|
|
+
|
|
|
|
|
+ // Update summary cards
|
|
|
|
|
+ const stats = state.aerotunStats || {};
|
|
|
|
|
+ const tunnels = state.aerotunTunnels || [];
|
|
|
|
|
+
|
|
|
|
|
+ const statActive = document.getElementById('aerotun-stat-active');
|
|
|
|
|
+ if (statActive) {
|
|
|
|
|
+ const activeCount = tunnels.filter(t => t.running).length;
|
|
|
|
|
+ statActive.innerText = `${activeCount} / ${tunnels.length}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const statSaved = document.getElementById('aerotun-stat-saved');
|
|
|
|
|
+ if (statSaved) {
|
|
|
|
|
+ statSaved.innerText = formatBytes(stats.total_saved_bytes || 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const statRatio = document.getElementById('aerotun-stat-ratio');
|
|
|
|
|
+ if (statRatio) {
|
|
|
|
|
+ statRatio.innerText = stats.average_ratio || '1.00x (0% saved)';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const statTraffic = document.getElementById('aerotun-stat-traffic');
|
|
|
|
|
+ if (statTraffic) {
|
|
|
|
|
+ statTraffic.innerText = `↓ ${formatBytes(stats.total_rx_bytes || 0)} / ↑ ${formatBytes(stats.total_tx_bytes || 0)}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (tunnels.length === 0) {
|
|
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ empty?.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ tunnels.forEach(t => {
|
|
|
|
|
+ const tr = document.createElement('tr');
|
|
|
|
|
+ tr.className = `hover:bg-slate-800/40 transition text-xs ${t.disabled ? 'opacity-50' : ''}`;
|
|
|
|
|
+
|
|
|
|
|
+ const modeBadge = t.mode === 'server'
|
|
|
|
|
+ ? `<span class="px-2 py-0.5 rounded text-[10px] font-mono bg-sky-500/20 text-sky-300 border border-sky-500/30 font-semibold uppercase">Server</span>`
|
|
|
|
|
+ : `<span class="px-2 py-0.5 rounded text-[10px] font-mono bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 font-semibold uppercase">Client</span>`;
|
|
|
|
|
+
|
|
|
|
|
+ const endpointInfo = t.mode === 'server'
|
|
|
|
|
+ ? `<span class="text-sky-300 font-mono">Port :${t.listen_port || 51830}</span>`
|
|
|
|
|
+ : `<span class="text-indigo-300 font-mono">${escapeHtml(t.remote_endpoint || '(not set)')}</span>`;
|
|
|
|
|
+
|
|
|
|
|
+ const compBadge = t.compression
|
|
|
|
|
+ ? `<span class="px-1.5 py-0.5 rounded text-[10px] font-mono bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-semibold" title="Adaptive LZ4 active">${t.compression_ratio || 'LZ4'}</span>`
|
|
|
|
|
+ : `<span class="text-slate-500 text-[10px]">Off</span>`;
|
|
|
|
|
+
|
|
|
|
|
+ const activePeer = t.active_peer_endpoint
|
|
|
|
|
+ ? `<span class="text-emerald-400 font-mono text-[11px]" title="Last seen: ${t.last_handshake || 'just now'}">● ${escapeHtml(t.active_peer_endpoint)}</span>`
|
|
|
|
|
+ : `<span class="text-slate-500 text-[11px]">${t.mode === 'server' ? 'Waiting for peer...' : 'Connecting...'}</span>`;
|
|
|
|
|
+
|
|
|
|
|
+ const rawRx = formatBytes(t.rx_bytes || 0);
|
|
|
|
|
+ const rawTx = formatBytes(t.tx_bytes || 0);
|
|
|
|
|
+ const wireRx = formatBytes(t.wire_bytes_in || 0);
|
|
|
|
|
+ const wireTx = formatBytes(t.wire_bytes_out || 0);
|
|
|
|
|
+
|
|
|
|
|
+ tr.innerHTML = `
|
|
|
|
|
+ <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(t.running, t.disabled)}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-bold text-white font-mono flex items-center gap-1.5">
|
|
|
|
|
+ <span>${escapeHtml(t.name)}</span>
|
|
|
|
|
+ ${t.default_route ? '<span class="text-[9px] px-1 rounded bg-amber-500/20 text-amber-300 border border-amber-500/30">DEF</span>' : ''}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-2.5 px-3">${modeBadge}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-xs font-mono">${endpointInfo}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-semibold text-emerald-400 font-mono">${escapeHtml(t.local_address || '-')}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3">${compBadge}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3">${activePeer}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-mono text-[11px]">
|
|
|
|
|
+ <span class="text-emerald-400">↓ ${rawRx}</span>
|
|
|
|
|
+ <span class="text-cyan-400 ml-1">↑ ${rawTx}</span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-2.5 px-3 font-mono text-[11px]">
|
|
|
|
|
+ <span class="text-slate-400">↓ ${wireRx}</span>
|
|
|
|
|
+ <span class="text-slate-400 ml-1">↑ ${wireTx}</span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]" title="${escapeHtml(t.comment || '')}">${escapeHtml(t.comment || '-')}</td>
|
|
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
|
|
|
|
|
+ ${t.running
|
|
|
|
|
+ ? `<button onclick="stopAeroTUNTunnel('${t.id}')" class="text-xs text-amber-400 hover:text-amber-300 font-medium">Stop</button>`
|
|
|
|
|
+ : `<button onclick="startAeroTUNTunnel('${t.id}')" class="text-xs text-emerald-400 hover:text-emerald-300 font-medium">Start</button>`
|
|
|
|
|
+ }
|
|
|
|
|
+ <button onclick="toggleAeroTUNState('${t.id}', ${!t.disabled})" class="text-xs ${t.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${t.disabled ? 'Enable' : 'Disable'}</button>
|
|
|
|
|
+ <button onclick="openEditAeroTUNModal('${t.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
|
|
|
|
|
+ <button onclick="deleteAeroTUN('${t.id}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ `;
|
|
|
|
|
+ tbody.appendChild(tr);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onAeroTUNModeChange() {
|
|
|
|
|
+ const mode = document.getElementById('aerotun-modal-mode').value;
|
|
|
|
|
+ const endpointInput = document.getElementById('aerotun-modal-endpoint');
|
|
|
|
|
+ const portInput = document.getElementById('aerotun-modal-port');
|
|
|
|
|
+
|
|
|
|
|
+ if (mode === 'server') {
|
|
|
|
|
+ if (!portInput.value) portInput.value = '51830';
|
|
|
|
|
+ endpointInput.placeholder = 'Optional / learned roaming peer';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ endpointInput.placeholder = 'vpn.example.com:51830 (required)';
|
|
|
|
|
+ if (!portInput.value) portInput.value = '';
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function generateAeroTUNKey() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/aerotun/genkey`);
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ document.getElementById('aerotun-modal-psk').value = data.key;
|
|
|
|
|
+ showToast('Key Generated', 'Cryptographic 256-bit PSK generated', 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ const arr = new Uint8Array(32);
|
|
|
|
|
+ crypto.getRandomValues(arr);
|
|
|
|
|
+ const hex = Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
|
|
|
+ document.getElementById('aerotun-modal-psk').value = hex;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openAddAeroTUNModal() {
|
|
|
|
|
+ const tunnels = state.aerotunTunnels || [];
|
|
|
|
|
+ document.getElementById('aerotun-modal-title').innerText = 'Add AeroTUN Interface';
|
|
|
|
|
+ document.getElementById('aerotun-modal-id').value = '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-name').value = `aerotun${tunnels.length}`;
|
|
|
|
|
+ document.getElementById('aerotun-modal-mode').value = 'server';
|
|
|
|
|
+ document.getElementById('aerotun-modal-port').value = 51830 + tunnels.length;
|
|
|
|
|
+ document.getElementById('aerotun-modal-endpoint').value = '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-local').value = `10.99.${tunnels.length}.1/24`;
|
|
|
|
|
+ document.getElementById('aerotun-modal-peer').value = `10.99.${tunnels.length}.2/24`;
|
|
|
|
|
+ document.getElementById('aerotun-modal-psk').value = '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-mtu').value = '1420';
|
|
|
|
|
+ document.getElementById('aerotun-modal-comment').value = '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-compression').checked = true;
|
|
|
|
|
+ document.getElementById('aerotun-modal-defroute').checked = false;
|
|
|
|
|
+ document.getElementById('aerotun-modal-disabled').checked = false;
|
|
|
|
|
+
|
|
|
|
|
+ generateAeroTUNKey();
|
|
|
|
|
+ onAeroTUNModeChange();
|
|
|
|
|
+ document.getElementById('aerotun-modal').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openEditAeroTUNModal(id) {
|
|
|
|
|
+ const t = (state.aerotunTunnels || []).find(tun => tun.id === id);
|
|
|
|
|
+ if (!t) return;
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('aerotun-modal-title').innerText = `Edit AeroTUN Interface (${t.name})`;
|
|
|
|
|
+ document.getElementById('aerotun-modal-id').value = t.id;
|
|
|
|
|
+ document.getElementById('aerotun-modal-name').value = t.name;
|
|
|
|
|
+ document.getElementById('aerotun-modal-mode').value = t.mode || 'server';
|
|
|
|
|
+ document.getElementById('aerotun-modal-port').value = t.listen_port || '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-endpoint').value = t.remote_endpoint || '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-local').value = t.local_address || '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-peer').value = t.peer_address || '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-psk').value = t.psk || '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-mtu').value = t.mtu || 1420;
|
|
|
|
|
+ document.getElementById('aerotun-modal-comment').value = t.comment || '';
|
|
|
|
|
+ document.getElementById('aerotun-modal-compression').checked = !!t.compression;
|
|
|
|
|
+ document.getElementById('aerotun-modal-defroute').checked = !!t.default_route;
|
|
|
|
|
+ document.getElementById('aerotun-modal-disabled').checked = !!t.disabled;
|
|
|
|
|
+
|
|
|
|
|
+ onAeroTUNModeChange();
|
|
|
|
|
+ document.getElementById('aerotun-modal').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeAeroTUNModal() {
|
|
|
|
|
+ document.getElementById('aerotun-modal').classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitSaveAeroTUN(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const id = document.getElementById('aerotun-modal-id').value;
|
|
|
|
|
+ const name = document.getElementById('aerotun-modal-name').value.trim();
|
|
|
|
|
+ const mode = document.getElementById('aerotun-modal-mode').value;
|
|
|
|
|
+ const port = parseInt(document.getElementById('aerotun-modal-port').value) || 0;
|
|
|
|
|
+ const endpoint = document.getElementById('aerotun-modal-endpoint').value.trim();
|
|
|
|
|
+ const local = document.getElementById('aerotun-modal-local').value.trim();
|
|
|
|
|
+ const peer = document.getElementById('aerotun-modal-peer').value.trim();
|
|
|
|
|
+ const psk = document.getElementById('aerotun-modal-psk').value.trim();
|
|
|
|
|
+ const mtu = parseInt(document.getElementById('aerotun-modal-mtu').value) || 1420;
|
|
|
|
|
+ const comment = document.getElementById('aerotun-modal-comment').value.trim();
|
|
|
|
|
+ const compression = document.getElementById('aerotun-modal-compression').checked;
|
|
|
|
|
+ const defaultRoute = document.getElementById('aerotun-modal-defroute').checked;
|
|
|
|
|
+ const disabled = document.getElementById('aerotun-modal-disabled').checked;
|
|
|
|
|
+
|
|
|
|
|
+ if (mode === 'client' && !endpoint) {
|
|
|
|
|
+ showToast('Validation Error', 'Remote endpoint (host:port) is required in client mode', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!psk) {
|
|
|
|
|
+ showToast('Validation Error', 'Pre-shared key (PSK) is required', 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ id,
|
|
|
|
|
+ name,
|
|
|
|
|
+ mode,
|
|
|
|
|
+ listen_port: port,
|
|
|
|
|
+ remote_endpoint: endpoint,
|
|
|
|
|
+ local_address: local,
|
|
|
|
|
+ peer_address: peer,
|
|
|
|
|
+ psk,
|
|
|
|
|
+ mtu,
|
|
|
|
|
+ compression,
|
|
|
|
|
+ default_route: defaultRoute,
|
|
|
|
|
+ comment,
|
|
|
|
|
+ disabled
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('AeroTUN Saved', `Tunnel ${name} configured successfully`, 'success');
|
|
|
|
|
+ closeAeroTUNModal();
|
|
|
|
|
+ loadAeroTUN(false);
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.text();
|
|
|
|
|
+ showToast('Error Saving Tunnel', err, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Network Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function startAeroTUNTunnel(id) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels/${encodeURIComponent(id)}/start`, { method: 'POST' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('AeroTUN Started', 'Tunnel interface is active', 'success');
|
|
|
|
|
+ loadAeroTUN(false);
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.text();
|
|
|
|
|
+ showToast('Failed to Start', err, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function stopAeroTUNTunnel(id) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels/${encodeURIComponent(id)}/stop`, { method: 'POST' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('AeroTUN Stopped', 'Tunnel stopped', 'info');
|
|
|
|
|
+ loadAeroTUN(false);
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.text();
|
|
|
|
|
+ showToast('Failed to Stop', err, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function toggleAeroTUNState(id, disabled) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels/${encodeURIComponent(id)}/state`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('State Updated', disabled ? 'Tunnel disabled' : 'Tunnel enabled', 'info');
|
|
|
|
|
+ loadAeroTUN(false);
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteAeroTUN(id) {
|
|
|
|
|
+ if (!confirm('Are you sure you want to delete this AeroTUN tunnel?')) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Deleted', 'AeroTUN tunnel removed', 'success');
|
|
|
|
|
+ loadAeroTUN(false);
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ======================== PPP Central Hub & Servers ========================
|
|
// ======================== PPP Central Hub & Servers ========================
|
|
|
function switchPPPSubTab(tab) {
|
|
function switchPPPSubTab(tab) {
|
|
|
state.pppSubTab = tab;
|
|
state.pppSubTab = tab;
|