From 9ee9c954b2ad872346bd225f5cafd18c4ab5c83f Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Tue, 19 May 2026 23:00:32 +0200 Subject: [PATCH] =?UTF-8?q?Neues=20Werkzeug:=20IP-Konflikt-Pr=C3=BCfung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - arpConflictScan (Plugin): pingt das Subnetz über mehrere Runden und liest je Runde die ARP-Tabelle; mehrere MACs pro IP = Konflikt. Kein Root nötig - Erkennt den Fall, dass /proc/net/arp nicht lesbar ist (Android-Limit) und meldet das ehrlich, statt fälschlich Entwarnung zu geben - ipconflict.ts: neues Protokoll-Tool, in der Tool-Registry eingetragen — listet betroffene IPs samt der konkurrierenden MAC-Adressen Co-Authored-By: Claude Opus 4.7 (1M context) --- .../netdiag/NetDiagScannerPlugin.kt | 67 ++++++++++++++ native-plugin/NetDiagScannerPlugin.kt | 67 ++++++++++++++ src/lib/scanner.ts | 24 +++++ src/lib/tools/index.ts | 2 + src/lib/tools/netzwerk/ipconflict.ts | 90 +++++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 src/lib/tools/netzwerk/ipconflict.ts 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 518a63c..1102e1d 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 @@ -450,6 +450,73 @@ class NetDiagScannerPlugin : Plugin() { private fun intToIpv4(i: Int): String = "${(i shr 24) and 0xFF}.${(i shr 16) and 0xFF}.${(i shr 8) and 0xFF}.${i and 0xFF}" + /* --------------------------------------------------------------------- */ + /* IP-Konflikt-Prüfung — eine IP, von zwei Geräten gleichzeitig benutzt */ + /* --------------------------------------------------------------------- */ + + /** + * Sucht IP-Adressen, die im Netz von mehr als einem Gerät benutzt werden. + * + * Ohne Root lässt sich kein Roh-ARP mitschneiden — der praktikable Weg: + * über mehrere Runden das Subnetz anpingen (das erzwingt jeweils eine + * ARP-Auflösung) und nach jeder Runde /proc/net/arp auslesen. Erscheint für + * dieselbe IP über die Runden hinweg mehr als eine MAC, nutzen zwei Geräte + * diese Adresse → Konflikt. + * + * Risiko: /proc/net/arp kann auf neueren Android-Versionen leer sein — + * dann meldet das Ergebnis `arpAvailable = false`. + */ + @PluginMethod + fun arpConflictScan(call: PluginCall) { + val subnet = call.getString("subnet") ?: return call.reject("subnet fehlt") + val rounds = (call.getInt("rounds") ?: 4).coerceIn(2, 10) + val delayMs = (call.getInt("delayMs") ?: 600).coerceIn(0, 5000).toLong() + val hosts = hostsInSubnet(subnet) + if (hosts.isEmpty()) { + return call.reject("Subnetz ungültig oder zu groß (max /16): $subnet") + } + io.launch { + try { + val seen = HashMap>() + var arpEverFilled = false + for (r in 0 until rounds) { + withContext(Dispatchers.IO) { + hosts.map { ipInt -> + async { + try { + InetAddress.getByName(intToIpv4(ipInt)).isReachable(300) + } catch (_: Exception) { + false + } + } + }.awaitAll() + } + val arp = readArpTable() + if (arp.isNotEmpty()) arpEverFilled = true + for ((ip, mac) in arp) { + seen.getOrPut(ip) { HashSet() }.add(mac) + } + if (r < rounds - 1 && delayMs > 0) Thread.sleep(delayMs) + } + val conflicts = JSArray() + for ((ip, macs) in seen) { + if (macs.size > 1) { + val macArr = JSArray() + macs.forEach { macArr.put(it) } + conflicts.put(JSObject().put("ip", ip).put("macs", macArr)) + } + } + resolve(call, JSObject() + .put("conflicts", conflicts) + .put("checked", seen.size) + .put("rounds", rounds) + .put("arpAvailable", arpEverFilled)) + } catch (e: Exception) { + call.reject("arpConflictScan: ${e.message}") + } + } + } + /* --------------------------------------------------------------------- */ /* Port-Scan */ /* --------------------------------------------------------------------- */ diff --git a/native-plugin/NetDiagScannerPlugin.kt b/native-plugin/NetDiagScannerPlugin.kt index 518a63c..1102e1d 100644 --- a/native-plugin/NetDiagScannerPlugin.kt +++ b/native-plugin/NetDiagScannerPlugin.kt @@ -450,6 +450,73 @@ class NetDiagScannerPlugin : Plugin() { private fun intToIpv4(i: Int): String = "${(i shr 24) and 0xFF}.${(i shr 16) and 0xFF}.${(i shr 8) and 0xFF}.${i and 0xFF}" + /* --------------------------------------------------------------------- */ + /* IP-Konflikt-Prüfung — eine IP, von zwei Geräten gleichzeitig benutzt */ + /* --------------------------------------------------------------------- */ + + /** + * Sucht IP-Adressen, die im Netz von mehr als einem Gerät benutzt werden. + * + * Ohne Root lässt sich kein Roh-ARP mitschneiden — der praktikable Weg: + * über mehrere Runden das Subnetz anpingen (das erzwingt jeweils eine + * ARP-Auflösung) und nach jeder Runde /proc/net/arp auslesen. Erscheint für + * dieselbe IP über die Runden hinweg mehr als eine MAC, nutzen zwei Geräte + * diese Adresse → Konflikt. + * + * Risiko: /proc/net/arp kann auf neueren Android-Versionen leer sein — + * dann meldet das Ergebnis `arpAvailable = false`. + */ + @PluginMethod + fun arpConflictScan(call: PluginCall) { + val subnet = call.getString("subnet") ?: return call.reject("subnet fehlt") + val rounds = (call.getInt("rounds") ?: 4).coerceIn(2, 10) + val delayMs = (call.getInt("delayMs") ?: 600).coerceIn(0, 5000).toLong() + val hosts = hostsInSubnet(subnet) + if (hosts.isEmpty()) { + return call.reject("Subnetz ungültig oder zu groß (max /16): $subnet") + } + io.launch { + try { + val seen = HashMap>() + var arpEverFilled = false + for (r in 0 until rounds) { + withContext(Dispatchers.IO) { + hosts.map { ipInt -> + async { + try { + InetAddress.getByName(intToIpv4(ipInt)).isReachable(300) + } catch (_: Exception) { + false + } + } + }.awaitAll() + } + val arp = readArpTable() + if (arp.isNotEmpty()) arpEverFilled = true + for ((ip, mac) in arp) { + seen.getOrPut(ip) { HashSet() }.add(mac) + } + if (r < rounds - 1 && delayMs > 0) Thread.sleep(delayMs) + } + val conflicts = JSArray() + for ((ip, macs) in seen) { + if (macs.size > 1) { + val macArr = JSArray() + macs.forEach { macArr.put(it) } + conflicts.put(JSObject().put("ip", ip).put("macs", macArr)) + } + } + resolve(call, JSObject() + .put("conflicts", conflicts) + .put("checked", seen.size) + .put("rounds", rounds) + .put("arpAvailable", arpEverFilled)) + } catch (e: Exception) { + call.reject("arpConflictScan: ${e.message}") + } + } + } + /* --------------------------------------------------------------------- */ /* Port-Scan */ /* --------------------------------------------------------------------- */ diff --git a/src/lib/scanner.ts b/src/lib/scanner.ts index 47437ed..d03b9d4 100644 --- a/src/lib/scanner.ts +++ b/src/lib/scanner.ts @@ -30,6 +30,16 @@ export interface MdnsDevice { /** angebotene Diensttypen, z.B. ['_googlecast._tcp', '_printer._tcp'] */ services: string[]; } +/** Ergebnis der IP-Konflikt-Prüfung */ +export interface ConflictScanResult { + /** IP-Adressen, die von mehr als einem Gerät benutzt werden */ + conflicts: { ip: string; macs: string[] }[]; + /** Anzahl der Adressen, für die überhaupt eine MAC gesehen wurde */ + checked: number; + rounds: number; + /** false = ARP-Tabelle nicht lesbar (Android-Einschränkung, dann keine Aussage möglich) */ + arpAvailable: boolean; +} export interface OpenPort { port: number; service?: string; @@ -78,6 +88,12 @@ export interface NetDiagScannerPlugin { ipScan(opts: { subnet: string }): Promise<{ devices: ScannedDevice[] }>; /** mDNS/Bonjour-Dienstsuche: Drucker, Kameras, Chromecast, AirPlay … */ mdnsScan(opts: { timeoutMs?: number }): Promise<{ devices: MdnsDevice[] }>; + /** IP-Konflikt-Prüfung: findet IP-Adressen, die zwei Geräte gleichzeitig benutzen */ + arpConflictScan(opts: { + subnet: string; + rounds?: number; + delayMs?: number; + }): Promise; /** Port-Scan eines Geräts */ portScan(opts: { ip: string; ports: number[] }): Promise<{ open: OpenPort[] }>; /** Ping-Qualität (Latenz, Jitter, Paketverlust) */ @@ -175,6 +191,14 @@ const mock: NetDiagScannerPlugin = { ], }; }, + async arpConflictScan() { + return { + conflicts: [{ ip: '192.168.1.40', macs: ['AA:BB:CC:00:00:28', 'AA:BB:CC:00:00:99'] }], + checked: 12, + rounds: 4, + arpAvailable: true, + }; + }, async portScan(opts) { const all: OpenPort[] = [ { port: 80, service: 'http' }, diff --git a/src/lib/tools/index.ts b/src/lib/tools/index.ts index 4064c86..1f9c903 100644 --- a/src/lib/tools/index.ts +++ b/src/lib/tools/index.ts @@ -10,6 +10,7 @@ import type { Tool, ToolCategory } from './types'; import { dhcpCheckTool } from './netzwerk/dhcpcheck'; +import { ipConflictTool } from './netzwerk/ipconflict'; import { ipScanTool } from './netzwerk/ipscan'; import { pingTool } from './netzwerk/ping'; import { portScanTool } from './netzwerk/portscan'; @@ -28,6 +29,7 @@ export const TOOLS: Tool[] = [ pingTool, wifiScanTool, dhcpCheckTool, + ipConflictTool, snmpTool, tracerouteTool, stressTestTool, diff --git a/src/lib/tools/netzwerk/ipconflict.ts b/src/lib/tools/netzwerk/ipconflict.ts new file mode 100644 index 0000000..8fab2ed --- /dev/null +++ b/src/lib/tools/netzwerk/ipconflict.ts @@ -0,0 +1,90 @@ +/** + * Tool: IP-Konflikt-Prüfung — findet IP-Adressen, die von zwei Geräten + * gleichzeitig benutzt werden und so das Netz durcheinanderbringen. + * + * Verfahren ohne Root: Das Subnetz wird über mehrere Runden angepingt und + * jeweils die ARP-Tabelle ausgelesen. Tauchen für eine IP mehrere MAC-Adressen + * auf, nutzen mehrere Geräte dieselbe Adresse — ein Konflikt. + */ + +import { scanner } from '../../scanner'; +import type { MeasureStatus, Tool } from '../types'; + +export const ipConflictTool: Tool = { + id: 'ipconflict', + category: 'netzwerk', + name: 'IP-Konflikt', + icon: 'alert-triangle', + description: 'Findet IP-Adressen, die zwei Geräte gleichzeitig benutzen.', + scope: 'protocol', + params: [ + { + key: 'subnet', + label: 'Netzbereich (CIDR) — leer = aktiver Adapter', + type: 'text', + placeholder: 'leer lassen → automatisch über WLAN/LAN', + }, + { + key: 'rounds', + label: 'Prüfrunden (mehr = zuverlässiger, dauert länger)', + type: 'number', + default: 4, + }, + ], + async run(ctx) { + // Netzbereich: Dialog → Protokoll → aktiver Adapter + let subnet = + String(ctx.params.subnet ?? '').trim() || String(ctx.protocol.subnet ?? '').trim(); + if (!subnet) { + try { + subnet = String((await scanner.getLocalSubnet()).subnet ?? '').trim(); + } catch { + /* unten abgefangen */ + } + } + if (!subnet) { + return { + label: 'Kein Netzbereich — WLAN/LAN nicht aktiv?', + result: { error: 'Netzbereich konnte nicht ermittelt werden' }, + measureStatus: 2, + }; + } + + const rounds = Number(ctx.params.rounds) || 4; + const res = await scanner.arpConflictScan({ subnet, rounds }); + + // ARP-Tabelle nicht lesbar → ehrliche Rückmeldung statt falscher Entwarnung + if (!res.arpAvailable) { + return { + label: 'ARP-Tabelle nicht lesbar — Konfliktprüfung nicht möglich', + result: { + subnet, + hinweis: + 'Android gibt /proc/net/arp auf diesem Gerät nicht frei. Eine ' + + 'zuverlässige IP-Konflikt-Erkennung ist ohne Root hier leider nicht möglich.', + }, + measureStatus: 1, + }; + } + + const n = res.conflicts.length; + const status: MeasureStatus = n > 0 ? 2 : 0; + return { + label: + n > 0 + ? `${n} IP-Konflikt${n > 1 ? 'e' : ''} gefunden!` + : `Kein Konflikt — ${res.checked} Adressen geprüft`, + result: { + subnet, + geprueft: res.checked, + runden: res.rounds, + konflikte: res.conflicts.map((c) => `${c.ip} → ${c.macs.join(' / ')}`), + hinweis: + n > 0 + ? 'Mehrere MAC-Adressen pro IP — diese Geräte stören sich gegenseitig.' + : 'Jede gefundene IP wird von genau einem Gerät benutzt.', + }, + measureStatus: status, + }; + }, +};