Neues Werkzeug: IP-Konflikt-Prüfung
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
fd75748cb9
commit
9ee9c954b2
5 changed files with 250 additions and 0 deletions
|
|
@ -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<String, MutableSet<String>>()
|
||||
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 */
|
||||
/* --------------------------------------------------------------------- */
|
||||
|
|
|
|||
|
|
@ -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<String, MutableSet<String>>()
|
||||
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 */
|
||||
/* --------------------------------------------------------------------- */
|
||||
|
|
|
|||
|
|
@ -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<ConflictScanResult>;
|
||||
/** 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' },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
90
src/lib/tools/netzwerk/ipconflict.ts
Normal file
90
src/lib/tools/netzwerk/ipconflict.ts
Normal file
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
Loading…
Reference in a new issue