- 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>
266 lines
8.3 KiB
TypeScript
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;
|