diff options
-rw-r--r-- | src-tauri/Cargo.lock | 2 | ||||
-rw-r--r-- | src-tauri/Cargo.toml | 2 | ||||
-rw-r--r-- | src-tauri/src/main.rs | 358 | ||||
-rw-r--r-- | src/styles.css | 4 | ||||
-rw-r--r-- | src/wan.html | 113 | ||||
-rw-r--r-- | src/wan.js | 117 |
6 files changed, 592 insertions, 4 deletions
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index aab5a57..e2638a0 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2496,6 +2496,8 @@ dependencies = [ name = "rsdsl_manager" version = "0.0.0" dependencies = [ + "chrono", + "hex", "reqwest", "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f555043..933089c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,6 +15,8 @@ tauri = { version = "1", features = [ "dialog-message", "dialog-ask", "shell-ope serde = { version = "1", features = ["derive"] } serde_json = "1" reqwest = { version = "0.12.5", features = ["json"] } +chrono = { version = "0.4.38", default-features = false, features = ["alloc", "clock"] } +hex = "0.4.3" [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 77b3a8f..b555123 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,11 +1,13 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6}; use std::sync::Mutex; +use std::time::{Duration, SystemTime}; use tauri::State; +use chrono::{DateTime, Local}; use reqwest::{Client, Response}; use reqwest::{StatusCode, Url}; use serde::{Deserialize, Serialize}; @@ -104,6 +106,170 @@ struct Ipv6Connection { raddr: Ipv6Addr, } +#[derive(Debug, Serialize)] +struct Dhcpv6Status { + timestamp: String, + srvaddr: String, + srvid: String, + t1: String, + t2: String, + prefix: String, + wanaddr: String, + preflft: String, + validlft: String, + dns1: String, + dns2: String, + aftr: String, +} + +impl Dhcpv6Status { + fn no_lease() -> Self { + Self::with_all(String::from( + "✖ Keine Lease vorhanden (erster Systemstart oder Stromausfall?)", + )) + } + + fn with_all(message: String) -> Self { + Self { + timestamp: message.clone(), + srvaddr: message.clone(), + srvid: message.clone(), + t1: message.clone(), + t2: message.clone(), + prefix: message.clone(), + wanaddr: message.clone(), + preflft: message.clone(), + validlft: message.clone(), + dns1: message.clone(), + dns2: message.clone(), + aftr: message, + } + } +} + +impl From<Dhcpv6Lease> for Dhcpv6Status { + fn from(lease: Dhcpv6Lease) -> Self { + let validity = if lease.is_valid() { "✅" } else { "❌" }; + + Self { + timestamp: format!( + "{} {}", + validity, + DateTime::<Local>::from(lease.timestamp).format("%d.%m.%Y %H:%M:%S UTC%Z") + ), + srvaddr: if lease.server + == SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)) + { + String::from("ff02::1:2 (Alle DHCPv6-Server, da der Server keine spezifische Adresse angegeben hat)") + } else { + format!("{}", lease.server) + }, + srvid: hex::encode(lease.server_id), + t1: if lease.t1 == 0 { + String::from("Sofort") + } else if lease.t1 == u32::MAX { + String::from("Nie") + } else { + let remaining_secs = std::cmp::max( + (Duration::from_secs(lease.t1.into()) + - lease.timestamp.elapsed().unwrap_or(Duration::ZERO)) + .as_secs(), + 0, + ); + format!( + "Alle {} Sekunden ({} Sekunden verbleibend)", + lease.t1, remaining_secs + ) + }, + t2: if lease.t2 == 0 { + String::from("Sofort") + } else if lease.t2 == u32::MAX { + String::from("Nie") + } else { + let remaining_secs = std::cmp::max( + (Duration::from_secs(lease.t2.into()) + - lease.timestamp.elapsed().unwrap_or(Duration::ZERO)) + .as_secs(), + 0, + ); + format!( + "Alle {} Sekunden ({} Sekunden verbleibend)", + lease.t2, remaining_secs + ) + }, + prefix: format!("{}/{}", lease.prefix, lease.len), + wanaddr: format!("{}1/64", lease.prefix), + preflft: if lease.preflft == 0 { + String::from("⚠ Niemals für neue Verbindungen verwenden") + } else if lease.preflft == u32::MAX { + String::from("Unendlich") + } else { + let remaining_secs = std::cmp::max( + (Duration::from_secs(lease.preflft.into()) + - lease.timestamp.elapsed().unwrap_or(Duration::ZERO)) + .as_secs(), + 0, + ); + format!( + "{} Sekunden ({} Sekunden verbleibend)", + lease.preflft, remaining_secs + ) + }, + validlft: if lease.validlft == 0 { + String::from("⚠ Internetanbieter verlangte manuell sofortigen Verfall") + } else if lease.validlft == u32::MAX { + String::from("Unendlich") + } else { + let remaining_secs = std::cmp::max( + (Duration::from_secs(lease.validlft.into()) + - lease.timestamp.elapsed().unwrap_or(Duration::ZERO)) + .as_secs(), + 0, + ); + format!( + "{} Sekunden ({} Sekunden verbleibend)", + lease.validlft, remaining_secs + ) + }, + dns1: format!("{}", lease.dns1), + dns2: format!("{}", lease.dns2), + aftr: match lease.aftr { + Some(aftr) => format!("🟢 Aktiviert | Tunnel-Endpunkt (AFTR): {}", aftr), + None => String::from("⚪ Deaktiviert"), + }, + } + } +} + +#[derive(Debug, Deserialize)] +struct Dhcpv6Lease { + timestamp: std::time::SystemTime, + server: SocketAddr, + server_id: Vec<u8>, + t1: u32, + t2: u32, + prefix: Ipv6Addr, + len: u8, + preflft: u32, + validlft: u32, + dns1: Ipv6Addr, + dns2: Ipv6Addr, + aftr: Option<String>, +} + +#[derive(Debug, Serialize)] +struct Duid { + duid: String, + status_text: String, +} + +impl Dhcpv6Lease { + fn is_valid(&self) -> bool { + let expiry = self.timestamp + Duration::from_secs(self.validlft.into()); + SystemTime::now() < expiry + } +} + // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command #[tauri::command] async fn connect( @@ -421,6 +587,191 @@ async fn handle_connection_status_response(response: Response) -> ConnectionStat } } +#[tauri::command] +async fn dhcpv6_status(state: State<'_, Mutex<Session>>) -> Result<Dhcpv6Status, ()> { + let (client, instance) = { + let state = state.lock().unwrap(); + (state.client.clone(), state.instance.clone()) + }; + let instance = match instance { + Some(instance) => instance, + None => { + return Ok(Dhcpv6Status::with_all(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/dhcp6.lease")]) + .basic_auth("rustkrazy", Some(&instance.password)) + .send(); + + Ok(match response.await { + Ok(response) => handle_dhcpv6_status_response(response).await, + Err(e) => Dhcpv6Status::with_all(format!("❗ Abfrage fehlgeschlagen: {}", e)), + }) +} + +async fn handle_dhcpv6_status_response(response: Response) -> Dhcpv6Status { + let status = response.status(); + if status.is_success() { + match response.json::<Dhcpv6Lease>().await { + Ok(lease) => Dhcpv6Status::from(lease), + Err(e) => Dhcpv6Status::with_all(format!("❗ Fehlerhafte Leasedatei. Fehler: {}", e)), + } + } else if status == StatusCode::UNAUTHORIZED { + Dhcpv6Status::with_all(String::from( + "❗ Ungültiges Verwaltungspasswort, bitte melden Sie sich neu an!", + )) + } else if status == StatusCode::NOT_FOUND { + Dhcpv6Status::no_lease() + } else if status.is_client_error() { + Dhcpv6Status::with_all(format!("❗ Clientseitiger Fehler: {}", status)) + } else if status.is_server_error() { + Dhcpv6Status::with_all(format!("❗ Serverseitiger Fehler: {}", status)) + } else { + Dhcpv6Status::with_all(format!("❗ Unerwarteter Statuscode: {}", status)) + } +} + +#[tauri::command] +async fn load_duid(state: State<'_, Mutex<Session>>) -> Result<Duid, ()> { + let (client, instance) = { + let state = state.lock().unwrap(); + (state.client.clone(), state.instance.clone()) + }; + let instance = match instance { + Some(instance) => instance, + None => { + return Ok(Duid { + duid: String::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/dhcp6.duid")]) + .basic_auth("rustkrazy", Some(&instance.password)) + .send(); + + Ok(match response.await { + Ok(response) => handle_load_duid_response(response).await, + Err(e) => Duid { + duid: String::new(), + status_text: format!("Abruf des aktuellen Client-DUID fehlgeschlagen: {}", e), + }, + }) +} + +async fn handle_load_duid_response(response: Response) -> Duid { + let status = response.status(); + if status.is_success() { + let bytes = match response.bytes().await { + Ok(bytes) => bytes, + Err(e) => { + return Duid { + duid: String::new(), + status_text: format!( + "Keine Rohdaten vom Server erhalten, bitte Neustart durchführen. Fehler: {}", + e + ), + } + } + }; + + Duid { + duid: hex::encode(bytes), + status_text: String::new(), + } + } else if status == StatusCode::UNAUTHORIZED { + Duid { + duid: String::new(), + status_text: String::from( + "Ungültiges Verwaltungspasswort, bitte melden Sie sich neu an!", + ), + } + } else if status == StatusCode::NOT_FOUND { + Duid{ + duid:String::new(), + status_text:String::from("Kein Client-DUID gespeichert (erster Systemstart oder Stromausfall?), wird bei Bedarf zufällig generiert und gespeichert"), + } + } else if status.is_client_error() { + Duid { + duid: String::new(), + status_text: format!("Clientseitiger Fehler: {}", status), + } + } else if status.is_server_error() { + Duid { + duid: String::new(), + status_text: format!("Serverseitiger Fehler: {}", status), + } + } else { + Duid { + duid: String::new(), + status_text: format!("Unerwarteter Statuscode: {}", status), + } + } +} + +#[tauri::command] +async fn change_duid(duid: 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 bytes = match hex::decode(&duid) { + Ok(bytes) => bytes, + Err(e) => { + return Ok(format!( + "Eingabe ist keine gültige Hexadezimalsequenz: {}", + e + )) + } + }; + + let response = client + .post(instance.url.join("/data/write").unwrap()) + .query(&[("path", "/data/dhcp6.duid")]) + .basic_auth("rustkrazy", Some(&instance.password)) + .body(bytes) + .send(); + + Ok(match response.await { + Ok(response) => handle_change_duid_response(response), + Err(e) => format!("Änderung fehlgeschlagen: {}", e), + }) +} + +fn handle_change_duid_response(response: Response) -> String { + let status = response.status(); + if status.is_success() { + String::from("Änderung 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 { @@ -436,7 +787,10 @@ fn main() { load_wan_credentials, change_wan_credentials, kill, - connection_status + connection_status, + dhcpv6_status, + load_duid, + change_duid ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/styles.css b/src/styles.css index f0cae8d..fe1e0e2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -98,6 +98,10 @@ input { width: 30em; } +.row label { + padding-right: 0.5em; +} + @media (prefers-color-scheme: dark) { :root { color: #f6f6f6; diff --git a/src/wan.html b/src/wan.html index b8b533b..fdfc943 100644 --- a/src/wan.html +++ b/src/wan.html @@ -116,6 +116,119 @@ <p id="credentials-status">Warte auf Initialisierung...</p> </fieldset> </form> + + <br /> + + <form id="dhcpv6-form"> + <fieldset> + <legend>DHCPv6-Client</legend> + + <div class="row"> + <label for="dhcpv6-timestamp" form="dhcpv6-form">Letzte Gültigkeitsbestätigung:</label> + <output id="dhcpv6-timestamp">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-srvaddr" form="dhcpv6-form">Serveradresse für weitere Anfragen:</label> + <output id="dhcpv6-srvaddr">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-srvid" form="dhcpv6-form">Server-DUID (Hexadezimal):</label> + <output id="dhcpv6-srvid">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-t1" form="dhcpv6-form">Verlängerung (Renew):</label> + <output id="dhcpv6-t1">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-t2" form="dhcpv6-form">Verlängerung mit Serversuche (Rebind):</label> + <output id="dhcpv6-t2">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-prefix" form="dhcpv6-form">Präfix:</label> + <output id="dhcpv6-prefix">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-wanaddr" form="dhcpv6-form">WAN-Adresse (aus Präfix abgeleitet):</label> + <output id="dhcpv6-wanaddr">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-preflft" form="dhcpv6-form">Verwendungsdauer:</label> + <output id="dhcpv6-preflft">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-validlft" form="dhcpv6-form">Gültigkeitsdauer:</label> + <output id="dhcpv6-validlft">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-dns1" form="dhcpv6-form">Primärer DNS-Server (nur für DS-Lite AFTR):</label> + <output id="dhcpv6-dns1">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-dns2" form="dhcpv6-form">Sekundärer DNS-Server (nur für DS-Lite AFTR):</label> + <output id="dhcpv6-dns2">❓ Abfrage...</output> + </div> + + <div class="row"> + <label for="dhcpv6-aftr" form="dhcpv6-form">DS-Lite:</label> + <output id="dhcpv6-aftr">❓ Abfrage...</output> + </div> + + <br /> + + <div class="row"> + <button id="dhcpv6-kill">🔄 Verlängerung mit Serversuche</button> + </div> + + <p>Information: Nur bei einem grünen Haken bei "Letzte + Gültigkeitsbestätigung" ist das Präfix gültig und eine Verbindung mit + dem IPv6-Internet hergestellt (die reine Einwahl, s. oben, genügt + nicht). Bei abgelaufener Verwendungsdauer sollen laut Standard keine + neuen Verbindungen mehr aufgebaut werden. Diese Implementation + ignoriert diese Begrenzung, um im (anbieterseitigen + Software-)Störungsfall eine höhere Verfügbarkeit zu erreichen. Eine + Verlängerung mit Serversuche startet den DHCPv6-Client vollständig + neu und dauert ca. 30 Sekunden, sollte die Internetverbindung aber + nicht beeinträchtigen. Sie kann Abhilfe schaffen, wenn die + IPv6-Einwahl gelingt, aber keine Verbindung zum IPv6-Internet besteht + oder in dieser Übersicht das Präfix als ungültig angezeigt wird.</p> + + <label for="dhcpv6-duid" form="dhcpv6-form">Client-DUID (Hexadezimal):</label> + <div class="row"> + <input id="dhcpv6-duid" /> + </div> + + <br /> + + <div class="row"> + <button id="dhcpv6-submit" type="submit">Client-DUID ändern</button> + </div> + + <p id="dhcpv6-status">Warte auf Initialisierung...</p> + + <p>Information: Der Client-DUID (<b>D</b>HCP <b>U</b>nique + <b>ID</b>entifier) ist ein eindeutiger Wert, den der Internetanbieter + für die IPv6-Präfixvergabe verwenden kann. Dieser Wert sollte + konstant sein, um das Präfix konstant zu halten und somit weniger oft + die Einträge des dynamisches DNS (für Serververfügbarkeit) + aktualisieren zu müssen. Die eingesetzte Implementation nutzt den + DUID-UUID-Untertyp, der hier eingegebene Wert bezieht sich aber auf + den vollständigen DUID, nicht nur auf den UUID-Bestandteil. Er wird + zufällig generiert, falls noch kein Wert gespeichert ist oder der + gespeicherte Wert ungültig ist. Falls nötig kann der DUID hier + eingesehen und verändert werden. Einige Internetanbieter schreiben + die Nutzung eines bestimmten DUIDs vor.</p> + </fieldset> + </form> </div> </body> </html> @@ -10,6 +10,23 @@ let credentialsPasswordEl; let credentialsSubmitEl; let credentialsStatusEl; +let dhcpv6TimestampEl; +let dhcpv6SrvAddrEl; +let dhcpv6SrvIdEl; +let dhcpv6T1El; +let dhcpv6T2El; +let dhcpv6PrefixEl; +let dhcpv6WanAddrEl; +let dhcpv6PrefLftEl; +let dhcpv6ValidLftEl; +let dhcpv6Dns1El; +let dhcpv6Dns2El; +let dhcpv6AftrEl; + +let dhcpv6DuidEl; +let dhcpv6SubmitEl; +let dhcpv6StatusEl; + async function refreshConnectionStatus() { const connectionStatus = await invoke("connection_status", {}); @@ -104,13 +121,77 @@ async function changeCredentials() { } } -window.addEventListener("DOMContentLoaded", () => { - refreshConnectionStatus(); +async function refreshDhcpv6Status() { + const dhcpv6Status = await invoke("dhcpv6_status", {}); + + dhcpv6TimestampEl.innerText = dhcpv6Status.timestamp; + dhcpv6SrvAddrEl.innerText = dhcpv6Status.srvaddr; + dhcpv6SrvIdEl.innerText = dhcpv6Status.srvid; + dhcpv6T1El.innerText = dhcpv6Status.t1; + dhcpv6T2El.innerText = dhcpv6Status.t2; + dhcpv6PrefixEl.innerText = dhcpv6Status.prefix; + dhcpv6WanAddrEl.innerText = dhcpv6Status.wanaddr; + dhcpv6PrefLftEl.innerText = dhcpv6Status.preflft; + dhcpv6ValidLftEl.innerText = dhcpv6Status.validlft; + dhcpv6Dns1El.innerText = dhcpv6Status.dns1; + dhcpv6Dns2El.innerText = dhcpv6Status.dns2; + dhcpv6AftrEl.innerText = dhcpv6Status.aftr; +} + +async function loadDuid() { + dhcpv6StatusEl.innerText = "Lade aktuellen Client-DUID..."; + document.body.style.cursor = "progress"; + + const currentDuid = await invoke("load_duid", {}); + + dhcpv6DuidEl.value = currentDuid.duid; + dhcpv6StatusEl.innerText = currentDuid.status_text; + document.body.style.cursor = "default"; +} + +async function changeDuid() { + dhcpv6DuidEl.disabled = true; + dhcpv6SubmitEl.disabled = true; + dhcpv6StatusEl.innerText = "Änderungsanfrage..."; + document.body.style.cursor = "progress"; + + const statusText = await invoke("change_duid", { duid: dhcpv6DuidEl.value }); + + dhcpv6DuidEl.disabled = false; + dhcpv6SubmitEl.disabled = false; + dhcpv6StatusEl.innerText = statusText; + document.body.style.cursor = "default"; + + if (statusText === "Änderung erfolgreich") { + const apply = await ask("Zum Übernehmen des neuen Client-DUID muss der DHCPv6-Client neu gestartet werden. Dies dauert ca. 30 Sekunden, sollte die Internetverbindung aber nicht unterbrechen. Dabei wird eine Verlängerung mit Serversuche durchgeführt. Möchten Sie den DHCPv6-Client jetzt neu starten?", { + kind: "info", + title: "DHCPv6-Client-Neustart erforderlich", + }); + + if (apply) { + await killDhcpv6(); + } + } +} + +async function killDhcpv6() { + const error = await invoke("kill", { process: "rsdsl_dhcp6", signal: "term" }); + if (error !== "") { + await message("Befehl konnte nicht erteilt werden: " + error, { + kind: "error", + title: "DHCPv6-Client-Neustart nicht erfolgt", + }); + } +} + +window.addEventListener("DOMContentLoaded", () => { connectionStatusEl = document.querySelector("#connection-status"); connectionIpv4El = document.querySelector("#connection-ipv4"); connectionIpv6El = document.querySelector("#connection-ipv6"); + refreshConnectionStatus(); + document.querySelector("#connection-warm-reconnect").addEventListener("click", (e) => { e.preventDefault(); warmReconnect(); @@ -140,6 +221,38 @@ window.addEventListener("DOMContentLoaded", () => { }); loadCredentials(); + + dhcpv6TimestampEl = document.querySelector("#dhcpv6-timestamp"); + dhcpv6SrvAddrEl = document.querySelector("#dhcpv6-srvaddr"); + dhcpv6SrvIdEl = document.querySelector("#dhcpv6-srvid"); + dhcpv6T1El = document.querySelector("#dhcpv6-t1"); + dhcpv6T2El = document.querySelector("#dhcpv6-t2"); + dhcpv6PrefixEl = document.querySelector("#dhcpv6-prefix"); + dhcpv6WanAddrEl = document.querySelector("#dhcpv6-wanaddr"); + dhcpv6PrefLftEl = document.querySelector("#dhcpv6-preflft"); + dhcpv6ValidLftEl = document.querySelector("#dhcpv6-validlft"); + dhcpv6Dns1El = document.querySelector("#dhcpv6-dns1"); + dhcpv6Dns2El = document.querySelector("#dhcpv6-dns2"); + dhcpv6AftrEl = document.querySelector("#dhcpv6-aftr"); + + refreshDhcpv6Status(); + + document.querySelector("#dhcpv6-kill").addEventListener("click", (e) => { + e.preventDefault(); + killDhcpv6(); + }); + + dhcpv6DuidEl = document.querySelector("#dhcpv6-duid"); + dhcpv6SubmitEl = document.querySelector("#dhcpv6-submit"); + dhcpv6StatusEl = document.querySelector("#dhcpv6-status"); + + document.querySelector("#dhcpv6-form").addEventListener("submit", (e) => { + e.preventDefault(); + changeDuid(); + }); + + loadDuid(); }); setInterval(refreshConnectionStatus, 3000); +setInterval(refreshDhcpv6Status, 3000); |