Phase 0: doppelte Plugin-Quelle aufgeloest, falsche Doku-Zusagen korrigiert

Aufraeumen vor den eigentlichen Korrekturen — ohne Verhaltensaenderung am Code.

Doppelte Plugin-Quelle:
Die drei Kotlin-Dateien lagen byte-identisch in native-plugin/ UND in
android/app/src/main/java/de/data_it_solution/netdiag/. Gebaut wurde immer nur die
Kopie unter android/ — eine Aenderung an der falschen Datei sah deshalb aus wie
"der Fix wirkt nicht". native-plugin/ enthaelt jetzt nur noch die Anleitung, die
auf den echten Ort verweist.

Doku sagte drei Dinge, die nicht stimmen:
- stresstest.ts behauptete "laeuft nativ als Foreground-Service". Tut es nicht —
  im Plugin steht an der Stelle nur ein Kommentar, dass man einen ergaenzen
  sollte. Bei ausgeschaltetem Display friert Android den Lauf ein.
- iperf.ts/scanner.ts/README nannten den Durchsatztest "iperf-kompatibel". Er
  spricht kein iperf3-Protokoll (kein Cookie, kein Parameterblock) und scheitert
  gegen einen echten iperf3-Server.
- ipscan.ts beschrieb sich als "ARP + Ping + Namen". Gefunden wird nur, wer auf
  Ping antwortet oder sich per mDNS meldet; die ARP-Tabelle wird lediglich
  nachgeschlagen und ist ab Android 10 meist gar nicht lesbar.

Statt der falschen Zusagen stehen dort jetzt die tatsaechlichen Grenzen plus
Verweis auf die Phase, die sie behebt.

README: DHCP-Check und WLAN-Scan als Werkzeuge entfernt (sind nicht mehr in
tools/index.ts registriert, tauchen aber in alten Protokollen noch auf).

Geprueft: vite build ok, svelte-check 0 Fehler (2 vorbestehende Warnungen).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-08-14 18:39:40 +02:00
parent 780115c67e
commit 881ca7956f
9 changed files with 80 additions and 1825 deletions

View file

