diff options
author | Himbeer <himbeer@disroot.org> | 2024-08-19 18:23:43 +0200 |
---|---|---|
committer | Himbeer <himbeer@disroot.org> | 2024-08-19 18:23:43 +0200 |
commit | 4be9a79311189238e2448c820ad3b0cb04bb7ac7 (patch) | |
tree | 164fccf680e0128f4d84e9833e8a0927ef21a261 | |
parent | d14e8876bcdabf9022afd72c01594fc68004c43e (diff) |
Populate VPN server management page
-rw-r--r-- | src-tauri/src/main.rs | 339 | ||||
-rw-r--r-- | src/vpn.html | 159 | ||||
-rw-r--r-- | src/vpn.js | 162 |
3 files changed, 659 insertions, 1 deletions
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f90cded..da58602 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,7 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use std::io::{BufRead, BufReader}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6}; use std::sync::Mutex; use std::time::{Duration, SystemTime}; @@ -298,6 +299,20 @@ struct Domain { status_text: String, } +#[derive(Debug, Serialize)] +struct VpnClient { + name: String, + pubkey: String, + psk: String, + allowed_ips: String, +} + +#[derive(Debug, Serialize)] +struct VpnClients { + clients: Vec<VpnClient>, + status_text: String, +} + impl FromIterator<Dhcpv4Lease> for Leases { fn from_iter<T>(iter: T) -> Self where @@ -1274,6 +1289,325 @@ async fn handle_log_read_response(response: Response) -> String { } } +#[tauri::command] +async fn vpnclients(state: State<'_, Mutex<Session>>) -> Result<VpnClients, ()> { + let (client, instance) = { + let state = state.lock().unwrap(); + (state.client.clone(), state.instance.clone()) + }; + let instance = match instance { + Some(instance) => instance, + None => { + return Ok(VpnClients { + clients: Vec::new(), + status_text: String::from( + "Keine Instanz ausgewählt, bitte melden Sie sich neu an!", + ), + }) + } + }; + + let response = client + .get(instance.url.join("/data/read").unwrap()) + .query(&[("path", "/data/wgd.peers")]) + .basic_auth("rustkrazy", Some(&instance.password)) + .send(); + + Ok(match response.await { + Ok(response) => handle_vpnclients_response(response).await, + Err(e) => VpnClients { + clients: Vec::new(), + status_text: format!("Abfrage fehlgeschlagen: {}", e), + }, + }) +} + +async fn handle_vpnclients_response(response: Response) -> VpnClients { + let status = response.status(); + if status.is_success() { + match response.text().await { + Ok(peertext) => parse_peertext(peertext), + Err(e) => VpnClients { + clients: Vec::new(), + status_text: format!("Keinen Text vom Server erhalten. Fehler: {}", e), + }, + } + } else if status == StatusCode::UNAUTHORIZED { + VpnClients { + clients: Vec::new(), + status_text: String::from( + "Ungültiges Verwaltungspasswort, bitte melden Sie sich neu an!", + ), + } + } else if status == StatusCode::NOT_FOUND { + VpnClients { + clients: Vec::new(), + status_text: String::new(), + } + } else if status.is_client_error() { + VpnClients { + clients: Vec::new(), + status_text: format!("Clientseitiger Fehler: {}", status), + } + } else if status.is_server_error() { + VpnClients { + clients: Vec::new(), + status_text: format!("Serverseitiger Fehler: {}", status), + } + } else { + VpnClients { + clients: Vec::new(), + status_text: format!("Unerwarteter Statuscode: {}", status), + } + } +} + +macro_rules! next { + ($c:expr, $v:ident) => { + match $c.next() { + Some(column) => column, + None => return VpnClients { + clients: $v.clients, + status_text: format!("Ungültige Clientdefinition: Fehlende Spalte. Konfigurationsdatei löschen und neu anlegen.") + }, + } + }; + + ($c:expr, $v:ident, $name:expr) => { + match $c.next() { + Some(column) => column, + None => { + $v.clients.push(VpnClient { + name: $name, + pubkey: String::from("--- FEHLER ---"), + psk: String::from("--- FEHLER ---"), + allowed_ips: String::from("--- FEHLER ---"), + }); + + return VpnClients { + clients: $v.clients, + status_text: format!("Ungültige Clientdefinition: Fehlende Spalte. Fehlerhaften Eintrag (letzte angezeigte Zeile) löschen und nach Korrektur neu anlegen.") + }; + } + } + } +} + +fn parse_peertext(peertext: String) -> VpnClients { + let mut vpnclients = VpnClients { + clients: Vec::new(), + status_text: String::new(), + }; + + let br = BufReader::new(peertext.as_bytes()); + for line in br.lines() { + let line = line.expect("read line from peertext string"); + + let mut columns = line.split_whitespace(); + + let name = next!(columns, vpnclients); + let pubkey = next!(columns, vpnclients, name.to_string()); + let psk = next!(columns, vpnclients, name.to_string()); + + let mut allowed_ips = String::new(); + let mut delim = ""; + for column in columns { + allowed_ips += delim; + allowed_ips += column; + delim = " "; + } + + vpnclients.clients.push(VpnClient { + name: name.to_string(), + pubkey: pubkey.to_string(), + psk: psk.to_string(), + allowed_ips, + }); + } + + vpnclients +} + +#[tauri::command] +async fn vpndel(name: String, state: State<'_, Mutex<Session>>) -> Result<String, ()> { + let (client, instance) = { + let state = state.lock().unwrap(); + (state.client.clone(), state.instance.clone()) + }; + let instance = match instance { + Some(instance) => instance, + None => { + return Ok(String::from( + "Keine Instanz ausgewählt, bitte melden Sie sich neu an!", + )) + } + }; + + let response = client + .get(instance.url.join("/data/read").unwrap()) + .query(&[("path", "/data/wgd.peers")]) + .basic_auth("rustkrazy", Some(&instance.password)) + .send(); + + let peertext = match response.await { + Ok(response) => match handle_vpndel_response(response).await { + Ok(peertext) => peertext, + Err(status_text) => return Ok(status_text), + }, + Err(e) => format!("Abfrage fehlgeschlagen: {}", e), + }; + + let mut new_peertext = String::new(); + + let br = BufReader::new(peertext.as_bytes()); + for line in br.lines() { + let line = line.expect("read line from peertext string"); + + if !line.starts_with(&name) { + new_peertext += &line; + new_peertext += "\n"; + } + } + + let response = client + .post(instance.url.join("/data/write").unwrap()) + .query(&[("path", "/data/wgd.peers")]) + .basic_auth("rustkrazy", Some(&instance.password)) + .body(new_peertext) + .send(); + + Ok(match response.await { + Ok(response) => handle_vpndel_response2(response), + Err(e) => format!("Löschung fehlgeschlagen: {}", e), + }) +} + +async fn handle_vpndel_response(response: Response) -> Result<String, String> { + let status = response.status(); + if status.is_success() { + match response.text().await { + Ok(peertext) => Ok(peertext), + Err(e) => Err(format!("Keinen Text vom Server erhalten. Fehler: {}", e)), + } + } else if status == StatusCode::NOT_FOUND { + Err(String::from( + "Konfigurationsdatei existiert nicht, Löschung nicht nötig", + )) + } else if status == StatusCode::UNAUTHORIZED { + Err(String::from( + "Ungültiges Verwaltungspasswort, bitte melden Sie sich neu an!", + )) + } else if status.is_client_error() { + Err(format!("Clientseitiger Fehler: {}", status)) + } else if status.is_server_error() { + Err(format!("Serverseitiger Fehler: {}", status)) + } else { + Err(format!("Unerwarteter Statuscode: {}", status)) + } +} + +fn handle_vpndel_response2(response: Response) -> String { + let status = response.status(); + if status.is_success() { + String::from("Löschung erfolgreich") + } else if status == StatusCode::UNAUTHORIZED { + String::from("Ungültiges Verwaltungspasswort, bitte melden Sie sich neu an!") + } else if status.is_client_error() { + format!("Clientseitiger Fehler: {}", status) + } else if status.is_server_error() { + format!("Serverseitiger Fehler: {}", status) + } else { + format!("Unerwarteter Statuscode: {}", status) + } +} + +#[tauri::command] +async fn vpnadd( + name: String, + pubkey: String, + psk: String, + allowed_ips: String, + state: State<'_, Mutex<Session>>, +) -> Result<String, ()> { + let (client, instance) = { + let state = state.lock().unwrap(); + (state.client.clone(), state.instance.clone()) + }; + let instance = match instance { + Some(instance) => instance, + None => { + return Ok(String::from( + "Keine Instanz ausgewählt, bitte melden Sie sich neu an!", + )) + } + }; + + let response = client + .get(instance.url.join("/data/read").unwrap()) + .query(&[("path", "/data/wgd.peers")]) + .basic_auth("rustkrazy", Some(&instance.password)) + .send(); + + let mut peertext = match response.await { + Ok(response) => match handle_vpnadd_response(response).await { + Ok(peertext) => peertext, + Err(status_text) => return Ok(status_text), + }, + Err(e) => format!("Abfrage fehlgeschlagen: {}", e), + }; + + peertext += &format!("{} {} {} {}\n", name, pubkey, psk, allowed_ips); + + let response = client + .post(instance.url.join("/data/write").unwrap()) + .query(&[("path", "/data/wgd.peers")]) + .basic_auth("rustkrazy", Some(&instance.password)) + .body(peertext) + .send(); + + Ok(match response.await { + Ok(response) => handle_vpnadd_response2(response), + Err(e) => format!("Hinzufügen fehlgeschlagen: {}", e), + }) +} + +async fn handle_vpnadd_response(response: Response) -> Result<String, String> { + let status = response.status(); + if status.is_success() { + match response.text().await { + Ok(peertext) => Ok(peertext), + Err(e) => Err(format!("Keinen Text vom Server erhalten. Fehler: {}", e)), + } + } else if status == StatusCode::NOT_FOUND { + Ok(String::new()) + } else if status == StatusCode::UNAUTHORIZED { + Err(String::from( + "Ungültiges Verwaltungspasswort, bitte melden Sie sich neu an!", + )) + } else if status.is_client_error() { + Err(format!("Clientseitiger Fehler: {}", status)) + } else if status.is_server_error() { + Err(format!("Serverseitiger Fehler: {}", status)) + } else { + Err(format!("Unerwarteter Statuscode: {}", status)) + } +} + +fn handle_vpnadd_response2(response: Response) -> String { + let status = response.status(); + if status.is_success() { + String::from("Hinzufügen erfolgreich") + } else if status == StatusCode::UNAUTHORIZED { + String::from("Ungültiges Verwaltungspasswort, bitte melden Sie sich neu an!") + } else if status.is_client_error() { + format!("Clientseitiger Fehler: {}", status) + } else if status.is_server_error() { + format!("Serverseitiger Fehler: {}", status) + } else { + format!("Unerwarteter Statuscode: {}", status) + } +} + fn main() { tauri::Builder::default() .manage(Mutex::new(Session { @@ -1304,7 +1638,10 @@ fn main() { change_sys_password, reboot, shutdown, - log_read + log_read, + vpnclients, + vpndel, + vpnadd ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/vpn.html b/src/vpn.html index 19bebe3..b4b11d7 100644 --- a/src/vpn.html +++ b/src/vpn.html @@ -42,6 +42,165 @@ <button id="disconnect-submit" type="submit">🚪 Abmelden</button> </form> </div> + + <br /> + + <form id="profile-form"> + <fieldset> + <legend>Clientseitige Konfiguration</legend> + + <p> + 1. WireGuard installieren: <a + href="https://www.wireguard.com/install/" + target="_blank">https://www.wireguard.com/install/</a> + + <br /> + + 2. Tunnel hinzufügen / Add a tunnel / + + + <br /> + + 3. Gerätenummer ermitteln: Beliebige Zahl zwischen 100 und 239 wählen, die nicht bereits für ein anderes Gerät verwendet wird + + <br /> + + 4. Daten selbst eingeben / Create from scratch + + <br /> + + 5. "Generate keypair" drücken und privaten Schlüssel / Private key in die Zwischenablage kopieren + + <br /> + + 6. Gerätedaten eintragen + + <br /><br /> + + - Name: wg0 (beliebig wählbar)<br /> + - Privater Schlüssel / Private key: Mit weiterer Betätigung von "Generate keypair" automatisch erzeugen lassen<br /> + - Öffentlicher Schlüssel / Public key: Wird ebenfalls von "Generate keypair" erzeugt<br /> + - Adressen / Addresses: 10.128.50.GERÄTENUMMER/32, fd0b:9272:534e:6::GERÄTENUMMER/128 (Achtung: Für Firewallausnahme stattdessen 10.128.60.GERÄTENUMMER/32, fd0b:9272:534e:7::GERÄTENUMMER/128 verwenden)<br /> + - Listen port: Keine Angabe (automatisch)<br /> + - MTU: Keine Angabe (automatisch)<br /> + - DNS-Server / DNS servers: 10.128.50.254, fd0b:9272:534e:6::1 (Achtung: Für Firewallausnahme stattdessen 10.128.60.254, fd0b:9272:534e:7::1 verwenden)<br /> + + <br /> + + 7. Gerät hinzufügen / Add peer + + <br /><br /> + + - Öffentlicher Schlüssel / Public key: <span id="profile-pubkey">Siehe Diagnoseprotokoll</span><br /> + - Symmetrischer Schlüssel / Preshared key: Aus Zwischenablage einfügen<br /> + - Endpunkt / Endpoint: rtr.himbeerserver.de:51820 (Achtung: Für Firewallausnahme stattdessen rtr.himbeerserver.de:51821 verwenden)<br /> + - Erlaubte IP-Adressen / Allowed IPs: 0.0.0.0/0, ::/0<br /> + - Private IP-Adressen ignorieren / Exclude private IPs: Nein (bei Zugriff aus dem öffentlichen Internet egal, bei Zugriff aus dem Heimnetz relevant)<br /> + - Keep-Alive / Persistent keepalive: Keine Angabe (deaktiviert), kann ggf. auf 25 gesetzt werden<br /> + + <br /> + + 8. Speichern + </p> + </fieldset> + </form> + + <form id="server-form"> + <fieldset> + <legend>Server</legend> + + <div class="row"> + <label for="server-pubkey">Öffentlicher Schlüssel / Public key:</label> + <span id="server-pubkey">Siehe Diagnoseprotokoll</span> + </div> + + <br /> + + <div class="row"> + <button id="server-restart">🔄 VPN-Server-Neustart</button> + </div> + + <p>Information: Ein Neustart des VPN-Servers dauert ca. 30 Sekunden. + Eventuell gehen dabei bestehende VPN-Verbindungen verloren. Bei + Fehlkonfiguration ist mit großer Wahrscheinlichkeit kein + VPN-Verbindungsaufbau mehr möglich.</p> + </fieldset> + </form> + + <br /> + + <form id="client-form"> + <fieldset> + <legend>Clients</legend> + + <table id="client-clients"> + <th> + <td>Öffentlicher Schlüssel / Public key</td> + <td>Symmetrischer Schlüssel / Preshared key</td> + <td>Erlaubte IP-Adressen / Allowed IPs</td> + </th> + </table> + + <div class="row"> + <output id="client-status">Warte auf Initialisierung...</output> + </div> + + <p>Information: Es ist zulässig (aber nicht sinnvoll), mehrere + Clients mit dem gleichen Namen zu erstellen. Beim Löschen werden alle + Clients mit passendem Namen entfernt, unabhängig davon, welcher + Client tatsächlich gelöscht werden sollte.</p> + </fieldset> + </form> + + <br /> + + <form id="add-form"> + <fieldset> + <legend>Client hinzufügen</legend> + + <label for="add-name" form="add-form">Name:</label> + <div class="row"> + <input id="add-name" /> + </div> + + <br /> + + <label for="add-pubkey" form="add-form">Öffentlicher Schlüssel / Public key:</label> + <div class="row"> + <input id="add-pubkey" /> + </div> + + <br /> + + <label for="add-psk" form="add-form">Symmetrischer Schlüssel / Preshared key:</label> + <div class="row"> + <input id="add-psk" type="password" /> + </div> + <br /> + <div class="row"> + <button id="add-show">🔒 Symmetrischen Schlüssel ein-/ausblenden</button> + </div> + + <br /> + + <label for="add-allowedips" form="add-form">Erlaubte IP-Adressen / Allowed IPs:</label> + <div class="row"> + <input id="add-allowedips" placeholder="z.B. 10.128.50.100/32 fd0b:9272:534e:6::100/128" /> + </div> + + <br /> + + <div class="row"> + <button id="add-submit" type="submit">Client hinzufügen</button> + </div> + + <p id="add-status"></p> + + <p>Information: Es ist zulässig (aber nicht sinnvoll), mehrere + Clients mit dem gleichen Namen zu erstellen. Beim Löschen wird der + erste Client mit passendem Namen entfernt, unabhängig davon, welcher + Client tatsächlich gelöscht werden sollte.</p> + </fieldset> + </form> </div> </body> </html> @@ -1,4 +1,166 @@ const { invoke } = window.__TAURI__.tauri; +const { ask, message } = window.__TAURI__.dialog; + +let clientTableEl; +let clientStatusEl; + +let addNameEl; +let addPubkeyEl; +let addPskEl; +let addAllowedIpsEl; +let addSubmitEl; +let addStatusEl; + +async function serverRestart() { + const error = await invoke("kill", { process: "rsdsl_wgd", signal: "term" }); + + if (error !== "") { + await message("Befehl konnte nicht erteilt werden: " + error, { + kind: "error", + title: "VPN-Server-Neustart nicht erfolgt" + }); + } +} + +async function deleteClient(name) { + const statusText = await invoke("vpndel", { name: name }); + + if (statusText === "Löschung erfolgreich") { + await loadClients(); + await message("Zum Anwenden der Änderung muss der VPN-Server manuell neu gestartet werden.", { + kind: "info", + title: "VPN-Server-Neustart erforderlich", + }); + } else { + await message("Befehl konnte nicht erteilt werden: " + statusText, { + kind: "error", + title: "Clientlöschung nicht erfolgt", + }); + } +} + +async function loadClients() { + const clients = await invoke("vpnclients", {}); + + clientStatusEl.innerText = clients.status_text; + + let first = true; + for (let child of clientTableEl.querySelectorAll("tr")) { + if (first) { + first = false; + continue; + } + + clientTableEl.removeChild(child); + child.remove(); + } + + for (let client of clients.clients) { + let name = document.createElement("td"); + name.innerText = client.name; + + let pubkey = document.createElement("td"); + pubkey.innerText = client.pubkey; + + let psk = document.createElement("td"); + psk.innerText = client.psk; + + let allowedIps = document.createElement("td"); + allowedIps.innerText = client.allowed_ips; + + let deleter = document.createElement("button"); + deleter.innerText = "🗑"; + deleter.addEventListener("click", async function(e) { + e.preventDefault(); + + const confirmed = await ask("Möchten Sie die VPN-Zugangsberechtigung für " + client.name + " wirklich entfernen?", { + kind: "warn", + title: "Entfernen bestätigen" + }); + + if (confirmed) { + await deleteClient(client.name); + } + }); + + let row = document.createElement("tr"); + row.appendChild(name); + row.appendChild(pubkey); + row.appendChild(psk); + row.appendChild(allowedIps); + row.appendChild(deleter); + + clientTableEl.appendChild(row); + } +} + +function showPsk() { + switch (addPskEl.type) { + case "password": + addPskEl.type = "text"; + break; + case "text": + addPskEl.type = "password"; + break; + } +} + +async function addClient() { + addNameEl.disabled = true; + addPubkeyEl.disabled = true; + addPskEl.disabled = true; + addAllowedIpsEl.disabled = true; + addSubmitEl.disabled = true; + addStatusEl.innerText = "Änderungsanfrage..."; + document.body.style.cursor = "progress"; + + addStatusEl.innerText = await invoke("vpnadd", { + name: addNameEl.value, + pubkey: addPubkeyEl.value, + psk: addPskEl.value, + allowedIps: addAllowedIpsEl.value, + }); + + addNameEl.disabled = false; + addPubkeyEl.disabled = false; + addPskEl.disabled = false; + addAllowedIpsEl.disabled = false; + addSubmitEl.disabled = false; + document.body.style.cursor = "default"; + + await loadClients(); + await message("Zum Anwenden der Änderung muss der VPN-Server manuell neu gestartet werden.", { + kind: "info", + title: "VPN-Server-Neustart erforderlich", + }); +} window.addEventListener("DOMContentLoaded", () => { + document.querySelector("#server-restart").addEventListener("click", (e) => { + e.preventDefault(); + serverRestart(); + }); + + clientTableEl = document.querySelector("#client-clients"); + clientStatusEl = document.querySelector("#client-status"); + + loadClients(); + + addNameEl = document.querySelector("#add-name"); + addPubkeyEl = document.querySelector("#add-pubkey"); + addPskEl = document.querySelector("#add-psk"); + addAllowedIpsEl = document.querySelector("#add-allowedips"); + addSubmitEl = document.querySelector("#add-submit"); + addStatusEl = document.querySelector("#add-status"); + + document.querySelector("#add-show").addEventListener("click", (e) => { + e.preventDefault(); + showPsk(); + }); + document.querySelector("#add-form").addEventListener("submit", (e) => { + e.preventDefault(); + addClient(); + }); }); + +setInterval(loadClients, 10000); |