feat: adguard-register — auto DNS registration agent for AdGuard Home
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.
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
@@ -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":"..."}}`
|
||||||
Executable
+88
@@ -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
|
||||||
Executable
+144
@@ -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}"
|
||||||
Executable
+31
@@ -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."
|
||||||
Reference in New Issue
Block a user