netdiag-app/src/lib/scanner.ts
Eduard Wisch 9ee9c954b2 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>
2026-05-19 23:00:32 +02:00

266 lines
8.3 KiB
TypeScript

/**
* Brücke zum nativen Scan-Plugin `NetDiagScanner` (Kotlin).
*
* Der WebView kann keine Raw-Sockets/ICMP/ARP — die eigentliche Netzwerk-
* Messung läuft im nativen Android-Plugin. Im Browser-Dev liefert ein Mock
* Beispieldaten, damit die Oberfläche ohne Gerät entwickelt werden kann.
*/
import { Capacitor, registerPlugin } from '@capacitor/core';
/* --- Datentypen der Plugin-Antworten --- */
export interface ScannedDevice {
ip: string;
mac?: string;
hostname?: string;
vendor?: string;
/** geschätzte Geräteart (Kamera, Drucker, Router …) */
deviceType?: string;
/** NetBIOS-Name (UDP-137-Abfrage) */
netbiosName?: string;
/** offene Ports aus der Quick-Port-Probe */
openPorts?: number[];
}
/** Ein per mDNS/Bonjour gefundenes Gerät */
export interface MdnsDevice {
ip: string;
/** Bonjour-Anzeigename */
name: string;
/** 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;
}
export interface PingQuality {
sent: number;
received: number;
lossPct: number;
minMs: number;
avgMs: number;
maxMs: number;
jitterMs: number;
}
export interface WifiNetwork {
ssid: string;
bssid: string;
channel: number;
rssi: number;
band: 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;
ip: string;
ms: number;
}
export interface ThroughputResult {
downMbps: number;
upMbps: number;
}
/** Schnittstelle des nativen Plugins */
export interface NetDiagScannerPlugin {
/** Aktuelles Subnetz des Geräts ermitteln (z.B. "192.168.1.0/24") */
getLocalSubnet(): Promise<{ subnet: string; ip: string; gateway: string }>;
/** IP-Scan: Geräte im Subnetz finden (ARP + Ping-Sweep + Namensauflösung) */
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) */
pingQuality(opts: { host: string; count: number }): Promise<PingQuality>;
/** WLAN-Scan: umliegende Netze, Kanäle, Signalstärke */
wifiScan(): Promise<{ networks: WifiNetwork[] }>;
/** 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>;
}>;
/** Traceroute zu einem Host */
traceroute(opts: { host: string }): Promise<{ hops: TracerouteHop[] }>;
/** Durchsatztest gegen eine Gegenstelle (iperf-kompatibel) */
throughput(opts: { host: string; port: number; durationSec: number }): Promise<ThroughputResult>;
/** Dauer-/Stresstest starten (läuft als Foreground-Service) */
startStressTest(opts: { host: string; durationSec: number }): Promise<{ runId: string }>;
/** Laufenden Stresstest beenden und Ergebnis holen */
stopStressTest(opts: { runId: string }): Promise<{
samples: number;
lossPct: number;
avgMs: number;
maxMs: number;
}>;
}
const native = registerPlugin<NetDiagScannerPlugin>('NetDiagScanner');
/* ----------------------------------------------------------------------- */
/* Mock für Browser-Entwicklung */
/* ----------------------------------------------------------------------- */
function rnd(min: number, max: number): number {
return Math.round((min + Math.random() * (max - min)) * 10) / 10;
}
const mock: NetDiagScannerPlugin = {
async getLocalSubnet() {
return { subnet: '192.168.1.0/24', ip: '192.168.1.50', gateway: '192.168.1.1' };
},
async ipScan() {
return {
devices: [
{
ip: '192.168.1.1',
mac: 'AA:BB:CC:00:00:01',
hostname: 'fritzbox',
vendor: 'AVM',
deviceType: 'Router',
openPorts: [53, 80, 443],
},
{
ip: '192.168.1.10',
mac: 'AA:BB:CC:00:00:0A',
hostname: 'switch-keller',
vendor: 'TP-Link',
deviceType: 'Switch',
openPorts: [80],
},
{
ip: '192.168.1.40',
mac: 'AA:BB:CC:00:00:28',
hostname: 'ipcam-hof',
vendor: 'Hikvision',
deviceType: 'Kamera',
openPorts: [80, 554],
},
{
ip: '192.168.1.50',
mac: 'AA:BB:CC:00:00:32',
hostname: 'handy',
vendor: 'Samsung',
deviceType: '',
openPorts: [],
},
{
ip: '192.168.1.77',
mac: 'AA:BB:CC:00:00:4D',
hostname: 'wallbox',
vendor: '',
netbiosName: 'WALLBOX',
deviceType: 'Wallbox',
openPorts: [80, 502],
},
],
};
},
async mdnsScan() {
return {
devices: [
{ ip: '192.168.1.20', name: 'Brother HL-L2350DW', services: ['_printer._tcp', '_ipp._tcp'] },
{ ip: '192.168.1.30', name: 'Wohnzimmer-TV', services: ['_googlecast._tcp'] },
{ ip: '192.168.1.40', name: 'IP-Kamera Hof', services: ['_rtsp._tcp'] },
],
};
},
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' },
{ port: 443, service: 'https' },
{ port: 22, service: 'ssh' },
];
return { open: all.filter((p) => opts.ports.includes(p.port)) };
},
async pingQuality(opts) {
const sent = opts.count;
const received = sent - (Math.random() < 0.3 ? 1 : 0);
return {
sent,
received,
lossPct: Math.round(((sent - received) / sent) * 100),
minMs: rnd(1, 4),
avgMs: rnd(4, 12),
maxMs: rnd(12, 40),
jitterMs: rnd(0.5, 5),
};
},
async wifiScan() {
return {
networks: [
{ ssid: 'AllesWattLaeuft', bssid: 'AA:BB:CC:11:22:33', channel: 6, rssi: -52, band: '2.4 GHz' },
{ ssid: 'Nachbar-WLAN', bssid: 'DD:EE:FF:44:55:66', channel: 11, rssi: -78, band: '2.4 GHz' },
{ ssid: 'AllesWattLaeuft-5G', bssid: 'AA:BB:CC:11:22:34', channel: 36, rssi: -58, band: '5 GHz' },
],
};
},
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> = {};
for (const oid of opts.oids) values[oid] = String(Math.floor(Math.random() * 1000));
return { values };
},
async traceroute() {
return {
hops: [
{ ttl: 1, ip: '192.168.1.1', ms: rnd(1, 3) },
{ ttl: 2, ip: '10.0.0.1', ms: rnd(8, 15) },
{ ttl: 3, ip: '8.8.8.8', ms: rnd(15, 30) },
],
};
},
async throughput() {
return { downMbps: rnd(80, 940), upMbps: rnd(40, 500) };
},
async startStressTest() {
return { runId: 'mock-run' };
},
async stopStressTest() {
return { samples: 120, lossPct: rnd(0, 2), avgMs: rnd(3, 10), maxMs: rnd(20, 90) };
},
};
/** Aktive Scanner-Implementierung: nativ auf dem Gerät, Mock im Browser */
export const scanner: NetDiagScannerPlugin = Capacitor.isNativePlatform() ? native : mock;