feat: Fingerprint-Attribute für Discovery-Scoring

Ergänzt DRIVER_NAME, HTTP_FINGERPRINT, SNMP_FINGERPRINT, SSH_FINGERPRINT,
PORT_SPECS und SNMP_OBJECT_ID_PREFIX gemäß docs/DISCOVERY_FINGERPRINTING.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-06-24 14:47:08 +02:00
co-authored by Claude Sonnet 4.6
parent 99e9431666
commit 9b65132949
+33 -27
View File
@@ -34,7 +34,7 @@ management methods (``load_merge_candidate``, ``commit_config``, ...).
from __future__ import annotations from __future__ import annotations
import socket import socket
from typing import Any, Dict, List, Optional from typing import Any
from fritzconnection import FritzConnection from fritzconnection import FritzConnection
from fritzconnection.core.exceptions import ( from fritzconnection.core.exceptions import (
@@ -45,7 +45,7 @@ from fritzconnection.core.exceptions import (
) )
from napalm.base.exceptions import ConnectionClosedException, ConnectionException from napalm.base.exceptions import ConnectionClosedException, ConnectionException
from napalm_device_types import ResidentialGatewayDriver from napalm_device_types import FingerprintRule, ResidentialGatewayDriver
from napalm_device_types.models import ( from napalm_device_types.models import (
HostDict, HostDict,
NATTranslationDict, NATTranslationDict,
@@ -66,6 +66,12 @@ class FritzBoxDriver(ResidentialGatewayDriver):
"""NAPALM driver for AVM FritzBox (read-only, TR-064).""" """NAPALM driver for AVM FritzBox (read-only, TR-064)."""
VENDOR = "AVM" VENDOR = "AVM"
DRIVER_NAME = "fritzbox"
HTTP_FINGERPRINT = [
FingerprintRule("fritz!box", weight=10.0, mandatory=True),
FingerprintRule("fritzbox", weight=4.0),
FingerprintRule("avm fritz", weight=4.0),
]
_WAN_IP_SERVICES = ("WANIPConnection1", "WANPPPConnection1") _WAN_IP_SERVICES = ("WANIPConnection1", "WANPPPConnection1")
_WLAN_SERVICES = ("WLANConfiguration1", "WLANConfiguration2", "WLANConfiguration3") _WLAN_SERVICES = ("WLANConfiguration1", "WLANConfiguration2", "WLANConfiguration3")
@@ -76,7 +82,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
username: str, username: str,
password: str, password: str,
timeout: int = 60, timeout: int = 60,
optional_args: Optional[Dict[str, Any]] = None, optional_args: dict[str, Any] | None = None,
) -> None: ) -> None:
self.hostname = hostname self.hostname = hostname
self.username = username self.username = username
@@ -91,7 +97,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
self.use_tls = bool(self.optional_args.get("use_tls", True)) self.use_tls = bool(self.optional_args.get("use_tls", True))
self.port = int(self.optional_args.get("port") or (49443 if self.use_tls else 49000)) self.port = int(self.optional_args.get("port") or (49443 if self.use_tls else 49000))
self.fc: Optional[FritzConnection] = None self.fc: FritzConnection | None = None
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Connection management # Connection management
@@ -121,7 +127,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
"""Drop the TR-064 connection.""" """Drop the TR-064 connection."""
self.fc = None self.fc = None
def is_alive(self) -> Dict[str, bool]: def is_alive(self) -> dict[str, bool]:
"""Return whether the connection is usable. """Return whether the connection is usable.
Performs a lightweight socket-level check without sending a full Performs a lightweight socket-level check without sending a full
@@ -140,7 +146,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
# Internal helpers # Internal helpers
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _call(self, service: str, action: str, **kwargs: Any) -> Dict[str, Any]: def _call(self, service: str, action: str, **kwargs: Any) -> dict[str, Any]:
"""Invoke a TR-064 SOAP action and return its output arguments. """Invoke a TR-064 SOAP action and return its output arguments.
:raises ConnectionClosedException: if called before :meth:`open`. :raises ConnectionClosedException: if called before :meth:`open`.
@@ -149,7 +155,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
raise ConnectionClosedException("Not connected – call open() first.") raise ConnectionClosedException("Not connected – call open() first.")
return self.fc.call_action(service, action, **kwargs) return self.fc.call_action(service, action, **kwargs)
def _wlan_services(self) -> List[str]: def _wlan_services(self) -> list[str]:
"""Return the WLAN service names actually present on this device.""" """Return the WLAN service names actually present on this device."""
if self.fc is None: if self.fc is None:
return [] return []
@@ -182,7 +188,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
# NAPALM standard getters # NAPALM standard getters
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def get_facts(self) -> Dict[str, Any]: def get_facts(self) -> dict[str, Any]:
"""Return a dictionary of general device facts. """Return a dictionary of general device facts.
Calls ``DeviceInfo1.GetInfo``. Calls ``DeviceInfo1.GetInfo``.
@@ -205,14 +211,14 @@ class FritzBoxDriver(ResidentialGatewayDriver):
"interface_list": interface_list, "interface_list": interface_list,
} }
def get_interfaces(self) -> Dict[str, Dict[str, Any]]: def get_interfaces(self) -> dict[str, dict[str, Any]]:
"""Return interface details keyed by interface name. """Return interface details keyed by interface name.
Combines ``LANEthernetInterfaceConfig1.GetInfo``, Combines ``LANEthernetInterfaceConfig1.GetInfo``,
``WANCommonInterfaceConfig1.GetCommonLinkProperties`` and ``WANCommonInterfaceConfig1.GetCommonLinkProperties`` and
``WLANConfiguration{1,2,3}.GetInfo``. ``WLANConfiguration{1,2,3}.GetInfo``.
""" """
interfaces: Dict[str, Dict[str, Any]] = {} interfaces: dict[str, dict[str, Any]] = {}
try: try:
lan = self._call("LANEthernetInterfaceConfig1", "GetInfo") lan = self._call("LANEthernetInterfaceConfig1", "GetInfo")
@@ -259,7 +265,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
return interfaces return interfaces
def get_interfaces_ip(self) -> Dict[str, Dict[str, Any]]: def get_interfaces_ip(self) -> dict[str, dict[str, Any]]:
"""Return IP addresses keyed by interface name. """Return IP addresses keyed by interface name.
WAN address comes from ``GetExternalIPAddress`` (and, if available, WAN address comes from ``GetExternalIPAddress`` (and, if available,
@@ -267,7 +273,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
service. The LAN address is the address used to reach the device service. The LAN address is the address used to reach the device
(TR-064 does not expose the router's own LAN IP generically). (TR-064 does not expose the router's own LAN IP generically).
""" """
result: Dict[str, Dict[str, Any]] = {} result: dict[str, dict[str, Any]] = {}
try: try:
service = self._wan_ip_service() service = self._wan_ip_service()
@@ -283,12 +289,12 @@ class FritzBoxDriver(ResidentialGatewayDriver):
result["lan"] = {"ipv4": {self.hostname: {"prefix_length": 24}}} result["lan"] = {"ipv4": {self.hostname: {"prefix_length": 24}}}
return result return result
def get_arp_table(self, vrf: str = "") -> List[Dict[str, Any]]: def get_arp_table(self, vrf: str = "") -> list[dict[str, Any]]:
"""Return the ARP table, derived from :meth:`get_hosts`. """Return the ARP table, derived from :meth:`get_hosts`.
Each entry contains: ``interface``, ``mac``, ``ip``, ``age``. Each entry contains: ``interface``, ``mac``, ``ip``, ``age``.
""" """
arp_table: List[Dict[str, Any]] = [] arp_table: list[dict[str, Any]] = []
for host in self.get_hosts(): for host in self.get_hosts():
if not host["is_active"] or not host["mac"] or not host["ip"]: if not host["is_active"] or not host["mac"] or not host["ip"]:
continue continue
@@ -342,14 +348,14 @@ class FritzBoxDriver(ResidentialGatewayDriver):
return result return result
def get_port_forwards(self) -> List[PortForwardDict]: def get_port_forwards(self) -> list[PortForwardDict]:
"""Return configured port forwarding rules. """Return configured port forwarding rules.
Iterates ``GetGenericPortMappingEntry`` on the active WAN connection Iterates ``GetGenericPortMappingEntry`` on the active WAN connection
service until the device reports an out-of-range index. service until the device reports an out-of-range index.
""" """
service = self._wan_ip_service() service = self._wan_ip_service()
forwards: List[PortForwardDict] = [] forwards: list[PortForwardDict] = []
index = 0 index = 0
while True: while True:
try: try:
@@ -373,7 +379,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
return forwards return forwards
def get_hosts(self) -> List[HostDict]: def get_hosts(self) -> list[HostDict]:
"""Return hosts known to the FritzBox. """Return hosts known to the FritzBox.
Iterates ``Hosts1.GetGenericHostEntry`` for Iterates ``Hosts1.GetGenericHostEntry`` for
@@ -386,7 +392,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
except _OPTIONAL_SERVICE_ERRORS: except _OPTIONAL_SERVICE_ERRORS:
return [] return []
hosts: List[HostDict] = [] hosts: list[HostDict] = []
for index in range(count): for index in range(count):
try: try:
entry = self._call("Hosts1", "GetGenericHostEntry", NewIndex=index) entry = self._call("Hosts1", "GetGenericHostEntry", NewIndex=index)
@@ -407,7 +413,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
return hosts return hosts
def get_nat_translations(self) -> List[NATTranslationDict]: def get_nat_translations(self) -> list[NATTranslationDict]:
"""Return NAT translation entries derived from active port forwards. """Return NAT translation entries derived from active port forwards.
TR-064 does not expose a live connection-tracking table; this TR-064 does not expose a live connection-tracking table; this
@@ -415,7 +421,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
rules, which is the closest equivalent FritzBox provides. rules, which is the closest equivalent FritzBox provides.
""" """
ext_ip = self._wan_external_ip() ext_ip = self._wan_external_ip()
translations: List[NATTranslationDict] = [] translations: list[NATTranslationDict] = []
for forward in self.get_port_forwards(): for forward in self.get_port_forwards():
if not forward["enabled"]: if not forward["enabled"]:
continue continue
@@ -432,7 +438,7 @@ class FritzBoxDriver(ResidentialGatewayDriver):
) )
return translations return translations
def get_vpn_tunnels(self) -> Dict[str, VPNTunnelDict]: def get_vpn_tunnels(self) -> dict[str, VPNTunnelDict]:
"""Return VPN tunnel status. """Return VPN tunnel status.
FritzOS does not expose VPN (IPsec/WireGuard) tunnel state through FritzOS does not expose VPN (IPsec/WireGuard) tunnel state through
@@ -440,13 +446,13 @@ class FritzBoxDriver(ResidentialGatewayDriver):
""" """
return {} return {}
def get_wireless_clients(self) -> List[WirelessClientDict]: def get_wireless_clients(self) -> list[WirelessClientDict]:
"""Return wireless clients associated with the built-in access point(s). """Return wireless clients associated with the built-in access point(s).
Iterates ``GetGenericAssociatedDeviceInfo`` for each Iterates ``GetGenericAssociatedDeviceInfo`` for each
``WLANConfiguration{1,2,3}`` service present on the device. ``WLANConfiguration{1,2,3}`` service present on the device.
""" """
clients: List[WirelessClientDict] = [] clients: list[WirelessClientDict] = []
for service in self._wlan_services(): for service in self._wlan_services():
try: try:
ssid = self._call(service, "GetInfo").get("NewSSID", "") ssid = self._call(service, "GetInfo").get("NewSSID", "")
@@ -481,13 +487,13 @@ class FritzBoxDriver(ResidentialGatewayDriver):
return clients return clients
def get_ssids(self) -> Dict[str, SSIDDict]: def get_ssids(self) -> dict[str, SSIDDict]:
"""Return configured wireless networks (SSIDs). """Return configured wireless networks (SSIDs).
Calls ``GetInfo`` and ``GetTotalAssociations`` on each Calls ``GetInfo`` and ``GetTotalAssociations`` on each
``WLANConfiguration{1,2,3}`` service present on the device. ``WLANConfiguration{1,2,3}`` service present on the device.
""" """
ssids: Dict[str, SSIDDict] = {} ssids: dict[str, SSIDDict] = {}
for service in self._wlan_services(): for service in self._wlan_services():
try: try:
info = self._call(service, "GetInfo") info = self._call(service, "GetInfo")
@@ -516,14 +522,14 @@ class FritzBoxDriver(ResidentialGatewayDriver):
return ssids return ssids
def get_radio_status(self) -> Dict[str, RadioStatusDict]: def get_radio_status(self) -> dict[str, RadioStatusDict]:
"""Return wireless radio status. """Return wireless radio status.
Calls ``GetInfo`` on each ``WLANConfiguration{1,2,3}`` service Calls ``GetInfo`` on each ``WLANConfiguration{1,2,3}`` service
present on the device. ``channel_width``, ``tx_power`` and present on the device. ``channel_width``, ``tx_power`` and
``frequency`` are not exposed via TR-064 and are reported as ``0``. ``frequency`` are not exposed via TR-064 and are reported as ``0``.
""" """
radios: Dict[str, RadioStatusDict] = {} radios: dict[str, RadioStatusDict] = {}
for service in self._wlan_services(): for service in self._wlan_services():
try: try:
info = self._call(service, "GetInfo") info = self._call(service, "GetInfo")