feat(vpn): VPN-Satelliten — WireGuard-Clients als Satelliten ohne Installation beim Kunden
Einstellungen → VPN → WireGuard → Clients: WireGuard-Datei des Kunden hochladen/einfuegen (editierbar), je Verbindung Schalter (an = auch nach Neustart, aus = bleibt aus), bearbeiten, Download, ℹ-Verlauf, loeschen; Log-Aufbewahrung im Reiter Einstellungen. Server-Reiter mit Anleitung fuer UDP 443 (NPM-Stream bzw. ohne Nginx), Server selbst folgt. Jede Verbindung ist ein eigener Container (aria-vpn-<id>, Image aus satellite/Dockerfile.vpn) mit eigenem Netz-Namensraum: WireGuard + normaler satellite.py (kind=vpn). Firewall im Container: Tunnel nur ausgehend, keine Verbindung Kundennetz → ARIA; ins ARIA-Netz nur RVS + Endpunkt. Routen ohne wg-quick, RVS-Weg nie durch den Tunnel (auch bei 0.0.0.0/0). Scan ueber den Tunnel aktiv per TCP (kein mDNS/ARP), FritzBox an Port 49000 erkannt. Satelliten-Ansicht: eigene Gruppe VPN-Satelliten. Lokal Ende-zu-Ende getestet (Kunde mit WG-Server + LAN, Fake-RVS): Tunnel, Verlauf, Scan, Sperren in beide Richtungen, Volltunnel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+321
-5
@@ -688,6 +688,7 @@
|
||||
<div class="settings-subnav">
|
||||
<button class="settings-subnav-btn active" data-sub="models" onclick="switchSettingsTab('models')">Modelle & KI</button>
|
||||
<button class="settings-subnav-btn" data-sub="integrations" onclick="switchSettingsTab('integrations')">Integrationen</button>
|
||||
<button class="settings-subnav-btn" data-sub="vpn" onclick="switchSettingsTab('vpn')">VPN</button>
|
||||
<button class="settings-subnav-btn" data-sub="system" onclick="switchSettingsTab('system')">System & Wartung</button>
|
||||
</div>
|
||||
|
||||
@@ -1359,6 +1360,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-subtab" data-sub="vpn">
|
||||
<div class="settings-section">
|
||||
<div class="vpn-tabs" style="display:flex;gap:4px;margin-bottom:10px;border-bottom:1px solid #1E1E2E;">
|
||||
<span style="color:#0096FF;border-bottom:2px solid #0096FF;padding:6px 12px;font-size:12px;">WireGuard</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;margin-bottom:10px;">
|
||||
<button class="btn secondary vpn-sub" data-vsub="clients" onclick="vpnTab('clients')" style="padding:4px 12px;font-size:12px;">Clients</button>
|
||||
<button class="btn secondary vpn-sub" data-vsub="server" onclick="vpnTab('server')" style="padding:4px 12px;font-size:12px;">Server</button>
|
||||
<button class="btn secondary vpn-sub" data-vsub="settings" onclick="vpnTab('settings')" style="padding:4px 12px;font-size:12px;">Einstellungen</button>
|
||||
</div>
|
||||
|
||||
<div class="vpn-pane" data-vsub="clients">
|
||||
<div class="card" style="max-width:900px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
|
||||
<p style="color:#8888AA;font-size:12px;margin:0;flex:1;">
|
||||
WireGuard-Dateien, die Du von Kunden bekommst. Jede Verbindung wird ein <b>VPN-Satellit</b>
|
||||
(eigener Container auf ARIA): gleiche Funktionen wie ein Satellit, ohne beim Kunden etwas zu installieren.
|
||||
<b>An</b> = verbindet sich auch nach einem Neustart wieder · <b>Aus</b> = bleibt aus.
|
||||
</p>
|
||||
<button class="btn" onclick="vpnEdit(null,'client')" style="padding:5px 12px;font-size:12px;white-space:nowrap;">+ Hinzufügen</button>
|
||||
</div>
|
||||
<div id="vpn-client-list" style="font-size:12px;color:#8888AA;">(lade …)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vpn-pane" data-vsub="server" style="display:none;">
|
||||
<div class="card" style="max-width:900px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<p style="color:#8888AA;font-size:12px;margin:0;flex:1;">
|
||||
Hier wählen sich Kunden (z.B. per FritzBox) bei ARIA ein — über <b>UDP 443</b> auf dem RVS-Hostnamen.
|
||||
Jede Verbindung wird ebenfalls ein VPN-Satellit; aus dem Kundennetz führt keine Verbindung zu ARIA.
|
||||
<br><span style="color:#FFD60A;">Kommt im nächsten Schritt.</span>
|
||||
</p>
|
||||
<button class="btn secondary" onclick="vpnNpmGuide()" style="padding:4px 10px;font-size:11px;white-space:nowrap;">ℹ Einrichtung Proxy/Router</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vpn-pane" data-vsub="settings" style="display:none;">
|
||||
<div class="card" style="max-width:520px;font-size:12px;">
|
||||
<label style="color:#AAB;">Verbindungs-Log aufbewahren (Tage):
|
||||
<input id="vpn-keep-days" type="number" min="1" max="3650" style="width:80px;margin-left:6px;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:4px 6px;color:#E0E0F0;">
|
||||
</label>
|
||||
<button class="btn secondary" onclick="vpnSaveSettings()" style="padding:3px 10px;font-size:11px;margin-left:6px;">Speichern</button>
|
||||
<div id="vpn-settings-msg" style="margin-top:6px;color:#8888AA;"></div>
|
||||
<p style="color:#6a6a88;margin:10px 0 0;">Ältere Einträge (verbunden/getrennt) werden stündlich gelöscht.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-subtab" data-sub="system">
|
||||
|
||||
<!-- Anhang-Limit -->
|
||||
@@ -1895,6 +1947,21 @@
|
||||
<div id="sat-list" style="font-size:12px;color:#8888AA;">(Lade...)</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin:16px 0 8px;">
|
||||
<h2 style="margin:0;">VPN-Satelliten 🔐</h2>
|
||||
<button class="btn secondary" onclick="switchMainTab('settings');switchSettingsTab('vpn')" style="padding:4px 10px;font-size:11px;">Verbindungen verwalten</button>
|
||||
</div>
|
||||
<div class="card" style="margin-bottom:8px;">
|
||||
<p style="color:#8888AA;font-size:12px;margin:0;">
|
||||
Kundennetze über WireGuard — laufen auf ARIA, nichts beim Kunden installiert. Gleiche Funktionen
|
||||
wie ein Satellit; gescannt wird aktiv über den Tunnel (kein mDNS/ARP). Aus dem Kundennetz
|
||||
führt keine Verbindung zurück zu ARIA.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div id="vpnsat-list" style="font-size:12px;color:#8888AA;">(Lade...)</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin:16px 0 8px;">
|
||||
<h2 style="margin:0;">Host-Agenten 💻</h2>
|
||||
<button class="btn secondary" onclick="requestHosts()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
|
||||
@@ -2693,6 +2760,8 @@
|
||||
if (msg.type === 'sat_result') { onSatResult(msg.payload || {}); return; }
|
||||
if (msg.type === 'llm_openai_config_result') { onOpenAiConfigResult(msg.payload || {}); return; }
|
||||
if (msg.type === 'llm_runtime_result') { onLlmRuntimeResult(msg.payload || {}); return; }
|
||||
if (msg.type === 'vpn_list') { _vpnData = msg; renderVpn(); return; }
|
||||
if (msg.type === 'vpn_result') { const f = _vpnWait[msg.requestId]; if (f) { delete _vpnWait[msg.requestId]; f(msg); } return; }
|
||||
if (msg.type === 'llm_runtime_progress') { onLlmRuntimeProgress(msg.payload || {}); return; }
|
||||
if (msg.type === 'agent_history_list') {
|
||||
if (agentHistoryHost && msg.payload && msg.payload.host === agentHistoryHost) {
|
||||
@@ -5322,6 +5391,7 @@
|
||||
document.querySelectorAll('.settings-subtab').forEach(el => el.classList.toggle('visible', el.dataset.sub === sub));
|
||||
document.querySelectorAll('.settings-subnav-btn').forEach(b => b.classList.toggle('active', b.dataset.sub === sub));
|
||||
try { localStorage.setItem('diag_settings_subtab', sub); } catch (e) {}
|
||||
if (sub === 'vpn') vpnOpen();
|
||||
}
|
||||
function switchMainTab(tab) {
|
||||
document.querySelectorAll('.main-tab').forEach(el => el.classList.remove('visible'));
|
||||
@@ -6243,12 +6313,22 @@
|
||||
function renderSatellites() {
|
||||
const box = document.getElementById('sat-list');
|
||||
if (!box) return;
|
||||
if (!satellites.length) {
|
||||
const lan = satellites.filter(s => s.kind !== 'vpn');
|
||||
const vpnSats = satellites.filter(s => s.kind === 'vpn');
|
||||
const vbox = document.getElementById('vpnsat-list');
|
||||
if (vbox) vbox.innerHTML = vpnSats.length ? vpnSats.map(s => _satCard(s)).join('') :
|
||||
'<span style="color:#8888AA;">Keine VPN-Verbindung aktiv. Anlegen unter Einstellungen → VPN.</span>';
|
||||
if (!lan.length) {
|
||||
box.innerHTML = '<span style="color:#8888AA;">Kein Satellit verbunden. ' +
|
||||
'Deploy einen satellite/-Container in einem Netz (siehe satellite/README).</span>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = satellites.map(s => {
|
||||
box.innerHTML = lan.map(s => _satCard(s)).join('');
|
||||
}
|
||||
|
||||
// Eine Satelliten-Karte (auch fuer VPN-Satelliten — gleiche Funktionen).
|
||||
function _satCard(s) {
|
||||
{
|
||||
const online = s.online !== false;
|
||||
const dot = online ? '#34C759' : '#FF3B30';
|
||||
const caps = (s.caps || []).join(', ') || 'nur beobachten';
|
||||
@@ -6319,7 +6399,7 @@
|
||||
return '<div style="border:1px solid #1E1E2E;border-radius:8px;padding:10px;margin-bottom:8px;">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
|
||||
'<div><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + dot + ';margin-right:6px;"></span>' +
|
||||
'<strong style="color:#E0E0F0;">🛰️ ' + escapeHtml(s.location || s.id) + '</strong> ' +
|
||||
'<strong style="color:#E0E0F0;">' + (s.kind === 'vpn' ? '🔐 ' : '🛰️ ') + escapeHtml(s.location || s.id) + '</strong> ' +
|
||||
(s.alias ? '<span style="color:#666;font-size:11px;" title="Der Satellit selbst nennt sich so">(' + escapeHtml(s.agentName || '') + ')</span> ' : '') +
|
||||
'<span style="color:#666;font-size:11px;">id=' + escapeHtml(s.id) + '</span></div>' +
|
||||
'<button class="btn secondary" onclick="renameEntity(\'satellite\',\'' + escapeHtml(s.id) + '\',\'' + escapeHtml(s.alias || '') + '\',\'' + escapeHtml(s.agentName || s.id) + '\')" title="Anzeigenamen vergeben — ARIA kann den Satelliten danach so ansprechen" style="padding:3px 8px;font-size:11px;">✎</button>' +
|
||||
@@ -6327,12 +6407,13 @@
|
||||
(scanning ? ' disabled' : '') + '>Geraete scannen</button>' +
|
||||
'</div>' +
|
||||
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">kann: ' + escapeHtml(caps) + ' · ' + ctrl +
|
||||
(s.net && s.net.primary_ip ? ' · Netz: ' + escapeHtml(s.net.primary_ip) : '') +
|
||||
(s.kind === 'vpn' && s.net && (s.net.vpnNets || []).length ? ' · VPN-Netze: ' + escapeHtml(s.net.vpnNets.join(', ')) :
|
||||
(s.net && s.net.primary_ip ? ' · Netz: ' + escapeHtml(s.net.primary_ip) : '')) +
|
||||
(s.net && s.net.mac ? ' · MAC: <span style="font-family:monospace;">' + escapeHtml(s.net.mac) + '</span>' : '') + '</div>' +
|
||||
(s.net && s.net.warning ? '<div style="color:#FF6E6E;font-size:11px;margin-top:6px;padding:6px;background:#3A1F1F;border-radius:4px;">⚠ ' + escapeHtml(s.net.warning) + '</div>' : '') +
|
||||
devHtml +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ── FritzBox-Sicherungen ───────────────────────────────
|
||||
@@ -9976,6 +10057,241 @@
|
||||
document.getElementById('llm-dl-modal').classList.remove('open');
|
||||
}
|
||||
|
||||
// ── VPN-Satelliten (Einstellungen → VPN → WireGuard) ──────────
|
||||
// Jede Verbindung = eigener Container auf ARIA (diagnostic/vpn.js).
|
||||
let _vpnData = { connections: [], settings: {} };
|
||||
const _vpnWait = {};
|
||||
let _vpnTimer = null;
|
||||
function vpnReq(action, extra = {}, timeoutMs = 60000) {
|
||||
const requestId = 'vpn_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7);
|
||||
return new Promise(resolve => {
|
||||
const t = setTimeout(() => { delete _vpnWait[requestId]; resolve({ ok: false, error: 'Keine Antwort von der Diagnostic.' }); }, timeoutMs);
|
||||
_vpnWait[requestId] = r => { clearTimeout(t); resolve(r); };
|
||||
send({ action, requestId, ...extra });
|
||||
});
|
||||
}
|
||||
function vpnOpen() {
|
||||
send({ action: 'vpn_list' });
|
||||
if (!_vpnTimer) _vpnTimer = setInterval(() => {
|
||||
const pane = document.querySelector('.settings-subtab[data-sub="vpn"]');
|
||||
if (pane && pane.classList.contains('visible')) send({ action: 'vpn_list' });
|
||||
}, 15000);
|
||||
vpnTab(window._vpnSub || 'clients');
|
||||
}
|
||||
function vpnTab(t) {
|
||||
window._vpnSub = t;
|
||||
document.querySelectorAll('.vpn-pane').forEach(el => el.style.display = el.dataset.vsub === t ? '' : 'none');
|
||||
document.querySelectorAll('.vpn-sub').forEach(b => b.style.borderColor = b.dataset.vsub === t ? '#0096FF' : '');
|
||||
if (t === 'settings') {
|
||||
const el = document.getElementById('vpn-keep-days');
|
||||
if (el && document.activeElement !== el) el.value = (_vpnData.settings || {}).keepDays || 30;
|
||||
}
|
||||
}
|
||||
function _vpnBytes(n) {
|
||||
n = +n || 0;
|
||||
return n > 1e9 ? (n / 1e9).toFixed(1).replace('.', ',') + ' GB' : n > 1e6 ? (n / 1e6).toFixed(1).replace('.', ',') + ' MB' : Math.round(n / 1e3) + ' kB';
|
||||
}
|
||||
function _vpnDur(s) {
|
||||
s = Math.max(0, Math.round(s));
|
||||
const d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60);
|
||||
return d ? d + ' T ' + h + ' h' : h ? h + ' h ' + m + ' min' : m ? m + ' min' : s + ' s';
|
||||
}
|
||||
function _vpnState(c) {
|
||||
const st = c.status || {}, ct = c.container || {};
|
||||
if (!c.enabled) return ['#8888AA', 'aus'];
|
||||
if (!ct.exists) return ['#FF9500', 'Container fehlt'];
|
||||
if (st.state === 'fehler') return ['#FF3B30', 'Fehler: ' + (st.error || '')];
|
||||
if (ct.restarting || !ct.running) return ['#FF9500', 'startet neu …'];
|
||||
if (st.state === 'verbunden') return ['#34C759', 'verbunden seit ' + _vpnDur(Date.now() / 1000 - st.since)];
|
||||
return ['#FFD60A', 'wartet auf Gegenstelle'];
|
||||
}
|
||||
function renderVpn() {
|
||||
const box = document.getElementById('vpn-client-list');
|
||||
if (!box) return;
|
||||
const list = (_vpnData.connections || []).filter(c => (c.kind || 'client') === 'client');
|
||||
if (!list.length) { box.innerHTML = '<span style="color:#6a6a88;">Noch keine Verbindung. „+ Hinzufügen“ und die WireGuard-Datei des Kunden einfügen.</span>'; return; }
|
||||
box.innerHTML = list.map(c => {
|
||||
const [col, txt] = _vpnState(c);
|
||||
const st = c.status || {};
|
||||
const q = "'" + escapeHtml(c.id) + "'";
|
||||
return '<div style="display:flex;align-items:center;gap:10px;padding:7px 8px;background:#0D0D1A;border-radius:6px;margin-bottom:5px;">' +
|
||||
'<span style="width:9px;height:9px;border-radius:50%;background:' + col + ';flex:none;"></span>' +
|
||||
'<div style="flex:1;min-width:0;"><b style="color:#E0E0F0;">🔐 ' + escapeHtml(c.name) + '</b>' +
|
||||
'<div style="color:#8888AA;font-size:11px;">' + escapeHtml(txt) +
|
||||
(c.enabled && st.rx != null ? ' · ↓ ' + _vpnBytes(st.rx) + ' ↑ ' + _vpnBytes(st.tx) : '') +
|
||||
(c.scanNets ? ' · Netze: ' + escapeHtml(c.scanNets) : '') + '</div></div>' +
|
||||
'<label title="' + (c.enabled ? 'Ausschalten' : 'Einschalten — verbindet sich dann auch nach einem Neustart wieder') + '" style="display:inline-flex;align-items:center;cursor:pointer;">' +
|
||||
'<input type="checkbox" ' + (c.enabled ? 'checked' : '') + ' onchange="vpnToggle(' + q + ',this.checked,this)" style="width:16px;height:16px;"></label>' +
|
||||
'<button class="btn secondary" title="Bearbeiten" onclick="vpnEdit(' + q + ')" style="padding:2px 7px;font-size:12px;">✏️</button>' +
|
||||
'<button class="btn secondary" title="WireGuard-Datei herunterladen" onclick="vpnDownload(' + q + ')" style="padding:2px 7px;font-size:12px;">⬇</button>' +
|
||||
'<button class="btn secondary" title="Verbindungs-Log und Statistik" onclick="vpnInfo(' + q + ')" style="padding:2px 7px;font-size:12px;">ℹ</button>' +
|
||||
'<button class="btn secondary" title="Löschen" onclick="vpnDelete(' + q + ')" style="padding:2px 7px;font-size:12px;">🗑</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
async function vpnToggle(id, on, el) {
|
||||
if (el) el.disabled = true;
|
||||
const r = await vpnReq('vpn_toggle', { id, enabled: on });
|
||||
if (!r.ok) uiAlert('Verbindung ' + (on ? 'einschalten' : 'ausschalten') + ' fehlgeschlagen\n\n' + (r.error || ''));
|
||||
if (el) el.disabled = false;
|
||||
}
|
||||
async function vpnDelete(id) {
|
||||
const c = (_vpnData.connections || []).find(x => x.id === id) || {};
|
||||
if (!await uiConfirm('Verbindung „' + (c.name || id) + '“ löschen?\n\nDer Tunnel wird getrennt, WireGuard-Datei, Log und die Zugangsdaten dieses VPN-Satelliten sind danach weg.')) return;
|
||||
const r = await vpnReq('vpn_delete', { id });
|
||||
if (!r.ok) uiAlert('Löschen fehlgeschlagen\n\n' + (r.error || ''));
|
||||
}
|
||||
async function vpnDownload(id) {
|
||||
const r = await vpnReq('vpn_get', { id });
|
||||
if (!r.ok) { uiAlert('Download fehlgeschlagen\n\n' + (r.error || '')); return; }
|
||||
const c = r.connection;
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(new Blob([c.conf], { type: 'text/plain' }));
|
||||
a.download = (c.name || 'wireguard').replace(/[^\w.-]+/g, '_') + '.conf';
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 2000);
|
||||
}
|
||||
function _vpnModal(html) {
|
||||
let m = document.getElementById('vpn-modal');
|
||||
if (!m) {
|
||||
m = document.createElement('div');
|
||||
m.id = 'vpn-modal';
|
||||
m.style.cssText = 'position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;';
|
||||
m.addEventListener('click', e => { if (e.target === m) vpnCloseModal(); });
|
||||
document.body.appendChild(m);
|
||||
}
|
||||
m.innerHTML = '<div style="background:#12121E;border:1px solid #444466;border-radius:8px;padding:16px 18px;max-width:700px;width:100%;max-height:88vh;overflow-y:auto;color:#E0E0F0;font-size:13px;line-height:1.5;">' + html + '</div>';
|
||||
m.style.display = 'flex';
|
||||
return m;
|
||||
}
|
||||
function vpnCloseModal() { const m = document.getElementById('vpn-modal'); if (m) m.style.display = 'none'; }
|
||||
async function vpnEdit(id, kind) {
|
||||
let c = { name: '', conf: '', scanNets: '', kind: kind || 'client' };
|
||||
if (id) {
|
||||
const r = await vpnReq('vpn_get', { id });
|
||||
if (!r.ok) { uiAlert('Laden fehlgeschlagen\n\n' + (r.error || '')); return; }
|
||||
c = r.connection;
|
||||
}
|
||||
const inp = 'width:100%;box-sizing:border-box;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-size:12px;';
|
||||
_vpnModal(
|
||||
'<b style="font-size:15px;">' + (id ? 'Verbindung bearbeiten' : 'WireGuard-Verbindung hinzufügen') + '</b>' +
|
||||
'<div style="margin-top:10px;">Name <span style="color:#8888AA;">(so heißt der VPN-Satellit, ARIA spricht ihn damit an)</span>' +
|
||||
'<input id="vpn-f-name" type="text" maxlength="60" placeholder="z.B. Kunde Müller Büro" style="' + inp + 'margin-top:3px;"></div>' +
|
||||
'<div style="margin-top:10px;display:flex;align-items:center;gap:8px;">WireGuard-Datei' +
|
||||
'<label class="btn secondary" style="padding:2px 10px;font-size:11px;cursor:pointer;">Datei wählen …' +
|
||||
'<input type="file" accept=".conf,.txt,text/plain" onchange="vpnLoadFile(this)" style="display:none;"></label>' +
|
||||
'<span style="color:#8888AA;font-size:11px;">oder unten einfügen — bleibt editierbar</span></div>' +
|
||||
'<textarea id="vpn-f-conf" rows="14" spellcheck="false" oninput="vpnCheckConf()" placeholder="[Interface]\nPrivateKey = …\nAddress = 10.8.0.2/24\n\n[Peer]\nPublicKey = …\nEndpoint = kunde.example.de:51820\nAllowedIPs = 192.168.178.0/24" ' +
|
||||
'style="' + inp + 'margin-top:4px;font-family:monospace;font-size:11.5px;"></textarea>' +
|
||||
'<div id="vpn-f-check" style="font-size:11px;min-height:16px;margin-top:3px;"></div>' +
|
||||
'<div style="margin-top:8px;">Netze zum Scannen <span style="color:#8888AA;">(optional, Komma-getrennt — leer = AllowedIPs der Datei)</span>' +
|
||||
'<input id="vpn-f-nets" type="text" placeholder="z.B. 192.168.178.0/24" style="' + inp + 'margin-top:3px;"></div>' +
|
||||
'<div id="vpn-f-msg" style="margin-top:8px;font-size:12px;"></div>' +
|
||||
'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:12px;">' +
|
||||
'<button class="btn secondary" onclick="vpnCloseModal()" style="padding:6px 14px;font-size:12px;">Abbrechen</button>' +
|
||||
'<button class="btn" id="vpn-f-save" onclick="vpnSave(' + (id ? "'" + escapeHtml(id) + "'" : 'null') + ',\'' + escapeHtml(c.kind || 'client') + '\')" style="padding:6px 14px;font-size:12px;">Speichern</button>' +
|
||||
'</div>');
|
||||
document.getElementById('vpn-f-name').value = c.name || '';
|
||||
document.getElementById('vpn-f-conf').value = c.conf || '';
|
||||
document.getElementById('vpn-f-nets').value = c.scanNets || '';
|
||||
vpnCheckConf();
|
||||
document.getElementById('vpn-f-name').focus();
|
||||
}
|
||||
function vpnLoadFile(input) {
|
||||
const f = input.files && input.files[0];
|
||||
if (!f) return;
|
||||
if (f.size > 64 * 1024) { uiAlert('Die Datei ist zu groß für eine WireGuard-Konfiguration.'); return; }
|
||||
const rd = new FileReader();
|
||||
rd.onload = () => {
|
||||
document.getElementById('vpn-f-conf').value = String(rd.result || '');
|
||||
const n = document.getElementById('vpn-f-name');
|
||||
if (n && !n.value.trim()) n.value = f.name.replace(/\.(conf|txt)$/i, '');
|
||||
vpnCheckConf();
|
||||
};
|
||||
rd.readAsText(f);
|
||||
}
|
||||
let _vpnChkT = null;
|
||||
function vpnCheckConf() {
|
||||
clearTimeout(_vpnChkT);
|
||||
_vpnChkT = setTimeout(async () => {
|
||||
const el = document.getElementById('vpn-f-check');
|
||||
const conf = (document.getElementById('vpn-f-conf') || {}).value || '';
|
||||
if (!el) return;
|
||||
if (!conf.trim()) { el.innerHTML = ''; return; }
|
||||
const r = await vpnReq('vpn_check', { conf }, 10000);
|
||||
const k = (r && r.check) || {};
|
||||
if (k.error) { el.innerHTML = '<span style="color:#FF6E6E;">✗ ' + escapeHtml(k.error) + '</span>'; return; }
|
||||
el.innerHTML = '<span style="color:#34C759;">✓ Gegenstelle ' + escapeHtml(k.endpoint || '?') + '</span>' +
|
||||
(k.nets.length ? ' <span style="color:#8888AA;">· Netze: ' + escapeHtml(k.nets.join(', ')) + '</span>' : '') +
|
||||
(k.all ? '<br><span style="color:#FF9500;">⚠ AllowedIPs = 0.0.0.0/0 (alles durch den Tunnel). Der Weg zu ARIA bleibt trotzdem draußen — trag bei „Netze zum Scannen“ das Kundennetz ein.</span>' : '');
|
||||
const nets = document.getElementById('vpn-f-nets');
|
||||
if (nets && !nets.value.trim() && k.nets.length) nets.placeholder = k.nets.join(', ');
|
||||
}, 300);
|
||||
}
|
||||
async function vpnSave(id, kind) {
|
||||
const name = document.getElementById('vpn-f-name').value.trim();
|
||||
const conf = document.getElementById('vpn-f-conf').value;
|
||||
const scanNets = document.getElementById('vpn-f-nets').value.trim();
|
||||
const msg = document.getElementById('vpn-f-msg');
|
||||
const btn = document.getElementById('vpn-f-save');
|
||||
if (!name) { msg.innerHTML = '<span style="color:#FF6E6E;">Bitte einen Namen vergeben.</span>'; return; }
|
||||
btn.disabled = true; msg.innerHTML = '<span style="color:#8888AA;">speichere …</span>';
|
||||
const r = await vpnReq('vpn_save', { id, kind, name, conf, scanNets });
|
||||
btn.disabled = false;
|
||||
if (!r.ok) { msg.innerHTML = '<span style="color:#FF6E6E;">✗ ' + escapeHtml(r.error || 'fehlgeschlagen') + '</span>'; return; }
|
||||
vpnCloseModal();
|
||||
}
|
||||
async function vpnInfo(id) {
|
||||
_vpnModal('<b>lade …</b>');
|
||||
const r = await vpnReq('vpn_info', { id });
|
||||
if (!r.ok) { _vpnModal('<b>Fehler</b><div style="color:#FF6E6E;margin-top:8px;">' + escapeHtml(r.error || '') + '</div>' +
|
||||
'<div style="text-align:right;margin-top:12px;"><button class="btn secondary" onclick="vpnCloseModal()">Schließen</button></div>'); return; }
|
||||
const i = r.info, st = i.status || {}, [col, txt] = _vpnState(i);
|
||||
const fmt = ts => new Date(ts * 1000).toLocaleString('de-DE');
|
||||
const ev = (i.events || []).slice().reverse().map(e => {
|
||||
const what = e.event === 'verbunden' ? '🟢 verbunden' + (e.endpoint ? ' (' + escapeHtml(e.endpoint) + ')' : '')
|
||||
: e.event === 'getrennt' ? '🔴 getrennt nach ' + _vpnDur(e.dauer_s || 0) + (e.rx != null ? ' · ↓ ' + _vpnBytes(e.rx) + ' ↑ ' + _vpnBytes(e.tx) : '')
|
||||
: e.event === 'fehler' ? '⚠ ' + escapeHtml(e.meldung || 'Fehler')
|
||||
: e.event === 'gestartet' ? '▶ gestartet' + ((e.netze || []).length ? ' · ' + escapeHtml(e.netze.join(', ')) : '')
|
||||
: e.event === 'gestoppt' ? '■ gestoppt' : escapeHtml(e.event);
|
||||
return '<tr><td style="color:#8888AA;padding:2px 10px 2px 0;white-space:nowrap;">' + fmt(e.ts) + '</td><td>' + what + '</td></tr>';
|
||||
}).join('');
|
||||
_vpnModal(
|
||||
'<div style="display:flex;align-items:center;"><b style="font-size:15px;">🔐 ' + escapeHtml(i.name) + '</b>' +
|
||||
'<button class="btn secondary" onclick="vpnCloseModal()" style="margin-left:auto;padding:2px 10px;font-size:11px;">Schließen</button></div>' +
|
||||
'<div style="margin-top:8px;"><span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:' + col + ';margin-right:6px;"></span>' + escapeHtml(txt) + '</div>' +
|
||||
'<div style="color:#8888AA;font-size:12px;margin-top:4px;">' +
|
||||
(st.endpoint ? 'Gegenstelle: ' + escapeHtml(st.endpoint) + ' · ' : '') +
|
||||
(st.handshake ? 'letzter Handshake: ' + fmt(st.handshake) + ' · ' : '') +
|
||||
(st.rx != null ? 'empfangen ' + _vpnBytes(st.rx) + ', gesendet ' + _vpnBytes(st.tx) : '') + '</div>' +
|
||||
'<div style="margin-top:12px;font-weight:600;">Verlauf</div>' +
|
||||
(ev ? '<table style="font-size:12px;margin-top:4px;border-collapse:collapse;">' + ev + '</table>' : '<div style="color:#8888AA;">Noch keine Einträge.</div>') +
|
||||
(i.logs ? '<details style="margin-top:12px;"><summary style="cursor:pointer;color:#8888AA;">Container-Log (technisch)</summary>' +
|
||||
'<pre style="background:#08080F;color:#9a9ab5;font-size:10.5px;padding:6px 8px;border-radius:4px;max-height:220px;overflow:auto;white-space:pre-wrap;">' + escapeHtml(i.logs) + '</pre></details>' : ''));
|
||||
}
|
||||
async function vpnSaveSettings() {
|
||||
const v = document.getElementById('vpn-keep-days').value;
|
||||
const r = await vpnReq('vpn_settings', { settings: { keepDays: v } });
|
||||
document.getElementById('vpn-settings-msg').innerHTML = r.ok
|
||||
? '<span style="color:#34C759;">✓ gespeichert (' + r.settings.keepDays + ' Tage)</span>'
|
||||
: '<span style="color:#FF6E6E;">✗ ' + escapeHtml(r.error || '') + '</span>';
|
||||
}
|
||||
function vpnNpmGuide() {
|
||||
_vpnModal(
|
||||
'<div style="display:flex;align-items:center;"><b style="font-size:15px;">ℹ WireGuard-Server erreichbar machen (UDP 443)</b>' +
|
||||
'<button class="btn secondary" onclick="vpnCloseModal()" style="margin-left:auto;padding:2px 10px;font-size:11px;">Schließen</button></div>' +
|
||||
'<p style="color:#AAB;">Kunden wählen sich auf <b>denselben Hostnamen wie der RVS</b> ein, nur über <b>UDP</b> statt TCP. ' +
|
||||
'TCP 443 (RVS, Webseiten) bleibt unverändert, beides stört sich nicht. Den Hostnamen nimmt ARIA automatisch aus den Einstellungen.</p>' +
|
||||
'<b>Mit Nginx Proxy Manager</b><ol style="color:#AAB;margin-top:4px;">' +
|
||||
'<li>Hosts → <b>Streams</b> → Add Stream: Incoming Port <code>443</code>, Forward Host = ARIA-VM, Forward Port = Port des WireGuard-Verteilers, ' +
|
||||
'<b>TCP Forwarding aus</b>, <b>UDP Forwarding an</b> (TCP an würde mit den Proxy-Hosts kollidieren).</li>' +
|
||||
'<li>Im docker-compose des NPM den UDP-Port freigeben und neu starten:<pre style="background:#08080F;padding:6px 8px;border-radius:4px;">ports:\n - \'443:443\'\n - \'443:443/udp\' # neu</pre></li>' +
|
||||
'<li>Am Router/an der Firewall <b>UDP 443</b> ebenfalls auf den NPM-Rechner weiterleiten (nicht nur TCP).</li></ol>' +
|
||||
'<b>Ohne Nginx</b><p style="color:#AAB;margin-top:4px;">Den RVS-Port am Router zusätzlich als <b>UDP</b> weiterleiten — direkt auf die ARIA-VM, Port des WireGuard-Verteilers.</p>' +
|
||||
'<p style="color:#8888AA;font-size:12px;">Hinweis: Läuft der Weg über nginx, sieht ARIA als Absender den Proxy statt der echten Kunden-IP. ' +
|
||||
'Die erzeugten Profile senden alle 25 s ein Lebenszeichen, damit nginx die Verbindung nicht vergisst.</p>');
|
||||
}
|
||||
|
||||
// ── llama.cpp/llama-swap der LLM-Boxen (Modell-Katalog → ⚙ llama.cpp) ──
|
||||
// Pruefen/Aktualisieren/Zuruecksetzen macht auf der Box der llm-updater
|
||||
// (einziger Dienst mit Docker-Zugriff). Ob es etwas Neues gibt, entscheidet
|
||||
|
||||
Reference in New Issue
Block a user