@ -24,7 +24,7 @@ Vite-Proxy an den Dolibarr-Testserver (`192.168.155.11`, siehe `vite.config.ts`)
```bash
npm run build
npx cap add android # einmalig
# native-plugin/ einbinden -> siehe native-plugin/README.md
# android/ liegt komplett im Repo — cap sync kopiert nur die Web-Assets
npx cap sync android
npx cap open android
```
@ -45,12 +45,13 @@ src/lib/
updater.ts APK-Auto-Update-Prüfung (KB #363)
tools/ erweiterbare Tool-Plattform
index.ts Registry — neues Tool hier eintragen
netzwerk/ IP-Scan, Port, Ping, WLAN, DHCP, SNMP, Traceroute, Stress
netzwerk/ IP-Scan, Port, Ping, IP-Konflikt, SNMP, Traceroute, Stress
internet/ Durchsatz-Test
telefonie/ (folgt: SIP, FreePBX, RTP)
src/routes/
login/ auftraege/ kunden/ protokoll/[id]/ einstellungen/
native-plugin/ Kotlin-Plugin NetDiagScanner (+ Integrationsanleitung)
android/ natives Android-Projekt inkl. Kotlin-Plugin (einzige Quelle)
native-plugin/ nur noch Anleitung/Hinweise zum Plugin
```
## Neues Tool hinzufügen

View file

@ -1,81 +0,0 @@
package de.data_it_solution.netdiag
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
/**
* Schlanker Vordergrund-Dienst für den Geräte-Monitor.
*
* Er hält den App-Prozess am Leben, solange die Überwachung läuft damit
* Android die Mess-Schleife bei ausgeschaltetem Display oder App-Wechsel nicht
* beendet. Die eigentliche Ping-Logik läuft im NetDiagScannerPlugin; dieser
* Dienst zeigt nur die dauerhafte Benachrichtigung.
*/
class MonitorService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val text = intent?.getStringExtra(EXTRA_TEXT) ?: "Geräte-Überwachung läuft"
val notification = buildNotification(text)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIF_ID, notification)
}
return START_STICKY
}
private fun buildNotification(text: String): Notification {
ensureChannel(this)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("NetDiag — Geräte-Monitor")
.setContentText(text)
.setSmallIcon(android.R.drawable.ic_menu_compass)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
companion object {
const val CHANNEL_ID = "netdiag-monitor"
const val NOTIF_ID = 4711
const val EXTRA_TEXT = "text"
/** Benachrichtigungskanal anlegen (idempotent) */
fun ensureChannel(ctx: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val mgr = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (mgr.getNotificationChannel(CHANNEL_ID) == null) {
mgr.createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
"Geräte-Monitor",
NotificationManager.IMPORTANCE_LOW,
),
)
}
}
fun start(ctx: Context, text: String) {
val i = Intent(ctx, MonitorService::class.java).putExtra(EXTRA_TEXT, text)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ctx.startForegroundService(i)
} else {
ctx.startService(i)
}
}
fun stop(ctx: Context) {
ctx.stopService(Intent(ctx, MonitorService::class.java))
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,45 @@
# Natives Scan-Plugin `NetDiagScanner`
Der WebView kann keine Raw-Sockets/ICMP/ARP — die eigentliche Netzwerk-Messung
läuft in diesem nativen Android-Plugin (Kotlin).
läuft in einem nativen Android-Plugin (Kotlin).
## Dateien
## Wo der Code liegt
- `NetDiagScannerPlugin.kt` — Capacitor-Plugin mit allen Scan-Methoden
- `Snmp.kt` — minimaler SNMP-v2c-GET-Client
**Einzige Quelle:**
## Integration (einmalig, nach `npx cap add android`)
```
android/app/src/main/java/de/data_it_solution/netdiag/
├── NetDiagScannerPlugin.kt Capacitor-Plugin mit allen Scan-Methoden
├── MonitorService.kt Vordergrund-Dienst des Geräte-Monitors
├── Snmp.kt minimaler SNMP-v2c-GET-Client
└── MainActivity.kt registriert das Plugin
```
1. **Android-Projekt erzeugen** (falls noch nicht vorhanden):
```bash
npm install
npm run build
npx cap add android
```
> **Bis 08/2026 lagen dieselben Dateien zusätzlich hier im Ordner `native-plugin/`.**
> Gebaut wurde aber immer nur die Kopie unter `android/` — eine Änderung an der
> falschen Datei sah deshalb aus wie „der Fix wirkt nicht". Die Doppelablage ist
> aufgelöst; dieser Ordner enthält nur noch diese Anleitung.
2. **Plugin-Dateien kopieren** nach:
```
android/app/src/main/java/de/data_it_solution/netdiag/
├── NetDiagScannerPlugin.kt
└── Snmp.kt
```
Das Verzeichnis `android/` liegt vollständig im Repo (Kotlin, Manifest, Gradle,
Signing-Config sind dort fest eingerichtet). `npx cap sync android` kopiert nur die
Web-Assets hinein und aktualisiert die Plugin-Liste — es überschreibt **keinen**
nativen Code.
3. **Plugin registrieren** in `android/app/src/main/java/de/data_it_solution/netdiag/MainActivity.kt`:
## Ein neues Plugin-Kommando ergänzen
1. Methode in `NetDiagScannerPlugin.kt` mit `@PluginMethod` anlegen.
2. Im TS-Wrapper `src/lib/scanner.ts` die Signatur ergänzen (inkl. Browser-Mock,
sonst lässt sich die Seite im Dev-Server nicht mehr bedienen).
3. Werkzeug unter `src/lib/tools/<kategorie>/` anlegen und in `tools/index.ts` eintragen.
## Erstmalige Einrichtung (nur relevant, wenn `android/` neu erzeugt wird)
Normalerweise **nicht nötig**`android/` ist eingecheckt. Falls doch:
1. `npm install && npx vite build && npx cap add android`
2. Kotlin-Dateien nach `android/app/src/main/java/de/data_it_solution/netdiag/` legen
3. Plugin in `MainActivity.kt` registrieren:
```kotlin
package de.data_it_solution.netdiag
import android.os.Bundle
import com.getcapacitor.BridgeActivity
class MainActivity : BridgeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
registerPlugin(NetDiagScannerPlugin::class.java)
@ -38,35 +47,37 @@ läuft in diesem nativen Android-Plugin (Kotlin).
}
}
```
4. **Berechtigungen** in `android/app/src/main/AndroidManifest.xml` (innerhalb `<manifest>`):
4. **`kotlin-android` im Gradle-Build aktivieren** — sonst werden die `.kt`-Dateien
stillschweigend ignoriert, der Build ist grün und das Plugin fehlt im APK
(„plugin is not implemented on android", siehe KB #660).
5. Berechtigungen im `AndroidManifest.xml` (Stand heute):
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
```
`ACCESS_FINE_LOCATION` ist Pflicht, damit Android WLAN-Scan-Ergebnisse liefert.
5. **Kotlin-Coroutines** sicherstellen — in `android/app/build.gradle`:
```gradle
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"
}
```
## Bekannte Einschränkungen
- **ARP/MAC**: `/proc/net/arp` ist ab Android 10 teils nicht mehr lesbar — dann
bleiben MAC/Hersteller leer; Geräte werden trotzdem über Ping/Hostname erkannt.
- **Hersteller-Lookup**: nur kleiner OUI-Auszug eingebaut. Für volle Abdeckung
die IEEE-OUI-Datei als Asset einbinden und in `ouiVendor()` nutzen.
- **DHCP-Discover**: Best-effort — Port 68 kann vom System-DHCP-Client belegt sein.
- **Durchsatz-Test**: benötigt eine TCP-Sink/Source-Gegenstelle (2. Gerät bzw.
iperf3-kompatibler Server). Misst sonst nur Fehlversuche.
- **Stresstest**: für lange Läufe sollte ein Foreground-Service ergänzt werden,
sonst kann Android den Prozess im Hintergrund beenden.
- **ARP/MAC**: `/proc/net/arp` ist ab Android 10 in der Regel nicht mehr lesbar —
dann bleiben MAC und Hersteller leer. Das ist der Grund, warum in den bisherigen
Kunden-PDFs die Spalten MAC/Hersteller leer sind.
- **Hersteller-Lookup**: nur kleiner OUI-Auszug eingebaut. Für volle Abdeckung die
IEEE-OUI-Datei als Asset einbinden und in `ouiVendor()` nutzen.
- **Durchsatz-Test**: spricht **kein** iperf3-Protokoll, sondern schreibt/liest roh
auf einen TCP-Socket. Gegen einen echten iperf3-Server schlägt er fehl; er braucht
eine schlichte TCP-Sink/Source-Gegenstelle. Umbau steht in
`../../ROADMAP_UMSETZUNG.md` (Phase 1).
- **Stresstest**: läuft **nicht** in einem Foreground-Service — bei ausgeschaltetem
Display friert Android den Lauf ein. Umbau in Phase 2.
- **IP-Konflikt-Prüfung**: hängt an der ARP-Tabelle und ist damit auf aktuellen
Android-Versionen praktisch nicht durchführbar.
- **PoE-/Strommessung**: bewusst nicht enthalten (Hardware-Grenze) — späteres Modul.

