aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHimbeer <himbeer@disroot.org>2024-08-06 16:52:24 +0200
committerHimbeer <himbeer@disroot.org>2024-08-06 16:52:24 +0200
commit7f87ea7bbbae04c9bde3f0ea5b0bb3d14900e993 (patch)
tree045d121b068892ca277258547fd27751fa81b5f5
parent8a610131b32b44e9f4acc99e46a4f453a6c00e10 (diff)
Add LAN information and settings
-rw-r--r--src-tauri/src/main.rs329
-rw-r--r--src/lan.html400
-rw-r--r--src/lan.js202
3 files changed, 927 insertions, 4 deletions
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index b555123..c10f666 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -162,7 +162,7 @@ impl From<Dhcpv6Lease> for Dhcpv6Status {
{
String::from("ff02::1:2 (Alle DHCPv6-Server, da der Server keine spezifische Adresse angegeben hat)")
} else {
- format!("{}", lease.server)
+ lease.server.to_string()
},
srvid: hex::encode(lease.server_id),
t1: if lease.t1 == 0 {
@@ -231,8 +231,8 @@ impl From<Dhcpv6Lease> for Dhcpv6Status {
lease.validlft, remaining_secs
)
},
- dns1: format!("{}", lease.dns1),
- dns2: format!("{}", lease.dns2),
+ dns1: lease.dns1.to_string(),
+ dns2: lease.dns2.to_string(),
aftr: match lease.aftr {
Some(aftr) => format!("🟱 Aktiviert | Tunnel-Endpunkt (AFTR): {}", aftr),
None => String::from("âšȘ Deaktiviert"),
@@ -270,6 +270,70 @@ impl Dhcpv6Lease {
}
}
+#[derive(Debug, Deserialize)]
+struct Dhcpv4Lease {
+ address: Ipv4Addr,
+ expires: SystemTime,
+ client_id: Vec<u8>,
+ hostname: Option<String>,
+}
+
+#[derive(Debug, Serialize)]
+struct LeaseRow {
+ addr: String,
+ client_id: String,
+ hostname: String,
+ expires: String,
+}
+
+#[derive(Debug, Serialize)]
+struct Leases {
+ clients: Vec<LeaseRow>,
+ status_text: String,
+}
+
+#[derive(Debug, Serialize)]
+struct Domain {
+ domain: String,
+ status_text: String,
+}
+
+impl FromIterator<Dhcpv4Lease> for Leases {
+ fn from_iter<T>(iter: T) -> Self
+ where
+ T: IntoIterator<Item = Dhcpv4Lease>,
+ {
+ Self {
+ clients: iter
+ .into_iter()
+ .map(|lease| {
+ let remaining_secs = std::cmp::max(
+ (lease
+ .expires
+ .duration_since(SystemTime::now())
+ .unwrap_or(Duration::ZERO))
+ .as_secs(),
+ 0,
+ );
+
+ LeaseRow {
+ addr: lease.address.to_string(),
+ client_id: hex::encode(lease.client_id),
+ hostname: lease.hostname.unwrap_or_default(),
+ expires: format!(
+ "{} ({} Sekunden verbleibend)",
+ DateTime::<Local>::from(lease.expires)
+ .format("%d.%m.%Y %H:%M:%S UTC%Z"),
+ remaining_secs
+ ),
+ }
+ })
+ .collect(),
+ status_text: String::new(),
+ }
+ }
+}
+
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
async fn connect(
@@ -772,6 +836,259 @@ fn handle_change_duid_response(response: Response) -> String {
}
}
+#[tauri::command]
+async fn leases(subnet: String, state: State<'_, Mutex<Session>>) -> Result<Leases, ()> {
+ let (client, instance) = {
+ let state = state.lock().unwrap();
+ (state.client.clone(), state.instance.clone())
+ };
+ let instance = match instance {
+ Some(instance) => instance,
+ None => {
+ return Ok(Leases {
+ clients: Vec::new(),
+ status_text: String::from("Keine Instanz ausgewÀhlt, bitte melden Sie sich neu an"),
+ })
+ }
+ };
+
+ let leases_path = format!(
+ "/data/dhcp4d.leases_eth0{}",
+ match subnet.as_str() {
+ "management" => "",
+ "trusted" => ".10",
+ "untrusted" => ".20",
+ "isolated" => ".30",
+ "exposed" => ".40",
+ subnet =>
+ return Ok(Leases {
+ clients: Vec::new(),
+ status_text: format!(
+ r#"Anwendungsinterner Fehler: UngĂŒltiges Subnetz ("management", "trusted", "untrusted", "isolated" oder "exposed" erwartet, "{}" erhalten)"#,
+ subnet
+ )
+ }),
+ }
+ );
+
+ let response = client
+ .get(instance.url.join("/data/read").unwrap())
+ .query(&[("path", leases_path)])
+ .basic_auth("rustkrazy", Some(&instance.password))
+ .send();
+
+ Ok(match response.await {
+ Ok(response) => handle_leases_response(response).await,
+ Err(e) => Leases {
+ clients: Vec::new(),
+ status_text: format!("Abfrage fehlgeschlagen: {}", e),
+ },
+ })
+}
+
+async fn handle_leases_response(response: Response) -> Leases {
+ let status = response.status();
+ if status.is_success() {
+ match response.json::<Vec<Dhcpv4Lease>>().await {
+ Ok(leases) => Leases::from_iter(leases),
+ Err(e) => Leases {
+ clients: Vec::new(),
+ status_text: format!("Fehlerhafte Leasedatei. Fehler: {}", e),
+ },
+ }
+ } else if status == StatusCode::UNAUTHORIZED {
+ Leases {
+ clients: Vec::new(),
+ status_text: String::from(
+ "UngĂŒltiges Verwaltungspasswort, bitte melden Sie sich neu an!",
+ ),
+ }
+ } else if status == StatusCode::NOT_FOUND {
+ Leases {
+ clients: Vec::new(),
+ status_text: String::from(
+ "Noch keine Leasedatei vorhanden (erster Systemstart oder Stromausfall?)",
+ ),
+ }
+ } else if status.is_client_error() {
+ Leases {
+ clients: Vec::new(),
+ status_text: format!("Clientseitiger Fehler: {}", status),
+ }
+ } else if status.is_server_error() {
+ Leases {
+ clients: Vec::new(),
+ status_text: format!("Serverseitiger Fehler: {}", status),
+ }
+ } else {
+ Leases {
+ clients: Vec::new(),
+ status_text: format!("Unerwarteter Statuscode: {}", status),
+ }
+ }
+}
+
+#[tauri::command]
+async fn load_domain(state: State<'_, Mutex<Session>>) -> Result<Domain, ()> {
+ let (client, instance) = {
+ let state = state.lock().unwrap();
+ (state.client.clone(), state.instance.clone())
+ };
+ let instance = match instance {
+ Some(instance) => instance,
+ None => {
+ return Ok(Domain {
+ domain: 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/dnsd.domain")])
+ .basic_auth("rustkrazy", Some(&instance.password))
+ .send();
+
+ Ok(match response.await {
+ Ok(response) => handle_load_domain_response(response).await,
+ Err(e) => Domain {
+ domain: String::new(),
+ status_text: format!("Abfrage fehlgeschlagen: {}", e),
+ },
+ })
+}
+
+async fn handle_load_domain_response(response: Response) -> Domain {
+ let status = response.status();
+ if status.is_success() {
+ match response.text().await {
+ Ok(domain) => Domain {
+ domain,
+ status_text: String::new(),
+ },
+ Err(e) => Domain {
+ domain: String::new(),
+ status_text: format!("Keinen Text vom Server erhalten. Fehler: {}", e),
+ },
+ }
+ } else if status == StatusCode::UNAUTHORIZED {
+ Domain {
+ domain: String::new(),
+ status_text: format!("UngĂŒltiges Verwaltungspasswort, bitte melden Sie sich neu an!"),
+ }
+ } else if status == StatusCode::NOT_FOUND {
+ Domain {
+ domain: String::new(),
+ status_text: format!("Keine lokale Domain eingestellt"),
+ }
+ } else if status.is_client_error() {
+ Domain {
+ domain: String::new(),
+ status_text: format!("Clientseitiger Fehler: {}", status),
+ }
+ } else if status.is_server_error() {
+ Domain {
+ domain: String::new(),
+ status_text: format!("Serverseitiger Fehler: {}", status),
+ }
+ } else {
+ Domain {
+ domain: String::new(),
+ status_text: format!("Unerwarteter Statuscode: {}", status),
+ }
+ }
+}
+
+#[tauri::command]
+async fn change_domain(domain: 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
+ .post(instance.url.join("/data/write").unwrap())
+ .query(&[("path", "/data/dnsd.domain")])
+ .basic_auth("rustkrazy", Some(&instance.password))
+ .body(domain)
+ .send();
+
+ Ok(match response.await {
+ Ok(response) => handle_change_domain_response(response),
+ Err(e) => format!("Änderung fehlgeschlagen: {}", e),
+ })
+}
+
+fn handle_change_domain_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)
+ }
+}
+
+#[tauri::command]
+async fn delete(file_path: 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
+ .post(instance.url.join("/data/remove").unwrap())
+ .query(&[("path", file_path)])
+ .basic_auth("rustkrazy", Some(&instance.password))
+ .send();
+
+ Ok(match response.await {
+ Ok(response) => handle_delete_response(response),
+ Err(e) => format!("Löschung fehlgeschlagen: {}", e),
+ })
+}
+
+fn handle_delete_response(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 == StatusCode::NOT_FOUND {
+ String::from("Bereits gelöscht (keine unnötige Änderung vorgenommen)")
+ } 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 {
@@ -790,7 +1107,11 @@ fn main() {
connection_status,
dhcpv6_status,
load_duid,
- change_duid
+ change_duid,
+ leases,
+ load_domain,
+ change_domain,
+ delete
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
diff --git a/src/lan.html b/src/lan.html
new file mode 100644
index 0000000..c179718
--- /dev/null
+++ b/src/lan.html
@@ -0,0 +1,400 @@
+<!doctype html>
+<html lang="de">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="stylesheet" href="styles.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>LAN - RSDSL Verwaltungswerkzeug</title>
+ <script type="module" src="/dashboard.js" defer></script>
+ <script type="module" src="/lan.js" defer></script>
+ </head>
+
+ <body>
+ <div class="container">
+ <h1>LAN</h1>
+
+ <div class="row">
+ <form id="dashboard-form">
+ <button id="dashboard-submit" type="submit">↩ ZurĂŒck zur Übersicht</button>
+ </form>
+
+ <form id="wan-open-form">
+ <button id="wan-open-submit" type="submit">Einwahl und Zugansdaten</button>
+ </form>
+
+ <form id="lan-open-form">
+ <button id="lan-open-submit" type="submit">LAN</button>
+ </form>
+
+ <form id="ddns-open-form">
+ <button id="ddns-open-submit" type="submit">Dynamisches DNS (INWX)</button>
+ </form>
+
+ <form id="log-open-form">
+ <button id="log-open-submit" type="submit">Diagnoseprotokolle</button>
+ </form>
+
+ <form id="sys-open-form">
+ <button id="sys-open-submit" type="submit">Systemeinstellungen</button>
+ </form>
+
+ <form id="disconnect-form">
+ <button id="disconnect-submit" type="submit">đŸšȘ Abmelden</button>
+ </form>
+ </div>
+
+ <br />
+
+ <form id="vlan-form">
+ <fieldset>
+ <legend>Virtuelle Netzwerke</legend>
+
+ <p>Zur Erhöhung der Sicherheit ist dieses Netzwerk in mehrere
+ virtuelle Subnetze unterteilt. Bei richtiger Verteilung neuer GerÀte
+ auf diese Netzwerke wird die Sicherheit der Kernnetze erheblich
+ gesteigert. Die folgende Tabelle listet alle Subnetze auf und
+ beschreibt ihre technischen Informationen und Zwecke.</p>
+
+ <table>
+ <th>
+ <td>WLAN-Name (SSID)</td>
+ <td>VLAN ID</td>
+ <td>Subnetznummer (IPv4)</td>
+ <td>Subnetznummer (IPv6, hexadezimal)</td>
+ <td>Beschreibung</td>
+ </th>
+
+ <tr>
+ <td>Management</td>
+ <td>- (nur kabelgebunden)</td>
+ <td>Kein VLAN</td>
+ <td>0</td>
+ <td>1</td>
+ <td>Verwaltungsnetz fĂŒr Access Points, Switch und Telefonadapter.
+ Voller Internet- und Verwaltungszugriff. Nur fĂŒr genannte GerĂ€te
+ oder vorĂŒbergehende Diagnosearbeiten bestimmt. Über dieses
+ Netzwerk tauschen sich Access Points und Switch mit dem
+ Konfigurationsserver im lokalen Netz aus.</td>
+ </tr>
+
+ <tr>
+ <td>Trusted</td>
+ <td>WLAN-Kabel</td>
+ <td>10</td>
+ <td>10</td>
+ <td>2</td>
+ <td>Kernnetz fĂŒr vertrauenswĂŒrdige GerĂ€te. Voller Internet- und
+ Verwaltungszugriff. Nur fĂŒr Computer, Tablets und Smartphones von
+ Familienmitgliedern.</td>
+ </tr>
+
+ <tr>
+ <td>Untrusted</td>
+ <td>Internet</td>
+ <td>20</td>
+ <td>20</td>
+ <td>3</td>
+ <td>Netz fĂŒr GĂ€ste und internetgebundene IoT-GerĂ€te. Nur fĂŒr
+ solche GerĂ€te bestimmt, die eine Internetverbindung fĂŒr ihre
+ KernfunktionalitÀt zwingend benötigen (ausgenommen Computer,
+ Tablets und Smartphones von GĂ€sten). Keine Kommunikation zwischen
+ diesem Netzwerk und dem Kernnetz möglich, aber voller
+ Internetzugriff. Kein Verwaltungszugriff auf wichtige
+ NetzwerkgerÀte, auch nicht mit korrekten Zugangsdaten.</td>
+ </tr>
+
+ <tr>
+ <td>Isolated</td>
+ <td>Schwefelhexafluorid</td>
+ <td>30</td>
+ <td>30</td>
+ <td>4</td>
+ <td>Abgeschottetes Netz fĂŒr IoT-GerĂ€te ohne Internetzwang (z.B.
+ Drucker). Keine Kommunikation zur Außenwelt möglich (auch nicht
+ zu anderen lokalen Netzen). Kein Verwaltungszugriff auf wichtige
+ NetzwerkgerÀte. GerÀte in diesem Netz erhalten IP-Adressen nach
+ dem gleichen Muster wie in den anderen Netzwerken, können aber
+ selbst mit den öffentlichen IPv6-Adressen die Firewall nicht
+ passieren. Verbindungen der GerÀte untereinander sind erlaubt. Um
+ mit hier untergebrachten GerÀten zu kommunizieren, muss der
+ eigene Rechner vorĂŒbergehend selbst mit diesem Netzwerk verbunden
+ werden.</td>
+ </tr>
+
+ <tr>
+ <td>Exposed</td>
+ <td>Neuland</td>
+ <td>40</td>
+ <td>40</td>
+ <td>5</td>
+ <td>Servernetz. Keine Kommunikation zu GerÀtenetzen möglich, aber
+ GerĂ€tenetze dĂŒrfen Verbindungen zu Servern in diesem Netzwerk
+ aufbauen. Voller Internetzugriff, aber kein Verwaltungszugriff
+ auf wichtige NetzwerkgerÀte. EnthÀlt den Hauptserver und
+ Telefonadapter. Die Router-Firewall schĂŒtzt GerĂ€te in diesem
+ Netzwerk nicht und lÀsst Verbindungsanfragen aus dem öffentlichen
+ Internet zu. Aufgrund von NAT ist nur Telefonie ĂŒber IPv4
+ freigegeben, alle anderen Dienste sind trotz Firewallausnahme nur
+ ĂŒber IPv6 erreichbar. Gleiches gilt fĂŒr temporĂ€re Server auf
+ gewöhnlichen Computern, die vorĂŒbergehend mit diesem Netzwerk
+ verbunden werden. Da die öffentliche IPv4-Adresse spÀtestens bei
+ einem Anbieterwechsel mit großer Wahrscheinlichkeit verschwinden
+ wird, sind bereits jetzt nur IPv6-Freigaben möglich. Dieses
+ Netzwerk wird von Servern verwendet, um bei Kompromittierung die
+ anderen GerÀte nicht zu gefÀhrden. Es kann aber auch von
+ gewöhnlichen Rechnern genutzt werden, wenn die Freigabe einer
+ Software fĂŒr das öffentliche Internet nötig ist.</td>
+ </tr>
+ </table>
+
+ <p>Information: Die privaten IPv4-Adressen folgen der Form
+ 10.128.<i>IPv4-Subnetznummer</i>.X, wobei X eine gerÀtespezifische
+ Zahl zwischen 1 und (einschließlich) 254 ist. Die öffentlichen
+ IPv6-Adressen werden zugewiesen, indem die IPv6-Subnetznummer an das
+ vom Internetanbieter zugewiesene PrÀfix angehÀngt wird (optimal ist
+ eine PrĂ€fixlĂ€nge von /56, die kleinste unterstĂŒtzte PrĂ€fixlĂ€nge ist
+ /61) und den GerĂ€ten die Wahl der zweiten AdresshĂ€lfte ĂŒberlassen
+ wird. Der Router wÀhlt stets ::1 als eigene zweite AdresshÀlfte. Die
+ verbindungslokalen IPv6-Adressen werden von den einzelnen GerÀten
+ selbst verwaltet, der Router nutzt in jedem Netzwerk (außer der
+ Internetverbindung nach außen) fe80::1.</p>
+ </fieldset>
+ </form>
+
+ <br />
+
+ <form id="dhcpv4-form">
+ <fieldset>
+ <legend>DHCPv4-Server</legend>
+
+ <span class="row" id="dhcpv4-starthostid">Startadresse der dynamischen Vergabe (nur letztes Oktett): 100</span>
+ <span class="row" id="dhcpv4-endhostid"></span>Endadresse (inkl.) der dynamischen Vergabe (nur letztes Oktett): 239</span>
+ <span class="row" id="dhcpv4-self">Routeradresse (nur letztes Oktett): 254</span>
+ <span class="row" id="dhcpv4-netmask">Subnetzmaske: 255.255.255.0 (CIDR: /24)</span>
+ <span class="row" id="dhcpv4-leasetime">GĂŒltigkeitsdauer dynamisch vergebener Adressen: 12 Stunden (43200 Sekunden)</span>
+
+ <p>Information: Wichtige NetzwerkgerÀte sind statisch mit Adressen
+ mit letztem Oktett 240 und aufwÀrts konfiguriert. Normale GerÀte, die
+ eine statische bzw. manuelle Adressvergabe benötigen, sollten
+ Adressen mit letztem Oktett echt kleiner als 100 verwenden.</p>
+
+ <p>Information: Dynamisch vergebene Adressen bleiben in der Regel
+ auch nach Ablauf der GĂŒltigkeitsdauer konstant, d.h. beim nĂ€chsten
+ Verbindungsaufbau des entsprechenden GerÀtes wird wieder die gleiche
+ Adresse zugewiesen. Hierzu wird ein Verfahren verwendet, welches die
+ IP-Adresse aus der Hardwareadresse berechnet. Ausnahmen können
+ entstehen, wenn der Zuweisungsalgorithmus eine Kollision erkennt,
+ also wenn mehrere GerÀte die gleiche IP-Adresse zugewiesen bekommen
+ wĂŒrden. In derartigen FĂ€llen wird eine alternative Adresse gesucht
+ und zugewiesen. Werden nun beide GerĂ€te fĂŒr die restliche
+ GĂŒltigkeitsdauer vom Netzwerk getrennt und danach in umgekehrter
+ Reihenfolge wieder verbunden, werden ihre IP-Adressen vertauscht.
+ Dies hat keine Auswirkung auf die Internetverbindung und bedeutet
+ lediglich, dass nicht garantiert ist, dass der DHCP-Server jedem
+ GerÀt stets die gleiche Adresse zuweist (Standardverhalten aller
+ gĂ€ngigen Implementationen). Werden feste Adressen benötigt, so mĂŒssen
+ diese direkt auf den jeweiligen GerÀten eingetragen und DHCP in deren
+ Einstellungen deaktiviert werden.</p>
+
+ <fieldset>
+ <legend>Wichtige, statisch konfigurierte NetzwerkgerÀte (nicht per DHCP verwaltet)</legend>
+
+ <span class="row">192.168.1.1/24: DSL-Modem</span>
+ <span class="row">10.128.0.248/24: Switch</span>
+ <span class="row">10.128.0.249/24: Access Point im Dachgeschoss</span>
+ <span class="row">10.128.0.250/24: Access Point im Obergeschoss</span>
+ <span class="row">10.128.0.251/24: Access Point im Erdgeschoss</span>
+ <span class="row">10.128.0.253/24 und 10.128.40.253/24: Hauptserver</span>
+ <span class="row">10.128.40.252/24: Telefonadapter</span>
+ </fieldset>
+
+ <fieldset>
+ <legend>Dynamisch vergebene Adressen</legend>
+
+ <h4>Management</h4>
+ <table id="dhcpv4-clients-management">
+ <th>
+ <td>Client-ID (hexadezimal, ohne Doppelpunkttrennung)</td>
+ <td>Rechnername</td>
+ <td>Ablaufzeitpunkt</td>
+ </th>
+ </table>
+
+ <div class="row">
+ <output id="dhcpv4-status-management">Warte auf Initialisierung...</output>
+ </div>
+
+ <h4>Trusted (WLAN-Kabel)</h4>
+ <table id="dhcpv4-clients-trusted">
+ <th>
+ <td>Client-ID (hexadezimal, ohne Doppelpunkttrennung)</td>
+ <td>Rechnername</td>
+ <td>Ablaufzeitpunkt</td>
+ </th>
+ </table>
+
+ <div class="row">
+ <output id="dhcpv4-status-trusted">Warte auf Initialisierung...</output>
+ </div>
+
+ <h4>Untrusted (Internet)</h4>
+ <table id="dhcpv4-clients-untrusted">
+ <th>
+ <td>Client-ID (hexadezimal, ohne Doppelpunkttrennung)</td>
+ <td>Rechnername</td>
+ <td>Ablaufzeitpunkt</td>
+ </th>
+ </table>
+
+ <div class="row">
+ <output id="dhcpv4-status-untrusted">Warte auf Initialisierung...</output>
+ </div>
+
+ <h4>Isolated (Schwefelhexafluorid)</h4>
+ <table id="dhcpv4-clients-isolated">
+ <th>
+ <td>Client-ID (hexadezimal, ohne Doppelpunkttrennung)</td>
+ <td>Rechnername</td>
+ <td>Ablaufzeitpunkt</td>
+ </th>
+ </table>
+
+ <div class="row">
+ <output id="dhcpv4-status-isolated">Warte auf Initialisierung...</output>
+ </div>
+
+ <h4>Exposed (Neuland)</h4>
+ <table id="dhcpv4-clients-exposed">
+ <th>
+ <td>Client-ID (hexadezimal, ohne Doppelpunkttrennung)</td>
+ <td>Rechnername</td>
+ <td>Ablaufzeitpunkt</td>
+ </th>
+ </table>
+
+ <div class="row">
+ <output id="dhcpv4-status-exposed">Warte auf Initialisierung...</output>
+ </div>
+
+ <br />
+
+ <p>Information: Abgelaufene Leases werden erst bei der nÀchsten
+ Anfrage an den Server aus der Liste entfernt. Dies kann Minuten
+ oder Wochen dauern.</p>
+
+ <p>Information: Die Client-ID wird nicht von allen GerÀten
+ mitgesendet. Insbesondere IoT-GerÀte teilen dem Server oft keinen
+ eigenen Wert mit. In diesen FĂ€llen erzeugt der Server automatisch
+ eine Client-ID nach dem Muster 01:HARDWAREADRESSE.</p>
+
+ <p>Information: Der Rechnername wird nicht von allen GerÀten
+ mitgesendet. Insbesondere iOS-GerÀte verschleiern ihn oft. Das
+ Deaktivieren der Option "Private WLAN-Adresse verwenden" in den
+ Einstellungen der WLAN-Verbindung (Info-I-Symbol) kann Abhilfe
+ schaffen. Unbekannte Rechnernamen beeintrÀchtigen die
+ Internetverbindung nicht, erschweren aber die Identifikation der
+ GerÀte (Client-ID und Diagnoseprotokoll des DNS-Forwarders können
+ sie weiterhin deanonymisieren).</p>
+ </fieldset>
+ </fieldset>
+ </form>
+
+ <br />
+
+ <form id="slaac-form">
+ <fieldset>
+ <legend>SLAAC-Server</legend>
+
+ <span class="row" id="slaac-flags">Flags: On-Link, Autonomous (A)</span>
+ <span class="row" id="slaac-ralft">Standardgateway-GĂŒltigkeitsdauer: 1800 Sekunden</span>
+ <span class="row" id="slaac-preflft">PrÀfix-Verwendungsdauer: 1500 Sekunden</span>
+ <span class="row" id="slaac-validlft">PrĂ€fix-GĂŒltigkeitsdauer: 1800 Sekunden</span>
+ <span class="row" id="slaac-dnslft">DNS-Server-GĂŒltigkeitsdauer: 1800 Sekunden</span>
+
+ <p>Information: IPv6-Adressen werden per SLAAC zugewiesen, DHCPv6 im
+ Heimnetz wird nicht unterstĂŒtzt. Das bedeutet, dass der Router auf
+ jedem Netzwerk periodisch sowie auf Anfrage beitretender GerÀte das
+ jeweilige öffentliche PrÀfix und weitere Parameter verbreitet (ULA
+ bzw. eindeutige lokale IPv6-Adressen werden nicht unterstĂŒtzt). Die
+ GerÀte wÀhlen die zweite AdresshÀlfte selbst und stellen sicher, dass
+ diese nicht mit existierenden Adressen anderer Rechner kollidieren.
+ Daher ist dem Router nicht bekannt, welche Adressen existieren,
+ weshalb sie hier nicht aufgelistet werden können. Scans mit anderen
+ Netzwerk-Dienstprogrammen zusammen mit den Diagnoseprotokollen können
+ dieses Problem lösen. Die beste Methode wÀre ein DHCPv6-Server, der
+ aber aufgrund des höheren Aufwands und der fehlenden KompatibilitÀt
+ mit Android-GerÀten nicht vorhanden ist. Zudem können GerÀte
+ beliebig viele Adressen beanspruchen, was aufgrund der GrĂ¶ĂŸe des
+ Adressraums (2⁶⁎) nicht problematisch, sondern insbesondere auf
+ Servern sogar hilfreich ist. Die PrĂ€fixableitung fĂŒr die
+ verschiedenen Subnetze ist unter dem Punkt "Virtuelle Netzwerke"
+ beschrieben.</p>
+
+ <p>Statische Adressen bzw. Suffixe können auf den GerÀten selbst
+ eingestellt werden. Auch andere Parameter können dort ĂŒberschrieben
+ werden. PrĂ€fixbezogenes dynamisches DNS ist unter dem MenĂŒpunkt
+ "Dynamisches DNS (INWX)" konfigurierbar.</p>
+ </fieldset>
+ </form>
+
+ <br />
+
+ <form id="dns-form">
+ <fieldset>
+ <legend>DNS</legend>
+
+ <span class="row" id="dns-primary-us">PrimÀrer DNS-Server: 2620:fe::fe (Quad9)</span>
+ <span class="row" id="dns-secondary-us">SekundÀrer DNS-Server: 9.9.9.9 (Quad9)</span>
+ <span class="row" id="dns-primary-ds">Lokaler DNS-Server (IPv4): 10.128.SUBNETZ.254</span>
+ <span class="row" id="dns-secondary-ds">Lokaler DNS-Server (IPv6): fe80::1</span>
+
+ <br />
+
+ <div class="row">
+ <button id="dns-kill">🔄 Forwarder-Neustart</button>
+ </div>
+
+ <p>Information: Anfragen lokaler GerÀte werden an die oberen beiden
+ DNS-Server weitergeleitet (zuerst an den primÀren Server, bei Fehlern
+ nach max. 3 Sekunden an den sekundÀren Server). Auf lokalen GerÀten
+ wird automatisch dieser Router als DNS-Server eingestellt. Sollte die
+ manuelle Konfiguration eines GerÀts nötig sein, nutzen Sie die
+ unteren beiden Werte. FĂŒr IPv4 ist SUBNETZ je nach Netzwerkname mit
+ folgender Zahl zu ersetzen: 10 fĂŒr "WLAN-Kabel", 20 fĂŒr "Internet",
+ 30 fĂŒr "Schwefelhexafluorid", 40 fĂŒr "Neuland" und 0 fĂŒr direkte
+ Kabelverbindungen ohne VLAN (s. "Virtuelle Netzwerke"). Ein Neustart
+ des DNS-Forwarders dauert ca. 30 Sekunden. WÀhrend dieser Zeit können
+ abgehende Verbindungen ins Internet fehlschlagen und bestehende
+ Verbindungen unterbrochen werden. Möglicherweise muss danach der
+ Telefonadapter ebenfalls neu gestartet werden, um den Telefoniedienst
+ wiederherzustellen (falls beeintrÀchtigt). Im Normalfall sind
+ derartige Neustarts nicht nötig, mit Ausnahme von Änderungen an der
+ lokalen Domain.</p>
+
+ <label for="dns-domain" form="dns-form">Lokale Domain:</label>
+ <div class="row">
+ <input id="dns-domain" placeholder="z.B. local, lan, home etc." />
+ </div>
+
+ <br />
+
+ <div class="row">
+ <button id="dns-unset">Domain entfernen</button>
+ <button id="dns-submit" type="submit">Domain Àndern</button>
+ </div>
+
+ <p id="dns-status">Warte auf Initialisierung...</p>
+
+ <p>Information: Die lokale Domain ist ein optionales, empfohlenes
+ Suffix, unter dem GerÀte in den lokalen Netzwerken erreichbar sind.
+ Beispielsweise wÀre ein GerÀt mit dem Namen "Arbeitsrechner" bei der
+ lokalen Domain "example.com" sowohl unter "Arbeitsrechner" als auch
+ unter "Arbeitsrechner.example.com" erreichbar. Diese Erreichbarkeit
+ gilt nicht fĂŒr Zugriffe aus dem öffentlichen Internet. Die lokale
+ Domain sollte fĂŒr dieses Netzwerk nur verĂ€ndert werden, wenn dies
+ wirklich nötig ist.</p>
+ </fieldset>
+ </form>
+ </div>
+ </body>
+</html>
diff --git a/src/lan.js b/src/lan.js
new file mode 100644
index 0000000..e0c99e7
--- /dev/null
+++ b/src/lan.js
@@ -0,0 +1,202 @@
+const { invoke } = window.__TAURI__.tauri;
+const { ask, message } = window.__TAURI__.dialog;
+
+let dnsDomainEl;
+let dnsUnsetEl;
+let dnsSubmitEl;
+let dnsStatusEl;
+
+function dashboard() {
+ window.location = "dashboard.html";
+}
+
+function wanOpen() {
+ window.location = "wan.html";
+}
+
+function lanOpen() {
+ window.location = "lan.html";
+}
+
+function ddnsOpen() {
+ window.location = "ddns.html";
+}
+
+function logOpen() {
+ window.location = "log.html";
+}
+
+function sysOpen() {
+ window.location = "sys.html";
+}
+
+async function disconnect() {
+ // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
+ await invoke("disconnect", {});
+ window.location = "index.html";
+}
+
+async function loadAllLeases() {
+ for (let subnet of ["management", "trusted", "untrusted", "isolated", "exposed"]) {
+ loadLeases(subnet);
+ }
+}
+
+async function loadLeases(subnet) {
+ let statusEl = document.querySelector("#dhcpv4-status-" + subnet);
+ let tableEl = document.querySelector("#dhcpv4-clients-" + subnet);
+
+ const leases = await invoke("leases", { subnet: subnet });
+
+ statusEl.innerText = leases.status_text;
+
+ let first = true;
+ for (let child of tableEl.querySelectorAll("tr")) {
+ if (first) {
+ first = false;
+ continue;
+ }
+
+ tableEl.removeChild(child);
+ child.remove();
+ }
+
+ for (let lease of leases.clients) {
+ let addr = document.createElement("td");
+ addr.innerText = lease.addr;
+
+ let clientId = document.createElement("td");
+ clientId.innerText = lease.client_id;
+
+ let hostname = document.createElement("td");
+ hostname.innerText = lease.hostname;
+
+ let expires = document.createElement("td");
+ expires.innerText = lease.expires;
+
+ let row = document.createElement("tr");
+ row.appendChild(addr);
+ row.appendChild(clientId);
+ row.appendChild(hostname);
+ row.appendChild(expires);
+
+ tableEl.appendChild(row);
+ }
+}
+
+async function killDns() {
+ const error = await invoke("kill", { process: "rsdsl_dnsd", signal: "term" });
+
+ if (error !== "") {
+ await message("Befehl konnte nicht erteilt werden: " + error, {
+ kind: "error",
+ title: "DNS-Forwarder-Neustart nicht erfolgt",
+ });
+ }
+}
+
+async function unsetDomain() {
+ dnsDomainEl.disabled = true;
+ dnsUnsetEl.disabled = true;
+ dnsSubmitEl.disabled = true;
+ dnsStatusEl.innerText = "Löschanfrage...";
+ document.body.style.cursor = "progress";
+
+ dnsStatusEl.innerText = await invoke("delete", { filePath: "/data/dnsd.domain" });
+
+ dnsDomainEl.value = "";
+ dnsDomainEl.disabled = false;
+ dnsUnsetEl.disabled = false;
+ dnsSubmitEl.disabled = false;
+ document.body.style.cursor = "default";
+}
+
+async function loadDomain() {
+ dnsStatusEl.innerText = "Lade aktuelle Domain...";
+ document.body.style.cursor = "progress";
+
+ const currentDomain = await invoke("load_domain", {});
+
+ dnsDomainEl.value = currentDomain.domain;
+ dnsStatusEl.innerText = currentDomain.status_text;
+ document.body.style.cursor = "default";
+}
+
+async function changeDomain() {
+ dnsDomainEl.disabled = true;
+ dnsUnsetEl.disabled = true;
+ dnsSubmitEl.disabled = true;
+ dnsStatusEl.innerText = "Änderungsanfrage...";
+ document.body.style.cursor = "progress";
+
+ dnsStatusEl.innerText = await invoke("change_domain", { domain: dnsDomainEl.value });
+
+ dnsDomainEl.disabled = false;
+ dnsUnsetEl.disabled = false;
+ dnsSubmitEl.disabled = false;
+ document.body.style.cursor = "default";
+
+ const reload = await ask("Zum Übernehmen der neuen lokalen Domain muss der DNS-Forwarder neu gestartet werden. Dies dauert ca. 30 Sekunden. WĂ€hrend dieser Zeit können abgehende Verbindungen ins Internet fehlschlagen und bestehende Verbindungen unterbrochen werden. Möglicherweis muss danach der Telefonadapter ebenfalls neu gestartet werden, um den Telefoniediesnt wiederherzustellen (falls beeintrĂ€chtigt). Möchten Sie den DNS-Forwarder jetzt neu starten?", {
+ kind: "info",
+ title: "DNS-Forwarder-Neustart erforderlich"
+ });
+
+ if (reload) {
+ await killDns();
+ }
+}
+
+window.addEventListener("DOMContentLoaded", () => {
+ document.querySelector("#dashboard-form").addEventListener("submit", (e) => {
+ e.preventDefault();
+ dashboard();
+ });
+ document.querySelector("#wan-open-form").addEventListener("submit", (e) => {
+ e.preventDefault();
+ wanOpen();
+ });
+ document.querySelector("#lan-open-form").addEventListener("submit", (e) => {
+ e.preventDefault();
+ lanOpen();
+ });
+ document.querySelector("#ddns-open-form").addEventListener("submit", (e) => {
+ e.preventDefault();
+ ddnsOpen();
+ });
+ document.querySelector("#log-open-form").addEventListener("submit", (e) => {
+ e.preventDefault();
+ logOpen();
+ });
+ document.querySelector("#sys-open-form").addEventListener("submit", (e) => {
+ e.preventDefault();
+ sysOpen();
+ });
+ document.querySelector("#disconnect-form").addEventListener("submit", (e) => {
+ e.preventDefault();
+ disconnect();
+ });
+
+ loadAllLeases();
+
+ dnsDomainEl = document.querySelector("#dns-domain");
+ dnsUnsetEl = document.querySelector("#dns-unset");
+ dnsSubmitEl = document.querySelector("#dns-submit");
+ dnsStatusEl = document.querySelector("#dns-status");
+
+ document.querySelector("#dns-kill").addEventListener("click", (e) => {
+ e.preventDefault();
+ killDns();
+ });
+ document.querySelector("#dns-unset").addEventListener("click", (e) => {
+ e.preventDefault();
+ unsetDomain();
+ });
+ document.querySelector("#dns-form").addEventListener("submit", (e) => {
+ e.preventDefault();
+ changeDomain();
+ });
+
+ loadDomain();
+});
+
+setInterval(loadAllLeases, 10000);