Add safe AdGuard DoH installer

This commit is contained in:
AlexChard
2026-08-29 19:16:00 +04:00
commit d97726aa91
6 changed files with 617 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
.env
conf/
work/
caddy-data/
caddy-config/
backups/
*.bak
*.log
+21
View File
@@ -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.
+107
View File
@@ -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
View File
@@ -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'
+16
View File
@@ -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>
+37
View File
@@ -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.'