1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-02-14 23:19:37 +00:00
Files
supervisor/supervisor/dbus/resolved.py
Stefan Agner 6877a8b210 Add D-Bus tolerant enum base classes to prevent crashes on unknown values (#6545)
* Add D-Bus tolerant enum base classes to prevent crashes on unknown values

D-Bus services (systemd, NetworkManager, RAUC, UDisks2) can introduce
new enum values at any time via OS updates. Standard Python enum
construction raises ValueError for unknown values, which would crash
the Supervisor.

Introduce DBusStrEnum and DBusIntEnum base classes that use Python's
_missing_ hook to create pseudo-members for unknown values. These
pseudo-members pass isinstance checks (satisfying typeguard), preserve
the original value, don't pollute __members__, and report unknown
values to Sentry (deduplicated per class+value) for observability.

Migrate 17 D-Bus enums in dbus/const.py and udisks2/const.py to the
new base classes. Enums only sent TO D-Bus (StopUnitMode, StartUnitMode,
etc.) are left unchanged. Remove the manual try/except workaround in
NetworkInterface.type now that DBusIntEnum handles it automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add explicit enum conversions for systemd-resolved D-Bus properties

The resolved properties (dns_over_tls, dns_stub_listener, dnssec, llmnr,
multicast_dns, resolv_conf_mode) were returning raw string values from
D-Bus without converting to their declared enum types. This would fail
runtime type checking with typeguard.

Now safe to add explicit conversions since these enums use DBusStrEnum,
which tolerates unknown values from D-Bus without crashing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Avoid blocking I/O in D-Bus enum Sentry reporting

Move sentry_sdk.capture_message out of the event loop by adding a
fire_and_forget_capture_message helper that offloads the call to the
executor when a running loop is detected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Handle exceptions when reporting message to Sentry

* Narrow typing of reported values

Use str/int explicitly since that is what the two existing Enum classes
can actually report.

* Adjust test style

* Apply suggestions from code review

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:53:19 +01:00

187 lines
6.1 KiB
Python

"""D-Bus interface for systemd-resolved."""
from __future__ import annotations
import logging
from dbus_fast.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
from .const import (
DBUS_ATTR_CACHE_STATISTICS,
DBUS_ATTR_CURRENT_DNS_SERVER,
DBUS_ATTR_CURRENT_DNS_SERVER_EX,
DBUS_ATTR_DNS,
DBUS_ATTR_DNS_EX,
DBUS_ATTR_DNS_OVER_TLS,
DBUS_ATTR_DNS_STUB_LISTENER,
DBUS_ATTR_DNSSEC,
DBUS_ATTR_DNSSEC_NEGATIVE_TRUST_ANCHORS,
DBUS_ATTR_DNSSEC_STATISTICS,
DBUS_ATTR_DNSSEC_SUPPORTED,
DBUS_ATTR_DOMAINS,
DBUS_ATTR_FALLBACK_DNS,
DBUS_ATTR_FALLBACK_DNS_EX,
DBUS_ATTR_LLMNR,
DBUS_ATTR_LLMNR_HOSTNAME,
DBUS_ATTR_MULTICAST_DNS,
DBUS_ATTR_RESOLV_CONF_MODE,
DBUS_ATTR_TRANSACTION_STATISTICS,
DBUS_IFACE_RESOLVED_MANAGER,
DBUS_NAME_RESOLVED,
DBUS_OBJECT_RESOLVED,
DNSAddressFamily,
DNSOverTLSEnabled,
DNSSECValidation,
DNSStubListenerEnabled,
MulticastProtocolEnabled,
ResolvConfMode,
)
from .interface import DBusInterfaceProxy, dbus_property
_LOGGER: logging.Logger = logging.getLogger(__name__)
class Resolved(DBusInterfaceProxy):
"""Handle D-Bus interface for systemd-resolved.
https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html
"""
name: str = DBUS_NAME_RESOLVED
bus_name: str = DBUS_NAME_RESOLVED
object_path: str = DBUS_OBJECT_RESOLVED
properties_interface: str = DBUS_IFACE_RESOLVED_MANAGER
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
await super().connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to systemd-resolved.")
except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning(
"Host has no systemd-resolved support. DNS will not work correctly."
)
@property
@dbus_property
def cache_statistics(self) -> tuple[int, int, int] | None:
"""Return current cache entries and hits and misses since last reset."""
return self.properties[DBUS_ATTR_CACHE_STATISTICS]
@property
@dbus_property
def current_dns_server(
self,
) -> tuple[int, DNSAddressFamily, bytes] | None:
"""Return current DNS server."""
return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER]
@property
@dbus_property
def current_dns_server_ex(
self,
) -> tuple[int, DNSAddressFamily, bytes, int, str] | None:
"""Return current DNS server including port and server name."""
return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER_EX]
@property
@dbus_property
def dns(self) -> list[tuple[int, DNSAddressFamily, bytes]] | None:
"""Return DNS servers in use."""
return self.properties[DBUS_ATTR_DNS]
@property
@dbus_property
def dns_ex(self) -> list[tuple[int, DNSAddressFamily, bytes, int, str]] | None:
"""Return DNS servers in use including port and server name."""
return self.properties[DBUS_ATTR_DNS_EX]
@property
@dbus_property
def dns_over_tls(self) -> DNSOverTLSEnabled | None:
"""Return DNS over TLS enabled."""
return DNSOverTLSEnabled(self.properties[DBUS_ATTR_DNS_OVER_TLS])
@property
@dbus_property
def dns_stub_listener(self) -> DNSStubListenerEnabled | None:
"""Return DNS stub listener enabled on port 53."""
return DNSStubListenerEnabled(self.properties[DBUS_ATTR_DNS_STUB_LISTENER])
@property
@dbus_property
def dnssec(self) -> DNSSECValidation | None:
"""Return DNSSEC validation enforced."""
return DNSSECValidation(self.properties[DBUS_ATTR_DNSSEC])
@property
@dbus_property
def dnssec_negative_trust_anchors(self) -> list[str] | None:
"""Return DNSSEC negative trust anchors."""
return self.properties[DBUS_ATTR_DNSSEC_NEGATIVE_TRUST_ANCHORS]
@property
@dbus_property
def dnssec_statistics(self) -> tuple[int, int, int, int] | None:
"""Return Secure, insecure, bogus, and indeterminate DNSSEC validations since last reset."""
return self.properties[DBUS_ATTR_DNSSEC_STATISTICS]
@property
@dbus_property
def dnssec_supported(self) -> bool | None:
"""Return DNSSEC enabled and selected DNS servers support it."""
return self.properties[DBUS_ATTR_DNSSEC_SUPPORTED]
@property
@dbus_property
def domains(self) -> list[tuple[int, str, bool]] | None:
"""Return search and routing domains in use."""
return self.properties[DBUS_ATTR_DOMAINS]
@property
@dbus_property
def fallback_dns(self) -> list[tuple[int, DNSAddressFamily, bytes]] | None:
"""Return fallback DNS servers."""
return self.properties[DBUS_ATTR_FALLBACK_DNS]
@property
@dbus_property
def fallback_dns_ex(
self,
) -> list[tuple[int, DNSAddressFamily, bytes, int, str]] | None:
"""Return fallback DNS servers including port and server name."""
return self.properties[DBUS_ATTR_FALLBACK_DNS_EX]
@property
@dbus_property
def llmnr(self) -> MulticastProtocolEnabled | None:
"""Return LLMNR enabled."""
return MulticastProtocolEnabled(self.properties[DBUS_ATTR_LLMNR])
@property
@dbus_property
def llmnr_hostname(self) -> str | None:
"""Return LLMNR hostname on network."""
return self.properties[DBUS_ATTR_LLMNR_HOSTNAME]
@property
@dbus_property
def multicast_dns(self) -> MulticastProtocolEnabled | None:
"""Return MDNS enabled."""
return MulticastProtocolEnabled(self.properties[DBUS_ATTR_MULTICAST_DNS])
@property
@dbus_property
def resolv_conf_mode(self) -> ResolvConfMode | None:
"""Return how /etc/resolv.conf managed on host."""
return ResolvConfMode(self.properties[DBUS_ATTR_RESOLV_CONF_MODE])
@property
@dbus_property
def transaction_statistics(self) -> tuple[int, int] | None:
"""Return transactions processing and processed since last reset."""
return self.properties[DBUS_ATTR_TRANSACTION_STATISTICS]