Bugfixes: IP-Scanner, DHCP, Aufträge, Messergebnisse [apk]
All checks were successful
Build APK / build-apk (push) Successful in 1m41s
All checks were successful
Build APK / build-apk (push) Successful in 1m41s
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
34356f25ef
commit
53d91d1526
9 changed files with 339 additions and 211 deletions
|
|
@ -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<String> {
|
||||
val found = LinkedHashSet<String>()
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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<Int> {
|
||||
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<Int>()
|
||||
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<String> {
|
||||
val found = LinkedHashSet<String>()
|
||||
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(
|
||||
|
|
|
|||
46
src/lib/components/MeasurementResult.svelte
Normal file
46
src/lib/components/MeasurementResult.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Stellt das `result`-Objekt einer Messung lesbar dar.
|
||||
*
|
||||
* Skalare Werte (Latenz, Anzahl, …) kommen kompakt in eine Zeile. Arrays
|
||||
* — WLAN-Netze, Traceroute-Hops — werden als echte, zeilenweise Liste
|
||||
* dargestellt statt in eine endlose Komma-Zeile gequetscht.
|
||||
*/
|
||||
|
||||
let { result }: { result: Record<string, unknown> } = $props();
|
||||
|
||||
/** Leere Werte ('', null, undefined, leeres Array) ausblenden */
|
||||
function isEmpty(v: unknown): boolean {
|
||||
if (v == null || v === '') return true;
|
||||
if (Array.isArray(v)) return v.length === 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const entries = $derived(Object.entries(result).filter(([, v]) => !isEmpty(v)));
|
||||
|
||||
/** Skalare als eine kompakte „schlüssel: wert · schlüssel: wert"-Zeile */
|
||||
const scalarText = $derived(
|
||||
entries
|
||||
.filter(([, v]) => !Array.isArray(v))
|
||||
.map(([k, v]) => `${k}: ${String(v)}`)
|
||||
.join(' · '),
|
||||
);
|
||||
|
||||
/** Array-Werte je als eigene Liste */
|
||||
const lists = $derived(entries.filter(([, v]) => Array.isArray(v)) as [string, unknown[]][]);
|
||||
</script>
|
||||
|
||||
{#if scalarText}
|
||||
<p class="break-words text-[11px] text-zinc-500">{scalarText}</p>
|
||||
{/if}
|
||||
|
||||
{#each lists as [key, items] (key)}
|
||||
<div class="mt-0.5">
|
||||
<span class="text-[11px] text-zinc-600">{key}:</span>
|
||||
<ul class="mt-0.5 flex flex-col gap-0.5">
|
||||
{#each items as item, i (i)}
|
||||
<li class="break-words pl-2 text-[11px] text-zinc-400">{String(item)}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -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<PingQuality>;
|
||||
/** 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<DhcpLease>;
|
||||
/** SNMP v2c Abfrage (Switch: Link-Speed, Fehlerzähler) */
|
||||
snmpGet(opts: { host: string; community: string; oids: string[] }): Promise<{
|
||||
values: Record<string, string>;
|
||||
|
|
@ -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<string, string> = {};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@
|
|||
|
||||
import type { Device, MeasureStatus, Protocol } from '../types';
|
||||
|
||||
// Für Tool-Dateien unter tools/<kategorie>/ 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) */
|
||||
|
|
|
|||
|
|
@ -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) */
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@
|
|||
|
||||
let orders = $state<Order[]>([]);
|
||||
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();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -103,8 +105,8 @@
|
|||
</div>
|
||||
|
||||
<label class="flex items-center gap-2 px-3 py-2 text-sm text-zinc-400">
|
||||
<input type="checkbox" checked={showAll} onchange={toggleShowAll} />
|
||||
Auch abgeschlossene Aufträge anzeigen
|
||||
<input type="checkbox" checked={onlyActive} onchange={toggleOnlyActive} />
|
||||
Nur aktive Aufträge anzeigen
|
||||
</label>
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
|
|
@ -141,11 +143,13 @@
|
|||
{#if order.note}
|
||||
<div class="truncate text-xs text-zinc-400">{order.note}</div>
|
||||
{/if}
|
||||
<!-- Auftragsnummer + Datum: nur Kleingedrucktes -->
|
||||
<!-- Auftragsnummer + Bearbeitungsdatum: nur Kleingedrucktes -->
|
||||
<div class="truncate text-[11px] text-zinc-500">
|
||||
{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)
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
{#if order.protocolCount && order.protocolCount > 0}
|
||||
|
|
|
|||
|
|
@ -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, unknown>): string {
|
||||
return Object.entries(r)
|
||||
.map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : v}`)
|
||||
.join(' · ');
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if protocol}
|
||||
|
|
@ -194,7 +190,7 @@
|
|||
<span class="text-sm font-medium">{getTool(m.tool)?.name ?? m.tool}</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs {ampel[m.measureStatus]}">{m.label}</p>
|
||||
<p class="mt-0.5 break-words text-[11px] text-zinc-500">{resultText(m.result)}</p>
|
||||
<div class="mt-0.5"><MeasurementResult result={m.result} /></div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
|
@ -225,7 +221,7 @@
|
|||
<span class="mt-1 h-2 w-2 shrink-0 rounded-full {ampelDot[m.measureStatus]}"></span>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs {ampel[m.measureStatus]}">{m.label}</p>
|
||||
<p class="break-words text-[11px] text-zinc-500">{resultText(m.result)}</p>
|
||||
<MeasurementResult result={m.result} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
|||
Loading…
Reference in a new issue