diff --git a/CLAUDE.md b/CLAUDE.md index e534b4e..8b49750 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,6 +7,8 @@ Refactored single-file installer for 3x-ui VPN panel (based on x-ui-pro). ``` x-ui-latest.sh — main installer script (single file, run remotely) x-ui-patch.sh — apply current features to an existing install (no DB changes) +x-ui-adguard.sh — optional: AdGuard Home on the panel domain (DoH at + /dns-query, admin UI at random /adg-/ path) assets/ backup/x-ui-backup.sh — backup / restore / list script clash/clash.yaml — Clash/Mihomo subscription template (served by UA sniffing) @@ -89,3 +91,13 @@ Patch an existing install (re-reads ports/paths from x-ui.db and nginx): ```bash bash <(curl -fsSL https://raw.githubusercontent.com/mozaroc/3x-ui-pro/main/x-ui-patch.sh) ``` + +Add AdGuard Home on the panel domain (standalone, re-run safe, `-uninstall y` +to remove). AGH binds localhost only; nginx bridges `/dns-query` (DoH, +`allow_unencrypted_doh`) and a random `/adg-/` admin path via +`snippets/adguard.conf` included in the panel vhost. Installer/patch +regenerate the vhost and drop that include — re-run this script after them: + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/mozaroc/3x-ui-pro/main/x-ui-adguard.sh) +``` diff --git a/x-ui-adguard.sh b/x-ui-adguard.sh new file mode 100644 index 0000000..fab92ac --- /dev/null +++ b/x-ui-adguard.sh @@ -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: (never exposed directly) +# - plain DNS on 127.0.0.1: (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-/ → 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/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" </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" <&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