From 53d91d152673d437ce09b5888e2f051d12499646 Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Tue, 19 May 2026 22:02:46 +0200 Subject: [PATCH] =?UTF-8?q?Bugfixes:=20IP-Scanner,=20DHCP,=20Auftr=C3=A4ge?= =?UTF-8?q?,=20Messergebnisse=20[apk]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IP-Scanner: ConnectivityManager.getLinkProperties statt hartcodiertem /24 – erkennt jetzt das echte Subnetz inkl. Prefix-Länge und Gateway - DHCP: dhcpDiscover durch dhcpInfo ersetzt (liest WifiManager.dhcpInfo, kein Root nötig) – zeigt Server, Gateway, Lease-Zeit, DNS - Aufträge: tms-Feld ergänzt, Order by tms DESC – "zuletzt bearbeitet" zuerst; Checkbox-Logik invertiert (Standard: alle Aufträge, Haken = nur aktive) - MeasurementResult-Komponente: Arrays (WLAN-Netze, Traceroute-Hops) als echte Liste statt Komma-String; Skalare kompakt in einer Zeile - Traceroute: 5 aufeinanderfolgende Timeouts → Abbruch statt endlos warten - tools/types.ts: MeasureStatus exportiert (behebt 5 Svelte-Check-Fehler) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../netdiag/NetDiagScannerPlugin.kt | 172 ++++++------- native-plugin/NetDiagScannerPlugin.kt | 229 +++++++++++------- src/lib/components/MeasurementResult.svelte | 46 ++++ src/lib/scanner.ts | 25 +- src/lib/tools/netzwerk/dhcpcheck.ts | 34 +-- src/lib/tools/types.ts | 4 + src/lib/types.ts | 2 + src/routes/auftraege/+page.svelte | 28 ++- src/routes/protokoll/[id]/+page.svelte | 10 +- 9 files changed, 339 insertions(+), 211 deletions(-) create mode 100644 src/lib/components/MeasurementResult.svelte diff --git a/android/app/src/main/java/de/data_it_solution/netdiag/NetDiagScannerPlugin.kt b/android/app/src/main/java/de/data_it_solution/netdiag/NetDiagScannerPlugin.kt index 7a59c76..413e296 100644 --- a/android/app/src/main/java/de/data_it_solution/netdiag/NetDiagScannerPlugin.kt +++ b/android/app/src/main/java/de/data_it_solution/netdiag/NetDiagScannerPlugin.kt @@ -3,6 +3,7 @@ package de.data_it_solution.netdiag import android.Manifest import android.content.Context import android.content.Intent +import android.net.ConnectivityManager import android.net.Uri import android.net.wifi.WifiManager import android.os.Build @@ -26,9 +27,8 @@ import java.io.BufferedReader import java.io.File import java.io.FileOutputStream import java.io.FileReader -import java.net.DatagramPacket -import java.net.DatagramSocket import java.net.HttpURLConnection +import java.net.Inet4Address import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket @@ -66,15 +66,58 @@ class NetDiagScannerPlugin : Plugin() { fun getLocalSubnet(call: PluginCall) { io.launch { try { - val wifi = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - @Suppress("DEPRECATION") val dhcp = wifi.dhcpInfo - val ipInt = dhcp?.ipAddress ?: 0 - val gwInt = dhcp?.gateway ?: 0 - val ip = if (ipInt != 0) intToIp(ipInt) else firstLocalIpv4() - val gateway = if (gwInt != 0) intToIp(gwInt) else "" - val base = ip.substringBeforeLast('.', "192.168.1") + var ip = "" + var prefix = 0 + var gateway = "" + + // 1. Aktives Netz (WLAN ODER Ethernet) über LinkProperties — + // liefert das ECHTE Präfix, nicht pauschal /24. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val cm = context.applicationContext + .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val lp = cm.activeNetwork?.let { cm.getLinkProperties(it) } + if (lp != null) { + for (la in lp.linkAddresses) { + val a = la.address + if (a is Inet4Address && !a.isLoopbackAddress && !a.isLinkLocalAddress) { + ip = a.hostAddress ?: "" + prefix = la.prefixLength + break + } + } + for (route in lp.routes) { + val gw = route.gateway + if (route.isDefaultRoute && gw is Inet4Address && !gw.isAnyLocalAddress) { + gateway = gw.hostAddress ?: "" + break + } + } + } + } + + // 2. Fallback: WLAN-DHCP-Info (ältere Geräte / LinkProperties leer) + if (ip.isEmpty() || prefix == 0) { + val wifi = context.applicationContext + .getSystemService(Context.WIFI_SERVICE) as WifiManager + @Suppress("DEPRECATION") val dhcp = wifi.dhcpInfo + if (dhcp != null) { + if (ip.isEmpty() && dhcp.ipAddress != 0) ip = intToIp(dhcp.ipAddress) + if (prefix == 0 && dhcp.netmask != 0) prefix = Integer.bitCount(dhcp.netmask) + if (gateway.isEmpty() && dhcp.gateway != 0) gateway = intToIp(dhcp.gateway) + } + } + + // 3. Letzter Fallback + if (ip.isEmpty()) ip = firstLocalIpv4() + if (prefix !in 1..32) prefix = 24 + + // Netzadresse aus IP + Präfix berechnen + val ipInt = ipv4ToInt(ip) ?: 0 + val mask = if (prefix == 0) 0 else (-1 shl (32 - prefix)) + val network = intToIpv4(ipInt and mask) + resolve(call, JSObject() - .put("subnet", "$base.0/24") + .put("subnet", "$network/$prefix") .put("ip", ip) .put("gateway", gateway)) } catch (e: Exception) { @@ -287,56 +330,46 @@ class NetDiagScannerPlugin : Plugin() { } /* --------------------------------------------------------------------- */ - /* DHCP-Discover (Rogue-DHCP-Erkennung) */ + /* DHCP-Info — DHCP-Server, von dem das Gerät seine Adresse bezieht */ /* --------------------------------------------------------------------- */ + /** + * Liefert den DHCP-Server, von dem das Gerät selbst seine IP bezogen hat, + * samt Lease-Dauer, Gateway und DNS. + * + * Eine aktive Rogue-DHCP-Suche (DHCPDISCOVER senden, OFFER empfangen) ist + * auf nicht gerootetem Android NICHT möglich: der Server antwortet auf + * UDP-Port 68, der ist privilegiert (< 1024) und vom System-DHCP-Client + * belegt. Darum hier nur die verlässliche, vom OS bezogene Lease-Info. + */ @PluginMethod - fun dhcpDiscover(call: PluginCall) { + fun dhcpInfo(call: PluginCall) { io.launch { try { - val servers = discoverDhcpServers() - val arr = JSArray() - val arp = readArpTable() - for (ip in servers) { - arr.put(JSObject().put("ip", ip).put("mac", arp[ip] ?: "")) - } - resolve(call, JSObject().put("servers", arr)) - } catch (e: Exception) { - call.reject("dhcpDiscover: ${e.message}") - } - } - } + val wifi = context.applicationContext + .getSystemService(Context.WIFI_SERVICE) as WifiManager + @Suppress("DEPRECATION") val dhcp = wifi.dhcpInfo - /** - * Sendet einen DHCPDISCOVER-Broadcast und sammelt antwortende Server. - * Best-effort: scheitert auf manchen Geräten an Port-68-Belegung. - */ - private fun discoverDhcpServers(): List { - val found = LinkedHashSet() - val socket = DatagramSocket() - socket.broadcast = true - socket.soTimeout = 3000 - try { - val xid = byteArrayOf(0x39, 0x03, 0x42.toByte(), 0x12) - val packet = buildDhcpDiscover(xid) - socket.send(DatagramPacket(packet, packet.size, InetAddress.getByName("255.255.255.255"), 67)) - val buf = ByteArray(1500) - val deadline = System.currentTimeMillis() + 3000 - while (System.currentTimeMillis() < deadline) { - try { - val resp = DatagramPacket(buf, buf.size) - socket.receive(resp) - // Server-Identifier (Option 54) aus dem Paket lesen, sonst Absender - val srv = parseDhcpServerId(buf, resp.length) ?: resp.address.hostAddress - if (srv != null) found.add(srv) - } catch (_: Exception) { - break + val out = JSObject() + val dns = JSArray() + if (dhcp != null && dhcp.serverAddress != 0) { + out.put("server", intToIp(dhcp.serverAddress)) + out.put("lease", dhcp.leaseDuration) + out.put("gateway", if (dhcp.gateway != 0) intToIp(dhcp.gateway) else "") + if (dhcp.dns1 != 0) dns.put(intToIp(dhcp.dns1)) + if (dhcp.dns2 != 0) dns.put(intToIp(dhcp.dns2)) + } else { + // Kein WLAN-DHCP greifbar (z. B. Ethernet) — Server nicht ermittelbar + out.put("server", "") + out.put("lease", 0) + out.put("gateway", "") } + out.put("dns", dns) + resolve(call, out) + } catch (e: Exception) { + call.reject("dhcpInfo: ${e.message}") } - } finally { - socket.close() } - return found.toList() } /* --------------------------------------------------------------------- */ @@ -372,13 +405,17 @@ class NetDiagScannerPlugin : Plugin() { io.launch { try { val hops = JSArray() - for (ttl in 1..20) { + var deadStreak = 0 + for (ttl in 1..30) { val hop = pingWithTtl(host, ttl) hops.put(JSObject() .put("ttl", ttl) .put("ip", hop.ip) .put("ms", hop.ms)) if (hop.ip == host || hop.reachedTarget) break + // Nach 5 toten Hops in Folge abbrechen statt stur bis TTL 30 + deadStreak = if (hop.ip == "*") deadStreak + 1 else 0 + if (deadStreak >= 5) break } resolve(call, JSObject().put("hops", hops)) } catch (e: Exception) { @@ -654,37 +691,6 @@ class NetDiagScannerPlugin : Plugin() { private fun round1(v: Double): Double = Math.round(v * 10.0) / 10.0 - private fun buildDhcpDiscover(xid: ByteArray): ByteArray { - val p = ByteArray(300) - p[0] = 1 // op = BOOTREQUEST - p[1] = 1 // htype = Ethernet - p[2] = 6 // hlen - System.arraycopy(xid, 0, p, 4, 4) - p[10] = 0x80.toByte() // Broadcast-Flag - // Magic Cookie - p[236] = 99; p[237] = 130.toByte(); p[238] = 83; p[239] = 99 - // Option 53: DHCP Message Type = DISCOVER - p[240] = 53; p[241] = 1; p[242] = 1 - p[243] = 255.toByte() // Ende - return p - } - - private fun parseDhcpServerId(buf: ByteArray, len: Int): String? { - var i = 240 - while (i + 1 < len) { - val opt = buf[i].toInt() and 0xFF - if (opt == 255) break - if (opt == 0) { i++; continue } - val l = buf[i + 1].toInt() and 0xFF - if (opt == 54 && l == 4) { - return "${buf[i+2].toInt() and 0xFF}.${buf[i+3].toInt() and 0xFF}." + - "${buf[i+4].toInt() and 0xFF}.${buf[i+5].toInt() and 0xFF}" - } - i += 2 + l - } - return null - } - companion object { /** Kleiner OUI-Auszug — bei Bedarf vollständige IEEE-OUI-Datei einbinden. */ private val OUI = mapOf( diff --git a/native-plugin/NetDiagScannerPlugin.kt b/native-plugin/NetDiagScannerPlugin.kt index e7cc5cc..413e296 100644 --- a/native-plugin/NetDiagScannerPlugin.kt +++ b/native-plugin/NetDiagScannerPlugin.kt @@ -3,6 +3,7 @@ package de.data_it_solution.netdiag import android.Manifest import android.content.Context import android.content.Intent +import android.net.ConnectivityManager import android.net.Uri import android.net.wifi.WifiManager import android.os.Build @@ -26,9 +27,8 @@ import java.io.BufferedReader import java.io.File import java.io.FileOutputStream import java.io.FileReader -import java.net.DatagramPacket -import java.net.DatagramSocket import java.net.HttpURLConnection +import java.net.Inet4Address import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket @@ -66,15 +66,58 @@ class NetDiagScannerPlugin : Plugin() { fun getLocalSubnet(call: PluginCall) { io.launch { try { - val wifi = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - @Suppress("DEPRECATION") val dhcp = wifi.dhcpInfo - val ipInt = dhcp?.ipAddress ?: 0 - val gwInt = dhcp?.gateway ?: 0 - val ip = if (ipInt != 0) intToIp(ipInt) else firstLocalIpv4() - val gateway = if (gwInt != 0) intToIp(gwInt) else "" - val base = ip.substringBeforeLast('.', "192.168.1") + var ip = "" + var prefix = 0 + var gateway = "" + + // 1. Aktives Netz (WLAN ODER Ethernet) über LinkProperties — + // liefert das ECHTE Präfix, nicht pauschal /24. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val cm = context.applicationContext + .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val lp = cm.activeNetwork?.let { cm.getLinkProperties(it) } + if (lp != null) { + for (la in lp.linkAddresses) { + val a = la.address + if (a is Inet4Address && !a.isLoopbackAddress && !a.isLinkLocalAddress) { + ip = a.hostAddress ?: "" + prefix = la.prefixLength + break + } + } + for (route in lp.routes) { + val gw = route.gateway + if (route.isDefaultRoute && gw is Inet4Address && !gw.isAnyLocalAddress) { + gateway = gw.hostAddress ?: "" + break + } + } + } + } + + // 2. Fallback: WLAN-DHCP-Info (ältere Geräte / LinkProperties leer) + if (ip.isEmpty() || prefix == 0) { + val wifi = context.applicationContext + .getSystemService(Context.WIFI_SERVICE) as WifiManager + @Suppress("DEPRECATION") val dhcp = wifi.dhcpInfo + if (dhcp != null) { + if (ip.isEmpty() && dhcp.ipAddress != 0) ip = intToIp(dhcp.ipAddress) + if (prefix == 0 && dhcp.netmask != 0) prefix = Integer.bitCount(dhcp.netmask) + if (gateway.isEmpty() && dhcp.gateway != 0) gateway = intToIp(dhcp.gateway) + } + } + + // 3. Letzter Fallback + if (ip.isEmpty()) ip = firstLocalIpv4() + if (prefix !in 1..32) prefix = 24 + + // Netzadresse aus IP + Präfix berechnen + val ipInt = ipv4ToInt(ip) ?: 0 + val mask = if (prefix == 0) 0 else (-1 shl (32 - prefix)) + val network = intToIpv4(ipInt and mask) + resolve(call, JSObject() - .put("subnet", "$base.0/24") + .put("subnet", "$network/$prefix") .put("ip", ip) .put("gateway", gateway)) } catch (e: Exception) { @@ -90,14 +133,18 @@ class NetDiagScannerPlugin : Plugin() { @PluginMethod fun ipScan(call: PluginCall) { val subnet = call.getString("subnet") ?: return call.reject("subnet fehlt") - val base = subnet.substringBeforeLast('.', "192.168.1") + val hosts = hostsInSubnet(subnet) + if (hosts.isEmpty()) { + return call.reject("Subnetz ungültig oder zu groß (max /16): $subnet") + } io.launch { try { - // Parallel-Ping über das gesamte /24 + // Parallel-Ping über ALLE Host-Adressen des Subnetzes — CIDR-genau, + // also exakt der Bereich, den die Netzmaske aufspannt (/24, /23, /22 …). val alive = withContext(Dispatchers.IO) { - (1..254).map { host -> + hosts.map { ipInt -> async { - val ip = "$base.$host" + val ip = intToIpv4(ipInt) if (InetAddress.getByName(ip).isReachable(350)) ip else null } }.awaitAll().filterNotNull() @@ -120,6 +167,51 @@ class NetDiagScannerPlugin : Plugin() { } } + /** + * Alle Host-IPs (als Int) eines CIDR-Subnetzes. + * "192.168.1.0/24" -> .1 bis .254, "10.0.0.0/22" -> 1022 Hosts usw. + * Ohne Praefix wird /24 angenommen. Netz- und Broadcast-Adresse sind + * ausgenommen (ausser /31, /32). Leer bei ungueltig oder > /16. + */ + private fun hostsInSubnet(cidr: String): List { + val parts = cidr.trim().split('/') + val ipInt = ipv4ToInt(parts[0].trim()) ?: return emptyList() + val prefix = if (parts.size > 1) (parts[1].trim().toIntOrNull() ?: 24) else 24 + if (prefix < 0 || prefix > 32) return emptyList() + val mask = if (prefix == 0) 0 else (-1 shl (32 - prefix)) + val network = ipInt and mask + val broadcast = network or mask.inv() + val out = ArrayList() + if (prefix >= 31) { + var i = network + while (true) { out.add(i); if (i == broadcast) break; i++ } + return out + } + val count = (broadcast.toLong() and 0xFFFFFFFFL) - (network.toLong() and 0xFFFFFFFFL) - 1L + if (count < 1L || count > 65534L) return emptyList() + var i = network + 1 + val last = broadcast - 1 + while (true) { out.add(i); if (i == last) break; i++ } + return out + } + + /** "192.168.1.50" -> 32-Bit-Int (big-endian), null bei ungueltig */ + private fun ipv4ToInt(s: String): Int? { + val o = s.split('.') + if (o.size != 4) return null + var v = 0 + for (part in o) { + val n = part.toIntOrNull() ?: return null + if (n < 0 || n > 255) return null + v = (v shl 8) or n + } + return v + } + + /** 32-Bit-Int (big-endian) -> "192.168.1.50" */ + private fun intToIpv4(i: Int): String = + "${(i shr 24) and 0xFF}.${(i shr 16) and 0xFF}.${(i shr 8) and 0xFF}.${i and 0xFF}" + /* --------------------------------------------------------------------- */ /* Port-Scan */ /* --------------------------------------------------------------------- */ @@ -238,56 +330,46 @@ class NetDiagScannerPlugin : Plugin() { } /* --------------------------------------------------------------------- */ - /* DHCP-Discover (Rogue-DHCP-Erkennung) */ + /* DHCP-Info — DHCP-Server, von dem das Gerät seine Adresse bezieht */ /* --------------------------------------------------------------------- */ + /** + * Liefert den DHCP-Server, von dem das Gerät selbst seine IP bezogen hat, + * samt Lease-Dauer, Gateway und DNS. + * + * Eine aktive Rogue-DHCP-Suche (DHCPDISCOVER senden, OFFER empfangen) ist + * auf nicht gerootetem Android NICHT möglich: der Server antwortet auf + * UDP-Port 68, der ist privilegiert (< 1024) und vom System-DHCP-Client + * belegt. Darum hier nur die verlässliche, vom OS bezogene Lease-Info. + */ @PluginMethod - fun dhcpDiscover(call: PluginCall) { + fun dhcpInfo(call: PluginCall) { io.launch { try { - val servers = discoverDhcpServers() - val arr = JSArray() - val arp = readArpTable() - for (ip in servers) { - arr.put(JSObject().put("ip", ip).put("mac", arp[ip] ?: "")) - } - resolve(call, JSObject().put("servers", arr)) - } catch (e: Exception) { - call.reject("dhcpDiscover: ${e.message}") - } - } - } + val wifi = context.applicationContext + .getSystemService(Context.WIFI_SERVICE) as WifiManager + @Suppress("DEPRECATION") val dhcp = wifi.dhcpInfo - /** - * Sendet einen DHCPDISCOVER-Broadcast und sammelt antwortende Server. - * Best-effort: scheitert auf manchen Geräten an Port-68-Belegung. - */ - private fun discoverDhcpServers(): List { - val found = LinkedHashSet() - val socket = DatagramSocket() - socket.broadcast = true - socket.soTimeout = 3000 - try { - val xid = byteArrayOf(0x39, 0x03, 0x42.toByte(), 0x12) - val packet = buildDhcpDiscover(xid) - socket.send(DatagramPacket(packet, packet.size, InetAddress.getByName("255.255.255.255"), 67)) - val buf = ByteArray(1500) - val deadline = System.currentTimeMillis() + 3000 - while (System.currentTimeMillis() < deadline) { - try { - val resp = DatagramPacket(buf, buf.size) - socket.receive(resp) - // Server-Identifier (Option 54) aus dem Paket lesen, sonst Absender - val srv = parseDhcpServerId(buf, resp.length) ?: resp.address.hostAddress - if (srv != null) found.add(srv) - } catch (_: Exception) { - break + val out = JSObject() + val dns = JSArray() + if (dhcp != null && dhcp.serverAddress != 0) { + out.put("server", intToIp(dhcp.serverAddress)) + out.put("lease", dhcp.leaseDuration) + out.put("gateway", if (dhcp.gateway != 0) intToIp(dhcp.gateway) else "") + if (dhcp.dns1 != 0) dns.put(intToIp(dhcp.dns1)) + if (dhcp.dns2 != 0) dns.put(intToIp(dhcp.dns2)) + } else { + // Kein WLAN-DHCP greifbar (z. B. Ethernet) — Server nicht ermittelbar + out.put("server", "") + out.put("lease", 0) + out.put("gateway", "") } + out.put("dns", dns) + resolve(call, out) + } catch (e: Exception) { + call.reject("dhcpInfo: ${e.message}") } - } finally { - socket.close() } - return found.toList() } /* --------------------------------------------------------------------- */ @@ -323,13 +405,17 @@ class NetDiagScannerPlugin : Plugin() { io.launch { try { val hops = JSArray() - for (ttl in 1..20) { + var deadStreak = 0 + for (ttl in 1..30) { val hop = pingWithTtl(host, ttl) hops.put(JSObject() .put("ttl", ttl) .put("ip", hop.ip) .put("ms", hop.ms)) if (hop.ip == host || hop.reachedTarget) break + // Nach 5 toten Hops in Folge abbrechen statt stur bis TTL 30 + deadStreak = if (hop.ip == "*") deadStreak + 1 else 0 + if (deadStreak >= 5) break } resolve(call, JSObject().put("hops", hops)) } catch (e: Exception) { @@ -605,37 +691,6 @@ class NetDiagScannerPlugin : Plugin() { private fun round1(v: Double): Double = Math.round(v * 10.0) / 10.0 - private fun buildDhcpDiscover(xid: ByteArray): ByteArray { - val p = ByteArray(300) - p[0] = 1 // op = BOOTREQUEST - p[1] = 1 // htype = Ethernet - p[2] = 6 // hlen - System.arraycopy(xid, 0, p, 4, 4) - p[10] = 0x80.toByte() // Broadcast-Flag - // Magic Cookie - p[236] = 99; p[237] = 130.toByte(); p[238] = 83; p[239] = 99 - // Option 53: DHCP Message Type = DISCOVER - p[240] = 53; p[241] = 1; p[242] = 1 - p[243] = 255.toByte() // Ende - return p - } - - private fun parseDhcpServerId(buf: ByteArray, len: Int): String? { - var i = 240 - while (i + 1 < len) { - val opt = buf[i].toInt() and 0xFF - if (opt == 255) break - if (opt == 0) { i++; continue } - val l = buf[i + 1].toInt() and 0xFF - if (opt == 54 && l == 4) { - return "${buf[i+2].toInt() and 0xFF}.${buf[i+3].toInt() and 0xFF}." + - "${buf[i+4].toInt() and 0xFF}.${buf[i+5].toInt() and 0xFF}" - } - i += 2 + l - } - return null - } - companion object { /** Kleiner OUI-Auszug — bei Bedarf vollständige IEEE-OUI-Datei einbinden. */ private val OUI = mapOf( diff --git a/src/lib/components/MeasurementResult.svelte b/src/lib/components/MeasurementResult.svelte new file mode 100644 index 0000000..31ff818 --- /dev/null +++ b/src/lib/components/MeasurementResult.svelte @@ -0,0 +1,46 @@ + + +{#if scalarText} +

{scalarText}

+{/if} + +{#each lists as [key, items] (key)} +
+ {key}: +
    + {#each items as item, i (i)} +
  • {String(item)}
  • + {/each} +
+
+{/each} diff --git a/src/lib/scanner.ts b/src/lib/scanner.ts index cc23500..6ccedf7 100644 --- a/src/lib/scanner.ts +++ b/src/lib/scanner.ts @@ -36,9 +36,15 @@ export interface WifiNetwork { rssi: number; band: string; } -export interface DhcpServer { - ip: string; - mac?: string; +export interface DhcpLease { + /** DHCP-Server, von dem das Gerät seine IP bezieht ('' wenn unbekannt) */ + server: string; + /** Lease-Dauer in Sekunden (0 wenn unbekannt) */ + lease: number; + /** Standard-Gateway */ + gateway: string; + /** zugewiesene DNS-Server */ + dns: string[]; } export interface TracerouteHop { ttl: number; @@ -62,8 +68,8 @@ export interface NetDiagScannerPlugin { pingQuality(opts: { host: string; count: number }): Promise; /** WLAN-Scan: umliegende Netze, Kanäle, Signalstärke */ wifiScan(): Promise<{ networks: WifiNetwork[] }>; - /** DHCP-Server im Netz erkennen (Rogue-DHCP-Erkennung) */ - dhcpDiscover(): Promise<{ servers: DhcpServer[] }>; + /** DHCP-Lease-Info des Geräts (Server, Lease-Dauer, Gateway, DNS) */ + dhcpInfo(): Promise; /** SNMP v2c Abfrage (Switch: Link-Speed, Fehlerzähler) */ snmpGet(opts: { host: string; community: string; oids: string[] }): Promise<{ values: Record; @@ -137,8 +143,13 @@ const mock: NetDiagScannerPlugin = { ], }; }, - async dhcpDiscover() { - return { servers: [{ ip: '192.168.1.1', mac: 'AA:BB:CC:00:00:01' }] }; + async dhcpInfo() { + return { + server: '192.168.1.1', + lease: 86400, + gateway: '192.168.1.1', + dns: ['192.168.1.1', '8.8.8.8'], + }; }, async snmpGet(opts) { const values: Record = {}; diff --git a/src/lib/tools/netzwerk/dhcpcheck.ts b/src/lib/tools/netzwerk/dhcpcheck.ts index 6a9717c..c50abe7 100644 --- a/src/lib/tools/netzwerk/dhcpcheck.ts +++ b/src/lib/tools/netzwerk/dhcpcheck.ts @@ -1,6 +1,10 @@ /** - * Tool: DHCP-Check — erkennt antwortende DHCP-Server. - * Mehr als ein Server deutet auf einen Rogue-DHCP hin (Warnung). + * Tool: DHCP-Info — zeigt den DHCP-Server, von dem das Gerät seine Adresse hat. + * + * Eine aktive Rogue-DHCP-Suche ist auf nicht gerootetem Android nicht möglich: + * der DHCP-Server antwortet auf den privilegierten UDP-Port 68, den eine + * normale App nicht binden kann. Darum hier nur die verlässliche Lease-Info, + * die das Betriebssystem selbst bezogen hat. */ import { scanner } from '../../scanner'; @@ -9,25 +13,25 @@ import type { MeasureStatus, Tool } from '../types'; export const dhcpCheckTool: Tool = { id: 'dhcpcheck', category: 'netzwerk', - name: 'DHCP-Check', + name: 'DHCP-Info', icon: 'server', - description: 'Findet DHCP-Server — erkennt unerwünschte Zweit-Server.', + description: 'Zeigt DHCP-Server, Lease-Dauer und DNS des Geräts.', scope: 'protocol', params: [], async run() { - const { servers } = await scanner.dhcpDiscover(); - let status: MeasureStatus = 0; - if (servers.length === 0) status = 2; // kein DHCP-Server - if (servers.length > 1) status = 2; // Rogue-DHCP + const info = await scanner.dhcpInfo(); + const hasServer = info.server !== ''; + const status: MeasureStatus = hasServer ? 0 : 1; return { - label: - servers.length === 1 - ? `1 DHCP-Server: ${servers[0].ip}` - : `${servers.length} DHCP-Server (!)`, + label: hasServer ? `DHCP-Server: ${info.server}` : 'Kein DHCP-Server ermittelbar', result: { - count: servers.length, - server: servers.map((s) => `${s.ip}${s.mac ? ' / ' + s.mac : ''}`), - hinweis: servers.length > 1 ? 'Mehrere DHCP-Server — Rogue-DHCP prüfen!' : '', + server: info.server || '—', + lease: info.lease ? `${info.lease} s` : '—', + gateway: info.gateway || '—', + dns: info.dns.length ? info.dns : ['—'], + hinweis: hasServer + ? 'Nur der DHCP-Server des Geräts — Rogue-DHCP-Erkennung braucht Root.' + : 'Auf Ethernet / ohne WLAN nicht ermittelbar.', }, measureStatus: status, }; diff --git a/src/lib/tools/types.ts b/src/lib/tools/types.ts index ea65c56..fd922c0 100644 --- a/src/lib/tools/types.ts +++ b/src/lib/tools/types.ts @@ -9,6 +9,10 @@ import type { Device, MeasureStatus, Protocol } from '../types'; +// Für Tool-Dateien unter tools// mit-exportieren, damit sie ihre +// Ampel-Bewertung typisieren können, ohne quer in src/lib/types zu greifen. +export type { MeasureStatus }; + export type ToolCategory = 'netzwerk' | 'internet' | 'telefonie'; /** Eingabefeld eines Tools (für das Parameter-Formular) */ diff --git a/src/lib/types.ts b/src/lib/types.ts index 06cd22c..bdef981 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -30,6 +30,8 @@ export interface Order { ref: string; refClient?: string; date?: number; + /** Unix-Zeit der letzten Änderung (Dolibarr commande.tms) — Sortierschlüssel der Liste */ + tms?: number; /** Dolibarr-Status: -1 storniert, 0 Entwurf, 1 validiert, 2 in Bearbeitung, 3 abgeschlossen */ status: number; /** true wenn Status 0/1/2 (aktiver Auftrag) */ diff --git a/src/routes/auftraege/+page.svelte b/src/routes/auftraege/+page.svelte index d1ee624..f3d957c 100644 --- a/src/routes/auftraege/+page.svelte +++ b/src/routes/auftraege/+page.svelte @@ -12,7 +12,9 @@ let orders = $state([]); let search = $state(''); - let showAll = $state(false); + // Standard: alle Aufträge, zuletzt bearbeitete zuerst (Server sortiert nach tms). + // Haken setzen blendet abgeschlossene aus und zeigt nur aktive Aufträge. + let onlyActive = $state(false); let loading = $state(false); let loadError = $state(''); @@ -40,7 +42,7 @@ loading = true; loadError = ''; try { - const res = await listOrders({ open: !showAll, q: search.trim() || undefined }); + const res = await listOrders({ open: onlyActive, q: search.trim() || undefined }); orders = res.orders; } catch (e) { loadError = e instanceof ApiError ? e.message : 'Laden fehlgeschlagen'; @@ -54,9 +56,9 @@ searchTimer = setTimeout(load, 300); } - async function toggleShowAll() { - showAll = !showAll; - await Preferences.set({ key: 'nd_show_all', value: showAll ? '1' : '0' }); + async function toggleOnlyActive() { + onlyActive = !onlyActive; + await Preferences.set({ key: 'nd_only_active', value: onlyActive ? '1' : '0' }); load(); } @@ -82,7 +84,7 @@ } onMount(async () => { - showAll = (await Preferences.get({ key: 'nd_show_all' })).value === '1'; + onlyActive = (await Preferences.get({ key: 'nd_only_active' })).value === '1'; load(); }); @@ -103,8 +105,8 @@
@@ -141,11 +143,13 @@ {#if order.note}
{order.note}
{/if} - +
- {order.ref}{order.refClient ? ' · ' + order.refClient : ''}{order.date - ? ' · ' + fmtDate(order.date) - : ''} + {order.ref}{order.refClient ? ' · ' + order.refClient : ''}{order.tms + ? ' · bearb. ' + fmtDate(order.tms) + : order.date + ? ' · ' + fmtDate(order.date) + : ''}
{#if order.protocolCount && order.protocolCount > 0} diff --git a/src/routes/protokoll/[id]/+page.svelte b/src/routes/protokoll/[id]/+page.svelte index b82aa7b..8e16c91 100644 --- a/src/routes/protokoll/[id]/+page.svelte +++ b/src/routes/protokoll/[id]/+page.svelte @@ -4,6 +4,7 @@ import { page } from '$app/stores'; import AppHeader from '$lib/components/AppHeader.svelte'; import ToolDialog from '$lib/components/ToolDialog.svelte'; + import MeasurementResult from '$lib/components/MeasurementResult.svelte'; import { getProtocol, saveProtocol, deleteProtocol } from '$lib/db'; import { addMeasurement, upsertDevice } from '$lib/protocols'; import { sync } from '$lib/sync.svelte'; @@ -119,11 +120,6 @@ function protocolMeasurements() { return protocol?.measurements.filter((m) => !m.deviceClientId) ?? []; } - function resultText(r: Record): string { - return Object.entries(r) - .map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : v}`) - .join(' · '); - } {#if protocol} @@ -194,7 +190,7 @@ {getTool(m.tool)?.name ?? m.tool}

{m.label}

-

{resultText(m.result)}

+
{/each} @@ -225,7 +221,7 @@

{m.label}

-

{resultText(m.result)}

+
{/each}