""" DNS Plugin — monitors DNS resolution, configured resolvers, and query health. Publishes to: dns.resolvers — list of configured nameservers dns.health — last query round-trip time and result dns.query.result — result of a DNS query """ from __future__ import annotations import asyncio import logging import socket import time from typing import Any, Dict, List, Optional from plugins.base import BasePlugin log = logging.getLogger(__name__) DEFAULT_POLL_INTERVAL = 30.0 HEALTH_CHECK_HOST = "one.one.one.one" def _read_resolvers() -> List[str]: """Parse /etc/resolv.conf for nameserver entries.""" resolvers: List[str] = [] try: with open("/etc/resolv.conf") as fh: for line in fh: line = line.strip() if line.startswith("nameserver"): parts = line.split() if len(parts) >= 2: resolvers.append(parts[1]) except OSError: pass return resolvers def _check_dns_health(host: str) -> Dict: """Perform a synchronous DNS health check and return timing info.""" start = time.monotonic() error: Optional[str] = None resolved: Optional[str] = None try: resolved = socket.gethostbyname(host) except socket.gaierror as exc: error = str(exc) elapsed_ms = (time.monotonic() - start) * 1000.0 return { "host": host, "resolved": resolved, "elapsed_ms": round(elapsed_ms, 2), "error": error, "healthy": error is None, } class DNSPlugin(BasePlugin): """ Monitors DNS resolver configuration and performs periodic health checks. Publishes resolver info and query health to the event bus. """ name = "dns" version = "1.0.0" description = "Monitors DNS resolver configuration and query health" tags = ["network", "dns"] def __init__( self, bus=None, state=None, poll_interval: float = DEFAULT_POLL_INTERVAL, health_host: str = HEALTH_CHECK_HOST, ) -> None: super().__init__(bus=bus, state=state) self._poll_interval = poll_interval self._health_host = health_host self._task: asyncio.Task | None = None self._running = False def on_load(self) -> None: super().on_load() self.subscribe("dns.query") # allow other plugins to request queries self._running = True try: loop = asyncio.get_running_loop() self._task = loop.create_task(self._poll_loop()) except RuntimeError: self._log.debug("no running event loop at load time; task deferred") def on_unload(self) -> None: self._running = False if self._task and not self._task.done(): self._task.cancel() super().on_unload() async def _poll_loop(self) -> None: self._log.debug("dns poll loop started (interval=%.1fs)", self._poll_interval) while self._running: try: resolvers = await asyncio.to_thread(_read_resolvers) self.state_set("dns.resolvers", resolvers) if self._bus: await self._bus.publish("dns.resolvers", {"nameservers": resolvers}) health = await asyncio.to_thread(_check_dns_health, self._health_host) self.state_set("dns.health", health) if self._bus: await self._bus.publish("dns.health", health) except asyncio.CancelledError: break except Exception as exc: self._log.error("dns poll error: %s", exc) await asyncio.sleep(self._poll_interval) self._log.debug("dns poll loop stopped") def on_event(self, topic: str, data: Any) -> None: if topic == "dns.query" and isinstance(data, dict): host = data.get("host", "") if host: asyncio.ensure_future(self._resolve_and_publish(host)) async def _resolve_and_publish(self, host: str) -> None: result = await asyncio.to_thread(_check_dns_health, host) if self._bus: await self._bus.publish("dns.query.result", result)