|
@@ -7619,6 +7619,8 @@ async function deleteEoIP(id) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ======================== AeroTUN (Custom Encrypted VPN with Adaptive LZ4) ========================
|
|
// ======================== AeroTUN (Custom Encrypted VPN with Adaptive LZ4) ========================
|
|
|
|
|
+const aeroRateSamples = new Map();
|
|
|
|
|
+const aeroLiveRates = new Map();
|
|
|
async function loadAeroTUN(populate = true) {
|
|
async function loadAeroTUN(populate = true) {
|
|
|
try {
|
|
try {
|
|
|
const [tunnelsRes, statsRes] = await Promise.all([
|
|
const [tunnelsRes, statsRes] = await Promise.all([
|
|
@@ -7629,6 +7631,14 @@ async function loadAeroTUN(populate = true) {
|
|
|
if (tunnelsRes.ok) {
|
|
if (tunnelsRes.ok) {
|
|
|
const data = await tunnelsRes.json();
|
|
const data = await tunnelsRes.json();
|
|
|
state.aerotunTunnels = Array.isArray(data) ? data : [];
|
|
state.aerotunTunnels = Array.isArray(data) ? data : [];
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ for (const t of state.aerotunTunnels) {
|
|
|
|
|
+ const old = aeroRateSamples.get(t.id), seconds = old ? (now-old.time)/1000 : 0;
|
|
|
|
|
+ if (seconds >= 0.5 && seconds <= 60 && t.rx_bytes >= old.rx && t.tx_bytes >= old.tx && t.packets_rx >= old.prx && t.packets_tx >= old.ptx) {
|
|
|
|
|
+ aeroLiveRates.set(t.id,{rx:(t.rx_bytes-old.rx)*8/seconds/1e6,tx:(t.tx_bytes-old.tx)*8/seconds/1e6,prx:(t.packets_rx-old.prx)/seconds,ptx:(t.packets_tx-old.ptx)/seconds});
|
|
|
|
|
+ } else if (!old || seconds > 60 || t.rx_bytes < old.rx || t.tx_bytes < old.tx) aeroLiveRates.delete(t.id);
|
|
|
|
|
+ if (!old || seconds >= 0.5) aeroRateSamples.set(t.id,{time:now,rx:t.rx_bytes||0,tx:t.tx_bytes||0,prx:t.packets_rx||0,ptx:t.packets_tx||0});
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (statsRes.ok) {
|
|
if (statsRes.ok) {
|
|
@@ -7642,6 +7652,7 @@ async function loadAeroTUN(populate = true) {
|
|
|
if (sideBadge) sideBadge.innerText = (state.aerotunTunnels || []).length;
|
|
if (sideBadge) sideBadge.innerText = (state.aerotunTunnels || []).length;
|
|
|
|
|
|
|
|
renderAeroTUN();
|
|
renderAeroTUN();
|
|
|
|
|
+ if (state.aeroHealthSelected) await refreshAeroTUNHealth();
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
console.error('Failed to load AeroTUN:', err);
|
|
console.error('Failed to load AeroTUN:', err);
|
|
|
}
|
|
}
|
|
@@ -7709,6 +7720,8 @@ function renderAeroTUN() {
|
|
|
const rawTx = formatBytes(t.tx_bytes || 0);
|
|
const rawTx = formatBytes(t.tx_bytes || 0);
|
|
|
const wireRx = formatBytes(t.wire_bytes_in || 0);
|
|
const wireRx = formatBytes(t.wire_bytes_in || 0);
|
|
|
const wireTx = formatBytes(t.wire_bytes_out || 0);
|
|
const wireTx = formatBytes(t.wire_bytes_out || 0);
|
|
|
|
|
+ const rate = aeroLiveRates.get(t.id);
|
|
|
|
|
+ const id = encodeURIComponent(String(t.id || '')).replace(/'/g, '%27');
|
|
|
|
|
|
|
|
tr.innerHTML = `
|
|
tr.innerHTML = `
|
|
|
<td class="py-2.5 px-3 text-center">${renderMikroTikFlag(t.connected, t.disabled)}</td>
|
|
<td class="py-2.5 px-3 text-center">${renderMikroTikFlag(t.connected, t.disabled)}</td>
|
|
@@ -7725,6 +7738,7 @@ function renderAeroTUN() {
|
|
|
<td class="py-2.5 px-3 font-mono text-[11px]">
|
|
<td class="py-2.5 px-3 font-mono text-[11px]">
|
|
|
<span class="text-emerald-400">↓ ${rawRx}</span>
|
|
<span class="text-emerald-400">↓ ${rawRx}</span>
|
|
|
<span class="text-cyan-400 ml-1">↑ ${rawTx}</span>
|
|
<span class="text-cyan-400 ml-1">↑ ${rawTx}</span>
|
|
|
|
|
+ ${rate ? `<div class="text-slate-400">↓ ${rate.rx.toFixed(2)} / ↑ ${rate.tx.toFixed(2)} Mbit/s</div>` : ''}
|
|
|
</td>
|
|
</td>
|
|
|
<td class="py-2.5 px-3 font-mono text-[11px]">
|
|
<td class="py-2.5 px-3 font-mono text-[11px]">
|
|
|
<span class="text-slate-400">↓ ${wireRx}</span>
|
|
<span class="text-slate-400">↓ ${wireRx}</span>
|
|
@@ -7732,6 +7746,7 @@ function renderAeroTUN() {
|
|
|
</td>
|
|
</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-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">
|
|
<td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
|
|
|
|
|
+ <button onclick="openAeroTUNHealth('${id}')" class="text-xs text-sky-400 hover:text-sky-300 font-medium">Health</button>
|
|
|
${t.running
|
|
${t.running
|
|
|
? `<button onclick="stopAeroTUNTunnel('${t.id}')" class="text-xs text-amber-400 hover:text-amber-300 font-medium">Stop</button>`
|
|
? `<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="startAeroTUNTunnel('${t.id}')" class="text-xs text-emerald-400 hover:text-emerald-300 font-medium">Start</button>`
|
|
@@ -7745,6 +7760,66 @@ function renderAeroTUN() {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function openAeroTUNHealth(encodedID) {
|
|
|
|
|
+ state.aeroHealthSelected = decodeURIComponent(encodedID);
|
|
|
|
|
+ const tunnel=(state.aerotunTunnels||[]).find(x=>x.id===state.aeroHealthSelected);
|
|
|
|
|
+ let underlay='';try {underlay=new URL(`http://${tunnel?.remote_endpoint||tunnel?.active_peer_endpoint||''}`).hostname.replace(/^\[|\]$/g,'')} catch {}
|
|
|
|
|
+ document.getElementById('aerotun-compare-host').value=underlay;
|
|
|
|
|
+ document.getElementById('aerotun-compare-result').textContent='';
|
|
|
|
|
+ document.getElementById('aerotun-health-result').textContent = '';
|
|
|
|
|
+ document.getElementById('aerotun-health-panel').classList.remove('hidden');
|
|
|
|
|
+ await refreshAeroTUNHealth();
|
|
|
|
|
+ document.getElementById('aerotun-health-panel').scrollIntoView({behavior:'smooth',block:'nearest'});
|
|
|
|
|
+}
|
|
|
|
|
+function closeAeroTUNHealth() { aeroCompareCancel=true; state.aeroHealthSelected = ''; document.getElementById('aerotun-health-panel').classList.add('hidden'); }
|
|
|
|
|
+async function refreshAeroTUNHealth() {
|
|
|
|
|
+ const id=state.aeroHealthSelected,t=(state.aerotunTunnels||[]).find(x=>x.id===id);if(!id)return;
|
|
|
|
|
+ if(!t){document.getElementById('aerotun-health-status').textContent='Tunnel no longer exists.';return}
|
|
|
|
|
+ document.getElementById('aerotun-health-title').textContent=`Health: ${t.name}`;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response=await fetch(`${API_BASE}/vpn/aerotun/health`);if(!response.ok)throw new Error(await responseError(response));const data=await response.json();if(state.aeroHealthSelected!==id)return;
|
|
|
|
|
+ const current=(data.states||[]).find(x=>x.tunnel_id===id);const status=current?.status||'unknown';const since=current?.since&&!current.since.startsWith('0001')?new Date(current.since).toLocaleString():'—';const rate=aeroLiveRates.get(id);
|
|
|
|
|
+ const d=t.drop_breakdown;
|
|
|
|
|
+ document.getElementById('aerotun-health-status').textContent=`Status: ${status} since ${since} · Backend: ${t.backend||'—'} · MTU: ${t.mtu||'—'} / configured ${t.configured_mtu||(!t.running?t.mtu:'—')||'—'} · Last authenticated packet: ${t.last_handshake||'never'} · Drops: ${t.dropped_packets||0}${d?` (outgoing invalid ${d.invalid_outgoing}, session ${d.missing_session}, send ${d.send_failure}, incoming invalid ${d.invalid_incoming}, source policy ${d.source_policy}, interface ${d.interface_write})`:''} · Authentication failures: ${t.auth_failures||0} · Replay drops: ${t.replay_drops||0}${Number.isFinite(data.service_cpu_percent)?` · Service CPU (all features): ${data.service_cpu_percent.toFixed(1)}% of one core`:''}${rate?` · RX/TX: ${rate.rx.toFixed(2)} / ${rate.tx.toFixed(2)} Mbit/s, ${rate.prx.toFixed(0)} / ${rate.ptx.toFixed(0)} packets/s`:''}${t.last_error?` · Last error: ${t.last_error}`:''}`;
|
|
|
|
|
+ const events=(data.events||[]).filter(e=>e.tunnel_id===id).slice(0,100),node=document.getElementById('aerotun-health-history');node.replaceChildren();
|
|
|
|
|
+ if(!events.length){node.textContent='No events recorded yet.';return}
|
|
|
|
|
+ for(const e of events){const line=document.createElement('div');line.textContent=`${new Date(e.time).toLocaleString()} · ${e.type==='check'?'Check':(e.previous?`${e.previous} → ${e.status}${e.previous_duration_seconds?` (after ${Math.round(e.previous_duration_seconds)} s)`:''}`:e.status)}${e.detail?` · ${e.detail}`:''}`;node.appendChild(line)}
|
|
|
|
|
+ } catch(e){document.getElementById('aerotun-health-history').textContent=`Health history unavailable: ${e.message}`}
|
|
|
|
|
+}
|
|
|
|
|
+async function runAeroTUNHealthCheck() {
|
|
|
|
|
+ const id=state.aeroHealthSelected;if(!id)return;const button=document.getElementById('aerotun-health-check-btn'),output=document.getElementById('aerotun-health-result');button.disabled=true;output.textContent='Checking…';
|
|
|
|
|
+ try {const response=await fetch(`${API_BASE}/vpn/aerotun/tunnels/${encodeURIComponent(id)}/health-check`,{method:'POST'});if(!response.ok)throw new Error(await responseError(response));const r=await response.json();if(state.aeroHealthSelected!==id)return;output.textContent=`Peer echo: ${r.reachability}${r.rtt_ms!=null?` · RTT ${r.rtt_upper_bound?'<':''}${r.rtt_ms} ms`:''} · ${r.mtu}-byte overlay probe: ${r.mtu_probe}. ${r.explanation}`;await refreshAeroTUNHealth()}
|
|
|
|
|
+ catch(e){if(state.aeroHealthSelected===id)output.textContent=`Health check failed: ${e.message}`}
|
|
|
|
|
+ finally {button.disabled=false}
|
|
|
|
|
+}
|
|
|
|
|
+let aeroCompareRunning=false, aeroCompareCancel=false;
|
|
|
|
|
+function stopAeroTUNComparison() { aeroCompareCancel=true;document.getElementById('aerotun-compare-result').textContent+='\nStopping after the current test.'; }
|
|
|
|
|
+async function runAeroTUNComparison() {
|
|
|
|
|
+ if(aeroCompareRunning)return;
|
|
|
|
|
+ const id=state.aeroHealthSelected,t=(state.aerotunTunnels||[]).find(x=>x.id===id),direct=document.getElementById('aerotun-compare-host').value.trim(),port=Number(document.getElementById('aerotun-compare-port').value),peer=(t?.peer_address||'').split('/')[0].trim();
|
|
|
|
|
+ const output=document.getElementById('aerotun-compare-result'),button=document.getElementById('aerotun-compare-btn'),stop=document.getElementById('aerotun-compare-stop');
|
|
|
|
|
+ if(!t?.connected||!peer||!direct||!Number.isInteger(port)||port<1||port>65535||peer===direct){output.textContent='A connected tunnel, peer IP, different direct server address and valid port are required.';return}
|
|
|
|
|
+ if(peer.includes(':')||direct.includes(':')){output.textContent='This comparison currently supports IPv4 addresses or IPv4-resolvable hostnames.';return}
|
|
|
|
|
+ aeroCompareRunning=true;aeroCompareCancel=false;button.disabled=true;stop.classList.remove('hidden');output.textContent='Starting. The direct address uses the system routing table.';
|
|
|
|
|
+ const get=async(path,body)=>{const r=await fetch(`${API_BASE}${path}`,body?{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}:{});if(!r.ok)throw new Error(await responseError(r));return r.json()};
|
|
|
|
|
+ const results=[];
|
|
|
|
|
+ try {
|
|
|
|
|
+ for(const [path,target,direction] of [['Tunnel',peer,'tx'],['Tunnel',peer,'rx'],['Direct',direct,'tx'],['Direct',direct,'rx']]) {
|
|
|
|
|
+ if(aeroCompareCancel)break;
|
|
|
|
|
+ if((await get('/tools/bwtest/status')).running)throw new Error('Another bandwidth test is running; comparison stopped.');
|
|
|
|
|
+ output.textContent+=`\n${path} ${direction.toUpperCase()}: testing…`;
|
|
|
|
|
+ const started=await get('/tools/bwtest/start',{target,port,protocol:'tcp',direction,bandwidth_limit:0,packet_size:1200,streams:2,duration:8}),session=started.session_id;let status,complete=false;
|
|
|
|
|
+ for(let i=0;i<30;i++){await new Promise(resolve=>setTimeout(resolve,1000));status=await get('/tools/bwtest/status');if(status.session_id!==session)throw new Error('Bandwidth test session changed; comparison stopped.');if(!status.running){complete=true;break}}
|
|
|
|
|
+ if(!complete)throw new Error('Timed out waiting for the bandwidth test.');if(status.error)throw new Error(`${path} ${direction.toUpperCase()}: ${status.error}`);
|
|
|
|
|
+ const value=status.summary?.[direction==='tx'?'avg_tx_mbps':'avg_rx_mbps'];if(!Number.isFinite(value)||value<=0)throw new Error(`${path} ${direction.toUpperCase()}: no delivered data.`);
|
|
|
|
|
+ results.push(`${path} ${direction.toUpperCase()}: ${value.toFixed(2)} Mbit/s`);output.textContent='Results:\n'+results.join('\n');
|
|
|
|
|
+ }
|
|
|
|
|
+ if(aeroCompareCancel)output.textContent+='\nComparison stopped.';
|
|
|
|
|
+ else output.textContent+='\nDirect results are meaningful only if both addresses reach the same server over different routes.';
|
|
|
|
|
+ } catch(e){output.textContent+=`\nComparison incomplete: ${e.message}`}
|
|
|
|
|
+ finally {aeroCompareRunning=false;button.disabled=false;stop.classList.add('hidden')}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
let aeroModalGeneration = 0;
|
|
let aeroModalGeneration = 0;
|
|
|
let aeroIdentityDraft = '';
|
|
let aeroIdentityDraft = '';
|
|
|
let aeroSaving = false;
|
|
let aeroSaving = false;
|
|
@@ -7905,7 +7980,7 @@ function openEditAeroTUNModal(id) {
|
|
|
document.getElementById('aerotun-modal-psk').value = '';
|
|
document.getElementById('aerotun-modal-psk').value = '';
|
|
|
document.getElementById('aerotun-modal-psk').type = 'password';
|
|
document.getElementById('aerotun-modal-psk').type = 'password';
|
|
|
document.getElementById('aerotun-modal-psk').placeholder = t.has_psk ? 'Leave blank to keep the existing key' : 'New random 256-bit PSK';
|
|
document.getElementById('aerotun-modal-psk').placeholder = t.has_psk ? 'Leave blank to keep the existing key' : 'New random 256-bit PSK';
|
|
|
- document.getElementById('aerotun-modal-mtu').value = t.mtu || 1420;
|
|
|
|
|
|
|
+ document.getElementById('aerotun-modal-mtu').value = t.configured_mtu || t.mtu || 1420;
|
|
|
document.getElementById('aerotun-modal-comment').value = t.comment || '';
|
|
document.getElementById('aerotun-modal-comment').value = t.comment || '';
|
|
|
document.getElementById('aerotun-modal-compression').checked = !!t.compression;
|
|
document.getElementById('aerotun-modal-compression').checked = !!t.compression;
|
|
|
document.getElementById('aerotun-modal-defroute').checked = !!t.default_route;
|
|
document.getElementById('aerotun-modal-defroute').checked = !!t.default_route;
|