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