Skip to content

AdGuard Home

AdGuard Home is read the same way as Pi-hole: the query log with the filter verdict becomes DNS events, the persistent and discovered clients become device names, and the hardware addresses AdGuard lists become identity anchors. It is a Free connector.

This page covers what differs from Pi-hole. Everything about what the connector emits, how the naming job ranks it and where it shows up is on the Pi-hole page and applies here with adguard in place of pihole.

AdGuard Home v0.107 or later, over its REST API:

Endpoint Purpose
/control/status Version and state, for the test connection
/control/querylog The query log, walked newest first with older_than
/control/stats AdGuard’s own counters for the statistics window
/control/clients Persistent clients (ids with address and MAC) and auto-discovered clients
/control/dhcp/status Leases, when AdGuard is the DHCP server

A query is blocked when AdGuard’s reason starts with Filtered; the reason is kept as recorded (FilteredBlackList: ||rule^). A client discovered through DHCP, or a lease AdGuard serves, gives a dhcp name; a persistent client and a client discovered through rDNS, the hosts file or ARP gives a resolver name. AdGuard on a platform without DHCP answers 501 on the DHCP endpoint, which the connector treats as not served, never as an error.

AdGuard Home authenticates with HTTP basic authentication and has no read-only role. Create a separate user — Settings → General settings, or users: in AdGuardHome.yaml — so the credential can be revoked on its own, and store both halves as credential files under the rules of UniFi, step 3:

Terminal window
sudo install -m 0640 -o root -g nyxtrace /dev/null /etc/ndr/secrets/adguard-username
sudo install -m 0640 -o root -g nyxtrace /dev/null /etc/ndr/secrets/adguard-password
sudo sh -c 'printf %s "<username>" > /etc/ndr/secrets/adguard-username'
sudo sh -c 'read -rs p && printf %s "$p" > /etc/ndr/secrets/adguard-password'
connectors:
- id: adguard
instance: adguard
tenant: example
enabled: true
config:
url: http://adguard.example.invalid:3000
# tls_fingerprint_sha256: "<sha256 of the leaf>" only for https with a self-signed certificate
poll_interval: 60s # floor 30s
page_size: 500 # 10..500 (AdGuard's own cap), twenty requests per poll
secrets:
username: file:/etc/ndr/secrets/adguard-username
password: file:/etc/ndr/secrets/adguard-password

NDR_CONNECTORS_ENABLED=true in /etc/ndr/ndr.env is required as for every connector declared in the file — see connectors.yaml. The same schema migrations, DROP VIEW grant and one-time backfill as for Pi-hole apply: see Pi-hole, step 3 and migrations.

Terminal window
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors validate
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors test adguard --live
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors collect adguard --once --live

test --live reports the AdGuard version and the queries and blocked count of AdGuard’s statistics window. The failure classes are the ones on the Pi-hole page; a 401 is auth.

NDR_CONNECTORS_ENABLED=true and systemctl restart ndr-api. The system view shows connector:adguard with the counters queries and blocked (delivered by the last poll), clients, and AdGuard’s own queries_window and blocked_window. Stale after three missed polls; degraded, with the reason on the health line, when a poll refused rows.

Terminal window
journalctl -u ndr-api -g 'instance=adguard'

The same stop → reset → start sequence as for Pi-hole, with this instance’s name:

Terminal window
systemctl stop ndr-api
systemd-run --pipe --quiet --collect --uid=nyxtrace \
-p EnvironmentFile=/etc/ndr/ndr.env -p EnvironmentFile=/etc/ndr/netflow.env \
/usr/local/bin/ndr connectors reset-cursor adguard
systemctl start ndr-api

Stop first: a running ndr-api holds the old bookmark in memory and would write it back after its next poll. The next poll then takes the newest page of the log, as a first poll does, and already delivered queries are absorbed by the event table’s replacing key.

  • Poll 60 s, floor 30 s; one request 5 s, one collection 1 min, one answer 4 MiB.
  • page_size 10..500 — AdGuard’s own cap — and at most twenty requests per poll, so 10,000 queries per poll at the default. A poll that needs more is truncated, and because the log is walked newest first the queries that fall off are the oldest of the window.
  • The first poll takes exactly the newest page of the log, not a time window: a window would depend on the appliance clock agreeing with the resolver’s. From then on the log is walked back to the cursor.
  • Queries from a client outside the tenant’s own networks are dropped and counted in dropped=.
  • A query stamped more than five minutes ahead of the appliance clock is refused and counted as failed, and the instance is degraded with the reason query time is more than 5m ahead of the appliance clock until a poll refuses nothing; the cursor never moves past the present, and a bookmark an earlier release let run into the future is rewound once. The comparison is in absolute time, so a resolver in another time zone is not skew. Fix the clock (NTP) and the state clears — the rule is described under Pi-hole.
  • resolver_blocked is shown and stored, but no detection model reads it yet.
  • Two AdGuard instances are two entries with different instance names; each keeps its own cursor, backoff and health line.