|
|
@@ -7728,6 +7728,37 @@ function renderAeroTUN() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+let aeroModalGeneration = 0;
|
|
|
+let aeroIdentityDraft = '';
|
|
|
+let aeroSaving = false;
|
|
|
+let aeroPSKRequest = 0;
|
|
|
+
|
|
|
+function discardAeroIdentityDraft(id) {
|
|
|
+ if (id) fetch(`${API_BASE}/vpn/aerotun/identity-drafts/${encodeURIComponent(id)}`, {method:'DELETE'}).catch(() => {});
|
|
|
+}
|
|
|
+
|
|
|
+async function generateAeroTUNIdentity() {
|
|
|
+ const generation = aeroModalGeneration;
|
|
|
+ const retry = document.getElementById('aerotun-retry-identity');
|
|
|
+ if (aeroIdentityDraft) { discardAeroIdentityDraft(aeroIdentityDraft); aeroIdentityDraft = ''; }
|
|
|
+ retry.classList.add('hidden');
|
|
|
+ document.getElementById('aerotun-modal-public').value = '';
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/vpn/aerotun/identity-drafts`, {method:'POST'});
|
|
|
+ if (!res.ok) throw new Error(await responseError(res));
|
|
|
+ const draft = await res.json();
|
|
|
+ if (generation !== aeroModalGeneration) { discardAeroIdentityDraft(draft.draft_id); return; }
|
|
|
+ aeroIdentityDraft = draft.draft_id;
|
|
|
+ document.getElementById('aerotun-modal-public').value = draft.public_key;
|
|
|
+ updateAeroTUNSetupHint();
|
|
|
+ } catch (err) {
|
|
|
+ if (generation !== aeroModalGeneration) return;
|
|
|
+ retry.classList.remove('hidden');
|
|
|
+ updateAeroTUNSetupHint();
|
|
|
+ showToast('Identity Generation Failed', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function onAeroTUNModeChange() {
|
|
|
const mode = document.getElementById('aerotun-modal-mode').value;
|
|
|
const endpointInput = document.getElementById('aerotun-modal-endpoint');
|
|
|
@@ -7742,15 +7773,28 @@ function onAeroTUNModeChange() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async function generateAeroTUNKey() {
|
|
|
+function onAeroTUNPeerChange() {
|
|
|
+ const peer = document.getElementById('aerotun-modal-peer');
|
|
|
+ const allowed = document.getElementById('aerotun-modal-allowed');
|
|
|
+ const previous = peer.dataset.allowedDefault || '';
|
|
|
+ const host = peer.value.trim().split('/')[0];
|
|
|
+ const next = host ? `${host}/${host.includes(':') ? 128 : 32}` : '';
|
|
|
+ if (allowed.value.trim() === previous) allowed.value = next;
|
|
|
+ peer.dataset.allowedDefault = next;
|
|
|
+}
|
|
|
+
|
|
|
+async function generateAeroTUNKey(automatic = false) {
|
|
|
+ const generation = aeroModalGeneration, request = ++aeroPSKRequest;
|
|
|
+ const input = document.getElementById('aerotun-modal-psk'), previous = input.value;
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/vpn/aerotun/genkey`);
|
|
|
if (!res.ok) throw new Error(await responseError(res));
|
|
|
const data = await res.json();
|
|
|
- document.getElementById('aerotun-modal-psk').value = data.key;
|
|
|
- showToast('Key Generated', 'Share this PSK securely with the peer before enabling', 'success');
|
|
|
+ if (generation !== aeroModalGeneration || request !== aeroPSKRequest || (automatic && input.value !== previous)) return;
|
|
|
+ input.value = data.key;
|
|
|
+ if (!automatic) showToast('Key Generated', 'Share this PSK securely with the peer before enabling', 'success');
|
|
|
} catch (err) {
|
|
|
- showToast('Key Generation Failed', err.message, 'error');
|
|
|
+ if (generation === aeroModalGeneration && request === aeroPSKRequest) showToast('Key Generation Failed', err.message, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -7759,13 +7803,14 @@ function updateAeroTUNSetupHint() {
|
|
|
const key = document.getElementById('aerotun-modal-public').value;
|
|
|
const peer = document.getElementById('aerotun-modal-peerkey').value.trim();
|
|
|
const hint = document.getElementById('aerotun-setup-hint');
|
|
|
+ document.getElementById('aerotun-v3-fields').classList.toggle('hidden', version !== '3');
|
|
|
if (!hint) return;
|
|
|
- hint.innerText = version !== '3' ? 'Legacy v2 uses a shared PSK. Both endpoints must use the same protocol.' : !key ? '1. Save disabled to create the local identity. 2. Exchange public keys and share the same PSK. 3. Set allowed peer prefixes, then enable.' : !peer ? 'Local identity saved. Copy the public key to the peer and paste its public key here. Use the same PSK on both endpoints.' : 'Peer identity configured. Check reciprocal addresses, allowed peer source prefixes and the shared PSK before enabling. Allowed prefixes do not create LAN routes.';
|
|
|
+ hint.innerText = version !== '3' ? 'Legacy v2 uses a shared PSK. Both endpoints must use the same protocol.' : !key ? (document.getElementById('aerotun-modal-id').value ? 'Save this existing tunnel disabled to create its v3 identity, then exchange public keys.' : 'Generating the local identity. You can fill in the other settings while you wait.') : !peer ? 'Copy the local public key to the peer and paste its public key here. Use the same PSK on both endpoints; save disabled if the peer key is not ready.' : 'Check reciprocal addresses, allowed peer source prefixes and the shared PSK before enabling. Allowed prefixes do not create LAN routes.';
|
|
|
}
|
|
|
|
|
|
async function copyAeroTUNPublicKey() {
|
|
|
const input = document.getElementById('aerotun-modal-public');
|
|
|
- if (!input.value) { showToast('Save First', 'Save the disabled tunnel to create its public key', 'info'); return; }
|
|
|
+ if (!input.value) { showToast('Key Not Ready', 'Wait for the local identity or retry key generation.', 'info'); return; }
|
|
|
try {
|
|
|
await navigator.clipboard.writeText(input.value);
|
|
|
showToast('Public Key Copied', 'Paste it into the other endpoint’s pinned peer key', 'success');
|
|
|
@@ -7775,20 +7820,34 @@ async function copyAeroTUNPublicKey() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+async function copyAeroTUNPSK() {
|
|
|
+ const input = document.getElementById('aerotun-modal-psk');
|
|
|
+ if (!input.value) { showToast('PSK Not Available', 'Generate or enter a PSK to copy it. Existing keys are not displayed.', 'info'); return; }
|
|
|
+ try { await navigator.clipboard.writeText(input.value); showToast('PSK Copied', 'Share it securely with the peer.', 'success'); }
|
|
|
+ catch { input.type = 'text'; input.focus(); input.select(); showToast('Copy PSK', 'Key selected; press Ctrl+C to copy.', 'info'); }
|
|
|
+}
|
|
|
+
|
|
|
function openAddAeroTUNModal() {
|
|
|
+ if (aeroSaving) return;
|
|
|
+ aeroModalGeneration++; discardAeroIdentityDraft(aeroIdentityDraft); aeroIdentityDraft = '';
|
|
|
const tunnels = state.aerotunTunnels || [];
|
|
|
+ const usedNames = new Set(tunnels.map(t => t.name));
|
|
|
+ const usedPorts = new Set(tunnels.map(t => Number(t.listen_port)));
|
|
|
+ let index = 0; while (index < 256 && (usedNames.has(`aerotun${index}`) || usedPorts.has(51830 + index) || tunnels.some(t => (t.local_address || '').startsWith(`10.99.${index}.`)))) index++;
|
|
|
+ if (index >= 256) index = 0;
|
|
|
document.getElementById('aerotun-modal-title').innerText = 'Add AeroTUN Interface';
|
|
|
document.getElementById('aerotun-modal-id').value = '';
|
|
|
document.getElementById('aerotun-modal-protocol').value='3';
|
|
|
document.getElementById('aerotun-modal-public').value='';
|
|
|
document.getElementById('aerotun-modal-peerkey').value='';
|
|
|
- document.getElementById('aerotun-modal-allowed').value=`10.99.${tunnels.length}.2/32`;
|
|
|
- document.getElementById('aerotun-modal-name').value = `aerotun${tunnels.length}`;
|
|
|
+ document.getElementById('aerotun-modal-allowed').value=`10.99.${index}.2/32`;
|
|
|
+ document.getElementById('aerotun-modal-name').value = `aerotun${index}`;
|
|
|
document.getElementById('aerotun-modal-mode').value = 'server';
|
|
|
- document.getElementById('aerotun-modal-port').value = 51830 + tunnels.length;
|
|
|
+ document.getElementById('aerotun-modal-port').value = 51830 + index;
|
|
|
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-local').value = `10.99.${index}.1/24`;
|
|
|
+ document.getElementById('aerotun-modal-peer').value = `10.99.${index}.2/24`;
|
|
|
+ document.getElementById('aerotun-modal-peer').dataset.allowedDefault = `10.99.${index}.2/32`;
|
|
|
document.getElementById('aerotun-modal-psk').value = '';
|
|
|
document.getElementById('aerotun-modal-mtu').value = '1420';
|
|
|
document.getElementById('aerotun-modal-comment').value = '';
|
|
|
@@ -7796,15 +7855,21 @@ function openAddAeroTUNModal() {
|
|
|
document.getElementById('aerotun-modal-defroute').checked = false;
|
|
|
document.getElementById('aerotun-modal-disabled').checked = true;
|
|
|
|
|
|
- generateAeroTUNKey();
|
|
|
+ document.getElementById('aerotun-modal-psk').type = 'password';
|
|
|
+ document.getElementById('aerotun-modal-psk').placeholder = 'Generate or enter the shared key';
|
|
|
onAeroTUNModeChange();
|
|
|
updateAeroTUNSetupHint();
|
|
|
document.getElementById('aerotun-modal').classList.remove('hidden');
|
|
|
+ generateAeroTUNKey(true);
|
|
|
+ generateAeroTUNIdentity();
|
|
|
}
|
|
|
|
|
|
function openEditAeroTUNModal(id) {
|
|
|
+ if (aeroSaving) return;
|
|
|
const t = (state.aerotunTunnels || []).find(tun => tun.id === id);
|
|
|
if (!t) return;
|
|
|
+ aeroModalGeneration++; discardAeroIdentityDraft(aeroIdentityDraft); aeroIdentityDraft = '';
|
|
|
+ document.getElementById('aerotun-retry-identity').classList.add('hidden');
|
|
|
|
|
|
document.getElementById('aerotun-modal-title').innerText = `Edit AeroTUN Interface (${t.name})`;
|
|
|
document.getElementById('aerotun-modal-id').value = t.id;
|
|
|
@@ -7818,7 +7883,10 @@ function openEditAeroTUNModal(id) {
|
|
|
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 || '';
|
|
|
+ const peerHost = (t.peer_address || '').split('/')[0];
|
|
|
+ document.getElementById('aerotun-modal-peer').dataset.allowedDefault = peerHost ? `${peerHost}/${peerHost.includes(':') ? 128 : 32}` : '';
|
|
|
document.getElementById('aerotun-modal-psk').value = '';
|
|
|
+ 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-mtu').value = t.mtu || 1420;
|
|
|
document.getElementById('aerotun-modal-comment').value = t.comment || '';
|
|
|
@@ -7832,11 +7900,15 @@ function openEditAeroTUNModal(id) {
|
|
|
}
|
|
|
|
|
|
function closeAeroTUNModal() {
|
|
|
+ if (aeroSaving) return;
|
|
|
+ aeroModalGeneration++; discardAeroIdentityDraft(aeroIdentityDraft); aeroIdentityDraft = '';
|
|
|
+ document.getElementById('aerotun-modal-psk').value = '';
|
|
|
document.getElementById('aerotun-modal').classList.add('hidden');
|
|
|
}
|
|
|
|
|
|
async function submitSaveAeroTUN(e) {
|
|
|
e.preventDefault();
|
|
|
+ if (aeroSaving) return;
|
|
|
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;
|
|
|
@@ -7845,7 +7917,8 @@ async function submitSaveAeroTUN(e) {
|
|
|
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 mtuValue = document.getElementById('aerotun-modal-mtu').value;
|
|
|
+ const mtu = mtuValue === '' ? 1420 : Number(mtuValue);
|
|
|
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;
|
|
|
@@ -7855,9 +7928,26 @@ async function submitSaveAeroTUN(e) {
|
|
|
showToast('Validation Error', 'Remote endpoint (host:port) is required in client mode', 'error');
|
|
|
return;
|
|
|
}
|
|
|
+ if (defaultRoute && (mode !== 'client' || !endpoint)) {
|
|
|
+ showToast('Validation Error', 'Default route requires client mode and a fixed remote endpoint.', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!Number.isInteger(mtu) || mtu < 576 || mtu > 9000) {
|
|
|
+ showToast('Validation Error', 'MTU must be between 576 and 9000.', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
const version = parseInt(document.getElementById('aerotun-modal-protocol').value, 10);
|
|
|
+ if (version === 3 && !id && !aeroIdentityDraft) {
|
|
|
+ showToast('Local Key Not Ready', 'Wait for the local identity or retry key generation before saving.', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (version === 3 && !document.getElementById('aerotun-modal-allowed').value.trim()) {
|
|
|
+ showToast('Allowed Prefix Required', 'Enter at least one allowed peer source prefix, such as the peer tunnel IP /32.', 'error');
|
|
|
+ document.getElementById('aerotun-modal-allowed').focus();
|
|
|
+ return;
|
|
|
+ }
|
|
|
if (version === 3 && !disabled && !document.getElementById('aerotun-modal-peerkey').value.trim()) {
|
|
|
- showToast('Peer Key Required', 'Save disabled first, exchange public keys, then enable', 'error');
|
|
|
+ showToast('Peer Key Required', 'Enter the peer public key, or save this tunnel disabled until it is available.', 'error');
|
|
|
return;
|
|
|
}
|
|
|
if (!psk && !id) {
|
|
|
@@ -7865,12 +7955,16 @@ async function submitSaveAeroTUN(e) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ aeroSaving = true;
|
|
|
+ const submit = document.querySelector('#aerotun-modal button[type="submit"]');
|
|
|
+ if (submit) submit.disabled = true;
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels`, {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
|
protocol_version:parseInt(document.getElementById('aerotun-modal-protocol').value,10),
|
|
|
+ identity_draft_id: version === 3 && !id ? aeroIdentityDraft : '',
|
|
|
peer_public_key:document.getElementById('aerotun-modal-peerkey').value.trim(),
|
|
|
allowed_ips:document.getElementById('aerotun-modal-allowed').value.split(',').map(s=>s.trim()).filter(Boolean),
|
|
|
id,
|
|
|
@@ -7890,22 +7984,29 @@ async function submitSaveAeroTUN(e) {
|
|
|
});
|
|
|
|
|
|
if (res.ok) {
|
|
|
- const saved = await res.json();
|
|
|
+ await res.json();
|
|
|
+ if (version !== 3) discardAeroIdentityDraft(aeroIdentityDraft);
|
|
|
+ aeroIdentityDraft = '';
|
|
|
+ aeroSaving = false;
|
|
|
+ closeAeroTUNModal();
|
|
|
await loadAeroTUN(false);
|
|
|
- if (!id && version === 3) {
|
|
|
- openEditAeroTUNModal(saved.id);
|
|
|
- showToast('Identity Saved', 'Copy the local public key and exchange keys with the peer', 'success');
|
|
|
- } else {
|
|
|
- closeAeroTUNModal();
|
|
|
- showToast('AeroTUN Saved', disabled ? 'Configuration saved; tunnel is disabled' : 'Configuration saved; waiting for authenticated peer traffic', 'success');
|
|
|
- }
|
|
|
+ showToast('AeroTUN Saved', disabled ? 'Configuration saved; tunnel is disabled' : 'Configuration saved; waiting for authenticated peer traffic', 'success');
|
|
|
fetchData(false);
|
|
|
} else {
|
|
|
- const err = await res.text();
|
|
|
- showToast('Error Saving Tunnel', err, 'error');
|
|
|
+ const err = await responseError(res);
|
|
|
+ if (err.includes('configuration saved but tunnel failed to start')) {
|
|
|
+ aeroIdentityDraft = '';
|
|
|
+ aeroSaving = false;
|
|
|
+ closeAeroTUNModal();
|
|
|
+ await loadAeroTUN(false);
|
|
|
+ showToast('Saved, but not started', err, 'error');
|
|
|
+ } else showToast('Error Saving Tunnel', err, 'error');
|
|
|
}
|
|
|
} catch (err) {
|
|
|
showToast('Network Error', err.message, 'error');
|
|
|
+ } finally {
|
|
|
+ aeroSaving = false;
|
|
|
+ if (submit) submit.disabled = false;
|
|
|
}
|
|
|
}
|
|
|
|