Source code for netgear_switch.protocols.nsdp.client
"""Shared NSDP transport seam: error, result check, and client Protocols.
Pure and transport-agnostic (mirrors ``protocols/snmp/client.py``). ``NsdpError``
lives here beside the protocol rather than in ``errors.py``, matching the
``SnmpError`` precedent; both subclass the shared ``NetgearSwitchError`` base so
callers can still catch the library-wide root.
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Protocol
from ...errors import NetgearSwitchError
from .write import RESULT_SUCCESS
if TYPE_CHECKING:
from .protocol import NSDPPacket, Tag, TLVEntry
[docs]
class NsdpError(NetgearSwitchError):
"""An NSDP transport operation failed (timeout, malformed, bad password)."""
[docs]
def first_tlv_value(packet: NSDPPacket, tag: Tag) -> bytes | None:
"""Return the value of the first TLV with ``tag``, or ``None`` if absent."""
for entry in packet.tlvs:
if entry.tag == tag:
return entry.value
return None
[docs]
def read_interface_mac(interface: str) -> bytes:
"""Read a network interface's 6-byte MAC from sysfs (Linux)."""
text = Path(f"/sys/class/net/{interface}/address").read_text().strip()
raw = bytes.fromhex(text.replace(":", ""))
if len(raw) != 6:
raise NsdpError(f"interface {interface!r} MAC is not 6 bytes: {text!r}")
return raw
def _attr_name(tag: int) -> str:
"""Name the TLV tag the switch blamed, for the error message."""
from .protocol import Tag as _Tag
try:
return f"{_Tag(tag).name} (0x{tag:04x})"
except ValueError:
return f"tag 0x{tag:04x}"
[docs]
def check_result(packet: NSDPPacket) -> None:
"""Raise ``NsdpError`` unless the response reports success (result 0x0000).
The 2-byte ``result`` is (error-byte << 8 | unk1); the error CODE alone is
``result >> 8`` (``NSDPPacket.error_code``). Codes seen on real rejections:
3 = read-only, 4 = write-only, 7 = v1 denial, 13 = write auth refused,
14 = write lockout.
Every message NAMES the TLV tag the switch blamed (header bytes 4-5),
because the switch tells us and a caller cannot debug "the request failed"
otherwise (principle 1). That blamed tag is also what separates the two
causes of error 13: attr 0x000A (ATTR_PASSWORD) means a v1 XOR password was
offered to a firmware that only accepts the v2 salted challenge-response --
a WIRING problem, not a credential one, and an operator told "bad password"
there rotates a credential that was never wrong. Error 13 on any other attr
really is a bad password: this library IMPLEMENTS v2 auth
(``auth.auth_v2_password``, live-verified on a GS110EMX), so a rejected
token means the password is wrong or the salt it was folded against
went stale.
"""
if packet.result == RESULT_SUCCESS:
return
from .protocol import (
ERROR_AUTH_REJECTED,
ERROR_DENIED,
ERROR_LOCKED,
ERROR_NAMES,
Tag,
)
code = packet.error_code
attr = packet.error_attr
blamed = _attr_name(attr) if attr else "no attribute"
detail = ERROR_NAMES.get(code, f"unknown error code {code}")
if code == ERROR_AUTH_REJECTED and attr == Tag.PASSWORD:
raise NsdpError(
f"NSDP write rejected at {blamed}: error {code} ({detail}) -- this "
"firmware refuses the v1 XOR password and requires the v2 salted "
"challenge-response, which this library implements; let the client "
"auto-detect the scheme (auth_scheme='auto') or force 'v2' rather "
"than 'v1'"
)
if code in (ERROR_DENIED, ERROR_AUTH_REJECTED):
raise NsdpError(
f"NSDP write rejected: bad password (error 0x{code:02x}) at {blamed}"
)
if code == ERROR_LOCKED:
raise NsdpError(
f"NSDP write locked out after repeated auth failures (error "
f"0x{code:02x}) at {blamed}; the switch goes silent for a cooldown "
"-- pace writes and retry"
)
raise NsdpError(
f"NSDP request failed with result 0x{packet.result:04x} "
f"(error {code}: {detail}) on {blamed}"
)
[docs]
class NsdpClient(Protocol):
"""Synchronous NSDP read client for a single switch."""
[docs]
def read(self, tags: list[Tag]) -> NSDPPacket: ...
[docs]
class NsdpWriteClient(NsdpClient, Protocol):
"""Synchronous NSDP read+write client for a single switch."""
[docs]
def write(self, tlvs: list[TLVEntry], *, password: str) -> NSDPPacket: ...
[docs]
class AsyncNsdpClient(Protocol):
"""Asynchronous NSDP read client for a single switch."""
[docs]
async def read(self, tags: list[Tag]) -> NSDPPacket: ...
[docs]
class AsyncNsdpWriteClient(AsyncNsdpClient, Protocol):
"""Asynchronous NSDP read+write client for a single switch."""
[docs]
async def write(self, tlvs: list[TLVEntry], *, password: str) -> NSDPPacket: ...