aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHimbeer <himbeer@disroot.org>2024-08-19 18:23:43 +0200
committerHimbeer <himbeer@disroot.org>2024-08-19 18:23:43 +0200
commit4be9a79311189238e2448c820ad3b0cb04bb7ac7 (patch)
tree164fccf680e0128f4d84e9833e8a0927ef21a261
parentd14e8876bcdabf9022afd72c01594fc68004c43e (diff)
Populate VPN server management page
-rw-r--r--src-tauri/src/main.rs339
-rw-r--r--src/vpn.html159
-rw-r--r--src/vpn.js162
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>
diff --git a/src/vpn.js b/src/vpn.js
index d6a2e4c..e7c080b 100644
--- a/src/vpn.js
+++ b/src/vpn.js
@@ -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);