View file

@ -1,138 +0,0 @@
package de.data_it_solution.netdiag
import java.io.ByteArrayOutputStream
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
/**
* Minimaler SNMP-v2c-GET-Client.
*
* Reicht für das Auslesen einzelner OIDs (Link-Speed, Fehlerzähler) von
* gemanagten Switches. Implementiert nur so viel BER-Kodierung wie nötig.
*/
object Snmp {
/**
* Eine OID per SNMP v2c GET abfragen.
*
* @return Wert als String oder null bei Fehler/Timeout
*/
fun get(host: String, community: String, oid: String): String? {
return try {
val request = buildGetRequest(community, oid)
val socket = DatagramSocket()
socket.soTimeout = 2500
socket.use {
it.send(DatagramPacket(request, request.size, InetAddress.getByName(host), 161))
val buf = ByteArray(2048)
val resp = DatagramPacket(buf, buf.size)
it.receive(resp)
parseFirstValue(buf, resp.length)
}
} catch (e: Exception) {
null
}
}
/* ---- BER-Kodierung ---- */
private fun tlv(tag: Int, value: ByteArray): ByteArray {
val out = ByteArrayOutputStream()
out.write(tag)
when {
value.size < 0x80 -> out.write(value.size)
value.size < 0x100 -> { out.write(0x81); out.write(value.size) }
else -> { out.write(0x82); out.write(value.size shr 8); out.write(value.size and 0xFF) }
}
out.write(value)
return out.toByteArray()
}
private fun integer(v: Int): ByteArray {
val bytes = when {
v == 0 -> byteArrayOf(0)
v < 0x80 -> byteArrayOf(v.toByte())
v < 0x8000 -> byteArrayOf((v shr 8).toByte(), v.toByte())
else -> byteArrayOf((v shr 24).toByte(), (v shr 16).toByte(), (v shr 8).toByte(), v.toByte())
}
return tlv(0x02, bytes)
}
private fun octetString(s: String): ByteArray = tlv(0x04, s.toByteArray())
private fun oid(o: String): ByteArray {
val parts = o.trim().trimStart('.').split('.').map { it.toInt() }
val out = ByteArrayOutputStream()
out.write(parts[0] * 40 + parts[1]) // erste zwei Subidentifier zusammengefasst
for (i in 2 until parts.size) {
var v = parts[i]
if (v < 0x80) {
out.write(v)
} else {
val stack = ArrayDeque<Int>()
stack.addFirst(v and 0x7F)
v = v shr 7
while (v > 0) { stack.addFirst((v and 0x7F) or 0x80); v = v shr 7 }
stack.forEach { out.write(it) }
}
}
return tlv(0x06, out.toByteArray())
}
private fun buildGetRequest(community: String, oidStr: String): ByteArray {
val varbind = tlv(0x30, oid(oidStr) + tlv(0x05, ByteArray(0))) // OID + NULL
val varbindList = tlv(0x30, varbind)
val requestId = (System.currentTimeMillis() and 0x7FFF).toInt()
val pdu = tlv(
0xA0, // GetRequest-PDU
integer(requestId) + integer(0) + integer(0) + varbindList,
)
val message = tlv(
0x30,
integer(1) + octetString(community) + pdu, // version 1 = SNMPv2c
)
return message
}
/* ---- Antwort parsen: ersten Variablen-Wert herausziehen ---- */
private fun parseFirstValue(buf: ByteArray, len: Int): String? {
var i = 0
// Durch die Struktur navigieren bis zum ersten primitiven Wert nach einer OID
var lastWasOid = false
while (i < len) {
val tag = buf[i].toInt() and 0xFF
i++
if (i >= len) break
var l = buf[i].toInt() and 0xFF
i++
if (l and 0x80 != 0) {
val n = l and 0x7F
l = 0
for (k in 0 until n) { l = (l shl 8) or (buf[i].toInt() and 0xFF); i++ }
}
// Konstruierte Typen (SEQUENCE, PDU) aufsteigen
if (tag == 0x30 || tag == 0xA2 || tag == 0xA0) continue
if (tag == 0x06) { lastWasOid = true; i += l; continue }
if (lastWasOid) {
return when (tag) {
0x02, 0x41, 0x42, 0x43, 0x44, 0x46 -> { // INTEGER, Counter, Gauge, TimeTicks ...
var v = 0L
for (k in 0 until l) v = (v shl 8) or (buf[i + k].toLong() and 0xFF)
v.toString()
}
0x04 -> String(buf, i, l) // OCTET STRING
0x05 -> "" // NULL -> kein Wert
else -> {
val sb = StringBuilder()
for (k in 0 until l) sb.append("%02X".format(buf[i + k]))
sb.toString()
}
}
}
i += l
}
return null
}
}

