Add safe AdGuard DoH installer
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
.env
|
||||||
|
conf/
|
||||||
|
work/
|
||||||
|
caddy-data/
|
||||||
|
caddy-config/
|
||||||
|
backups/
|
||||||
|
*.bak
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 AlexChard
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# AdGuard DoH Installer
|
||||||
|
|
||||||
|
Безопасный повторяемый установщик AdGuard Home + Caddy для VPS.
|
||||||
|
|
||||||
|
Он разворачивает:
|
||||||
|
|
||||||
|
- публичный DNS-over-HTTPS на `https://dns.example.com/dns-query`;
|
||||||
|
- отдельную HTTPS-панель на нейтральном домене вроде `https://node.example.com`;
|
||||||
|
- нейтральную статическую заглушку на корне DoH-домена;
|
||||||
|
- автоматические сертификаты Let's Encrypt;
|
||||||
|
- Cloudflare и Google DoH как основные upstream, несколько обычных DNS только как bootstrap/fallback;
|
||||||
|
- Docker Compose-стек с лимитами памяти и автоматическим перезапуском.
|
||||||
|
|
||||||
|
Прямой порт AdGuard `3000` публикуется только на `127.0.0.1`. Установщик не меняет VPN, маршруты, UFW/iptables и SSH.
|
||||||
|
|
||||||
|
## Быстрая установка
|
||||||
|
|
||||||
|
Сначала создайте две A-записи, указывающие на IP нового VPS:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dns.example.com -> VPS_IP
|
||||||
|
node.example.com -> VPS_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
Затем выполните одну команду:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.chardb.ru/VPS/adguard-doh-installer/raw/branch/main/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт спросит оба домена и пароль администратора. Пароль не попадает в историю shell. Если оставить его пустым, будет создан случайный пароль и показан один раз.
|
||||||
|
|
||||||
|
Неинтерактивный запуск:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.chardb.ru/VPS/adguard-doh-installer/raw/branch/main/install.sh | \
|
||||||
|
sudo bash -s -- --doh-domain dns.example.com --admin-domain node.example.com --email admin@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Безопасное поведение
|
||||||
|
|
||||||
|
Перед изменениями скрипт:
|
||||||
|
|
||||||
|
1. проверяет синтаксис доменов;
|
||||||
|
2. проверяет их A-записи и публичный IP VPS;
|
||||||
|
3. обнаруживает уже работающий AdGuard Home;
|
||||||
|
4. проверяет занятость TCP-портов `80` и `443`;
|
||||||
|
5. отказывается захватывать чужой Nginx/Caddy/Apache;
|
||||||
|
6. не принимает непустую чужую директорию без явного `--adopt-existing`;
|
||||||
|
7. сохраняет резервные копии управляемых файлов при повторном запуске;
|
||||||
|
8. сохраняет существующий `AdGuardHome.yaml`, пользователей, клиентов и правила при обновлении;
|
||||||
|
9. проверяет Compose до запуска и HTTPS после запуска.
|
||||||
|
|
||||||
|
Проверить новый VPS без внесения изменений:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.chardb.ru/VPS/adguard-doh-installer/raw/branch/main/install.sh | \
|
||||||
|
sudo bash -s -- --check --doh-domain dns.example.com --admin-domain node.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Если DNS-записи ещё распространяются, можно установить с `--allow-dns-mismatch`. Caddy будет повторять выпуск сертификата автоматически.
|
||||||
|
|
||||||
|
## Повторный запуск и обновление
|
||||||
|
|
||||||
|
Повторите ту же команду. Для стека, созданного этим установщиком, данные AdGuard и пароль сохраняются. Перед обновлением создаётся каталог:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/opt/stacks/adguard-doh/backups/<UTC_TIMESTAMP>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Чужой стек автоматически не усыновляется. Для совместимой установки можно явно указать:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash install.sh --adopt-existing --stack-dir /opt/stacks/adguard-doh \
|
||||||
|
--doh-domain dns.example.com --admin-domain node.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash verify.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается:
|
||||||
|
|
||||||
|
- админка: HTTP `302` или `200`;
|
||||||
|
- `/dns-query` без DNS-пакета: HTTP `400`;
|
||||||
|
- заглушка: HTTP `200`;
|
||||||
|
- порт `3000`: только `127.0.0.1:3000`.
|
||||||
|
|
||||||
|
## Требования и ограничения
|
||||||
|
|
||||||
|
- Debian/Ubuntu для автоматической установки Docker;
|
||||||
|
- root/sudo;
|
||||||
|
- свободные TCP `80` и `443`;
|
||||||
|
- домены с A-записями на VPS;
|
||||||
|
- существующий reverse proxy намеренно не перенастраивается автоматически.
|
||||||
|
|
||||||
|
Если на VPS уже занят `80/443`, установщик завершится до изменений. Это безопаснее автоматического удаления или перенастройки чужого сервиса.
|
||||||
|
|
||||||
|
## Заглушка
|
||||||
|
|
||||||
|
Исходник находится в `templates/cover/index.html`. Установщик кладёт автономную копию в `site/index.html`. На `/dns-query` заглушка не влияет.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT.
|
||||||
+428
@@ -0,0 +1,428 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
PROJECT_NAME="berloga-dns"
|
||||||
|
STACK_DIR="/opt/stacks/adguard-doh"
|
||||||
|
DOH_DOMAIN=""
|
||||||
|
ADMIN_DOMAIN=""
|
||||||
|
ACME_EMAIL=""
|
||||||
|
ADMIN_USER="admin"
|
||||||
|
ADMIN_PASSWORD=""
|
||||||
|
ALLOW_DNS_MISMATCH=0
|
||||||
|
ADOPT_EXISTING=0
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
log() { printf '\033[1;34m[DNS]\033[0m %s\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*" >&2; }
|
||||||
|
die() { printf '\033[1;31m[ERROR]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: install.sh [options]
|
||||||
|
|
||||||
|
--doh-domain DOMAIN Public DoH domain, for example dns.example.com
|
||||||
|
--admin-domain DOMAIN HTTPS admin domain, for example node.example.com
|
||||||
|
--email EMAIL Optional ACME account email
|
||||||
|
--admin-user USER AdGuard administrator name (default: admin)
|
||||||
|
--stack-dir PATH Installation directory (default: /opt/stacks/adguard-doh)
|
||||||
|
--allow-dns-mismatch Continue when DNS does not yet point to this VPS
|
||||||
|
--adopt-existing Adopt a compatible unmarked stack directory
|
||||||
|
--check Validate only; do not write or start containers
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
The administrator password is requested from /dev/tty and is never written to
|
||||||
|
shell history. If left empty, a random password is generated and shown once.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while (($#)); do
|
||||||
|
case "$1" in
|
||||||
|
--doh-domain) DOH_DOMAIN="${2:-}"; shift 2 ;;
|
||||||
|
--admin-domain) ADMIN_DOMAIN="${2:-}"; shift 2 ;;
|
||||||
|
--email) ACME_EMAIL="${2:-}"; shift 2 ;;
|
||||||
|
--admin-user) ADMIN_USER="${2:-}"; shift 2 ;;
|
||||||
|
--stack-dir) STACK_DIR="${2:-}"; shift 2 ;;
|
||||||
|
--allow-dns-mismatch) ALLOW_DNS_MISMATCH=1; shift ;;
|
||||||
|
--adopt-existing) ADOPT_EXISTING=1; shift ;;
|
||||||
|
--check) DRY_RUN=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) die "Unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ ${EUID:-$(id -u)} -eq 0 ]] || die "Run as root (sudo)."
|
||||||
|
|
||||||
|
read_tty() {
|
||||||
|
local prompt="$1" value=""
|
||||||
|
[[ -r /dev/tty ]] || die "No terminal available; pass domains as arguments."
|
||||||
|
printf '%s' "$prompt" > /dev/tty
|
||||||
|
IFS= read -r value < /dev/tty
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -n "$DOH_DOMAIN" ]] || DOH_DOMAIN="$(read_tty 'DoH domain (dns.example.com): ')"
|
||||||
|
[[ -n "$ADMIN_DOMAIN" ]] || ADMIN_DOMAIN="$(read_tty 'Admin domain (node.example.com): ')"
|
||||||
|
|
||||||
|
valid_domain() {
|
||||||
|
[[ "$1" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]]
|
||||||
|
}
|
||||||
|
valid_domain "$DOH_DOMAIN" || die "Invalid DoH domain: $DOH_DOMAIN"
|
||||||
|
valid_domain "$ADMIN_DOMAIN" || die "Invalid admin domain: $ADMIN_DOMAIN"
|
||||||
|
[[ "$DOH_DOMAIN" != "$ADMIN_DOMAIN" ]] || die "DoH and admin domains must be different."
|
||||||
|
[[ "$ADMIN_USER" =~ ^[A-Za-z0-9._-]{1,64}$ ]] || die "Invalid administrator name."
|
||||||
|
[[ "$STACK_DIR" == /* && "$STACK_DIR" != "/" ]] || die "Stack directory must be an absolute non-root path."
|
||||||
|
|
||||||
|
if [[ -e "$STACK_DIR" && ! -f "$STACK_DIR/.berloga-adguard-installer" ]]; then
|
||||||
|
if ((ADOPT_EXISTING)) && [[ -f "$STACK_DIR/compose.yaml" && -f "$STACK_DIR/conf/AdGuardHome.yaml" ]]; then
|
||||||
|
warn "Adopting compatible existing stack: $STACK_DIR"
|
||||||
|
else
|
||||||
|
die "$STACK_DIR already exists and is not managed by this installer. Nothing was changed. Use --adopt-existing only after reviewing it."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Image}} {{.Names}}' | grep -qi 'adguard/adguardhome'; then
|
||||||
|
if [[ ! -f "$STACK_DIR/.berloga-adguard-installer" && $ADOPT_EXISTING -eq 0 ]]; then
|
||||||
|
die "Another AdGuard Home container is already running. Nothing was changed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v ss >/dev/null 2>&1 || die "The 'ss' utility is required (package: iproute2). Nothing was changed."
|
||||||
|
command -v curl >/dev/null 2>&1 || die "The 'curl' utility is required. Nothing was changed."
|
||||||
|
command -v getent >/dev/null 2>&1 || die "The 'getent' utility is required (package: libc-bin). Nothing was changed."
|
||||||
|
|
||||||
|
port_busy() { ss -H -ltn "sport = :$1" 2>/dev/null | grep -q .; }
|
||||||
|
our_stack_running() {
|
||||||
|
command -v docker >/dev/null 2>&1 &&
|
||||||
|
docker ps --filter "label=com.docker.compose.project=$PROJECT_NAME" --format '{{.ID}}' | grep -q .
|
||||||
|
}
|
||||||
|
for port in 80 443; do
|
||||||
|
if port_busy "$port" && ! our_stack_running; then
|
||||||
|
ss -ltnp "sport = :$port" >&2 || true
|
||||||
|
die "TCP port $port is already occupied by another service. Nothing was changed. Integrate with the existing reverse proxy instead of forcing takeover."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
PUBLIC_IP="$(curl -4fsS --max-time 6 https://api.ipify.org 2>/dev/null || true)"
|
||||||
|
for domain in "$DOH_DOMAIN" "$ADMIN_DOMAIN"; do
|
||||||
|
resolved="$(getent ahostsv4 "$domain" 2>/dev/null | awk 'NR==1{print $1}')"
|
||||||
|
if [[ -z "$resolved" || (-n "$PUBLIC_IP" && "$resolved" != "$PUBLIC_IP") ]]; then
|
||||||
|
if ((ALLOW_DNS_MISMATCH)); then
|
||||||
|
warn "$domain currently resolves to '${resolved:-nothing}', VPS public IP is '${PUBLIC_IP:-unknown}'. Caddy will retry certificate issuance."
|
||||||
|
else
|
||||||
|
die "$domain does not point to this VPS (${PUBLIC_IP:-unknown}). Add A records first or use --allow-dns-mismatch. Nothing was changed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ((DRY_RUN)); then
|
||||||
|
log "Checks passed. Ports are available and domains are valid. No changes made."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
command -v apt-get >/dev/null 2>&1 || die "Automatic Docker installation supports Debian/Ubuntu only."
|
||||||
|
log "Docker is missing; installing distribution packages."
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y ca-certificates curl openssl docker.io
|
||||||
|
apt-get install -y docker-compose-v2 || apt-get install -y docker-compose-plugin
|
||||||
|
systemctl enable --now docker
|
||||||
|
fi
|
||||||
|
docker compose version >/dev/null 2>&1 || die "Docker Compose v2 is required."
|
||||||
|
|
||||||
|
new_install=0
|
||||||
|
[[ -f "$STACK_DIR/conf/AdGuardHome.yaml" ]] || new_install=1
|
||||||
|
if ((new_install)); then
|
||||||
|
if [[ -r /dev/tty ]]; then
|
||||||
|
printf 'Administrator password (leave empty to generate): ' > /dev/tty
|
||||||
|
IFS= read -rs ADMIN_PASSWORD < /dev/tty
|
||||||
|
printf '\n' > /dev/tty
|
||||||
|
fi
|
||||||
|
if [[ -z "$ADMIN_PASSWORD" ]]; then
|
||||||
|
ADMIN_PASSWORD="$(openssl rand -hex 16)"
|
||||||
|
fi
|
||||||
|
((${#ADMIN_PASSWORD} >= 12)) || die "Administrator password must contain at least 12 characters."
|
||||||
|
log "Creating password hash locally in the Caddy container."
|
||||||
|
ADMIN_HASH="$(printf '%s\n%s\n' "$ADMIN_PASSWORD" "$ADMIN_PASSWORD" | docker run --rm -i caddy:2-alpine caddy hash-password)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
backup_dir=""
|
||||||
|
if [[ -d "$STACK_DIR" ]]; then
|
||||||
|
backup_dir="$STACK_DIR/backups/$timestamp"
|
||||||
|
mkdir -p "$backup_dir"
|
||||||
|
for item in compose.yaml Caddyfile .env site/index.html conf/AdGuardHome.yaml; do
|
||||||
|
[[ -e "$STACK_DIR/$item" ]] || continue
|
||||||
|
mkdir -p "$backup_dir/$(dirname "$item")"
|
||||||
|
cp -a "$STACK_DIR/$item" "$backup_dir/$item"
|
||||||
|
done
|
||||||
|
log "Backup created: $backup_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$STACK_DIR"/{conf,work,caddy-data,caddy-config,site,backups}
|
||||||
|
touch "$STACK_DIR/.berloga-adguard-installer"
|
||||||
|
|
||||||
|
cat > "$STACK_DIR/.env" <<EOF
|
||||||
|
COMPOSE_PROJECT_NAME=$PROJECT_NAME
|
||||||
|
DOH_DOMAIN=$DOH_DOMAIN
|
||||||
|
ADMIN_DOMAIN=$ADMIN_DOMAIN
|
||||||
|
ACME_EMAIL=$ACME_EMAIL
|
||||||
|
EOF
|
||||||
|
chmod 600 "$STACK_DIR/.env"
|
||||||
|
|
||||||
|
cat > "$STACK_DIR/compose.yaml" <<'EOF'
|
||||||
|
services:
|
||||||
|
adguardhome:
|
||||||
|
image: adguard/adguardhome:v0.107.78
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./work:/opt/adguardhome/work
|
||||||
|
- ./conf:/opt/adguardhome/conf
|
||||||
|
expose:
|
||||||
|
- "53/tcp"
|
||||||
|
- "53/udp"
|
||||||
|
- "3000/tcp"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:3000/tcp"
|
||||||
|
mem_limit: 256m
|
||||||
|
oom_score_adj: -500
|
||||||
|
networks:
|
||||||
|
- dns-internal
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- adguardhome
|
||||||
|
ports:
|
||||||
|
- "80:80/tcp"
|
||||||
|
- "443:443/tcp"
|
||||||
|
- "443:443/udp"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- ./site:/srv/cover:ro
|
||||||
|
- ./caddy-data:/data
|
||||||
|
- ./caddy-config:/config
|
||||||
|
mem_limit: 96m
|
||||||
|
oom_score_adj: -500
|
||||||
|
networks:
|
||||||
|
- dns-internal
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dns-internal:
|
||||||
|
driver: bridge
|
||||||
|
EOF
|
||||||
|
|
||||||
|
global_options=""
|
||||||
|
[[ -n "$ACME_EMAIL" ]] && global_options="{
|
||||||
|
email $ACME_EMAIL
|
||||||
|
}
|
||||||
|
|
||||||
|
"
|
||||||
|
cat > "$STACK_DIR/Caddyfile" <<EOF
|
||||||
|
${global_options}$DOH_DOMAIN {
|
||||||
|
@doh path /dns-query /dns-query/*
|
||||||
|
handle @doh {
|
||||||
|
reverse_proxy adguardhome:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
root * /srv/cover
|
||||||
|
file_server
|
||||||
|
header {
|
||||||
|
X-Content-Type-Options nosniff
|
||||||
|
Referrer-Policy no-referrer
|
||||||
|
X-Frame-Options DENY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ADMIN_DOMAIN {
|
||||||
|
reverse_proxy adguardhome:3000
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$STACK_DIR/site/index.html" <<'EOF'
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex,nofollow">
|
||||||
|
<title>Network Services</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:dark;--bg:#0b1220;--card:#111c30;--text:#dce7f7;--muted:#8ea3bf;--accent:#55d6be}
|
||||||
|
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:radial-gradient(circle at 25% 10%,#16304a 0,var(--bg) 50%);font:16px/1.55 system-ui,sans-serif;color:var(--text)}
|
||||||
|
main{width:min(680px,calc(100% - 32px));padding:42px;border:1px solid #243653;border-radius:24px;background:color-mix(in srgb,var(--card) 90%,transparent);box-shadow:0 24px 80px #0008}
|
||||||
|
.mark{width:54px;height:54px;border-radius:16px;display:grid;place-items:center;background:#173649;color:var(--accent);font-size:28px}h1{margin:24px 0 8px;font-size:clamp(28px,5vw,44px)}p{margin:0;color:var(--muted)}.status{display:flex;gap:10px;align-items:center;margin-top:30px;padding-top:22px;border-top:1px solid #243653}.dot{width:10px;height:10px;border-radius:50%;background:var(--accent);box-shadow:0 0 18px var(--accent)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body><main><div class="mark">◆</div><h1>Network services</h1><p>This endpoint provides infrastructure services for authorized clients.</p><div class="status"><span class="dot"></span><span>Service operational</span></div></main></body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if ((new_install)); then
|
||||||
|
cat > "$STACK_DIR/conf/AdGuardHome.yaml" <<EOF
|
||||||
|
http:
|
||||||
|
pprof:
|
||||||
|
port: 6060
|
||||||
|
enabled: false
|
||||||
|
doh:
|
||||||
|
routes:
|
||||||
|
- GET /dns-query
|
||||||
|
- POST /dns-query
|
||||||
|
- GET /dns-query/{ClientID}
|
||||||
|
- POST /dns-query/{ClientID}
|
||||||
|
insecure_enabled: true
|
||||||
|
address: 0.0.0.0:3000
|
||||||
|
session_ttl: 30d
|
||||||
|
users:
|
||||||
|
- name: $ADMIN_USER
|
||||||
|
password: $ADMIN_HASH
|
||||||
|
auth_attempts: 5
|
||||||
|
block_auth_min: 15
|
||||||
|
http_proxy: ""
|
||||||
|
language: ""
|
||||||
|
theme: auto
|
||||||
|
dns:
|
||||||
|
bind_hosts:
|
||||||
|
- 0.0.0.0
|
||||||
|
port: 53
|
||||||
|
anonymize_client_ip: false
|
||||||
|
ratelimit: 100
|
||||||
|
ratelimit_subnet_len_ipv4: 24
|
||||||
|
ratelimit_subnet_len_ipv6: 56
|
||||||
|
ratelimit_whitelist: []
|
||||||
|
refuse_any: true
|
||||||
|
upstream_dns:
|
||||||
|
- https://cloudflare-dns.com/dns-query
|
||||||
|
- https://dns.google/dns-query
|
||||||
|
upstream_dns_file: ""
|
||||||
|
bootstrap_dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
|
- 9.9.9.10
|
||||||
|
fallback_dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
|
- 9.9.9.10
|
||||||
|
upstream_mode: parallel
|
||||||
|
fastest_timeout: 1s
|
||||||
|
allowed_clients: []
|
||||||
|
disallowed_clients: []
|
||||||
|
blocked_hosts: [version.bind, id.server, hostname.bind]
|
||||||
|
trusted_proxies:
|
||||||
|
- 127.0.0.0/8
|
||||||
|
- ::1/128
|
||||||
|
- 172.16.0.0/12
|
||||||
|
cache_enabled: true
|
||||||
|
cache_size: 4194304
|
||||||
|
cache_ttl_min: 0
|
||||||
|
cache_ttl_max: 0
|
||||||
|
cache_optimistic: true
|
||||||
|
cache_optimistic_answer_ttl: 30s
|
||||||
|
cache_optimistic_max_age: 12h
|
||||||
|
bogus_nxdomain: []
|
||||||
|
aaaa_disabled: false
|
||||||
|
enable_dnssec: true
|
||||||
|
edns_client_subnet: {custom_ip: "", enabled: false, use_custom: false}
|
||||||
|
max_goroutines: 150
|
||||||
|
handle_ddr: true
|
||||||
|
ipset: []
|
||||||
|
ipset_file: ""
|
||||||
|
bootstrap_prefer_ipv6: false
|
||||||
|
upstream_timeout: 10s
|
||||||
|
private_networks: []
|
||||||
|
use_private_ptr_resolvers: false
|
||||||
|
local_ptr_upstreams: []
|
||||||
|
use_dns64: false
|
||||||
|
dns64_prefixes: []
|
||||||
|
serve_http3: false
|
||||||
|
use_http3_upstreams: false
|
||||||
|
serve_plain_dns: true
|
||||||
|
hostsfile_enabled: true
|
||||||
|
pending_requests: {enabled: true}
|
||||||
|
tls: {enabled: false, server_name: "", force_https: false, port_https: 443, port_dns_over_tls: 853, port_dns_over_quic: 853, port_dnscrypt: 0, dnscrypt_config_file: "", certificate_chain: "", private_key: "", certificate_path: "", private_key_path: "", strict_sni_check: false}
|
||||||
|
querylog: {dir_path: "", ignored: [], interval: 90d, size_memory: 1000, enabled: true, ignored_enabled: false, file_enabled: true}
|
||||||
|
statistics: {dir_path: "", ignored: [], interval: 1d, enabled: true, ignored_enabled: false}
|
||||||
|
filters:
|
||||||
|
- enabled: true
|
||||||
|
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
|
||||||
|
name: AdGuard DNS filter
|
||||||
|
id: 1
|
||||||
|
whitelist_filters: []
|
||||||
|
user_rules: []
|
||||||
|
dhcp:
|
||||||
|
enabled: false
|
||||||
|
interface_name: ""
|
||||||
|
local_domain_name: lan
|
||||||
|
dhcpv4: {gateway_ip: "", subnet_mask: "", range_start: "", range_end: "", lease_duration: 86400, icmp_timeout_msec: 1000, options: []}
|
||||||
|
dhcpv6: {range_start: "", lease_duration: 86400, ra_slaac_only: false, ra_allow_slaac: false}
|
||||||
|
filtering:
|
||||||
|
blocking_ipv4: ""
|
||||||
|
blocking_ipv6: ""
|
||||||
|
blocked_services: {schedule: {time_zone: UTC}, ids: []}
|
||||||
|
protection_disabled_until: null
|
||||||
|
safe_search: {enabled: false, bing: true, duckduckgo: true, ecosia: true, google: true, pixabay: true, yandex: true, youtube: true}
|
||||||
|
blocking_mode: default
|
||||||
|
parental_block_host: family-block.dns.adguard.com
|
||||||
|
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||||
|
rewrites: []
|
||||||
|
safe_fs_patterns: [/opt/adguardhome/work/userfilters/*]
|
||||||
|
max_http_size: 256MB
|
||||||
|
safebrowsing_cache_size: 1048576
|
||||||
|
safesearch_cache_size: 1048576
|
||||||
|
parental_cache_size: 1048576
|
||||||
|
cache_time: 30
|
||||||
|
filters_update_interval: 24
|
||||||
|
blocked_response_ttl: 10
|
||||||
|
filtering_enabled: true
|
||||||
|
rewrites_enabled: true
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
protection_enabled: true
|
||||||
|
clients:
|
||||||
|
runtime_sources: {whois: true, arp: true, rdns: true, dhcp: true, hosts: true}
|
||||||
|
persistent: []
|
||||||
|
log: {enabled: true, file: "", max_backups: 0, max_size: 100, max_age: 3, compress: false, local_time: false, verbose: false}
|
||||||
|
os: {group: "", user: "", rlimit_nofile: 0}
|
||||||
|
schema_version: 34
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod 600 "$STACK_DIR/conf/AdGuardHome.yaml"
|
||||||
|
cd "$STACK_DIR"
|
||||||
|
docker compose config -q
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
rollback() {
|
||||||
|
warn "Deployment failed. Existing stack files remain backed up at ${backup_dir:-not-created}."
|
||||||
|
}
|
||||||
|
trap rollback ERR
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
log "Waiting for AdGuard Home and HTTPS certificates."
|
||||||
|
for _ in $(seq 1 24); do
|
||||||
|
admin_code="$(curl -sS -o /dev/null -w '%{http_code}' "https://$ADMIN_DOMAIN/" || true)"
|
||||||
|
doh_code="$(curl -sS -o /dev/null -w '%{http_code}' "https://$DOH_DOMAIN/dns-query" || true)"
|
||||||
|
[[ "$admin_code" == "302" || "$admin_code" == "200" ]] && [[ "$doh_code" == "400" ]] && break
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
trap - ERR
|
||||||
|
|
||||||
|
admin_code="$(curl -sS -o /dev/null -w '%{http_code}' "https://$ADMIN_DOMAIN/" || true)"
|
||||||
|
doh_code="$(curl -sS -o /dev/null -w '%{http_code}' "https://$DOH_DOMAIN/dns-query" || true)"
|
||||||
|
[[ "$admin_code" == "302" || "$admin_code" == "200" ]] || die "Admin HTTPS verification failed (HTTP $admin_code). Check: docker compose logs caddy"
|
||||||
|
[[ "$doh_code" == "400" ]] || die "DoH endpoint verification failed (HTTP $doh_code)."
|
||||||
|
|
||||||
|
log "Installation complete."
|
||||||
|
printf 'DoH URL: https://%s/dns-query\n' "$DOH_DOMAIN"
|
||||||
|
printf 'Admin URL: https://%s\n' "$ADMIN_DOMAIN"
|
||||||
|
printf 'Cover URL: https://%s/\n' "$DOH_DOMAIN"
|
||||||
|
if ((new_install)); then
|
||||||
|
printf 'Admin user: %s\n' "$ADMIN_USER"
|
||||||
|
printf 'Admin password (shown once): %s\n' "$ADMIN_PASSWORD"
|
||||||
|
fi
|
||||||
|
printf 'Direct port 3000 is bound to localhost only. Firewall and VPN settings were not changed.\n'
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex,nofollow">
|
||||||
|
<title>Network Services</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:dark;--bg:#0b1220;--card:#111c30;--text:#dce7f7;--muted:#8ea3bf;--accent:#55d6be}
|
||||||
|
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:radial-gradient(circle at 25% 10%,#16304a 0,var(--bg) 50%);font:16px/1.55 system-ui,sans-serif;color:var(--text)}
|
||||||
|
main{width:min(680px,calc(100% - 32px));padding:42px;border:1px solid #243653;border-radius:24px;background:#111c30;box-shadow:0 24px 80px #0008}
|
||||||
|
.mark{width:54px;height:54px;border-radius:16px;display:grid;place-items:center;background:#173649;color:var(--accent);font-size:28px}h1{margin:24px 0 8px;font-size:clamp(28px,5vw,44px)}p{margin:0;color:var(--muted)}.status{display:flex;gap:10px;align-items:center;margin-top:30px;padding-top:22px;border-top:1px solid #243653}.dot{width:10px;height:10px;border-radius:50%;background:var(--accent);box-shadow:0 0 18px var(--accent)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body><main><div class="mark">◆</div><h1>Network services</h1><p>This endpoint provides infrastructure services for authorized clients.</p><div class="status"><span class="dot"></span><span>Service operational</span></div></main></body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
STACK_DIR="${1:-/opt/stacks/adguard-doh}"
|
||||||
|
[[ -f "$STACK_DIR/.env" && -f "$STACK_DIR/compose.yaml" ]] || {
|
||||||
|
echo "Stack not found: $STACK_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$STACK_DIR/.env"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
cd "$STACK_DIR"
|
||||||
|
docker compose ps
|
||||||
|
docker compose config -q
|
||||||
|
|
||||||
|
admin_code="$(curl -sS -o /dev/null -w '%{http_code}' "https://$ADMIN_DOMAIN/")"
|
||||||
|
doh_code="$(curl -sS -o /dev/null -w '%{http_code}' "https://$DOH_DOMAIN/dns-query")"
|
||||||
|
cover_code="$(curl -sS -o /dev/null -w '%{http_code}' "https://$DOH_DOMAIN/")"
|
||||||
|
|
||||||
|
printf 'Admin HTTPS: HTTP %s\n' "$admin_code"
|
||||||
|
printf 'DoH endpoint: HTTP %s (400 is expected without a DNS payload)\n' "$doh_code"
|
||||||
|
printf 'Cover site: HTTP %s\n' "$cover_code"
|
||||||
|
|
||||||
|
[[ "$admin_code" == "302" || "$admin_code" == "200" ]]
|
||||||
|
[[ "$doh_code" == "400" ]]
|
||||||
|
[[ "$cover_code" == "200" ]]
|
||||||
|
|
||||||
|
listeners="$(ss -H -ltn 'sport = :3000')"
|
||||||
|
if [[ -z "$listeners" ]] || printf '%s\n' "$listeners" | awk '{print $4}' | grep -Evq '^127\.0\.0\.1:3000$'; then
|
||||||
|
echo 'Port 3000 is exposed on a non-loopback address.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo 'Verification passed.'
|
||||||
Reference in New Issue
Block a user