feat(vpn): WireGuard-Server — Kunden waehlen sich per UDP 443 ein, ein Verteiler fuer alle
Einstellungen → VPN → WireGuard → Server: Einwahl anlegen (Name, Kundennetz, LAN-IP der FritzBox); ARIA erzeugt beide Schluesselpaare + PSK, ⬇ liefert die Datei fuer die Gegenseite (FritzBox-Netzwerk-Kopplung oder Standard-WireGuard). Jede Einwahl = eigener VPN-Satellit-Container mit fester IP im Netz aria-vpn (10.213.213.0/24, ausserhalb von Dockers Pools). Neuer Verteiler (satellite/vpn_relay.py, aria-vpn-relay, Host-Netz, VPN_RELAY_PORT 51820): ordnet jedes Paket per WireGuard-mac1 bzw. Sitzungsnummer dem richtigen Container zu, ohne zu entschluesseln; Fremdes wird still verworfen. Anleitung NPM-Stream / ohne Nginx / ufw im ℹ des Server-Reiters und im satellite/README. Lokal Ende-zu-Ende getestet: zwei Kunden mit demselben Netz 192.168.178.0/24 gleichzeitig ueber einen Port, jeder erreicht nur sein eigenes LAN; kein Weg Kunde → ARIA, Kunde 1 → Einwahl 2, Container → Internet; fremder Schluessel und Muell am Verteiler ohne Antwort. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+87
-11
@@ -1387,14 +1387,16 @@
|
||||
|
||||
<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;">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
|
||||
<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>
|
||||
Hier wählen sich Kunden (z.B. per FritzBox) bei ARIA ein — alle über <b>UDP 443</b> auf dem RVS-Hostnamen.
|
||||
ARIA erzeugt die Schlüssel; ⬇ liefert die Datei für die Gegenseite (FritzBox oder Standard-WireGuard).
|
||||
Jede Verbindung wird ein VPN-Satellit; aus dem Kundennetz führt keine Verbindung zu ARIA.
|
||||
</p>
|
||||
<button class="btn secondary" onclick="vpnNpmGuide()" style="padding:4px 10px;font-size:11px;white-space:nowrap;">ℹ Einrichtung Proxy/Router</button>
|
||||
<button class="btn" onclick="vpnEdit(null,'server')" style="padding:5px 12px;font-size:12px;white-space:nowrap;">+ Hinzufügen</button>
|
||||
</div>
|
||||
<div id="vpn-server-list" style="font-size:12px;color:#8888AA;">(lade …)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10106,10 +10108,14 @@
|
||||
return ['#FFD60A', 'wartet auf Gegenstelle'];
|
||||
}
|
||||
function renderVpn() {
|
||||
const box = document.getElementById('vpn-client-list');
|
||||
_renderVpnList('vpn-client-list', 'client', 'Noch keine Verbindung. „+ Hinzufügen“ und die WireGuard-Datei des Kunden einfügen.');
|
||||
_renderVpnList('vpn-server-list', 'server', 'Noch keine Einwahl. „+ Hinzufügen“, Kundennetz eintragen, dann die Datei für die FritzBox herunterladen.');
|
||||
}
|
||||
function _renderVpnList(boxId, kind, empty) {
|
||||
const box = document.getElementById(boxId);
|
||||
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; }
|
||||
const list = (_vpnData.connections || []).filter(c => (c.kind || 'client') === kind);
|
||||
if (!list.length) { box.innerHTML = '<span style="color:#6a6a88;">' + empty + '</span>'; return; }
|
||||
box.innerHTML = list.map(c => {
|
||||
const [col, txt] = _vpnState(c);
|
||||
const st = c.status || {};
|
||||
@@ -10119,11 +10125,11 @@
|
||||
'<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>' +
|
||||
(kind === 'server' && c.lan ? ' · Kundennetz: ' + escapeHtml(c.lan.join(', ')) : (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="' + (kind === 'server' ? 'Datei für die Gegenseite (FritzBox / Standard)' : 'WireGuard-Datei herunterladen') + '" onclick="' + (kind === 'server' ? 'vpnServerDownload' : '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>';
|
||||
@@ -10172,6 +10178,7 @@
|
||||
if (!r.ok) { uiAlert('Laden fehlgeschlagen\n\n' + (r.error || '')); return; }
|
||||
c = r.connection;
|
||||
}
|
||||
if (c.kind === 'server') return _vpnServerForm(id, c);
|
||||
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>' +
|
||||
@@ -10197,6 +10204,71 @@
|
||||
vpnCheckConf();
|
||||
document.getElementById('vpn-f-name').focus();
|
||||
}
|
||||
function _vpnServerForm(id, c) {
|
||||
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 ? 'Einwahl bearbeiten' : 'Einwahl für einen Kunden anlegen') + '</b>' +
|
||||
'<p style="color:#8888AA;font-size:12px;margin:6px 0 0;">ARIA erzeugt die Schlüssel. Danach mit ⬇ die Datei für die Gegenseite holen und dort importieren ' +
|
||||
'(FritzBox: Internet → Freigaben → VPN (WireGuard) → Verbindung hinzufügen → Netzwerke koppeln → Datei übernehmen).</p>' +
|
||||
'<div style="margin-top:10px;">Name <span style="color:#8888AA;">(so heißt der VPN-Satellit)</span>' +
|
||||
'<input id="vpn-s-name" type="text" maxlength="60" placeholder="z.B. Kunde Müller" style="' + inp + 'margin-top:3px;"></div>' +
|
||||
'<div style="margin-top:10px;">Kundennetz <span style="color:#8888AA;">(LAN hinter der FritzBox, mehrere mit Komma)</span>' +
|
||||
'<input id="vpn-s-lan" type="text" placeholder="192.168.178.0/24" oninput="vpnServerHint()" style="' + inp + 'margin-top:3px;"></div>' +
|
||||
'<div style="margin-top:10px;">LAN-IP der FritzBox / des Routers <span style="color:#8888AA;">(leer = .1 im Kundennetz)</span>' +
|
||||
'<input id="vpn-s-router" type="text" placeholder="192.168.178.1" style="' + inp + 'margin-top:3px;"></div>' +
|
||||
'<div style="margin-top:10px;">Netze zum Scannen <span style="color:#8888AA;">(optional — leer = Kundennetz)</span>' +
|
||||
'<input id="vpn-s-nets" type="text" style="' + inp + 'margin-top:3px;"></div>' +
|
||||
'<div id="vpn-s-hint" style="font-size:11px;color:#8888AA;margin-top:6px;"></div>' +
|
||||
'<div id="vpn-s-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-s-save" onclick="vpnServerSave(' + (id ? "'" + escapeHtml(id) + "'" : 'null') + ')" style="padding:6px 14px;font-size:12px;">Speichern</button>' +
|
||||
'</div>');
|
||||
document.getElementById('vpn-s-name').value = c.name || '';
|
||||
document.getElementById('vpn-s-lan').value = (c.lan || []).join(', ');
|
||||
document.getElementById('vpn-s-router').value = c.routerIp || '';
|
||||
document.getElementById('vpn-s-nets').value = (c.scanNets && (c.lan || []).join(',') !== c.scanNets) ? c.scanNets : '';
|
||||
vpnServerHint();
|
||||
document.getElementById('vpn-s-name').focus();
|
||||
}
|
||||
function vpnServerHint() {
|
||||
const el = document.getElementById('vpn-s-hint');
|
||||
const lan = (document.getElementById('vpn-s-lan') || {}).value || '';
|
||||
if (!el) return;
|
||||
el.innerHTML = /^192\.168\.178\./.test(lan.trim())
|
||||
? 'Hinweis: 192.168.178.0/24 ist das FritzBox-Standardnetz. Das ist hier kein Problem — jede Einwahl hat ihren eigenen Container.'
|
||||
: '';
|
||||
}
|
||||
async function vpnServerSave(id) {
|
||||
const name = document.getElementById('vpn-s-name').value.trim();
|
||||
const lan = document.getElementById('vpn-s-lan').value.trim();
|
||||
const routerIp = document.getElementById('vpn-s-router').value.trim();
|
||||
const scanNets = document.getElementById('vpn-s-nets').value.trim();
|
||||
const msg = document.getElementById('vpn-s-msg');
|
||||
const btn = document.getElementById('vpn-s-save');
|
||||
btn.disabled = true; msg.innerHTML = '<span style="color:#8888AA;">speichere …</span>';
|
||||
const r = await vpnReq('vpn_save', { id, kind: 'server', name, lan, routerIp, scanNets });
|
||||
btn.disabled = false;
|
||||
if (!r.ok) { msg.innerHTML = '<span style="color:#FF6E6E;">✗ ' + escapeHtml(r.error || 'fehlgeschlagen') + '</span>'; return; }
|
||||
vpnCloseModal();
|
||||
if (!id) vpnServerDownload(r.id);
|
||||
}
|
||||
async function vpnServerDownload(id) {
|
||||
const type = await _uiDialog({ message: 'Datei für die Gegenseite\n\n' +
|
||||
'FritzBox: Netzwerk-Kopplung (Address = LAN-IP der Box). Standard: eigene Tunnel-Adresse — ' +
|
||||
'Router/Linux muss das Kundennetz dann selbst weiterleiten.\n\nDie Datei enthält den privaten Schlüssel der Gegenseite — nur dort importieren.',
|
||||
buttons: [{ label: 'Abbrechen', value: null, cancel: true },
|
||||
{ label: 'Standard-WireGuard', value: 'standard' },
|
||||
{ label: 'FritzBox', value: 'fritz', primary: true }] });
|
||||
if (!type) return;
|
||||
const r = await vpnReq('vpn_profile', { id, type });
|
||||
if (!r.ok) { uiAlert('Download fehlgeschlagen\n\n' + (r.error || '')); return; }
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(new Blob([r.profile.conf], { type: 'text/plain' }));
|
||||
a.download = (r.profile.name || 'aria').replace(/[^\w.-]+/g, '_') + (type === 'fritz' ? '_fritzbox' : '') + '.conf';
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 2000);
|
||||
}
|
||||
function vpnLoadFile(input) {
|
||||
const f = input.files && input.files[0];
|
||||
if (!f) return;
|
||||
@@ -10283,11 +10355,15 @@
|
||||
'<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, ' +
|
||||
'<li>Hosts → <b>Streams</b> → Add Stream: Incoming Port <code>443</code>, Forward Host = ARIA-VM, Forward Port <code>51820</code> (Verteiler, <code>VPN_RELAY_PORT</code>), ' +
|
||||
'<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>' +
|
||||
'<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 <code>51820</code>.</p>' +
|
||||
'<b>Mit ufw als Firewall</b><ul style="color:#AAB;margin-top:4px;">' +
|
||||
'<li>Auf dem <b>NPM-Rechner</b> UDP 443 aus dem Internet erlauben: <pre style="background:#08080F;padding:6px 8px;border-radius:4px;">ufw allow 443/udp comment \'WireGuard -> ARIA\'</pre></li>' +
|
||||
'<li>Auf der <b>ARIA-VM</b> den Verteiler-Port <b>nur vom NPM</b> erlauben (sonst niemand):<pre style="background:#08080F;padding:6px 8px;border-radius:4px;">ufw allow proto udp from <IP-des-NPM> to any port 51820 comment \'VPN-Verteiler nur vom NPM\'</pre>' +
|
||||
'Ohne Nginx statt dessen: <code>ufw allow 51820/udp</code> (der Router leitet direkt hierher).</li></ul>' +
|
||||
'<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>');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user