Später · 21 Neue Geräte im Heimnetz (Nachbartabelle des Proxmox-Hosts statt AdGuard)
AdGuard sieht in diesem Netz fast nur die Fritzbox, die die Anfragen der meisten Geräte weiterreicht (nachgesehen 26.09.). Deshalb: - Ausführer: alle 30 min Ping an jede Adresse des /24 der Brücke vmbr0, dann ip neigh (MAC, IP, Rückwärtsname); nur lesend, im Bericht als host.netz (größer als /24 wird nicht abgesucht) - services/homelab/netzgeraete.py: Register netz-bekannt.json; erster Abgleich = alles bekannt, Gäste des Hosts und neue MACs mit bekanntem Namen (Handys mit zufälliger MAC) gelten als bekannt; Neues einmal gemeldet - Wächter: gelber Hinweis „Neues Gerät im Netz“ mit „Ist bekannt“; GET /api/homelab/netz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
241e91c03f
commit
5325544d89
@@ -54,17 +54,20 @@ Aufruf: ausfuehrer.py Dauerbetrieb (systemd)
|
||||
import argparse
|
||||
import base64
|
||||
import glob
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timezone
|
||||
|
||||
KONFIG = os.environ.get("MC2_AUSFUEHRER_KONFIG", "/etc/mc2-ausfuehrer.json")
|
||||
@@ -1108,10 +1111,87 @@ def bericht() -> dict:
|
||||
host["zertifikate"] = _zertifikate(gaeste)
|
||||
except Exception as exc:
|
||||
host["zertifikate_fehler"] = str(exc)[:200]
|
||||
try:
|
||||
host["netz"] = _netz_stand()
|
||||
except Exception as exc:
|
||||
host["netz_fehler"] = str(exc)[:200]
|
||||
return {"zeit": datetime.now(timezone.utc).isoformat(timespec="seconds"), "host": host,
|
||||
"gaeste": sorted(gaeste, key=lambda g: g["vmid"])}
|
||||
|
||||
|
||||
# --- Geräte im Heimnetz (seit 26.09.2026, nur lesend) ----------------------------------------------------
|
||||
# AdGuard sieht in diesem Netz fast nur die Fritzbox (sie reicht die Anfragen der meisten Geräte weiter). Wer im
|
||||
# Heimnetz ist, verrät deshalb die Nachbartabelle dieses Hosts: Alle NETZ_ALLE_S ein Ping an jede Adresse des Netzes
|
||||
# der Brücke (antwortet ein Gerät nicht auf Ping, beantwortet es trotzdem die ARP-Anfrage davor), dann `ip neigh`.
|
||||
# Namen kommen aus der Rückwärtsauflösung (die Fritzbox kennt ihre DHCP-Namen). Größer als /24 wird nicht abgesucht.
|
||||
|
||||
NETZ_ALLE_S = 1800
|
||||
NETZ_BRUECKE = "vmbr0"
|
||||
_netz: dict = {"zeit": 0.0, "stand": None}
|
||||
|
||||
|
||||
def netz_der_bruecke(addr_json: str) -> ipaddress.IPv4Network | None:
|
||||
"""Das IPv4-Netz der Brücke aus `ip -j -4 addr show dev vmbr0`; None, wenn keins oder größer als /24."""
|
||||
try:
|
||||
for eintrag in json.loads(addr_json or "[]"):
|
||||
for a in eintrag.get("addr_info") or []:
|
||||
if a.get("family") in (None, "inet") and a.get("local") and int(a.get("prefixlen") or 0) >= 24:
|
||||
return ipaddress.ip_interface(f"{a['local']}/{a['prefixlen']}").network
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def nachbarn(neigh_json: str, netz: ipaddress.IPv4Network) -> list[dict]:
|
||||
"""Die Einträge von `ip -j neigh`, die eine MAC haben und im Netz liegen (FAILED/INCOMPLETE fallen weg)."""
|
||||
geraete = {}
|
||||
try:
|
||||
eintraege = json.loads(neigh_json or "[]")
|
||||
except ValueError:
|
||||
return []
|
||||
for e in eintraege:
|
||||
zustand = set(e.get("state") or [])
|
||||
mac = str(e.get("lladdr") or "").lower()
|
||||
try:
|
||||
ip = ipaddress.ip_address(str(e.get("dst")))
|
||||
except ValueError:
|
||||
continue
|
||||
if not mac or zustand & {"FAILED", "INCOMPLETE"} or ip.version != 4 or ip not in netz:
|
||||
continue
|
||||
geraete[mac] = {"ip": str(ip), "mac": mac}
|
||||
return sorted(geraete.values(), key=lambda g: ipaddress.ip_address(g["ip"]))
|
||||
|
||||
|
||||
def _name(ip: str) -> str | None:
|
||||
try:
|
||||
return socket.gethostbyaddr(ip)[0]
|
||||
except (OSError, UnicodeError):
|
||||
return None
|
||||
|
||||
|
||||
def netz_geraete() -> dict:
|
||||
code, addr = _laufen(["ip", "-j", "-4", "addr", "show", "dev", NETZ_BRUECKE], 10)
|
||||
netz = netz_der_bruecke(addr) if code == 0 else None
|
||||
if netz is None:
|
||||
return {"zeit": time.time(), "netz": None, "geraete": [], "grund": f"kein /24-Netz an {NETZ_BRUECKE}"}
|
||||
with ThreadPoolExecutor(max_workers=64) as pool:
|
||||
list(pool.map(lambda ip: _laufen(["ping", "-c", "1", "-W", "1", "-q", str(ip)], 5), netz.hosts()))
|
||||
code, neigh = _laufen(["ip", "-j", "neigh", "show", "dev", NETZ_BRUECKE], 10)
|
||||
geraete = nachbarn(neigh, netz) if code == 0 else []
|
||||
with ThreadPoolExecutor(max_workers=16) as pool:
|
||||
for g, name in zip(geraete, pool.map(_name, [g["ip"] for g in geraete]), strict=True):
|
||||
g["name"] = name
|
||||
return {"zeit": time.time(), "netz": str(netz), "geraete": geraete}
|
||||
|
||||
|
||||
def _netz_stand() -> dict:
|
||||
"""Höchstens alle NETZ_ALLE_S neu absuchen; dazwischen der letzte Stand."""
|
||||
if _netz["stand"] is None or time.time() - _netz["zeit"] >= NETZ_ALLE_S:
|
||||
_netz["stand"] = netz_geraete()
|
||||
_netz["zeit"] = time.time()
|
||||
return _netz["stand"]
|
||||
|
||||
|
||||
# --- Aufträge (feste Liste) ---------------------------------------------------------------
|
||||
|
||||
def _gast_pruefen(vmid: object) -> tuple[str, int]:
|
||||
|
||||
Reference in New Issue
Block a user