Add x-ui-adguard.sh: AdGuard Home on panel domain via nginx paths

DoH at /dns-query, admin UI at random /adg-<rand>/ path. AGH binds
localhost only (web UI + plain DNS on random ports); nginx bridges both
paths on the existing panel vhost, so no new domain, cert, or open port.
Config pre-seeded (no wizard), bcrypt password via htpasswd, re-run
safe, -uninstall y supported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ivan Razin
2026-07-14 09:39:44 +03:00
co-authored by Claude Fable 5
parent f08203ef51
commit d10a16a1a2
2 changed files with 334 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
#!/usr/bin/env bash
# x-ui-adguard.sh — install AdGuard Home behind the existing 3x-ui panel domain
#
# What it does:
# 1. Reads the panel domain from x-ui.db / nginx (same logic as x-ui-patch.sh)
# 2. Downloads the latest AdGuard Home release to /opt/AdGuardHome
# 3. Pre-seeds AdGuardHome.yaml (skips the first-run wizard):
# - web UI on 127.0.0.1:<random port> (never exposed directly)
# - plain DNS on 127.0.0.1:<random port> (localhost only, no port-53
# conflict with systemd-resolved)
# - DoH upstreams, allow_unencrypted_doh for the nginx bridge
# 4. Adds nginx locations on the PANEL domain (no separate domain needed):
# - /dns-query → DNS-over-HTTPS endpoint (standard path)
# - /adg-<random>/ → AdGuard Home admin UI
# 5. Prints the admin URL, credentials and the DoH URL for clients
#
# Re-run safe: keeps the existing AdGuardHome.yaml (settings + password),
# reuses the admin path and ports already present in nginx/yaml.
#
# Usage:
# bash x-ui-adguard.sh # install / repair
# bash x-ui-adguard.sh -uninstall y # remove AdGuard Home + nginx config
#
# NOTE: x-ui-latest.sh and x-ui-patch.sh regenerate the panel vhost and drop
# the AdGuard include line — re-run this script after either of them
# (it restores the include; config and credentials are kept).
set -Eeuo pipefail
XUIDB="/etc/x-ui/x-ui.db"
AGH_DIR="/opt/AdGuardHome"
AGH_YAML="${AGH_DIR}/AdGuardHome.yaml"
AGH_SNIPPET="/etc/nginx/snippets/adguard.conf"
AGH_SERVICE="AdGuardHome"
# ── colour helpers ────────────────────────────────────────────────────────────
red() { printf '\033[31m%s\033[0m\n' "$*"; }
green() { printf '\033[32m%s\033[0m\n' "$*"; }
blue() { printf '\033[34m[*] %s\033[0m\n' "$*"; }
die() { red "ERROR: $*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Run as root: sudo bash $0"
# ── parse args ────────────────────────────────────────────────────────────────
uninstall=""
while [[ $# -gt 0 ]]; do
case "$1" in
-uninstall) uninstall="${2:-}"; shift 2;;
*) die "Unknown argument: $1 (supported: -uninstall y)";;
esac
done
# ── uninstall ─────────────────────────────────────────────────────────────────
if [[ "$uninstall" == "y" ]]; then
blue "Uninstalling AdGuard Home..."
systemctl stop "$AGH_SERVICE" 2>/dev/null || true
[[ -x "${AGH_DIR}/AdGuardHome" ]] && "${AGH_DIR}/AdGuardHome" -s uninstall 2>/dev/null || true
rm -rf "$AGH_DIR" "$AGH_SNIPPET"
for f in /etc/nginx/sites-available/*; do
[[ -f "$f" ]] || continue
sed -i '\|snippets/adguard.conf|d' "$f"
done
if nginx -t &>/dev/null; then systemctl reload nginx; fi
green "AdGuard Home removed."
exit 0
fi
[[ -f "$XUIDB" ]] || die "x-ui.db not found at $XUIDB — install 3x-ui first"
command -v sqlite3 &>/dev/null || apt-get install -y -q sqlite3
db() { sqlite3 "$XUIDB" "$1"; }
# ── helpers ───────────────────────────────────────────────────────────────────
rand_str() { tr -dc 'a-zA-Z0-9' </dev/urandom | head -c "$1" || true; }
free_port() {
local p
while true; do
p=$(( ((RANDOM<<15)|RANDOM) % 49152 + 10000 ))
ss -Hln "sport = :$p" 2>/dev/null | grep -q . || { echo "$p"; return; }
done
}
# ── detect panel domain ───────────────────────────────────────────────────────
blue "Detecting panel domain..."
domain=""
web_cert=$(db "SELECT value FROM settings WHERE key='webCertFile';")
if [[ "$web_cert" =~ ^/root/cert/([^/]+)/ || "$web_cert" =~ /etc/letsencrypt/live/([^/]+)/ ]]; then
domain="${BASH_REMATCH[1]}"
fi
if [[ -z "$domain" ]]; then
for f in /etc/nginx/sites-available/*; do
[[ -f "$f" ]] || continue
case "$(basename "$f")" in 80.conf|00-maps.conf) continue;; esac
if grep -q 'listen 7443' "$f" 2>/dev/null; then
domain=$(awk '/server_name/{print $2; exit}' "$f" | tr -d ';')
break
fi
done
fi
[[ -n "$domain" ]] || die "Could not determine panel domain (no webCertFile, no vhost with 'listen 7443')"
# Panel vhost file the include line goes into
vhost="/etc/nginx/sites-available/${domain}"
if [[ ! -f "$vhost" ]] || ! grep -q 'listen 7443' "$vhost"; then
vhost=""
for f in /etc/nginx/sites-available/*; do
[[ -f "$f" ]] || continue
grep -q 'listen 7443' "$f" 2>/dev/null && { vhost="$f"; break; }
done
fi
[[ -n "$vhost" ]] || die "Panel vhost (listen 7443) not found in /etc/nginx/sites-available"
printf " domain = %s\n vhost = %s\n" "$domain" "$vhost"
# ── detect or generate admin path ─────────────────────────────────────────────
agh_path=""
if [[ -f "$AGH_SNIPPET" ]]; then
agh_path=$(grep -oP 'location /\Kadg-[a-zA-Z0-9]+' "$AGH_SNIPPET" | head -1 || true)
fi
if [[ -n "$agh_path" ]]; then
blue "Admin path reused: /${agh_path}/"
else
agh_path="adg-$(rand_str 12)"
blue "Admin path generated: /${agh_path}/"
fi
# ── detect or generate ports ──────────────────────────────────────────────────
agh_web_port=""
if [[ -f "$AGH_YAML" ]]; then
agh_web_port=$(grep -oP '^\s*address:\s*127\.0\.0\.1:\K\d+' "$AGH_YAML" | head -1 || true)
fi
if [[ -n "$agh_web_port" ]]; then
blue "Web port reused: $agh_web_port"
else
agh_web_port=$(free_port)
blue "Web port generated: $agh_web_port"
fi
agh_dns_port=$(free_port)
# ── install packages ──────────────────────────────────────────────────────────
blue "Installing packages..."
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl tar ca-certificates apache2-utils
# ── download AdGuard Home ─────────────────────────────────────────────────────
case "$(uname -m)" in
x86_64) agh_arch="amd64";;
aarch64) agh_arch="arm64";;
armv7l) agh_arch="armv7";;
*) die "Unsupported architecture: $(uname -m)";;
esac
if [[ -x "${AGH_DIR}/AdGuardHome" ]]; then
blue "AdGuard Home binary already present — keeping it (it self-updates from the UI)."
else
blue "Downloading AdGuard Home (linux_${agh_arch})..."
curl -fsSL "https://github.com/AdguardTeam/AdGuardHome/releases/latest/download/AdGuardHome_linux_${agh_arch}.tar.gz" \
| tar -xz -C /opt
[[ -x "${AGH_DIR}/AdGuardHome" ]] || die "Download/extract failed — ${AGH_DIR}/AdGuardHome missing"
fi
# ── seed config (first install only — never clobber an existing config) ──────
new_credentials=0
agh_user="admin"
agh_pass=""
if [[ -f "$AGH_YAML" ]]; then
blue "Existing AdGuardHome.yaml found — keeping settings and credentials."
else
blue "Generating AdGuardHome.yaml..."
new_credentials=1
agh_pass=$(rand_str 20)
agh_hash=$(htpasswd -nbB x "$agh_pass" | cut -d: -f2)
[[ "$agh_hash" == \$2* ]] || die "bcrypt hash generation failed (htpasswd)"
systemctl stop "$AGH_SERVICE" 2>/dev/null || true
cat > "$AGH_YAML" <<EOF
http:
address: 127.0.0.1:${agh_web_port}
users:
- name: ${agh_user}
password: ${agh_hash}
auth_attempts: 5
block_auth_min: 15
theme: auto
dns:
bind_hosts:
- 127.0.0.1
port: ${agh_dns_port}
upstream_dns:
- https://dns.cloudflare.com/dns-query
- https://dns.google/dns-query
- https://dns.quad9.net/dns-query
bootstrap_dns:
- 1.1.1.1
- 8.8.8.8
- 9.9.9.9
trusted_proxies:
- 127.0.0.0/8
tls:
enabled: false
allow_unencrypted_doh: true
filters:
- enabled: true
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
name: AdGuard DNS filter
id: 1
schema_version: 28
EOF
chmod 600 "$AGH_YAML"
fi
# ── systemd service ───────────────────────────────────────────────────────────
if systemctl list-unit-files --type=service 2>/dev/null | grep -q "^${AGH_SERVICE}\.service"; then
blue "Restarting AdGuard Home service..."
systemctl restart "$AGH_SERVICE"
else
blue "Installing AdGuard Home service..."
"${AGH_DIR}/AdGuardHome" -s install
fi
blue "Waiting for AdGuard Home to come up..."
agh_up=0
for _ in $(seq 1 20); do
if curl -fso /dev/null "http://127.0.0.1:${agh_web_port}/"; then agh_up=1; break; fi
sleep 0.5
done
[[ $agh_up -eq 1 ]] || die "AdGuard Home did not start on 127.0.0.1:${agh_web_port} — check: journalctl -u ${AGH_SERVICE}"
# ── nginx snippet ─────────────────────────────────────────────────────────────
blue "Writing nginx snippet..."
mkdir -p /etc/nginx/snippets
cat > "$AGH_SNIPPET" <<EOF
# AdGuard Home — DNS-over-HTTPS (standard path, no auth: DoH clients
# can't log in; the endpoint only answers DNS wireformat)
location /dns-query {
proxy_pass http://127.0.0.1:${agh_web_port};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
# server-level proxy_intercept_errors would rewrite DoH error bodies
proxy_intercept_errors off;
access_log off;
}
# AdGuard Home admin UI (random path). The UI uses relative URLs, so a
# trailing-slash proxy_pass works; Location headers and the session
# cookie (Path=/) still need rewriting to the sub-path.
location /${agh_path}/ {
proxy_pass http://127.0.0.1:${agh_web_port}/;
proxy_redirect / /${agh_path}/;
proxy_cookie_path / /${agh_path}/;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_intercept_errors off;
add_header X-Robots-Tag "noindex, nofollow" always;
}
location = /${agh_path} { return 302 /${agh_path}/; }
EOF
# Include the snippet in the panel vhost (before the shared includes so it
# sits inside the server block; prefix locations are order-independent)
if ! grep -q 'snippets/adguard.conf' "$vhost"; then
if grep -q 'include /etc/nginx/snippets/includes.conf;' "$vhost"; then
sed -i 's|^\(\s*\)include /etc/nginx/snippets/includes.conf;|\1include /etc/nginx/snippets/adguard.conf;\n\1include /etc/nginx/snippets/includes.conf;|' "$vhost"
else
# No shared snippet (unexpected layout) — insert before the closing brace
sed -i '$ s|^}$| include /etc/nginx/snippets/adguard.conf;\n}|' "$vhost"
fi
blue "Include added to ${vhost}"
else
blue "Include already present in ${vhost}"
fi
blue "Testing nginx config..."
if nginx -t 2>&1 | grep -q successful; then
systemctl reload nginx
green "nginx reloaded OK."
else
red "nginx config test failed — check errors above."
nginx -t
exit 1
fi
# ── verify DoH end-to-end through nginx-side port ─────────────────────────────
# RFC 8484 example query (www.example.com A) against the local AGH listener
doh_status=$(curl -so /dev/null -w '%{http_code}' \
-H 'Accept: application/dns-message' \
"http://127.0.0.1:${agh_web_port}/dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB" || true)
if [[ "$doh_status" == "200" ]]; then
green "DoH endpoint answers (HTTP 200)."
else
red "DoH self-test returned HTTP ${doh_status} (expected 200) — check AdGuard Home logs."
fi
# ─────────────────────────────────────────────────────────────────────────────
# RESULTS
# ─────────────────────────────────────────────────────────────────────────────
echo
green "══════════════════════════════════════════════"
green " AdGuard Home installed"
green "══════════════════════════════════════════════"
printf "\n Admin UI: https://%s/%s/\n" "$domain" "$agh_path"
if [[ $new_credentials -eq 1 ]]; then
printf " Login: %s\n" "$agh_user"
printf " Password: %s\n" "$agh_pass"
echo
red " Save the password now — it is stored only as a bcrypt hash in ${AGH_YAML}."
else
printf " Login: unchanged (existing credentials kept)\n"
fi
printf "\n DNS-over-HTTPS for clients:\n"
printf " https://%s/dns-query\n" "$domain"
echo
blue "Re-run this script after x-ui-latest.sh / x-ui-patch.sh — they regenerate"
blue "the panel vhost and drop the AdGuard include (settings are kept)."
echo