Skip to content

adguard plugin: DNS sinkhole per managed device - #29

Merged
luma-founder merged 1 commit into
mainfrom
adguard-plugin
May 9, 2026
Merged

luma-founder merged 1 commit into
mainfrom
adguard-plugin

Conversation

@luma-founder

@luma-founder luma-founder commented May 9, 2026 •

Copy link
Copy Markdown

Summary

  • First in-core plugin. When a managed user is locked, every device they own with a MAC gets its AdGuard client upstream set to 0.0.0.0 (configurable). On unlock, upstreams clear.
  • Auto-registers AdGuard clients on first lock — curfew is the source of truth for managed devices, so no separate push step is required to make locking work end-to-end. (Bulk hygiene push for unmanaged adult devices waits for the plugin actions surface from ADR-018.)
  • Idempotent on both legs (safety-net resync re-runs reconcile periodically). HTTP errors → ReconcileResult.error → audited as plugin.reconcile_failed per PLUGINS.md.
  • Per ADR-013's secret convention, AdGuard admin password is read from an env var named in config (password_env), never stored in state.sqlite.

Files

  • plugins/adguard/manifest.toml, plugins/adguard/plugin.py — plugin (Pydantic Config + httpx.AsyncClient with Basic auth).
  • core/tests/test_adguard_plugin.py — 18 unit tests using httpx.MockTransport (no respx dep needed).

Config

{
  "url": "http://adguard.lan",
  "username": "admin",
  "password_env": "ADGUARD_PASSWORD",
  "sinkhole_upstream": "0.0.0.0"
}

Test plan

  • just check clean (lint, mypy, all Python tests, web, build)
  • 18 unit tests cover: auto-register on lock, update on wrong upstream, idempotent skip when already sinkholed, MAC matching by any of a device's MACs, case-insensitive MAC lookup, multi-MAC consolidation into one client, unblock clears upstream / no-op when missing or already cleared, leaves non-sinkhole operator-set upstreams alone, empty-MAC and unmanaged devices skipped, no-devices short-circuit (no GET issued), AdGuard 5xx → ReconcileResult.error, custom sinkhole upstream, missing-env-var fails at __init__
  • Discovery sanity-checked locally (discover_plugins("plugins") returns adguard: AdGuard Home v1.0.0)
  • Live smoke against a real AdGuard Home (only useful if you have one running)

First in-core plugin (PLAN.md §"Plugin lifecycle"). Reconcile semantics:

- Block leg: for every managed device of the locked user with a known
  MAC, ensure an AdGuard client exists with that MAC and
  upstreams=[sinkhole_upstream] (default 0.0.0.0). Auto-registers on
  first lock — curfew is the source of truth, no separate push step
  required to make locking work end-to-end.
- Unblock leg: clear upstreams on AdGuard clients we sinkholed; leave
  any with operator-set non-default upstreams alone. No auto-create
  on this leg — there's nothing to clear if no client exists.

Both legs idempotent (safety-net resync re-runs reconcile periodically).
HTTP errors caught + converted to ReconcileResult.error so the runtime
audits as plugin.reconcile_failed rather than letting an exception
escape the reconcile boundary.

Per ADR-013's secret convention, the AdGuard admin password is read
from an env var named in config — never stored in state.sqlite.

Tests use httpx.MockTransport (no respx dep) and cover: auto-register,
update with wrong upstream, idempotent skip, MAC matching by any of a
device's MACs, case-insensitive MAC lookup, multi-MAC consolidation,
unblock paths, empty-mac/unmanaged skip, no-devices short-circuit
(saves the GET), 5xx → ReconcileResult.error, custom sinkhole_upstream,
missing-env-var KeyError at __init__.
@luma-founder
luma-founder merged commit b3f71a2 into main May 9, 2026
4 checks passed
@luma-founder
luma-founder deleted the adguard-plugin branch May 9, 2026 02:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants