From 00a95fda80e74e2e17e6cd878cd83c014fd7dd91 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Sat, 2 May 2026 17:41:19 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20adguard-register=20=E2=80=94=20auto=20D?= =?UTF-8?q?NS=20registration=20agent=20for=20AdGuard=20Home?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registers {hostname}.home.dustin.coffee → host IP on boot via AdGuard Home API. Deploy to any LXC/VM with ./install.sh — supports root (system service), sudo user (system via sudo), and unprivileged user (systemd --user or crontab). Files: - adguard-register.sh — detects hostname+IP, idempotent create/update - install.sh — deployment script with auto-detection of install method - uninstall.sh — removes service and binary - README.md — full documentation Tested across 24 hosts (LXCs and VMs) on a 172.20.0.0/16 home lab network. --- .gitignore | 3 + README.md | 91 ++++++++++++++++++++++++++++ adguard-register.sh | 88 +++++++++++++++++++++++++++ install.sh | 144 ++++++++++++++++++++++++++++++++++++++++++++ uninstall.sh | 31 ++++++++++ 5 files changed, 357 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 adguard-register.sh create mode 100755 install.sh create mode 100755 uninstall.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a22eff --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.swp +*~ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf3d6ef --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# AdGuard Home DNS Auto-Registration + +Automatically registers `{hostname}.home.dustin.coffee` → its own IP with AdGuard Home +DNS rewrites on boot. Designed for the `home.dustin.coffee` home lab domain. + +## How It Works + +1. On boot (via systemd oneshot service), the script determines the host's short hostname + and primary IPv4 address +2. It authenticates with the AdGuard Home API at `172.20.1.1` +3. It checks if a DNS rewrite for `{hostname}.home.dustin.coffee` already exists: + - **No entry**: creates a new rewrite pointing to the current IP + - **Entry exists with same IP**: skips (idempotent) + - **Entry exists with different IP**: updates the rewrite (handles renumbering) +4. Verifies the rewrite was applied successfully + +The IP detection skips Docker bridges, Tailscale interfaces, and loopback to find the +host's real primary address. + +## Deployment + +### On a new LXC/VM + +```bash +# Copy the agent directory to the target host +scp -r adguard-register/ user@target-host:/tmp/ + +# SSH in and install +ssh user@target-host +cd /tmp/adguard-register +sudo ./install.sh +``` + +### Manual registration (one-off, no install) + +```bash +sudo /usr/local/bin/adguard-register +``` + +### Uninstall + +```bash +sudo /path/to/adguard-register/uninstall.sh +``` + +## Requirements + +- `curl` — for API calls +- `python3` — for JSON parsing +- `systemd` — for boot-time execution (or use `@reboot` cron as fallback) +- Network access to `172.20.1.1:80` + +## Files + +| File | Purpose | +|------|---------| +| `adguard-register.sh` | Main registration script | +| `install.sh` | Copies script to `/usr/local/bin/` and sets up systemd service | +| `uninstall.sh` | Removes the service and script | +| `README.md` | This file | + +## Troubleshooting + +### Check service status +```bash +systemctl status adguard-register +journalctl -u adguard-register +``` + +### Test manually +```bash +sudo /usr/local/bin/adguard-register +``` + +### Common issues + +- **"Failed to authenticate"**: Verify AdGuard Home is running at `172.20.1.1` + and credentials are correct +- **"Could not determine primary IPv4 address"**: Ensure the host has a + non-loopback, non-Docker, non-Tailscale IPv4 address +- **"Verification failed"**: The API accepted the change but the read-back + didn't match; check AdGuard Home logs +- **Python3 not found**: Install with `apt install python3` or equivalent + +## AdGuard Home API Reference + +- Base URL: `http://172.20.1.1/control/` +- Auth: `POST /control/login` with `{"name":"...","password":"..."}` +- List rewrites: `GET /control/rewrite/list` +- Add rewrite: `POST /control/rewrite/add` with `{"domain":"...","answer":"..."}` +- Update rewrite: `POST /control/rewrite/update` with `{"target":{"domain":"..."},"update":{"domain":"...","answer":"..."}}` diff --git a/adguard-register.sh b/adguard-register.sh new file mode 100755 index 0000000..f67171a --- /dev/null +++ b/adguard-register.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# adguard-register — registers this host with AdGuard Home DNS rewrites +# Deploy to each LXC/VM, run at boot via systemd oneshot or @reboot cron + +set -euo pipefail + +ADGUARD_URL="http://172.20.1.1" +USERNAME="newkirk" +PASSWORD="Vaxjy911!" +# Get short hostname (portable: try hostname -s, fall back to /etc/hostname or /proc) +if command -v hostname &>/dev/null; then + HOSTNAME=$(hostname -s) +elif [ -f /etc/hostname ]; then + HOSTNAME=$(cat /etc/hostname | cut -d. -f1) +else + HOSTNAME=$(cat /proc/sys/kernel/hostname 2>/dev/null | cut -d. -f1) +fi + +# Get primary IPv4 (non-loopback, non-docker, non-tailscale) +IP=$(ip -4 addr show scope global | grep -v 'docker\|tailscale\|br-\|veth' | grep -oP 'inet \K[\d.]+' | head -1) + +if [ -z "$IP" ]; then + echo "ERROR: Could not determine primary IPv4 address" + exit 1 +fi + +DOMAIN="${HOSTNAME}.home.dustin.coffee" + +# Login +COOKIE=$(curl -s -D - "${ADGUARD_URL}/control/login" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"${USERNAME}\",\"password\":\"${PASSWORD}\"}" \ + | grep -i 'set-cookie' | grep -oP 'agh_session=[a-f0-9]+' | head -1) + +if [ -z "$COOKIE" ]; then + echo "ERROR: Failed to authenticate with AdGuard Home" + exit 1 +fi + +# Check if rewrite already exists +EXISTING=$(curl -s "${ADGUARD_URL}/control/rewrite/list" -b "$COOKIE") +EXISTING_ANSWER=$(echo "$EXISTING" | python3 -c " +import sys,json +data=json.load(sys.stdin) +for r in data: + if r.get('domain') == '$DOMAIN': + print(r.get('answer','')) + break +" 2>/dev/null) + +if [ -n "$EXISTING_ANSWER" ]; then + if [ "$EXISTING_ANSWER" = "$IP" ]; then + echo "OK: $DOMAIN already points to $IP — nothing to do" + exit 0 + fi + # Update existing + echo "Updating $DOMAIN: $EXISTING_ANSWER → $IP" + curl -s -X POST "${ADGUARD_URL}/control/rewrite/update" \ + -b "$COOKIE" \ + -H "Content-Type: application/json" \ + -d "{\"target\":{\"domain\":\"$DOMAIN\"},\"update\":{\"domain\":\"$DOMAIN\",\"answer\":\"$IP\"}}" \ + > /dev/null +else + # Create new + echo "Registering $DOMAIN → $IP" + curl -s -X POST "${ADGUARD_URL}/control/rewrite/add" \ + -b "$COOKIE" \ + -H "Content-Type: application/json" \ + -d "{\"domain\":\"$DOMAIN\",\"answer\":\"$IP\"}" \ + > /dev/null +fi + +# Verify +VERIFY=$(curl -s "${ADGUARD_URL}/control/rewrite/list" -b "$COOKIE" | python3 -c " +import sys,json +data=json.load(sys.stdin) +for r in data: + if r.get('domain') == '$DOMAIN': + print(r.get('answer','')) +" 2>/dev/null) + +if [ "$VERIFY" = "$IP" ]; then + echo "SUCCESS: $DOMAIN → $IP verified" + exit 0 +else + echo "WARN: Verification failed — $DOMAIN resolves to '$VERIFY', expected '$IP'" + exit 1 +fi diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..2a4a61b --- /dev/null +++ b/install.sh @@ -0,0 +1,144 @@ +#!/bin/bash +# install.sh — deploy adguard-register to this host +# Works as root (system install) or regular user (user install with sudo) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [ "$(id -u)" -eq 0 ]; then + # Root: system-wide install + BIN_PATH="/usr/local/bin/adguard-register" + SERVICE_PATH="/etc/systemd/system/adguard-register.service" + SERVICE_TYPE="system" +else + # Non-root: user install, try sudo for systemd, fall back to cron + BIN_PATH="${HOME}/.local/bin/adguard-register" + mkdir -p "${HOME}/.local/bin" + SERVICE_TYPE="user" + + # Check if we can sudo + if sudo -n true 2>/dev/null; then + HAS_SUDO=1 + else + HAS_SUDO=0 + fi +fi + +echo "=== Installing adguard-register (${SERVICE_TYPE}) ===" + +# Copy script +if [ "$(id -u)" -eq 0 ]; then + cp "${SCRIPT_DIR}/adguard-register.sh" "${BIN_PATH}" +else + # May need sudo for the copy if dir is protected + cp "${SCRIPT_DIR}/adguard-register.sh" "${BIN_PATH}" 2>/dev/null || \ + sudo cp "${SCRIPT_DIR}/adguard-register.sh" "${BIN_PATH}" +fi +chmod +x "${BIN_PATH}" +echo "→ Installed ${BIN_PATH}" + +# Install service +if [ "${SERVICE_TYPE}" = "system" ]; then + # System service (root) + cat > "${SERVICE_PATH}" << 'SERVICEOF' +[Unit] +Description=Register host with AdGuard Home DNS +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/adguard-register +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +SERVICEOF + echo "→ Created ${SERVICE_PATH}" + systemctl daemon-reload + systemctl enable adguard-register.service + echo "→ Enabled adguard-register.service" + systemctl start adguard-register.service 2>&1 || true + echo "→ Registration result:" + systemctl status adguard-register.service --no-pager --lines=5 2>&1 || true + +elif [ "${HAS_SUDO}" -eq 1 ]; then + # User with sudo — install system service via sudo + sudo tee "${SERVICE_PATH:-/etc/systemd/system/adguard-register.service}" > /dev/null << 'SERVICEOF' +[Unit] +Description=Register host with AdGuard Home DNS +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/adguard-register +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +SERVICEOF + sudo cp "${BIN_PATH}" /usr/local/bin/adguard-register + sudo chmod +x /usr/local/bin/adguard-register + BIN_PATH="/usr/local/bin/adguard-register" + sudo systemctl daemon-reload + sudo systemctl enable adguard-register.service + echo "→ Enabled adguard-register.service (system via sudo)" + sudo systemctl start adguard-register.service 2>&1 || true + sudo systemctl status adguard-register.service --no-pager --lines=5 2>&1 || true + +else + # User without sudo — try user systemd, then crontab, then just run once + INSTALLED=0 + + # Attempt 1: user systemd service (works if lingering is enabled) + if command -v systemctl &>/dev/null && systemctl --user daemon-reload 2>/dev/null; then + mkdir -p ~/.config/systemd/user + cat > ~/.config/systemd/user/adguard-register.service << SERVICEOF +[Unit] +Description=Register host with AdGuard Home DNS +After=network-online.target + +[Service] +Type=oneshot +ExecStart=${BIN_PATH} +RemainAfterExit=yes + +[Install] +WantedBy=default.target +SERVICEOF + systemctl --user daemon-reload + systemctl --user enable adguard-register.service 2>/dev/null && { + echo "→ Enabled user systemd service" + systemctl --user start adguard-register.service 2>&1 || true + INSTALLED=1 + } + fi + + # Attempt 2: crontab + if [ "$INSTALLED" -eq 0 ] && command -v crontab &>/dev/null; then + (crontab -l 2>/dev/null | grep -v 'adguard-register' || true) > /tmp/adguard-crontab + echo "@reboot ${BIN_PATH}" >> /tmp/adguard-crontab + crontab /tmp/adguard-crontab 2>/dev/null && { + echo "→ Added @reboot cron job" + INSTALLED=1 + } + rm -f /tmp/adguard-crontab + fi + + # Run once now regardless + echo "→ Running registration..." + bash "${BIN_PATH}" 2>&1 || true + + if [ "$INSTALLED" -eq 0 ]; then + echo "" + echo "⚠️ Could not set up auto-start (no sudo, no systemd --user, no crontab)." + echo " The script is installed at ${BIN_PATH}" + echo " Run it manually or set up auto-start yourself." + fi +fi + +echo "" +echo "=== Installation complete ===" +echo "The agent will auto-run on every boot." +echo "To manually re-register: ${BIN_PATH}" diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..fa4665d --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# uninstall.sh — remove adguard-register from this host +# Run as root (or with sudo): ./uninstall.sh + +set -euo pipefail + +INSTALL_PATH="/usr/local/bin/adguard-register" +SERVICE_PATH="/etc/systemd/system/adguard-register.service" + +if [ "$(id -u)" -ne 0 ]; then + echo "ERROR: This script must be run as root (use sudo)" + exit 1 +fi + +echo "=== Uninstalling adguard-register ===" + +# Stop and disable service (ignore errors if not installed) +systemctl stop adguard-register.service 2>/dev/null || true +systemctl disable adguard-register.service 2>/dev/null || true +rm -f "${SERVICE_PATH}" +systemctl daemon-reload +echo "→ Removed systemd service" + +# Remove script +rm -f "${INSTALL_PATH}" +echo "→ Removed ${INSTALL_PATH}" + +echo "" +echo "=== Uninstall complete ===" +echo "NOTE: Existing DNS rewrites in AdGuard Home were NOT removed." +echo "To clean up entries, use the AdGuard Home web UI or API directly."