View file

@ -126,9 +126,9 @@ export interface NetDiagScannerPlugin {
}>;
/** Traceroute zu einem Host */
traceroute(opts: { host: string }): Promise<{ hops: TracerouteHop[] }>;
/** Durchsatztest gegen eine Gegenstelle (iperf-kompatibel) */
/** Durchsatztest gegen eine TCP-Sink/Source-Gegenstelle (KEIN iperf3-Protokoll) */
throughput(opts: { host: string; port: number; durationSec: number }): Promise<ThroughputResult>;
/** Dauer-/Stresstest starten (läuft als Foreground-Service) */
/** Dauer-/Stresstest starten (läuft NICHT im Foreground-Service, s. stresstest.ts) */
startStressTest(opts: { host: string; durationSec: number }): Promise<{ runId: string }>;
/** Laufenden Stresstest beenden und Ergebnis holen */
stopStressTest(opts: { runId: string }): Promise<{

View file

@ -1,7 +1,12 @@
/**
* Tool: Durchsatz-Test misst die Bandbreite gegen eine Gegenstelle.
*
* Benötigt eine iperf-kompatible Gegenstelle (2. Gerät oder iperf3-Server).
* ACHTUNG: Der Test spricht **kein** iperf3-Protokoll. Nativ wird schlicht auf
* einen TCP-Socket geschrieben bzw. von ihm gelesen ohne Cookie, ohne
* Parameterblock, ohne Zustandsautomat. Gegen einen echten iperf3-Server bricht
* die Verbindung deshalb ab; er braucht eine einfache TCP-Sink/Source-Gegenstelle.
* Umbau (eigenes Protokoll oder mitgeliefertes iperf3-Binary) steht in
* ROADMAP_UMSETZUNG.md Phase 1.
*/
import { scanner } from '../../scanner';

View file

@ -31,7 +31,11 @@ export const ipScanTool: Tool = {
category: 'netzwerk',
name: 'IP-Scanner',
icon: 'radar',
description: 'Sucht alle Geräte im Netzbereich (ARP + Ping + Namen).',
// Ehrliche Beschreibung: gefunden wird ein Gerät nur, wenn es auf Ping
// antwortet — oder wenn es sich per mDNS meldet. Die ARP-Tabelle wird
// lediglich nachgeschlagen, um MAC/Hersteller zu ergänzen, und ist ab
// Android 10 meist gar nicht lesbar. Mehrgleisige Suche: Phase 3.
description: 'Sucht Geräte, die auf Ping antworten oder sich per mDNS melden.',
scope: 'protocol',
params: [
{

View file

@ -1,7 +1,14 @@
/**
* Tool: Dauer-/Stresstest Langzeitmessung von Verlust und Latenz.
*
* Läuft nativ als Foreground-Service, damit Android den Lauf nicht beendet.
* ACHTUNG, bekannte Schwächen (Umbau in ROADMAP_UMSETZUNG.md Phase 2):
* - Der Lauf hängt an einer nackten Coroutine im Plugin, NICHT an einem
* Foreground-Service. Bei ausgeschaltetem Display friert Android ihn ein.
* - Die native Schleife hat kein festes Intervall die Zahl der Messpunkte
* hängt davon ab, wie schnell das Ziel antwortet (guter Link ~250 Punkte in
* 5 min, Totalausfall ~38). Sie ist damit keine Messgröße.
* - Es wird keine Zeitreihe gespeichert, nur Summen. Wann war die Störung?"
* lässt sich aus dem Ergebnis nicht beantworten.
* Die Dauer wird als Parameter vorgegeben; run() wartet auf das Ergebnis.
*/
@ -35,7 +42,9 @@ export const stressTestTool: Tool = {
const durationSec = Number(ctx.params.duration || 300);
const { runId } = await scanner.startStressTest({ host, durationSec });
// Auf das Ende des Laufs warten (Foreground-Service misst weiter)
// Auf das Ende des Laufs warten. Der Timer läuft in der WebView und wird von
// Chromium gedrosselt, sobald die Seite in den Hintergrund gerät — das Ergebnis
// kann dadurch später kommen als die angegebene Dauer.
await new Promise((r) => setTimeout(r, durationSec * 1000));
const res = await scanner.stopStressTest({ runId });