Files
darkproxy/scripts/validate-config.sh
auto-ci 21718042b6
Configuration validation / lint (push) Has been cancelled
Configuration validation / smoke (push) Has been cancelled
Harden release defaults and validation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-03 19:29:00 -04:00

247 lines
10 KiB
Bash

#!/usr/bin/env bash
# Utility to validate the Compose file and other service configurations.
# Intended to be run locally or in CI; it exits non-zero on any failure.
set -euo pipefail
BASELINE_FILE="docker-compose.lock.yml"
TEMP_FILE=$(mktemp)
# validate docker-compose configuration if Docker is present
if command -v docker >/dev/null 2>&1; then
echo "[validate-config] generating normalized compose manifest"
docker compose config > "$TEMP_FILE"
if [ -f "$BASELINE_FILE" ]; then
echo "[validate-config] comparing against baseline $BASELINE_FILE"
if ! diff -u "$BASELINE_FILE" "$TEMP_FILE"; then
echo "[validate-config] configuration drift detected!"
echo "Please review and, if the change is intentional, update $BASELINE_FILE."
exit 1
fi
echo "[validate-config] docker-compose.yml matches baseline"
else
echo "[validate-config] baseline file not found, creating $BASELINE_FILE"
cp "$TEMP_FILE" "$BASELINE_FILE"
echo "[validate-config] please commit $BASELINE_FILE to the repository"
fi
else
echo "[validate-config] docker binary not found; skipping compose validation"
fi
# CoreDNS syntax check must use the compiled image because the Corefile relies
# on custom plugins (meshname/meship) that are not present in the stock binary.
if command -v docker >/dev/null 2>&1; then
echo "[validate-config] checking PopuraDNS/Corefile with compiled coredns image"
docker compose build coredns >/dev/null
COREDNS_IMAGE="$(docker compose images -q coredns | head -n1)"
if [ -z "$COREDNS_IMAGE" ]; then
echo "[validate-config] error: failed to resolve compiled coredns image" >&2
exit 1
fi
set +e
timeout -k 2s 8s docker run --rm "$COREDNS_IMAGE" -conf /Corefile -dns.port=0
COREDNS_STATUS=$?
set -e
if [ "$COREDNS_STATUS" -ne 0 ] && [ "$COREDNS_STATUS" -ne 124 ]; then
echo "[validate-config] error: compiled coredns failed to load PopuraDNS/Corefile" >&2
exit "$COREDNS_STATUS"
fi
elif command -v coredns >/dev/null 2>&1 && coredns -plugins 2>/dev/null | grep -q '^meshname$'; then
echo "[validate-config] checking PopuraDNS/Corefile with local compiled coredns"
set +e
timeout -k 2s 8s coredns -conf PopuraDNS/Corefile -dns.port=0
COREDNS_STATUS=$?
set -e
if [ "$COREDNS_STATUS" -ne 0 ] && [ "$COREDNS_STATUS" -ne 124 ]; then
echo "[validate-config] error: local compiled coredns failed to load PopuraDNS/Corefile" >&2
exit "$COREDNS_STATUS"
fi
else
echo "[validate-config] cannot check Corefile syntax (need docker or a compiled local coredns with meshname plugin)"
fi
# enforce portable defaults for host/LAN-specific settings in the base stack
if ! grep -q 'ROUTER_NO_AUTH_CIDRS: ${ROUTER_NO_AUTH_CIDRS:-}' docker-compose.yml; then
echo "[validate-config] error: ROUTER_NO_AUTH_CIDRS should default to empty and be overridden explicitly per deployment" >&2
exit 1
fi
if ! grep -q '\${PIHOLE_WEB_BIND_HOST:-127.0.0.1}:\${PIHOLE_WEB_BIND_PORT:-2003}:80/tcp' docker-compose.yml; then
echo "[validate-config] error: Pi-hole web binding should default to 127.0.0.1 and be configurable via env vars" >&2
exit 1
fi
if ! grep -q '\${STATUS_DASHBOARD_BIND_HOST:-127.0.0.1}:\${STATUS_DASHBOARD_BIND_PORT:-2004}:8080' docker-compose.yml; then
echo "[validate-config] error: status dashboard binding should default to 127.0.0.1 and be configurable via env vars" >&2
exit 1
fi
# ensure onion and i2p zones are present in Corefile
for zone in "onion.:53" "i2p.:53" "eth.:53" "bit.:53" "alt.:53" "loki.:53" "zil.:53" "web3.:53" "exit.:53" "onion4.:53" "onion6.:53"; do
if ! grep -q "$zone" PopuraDNS/Corefile; then
echo "[validate-config] error: Corefile missing $zone block" >&2
exit 1
fi
done
if ! grep -A6 '^\.:53 {' PopuraDNS/Corefile | grep -q 'acl {'; then
echo "[validate-config] error: Corefile catch-all zone must include an ACL to avoid open recursion" >&2
exit 1
fi
# verify the current .i2p DNS architecture is documented in config
if grep -q '^\[dns\]' i2pd_yggdrasil_docker/src/i2pd.conf; then
if ! awk '/^\[dns\]/,/^\[/{if($0~/enabled/ && $0~/true/) ok=1} END{exit !ok}' i2pd_yggdrasil_docker/src/i2pd.conf; then
echo "[validate-config] warning: i2pd [dns] section exists but is disabled; the stack currently expects the separate i2pdns bridge service"
fi
elif ! grep -qE 'i2pdns bridge|local i2pdns bridge service' i2pd_yggdrasil_docker/src/i2pd.conf; then
echo "[validate-config] warning: i2pd.conf should document that .i2p DNS is served by the separate i2pdns bridge service"
fi
# tor configuration should expose DNSPort for onion resolution
if ! grep -q '^DNSPort' tor_yggdrasil_docker/torrc; then
echo "[validate-config] warning: torrc does not specify DNSPort; onion DNS will not work"
fi
# optional live DNS smoke queries (requires dig)
if command -v dig >/dev/null 2>&1; then
echo "[validate-config] performing live DNS query for .onion via CoreDNS"
if ! dig @10.5.0.4 -p 53 facebookcorewwwi.onion +short >/dev/null; then
echo "[validate-config] onion query failed" >&2
exit 1
fi
echo "[validate-config] performing live DNS query for .i2p via CoreDNS"
if ! dig @10.5.0.4 -p 53 stats.i2p +short >/dev/null; then
echo "[validate-config] i2p query failed" >&2
exit 1
fi
echo "[validate-config] performing live DNS query for .eth via CoreDNS"
if ! dig @10.5.0.4 -p 53 TXT vitalik.eth +short | grep -q 'address='; then
echo "[validate-config] eth query failed" >&2
exit 1
fi
echo "[validate-config] performing live DNS query for .zil via CoreDNS"
if ! dig @10.5.0.4 -p 53 TXT brad.zil +short | grep -q 'zil_address='; then
echo "[validate-config] zil query failed" >&2
exit 1
fi
fi
# ensure referenced secrets exist
for s in \
./secrets/tz.txt \
./secrets/YGGDRASIL_GENERATE_KEYS.txt \
./secrets/pihole_webpassword.txt \
./secrets/3proxy_users.txt \
./secrets/namecoin_rpc_password.txt; do
if [ ! -f "$s" ]; then
echo "[validate-config] error: secret file $s is missing" >&2
exit 1
fi
done
# check for mutable :latest tags in deployment compose files
if grep -qE 'image: .*:latest' docker-compose.yml; then
echo "[validate-config] error: mutable ':latest' image tags are not allowed in the release compose file" >&2
exit 1
fi
# warn if there are no healthchecks at all
if ! grep -q 'healthcheck:' docker-compose.yml; then
echo "[validate-config] warning: no healthcheck definitions found in docker-compose.yml"
fi
# verify every service has a restart policy
if ! grep -q "restart:" docker-compose.yml; then
echo "[validate-config] warning: some services lack a restart policy."
fi
# detect duplicate host ports
ports=$(grep -oP ':[0-9]+:' docker-compose.yml | tr -d ':' | sort)
if [ -n "$(echo "$ports" | uniq -d)" ]; then
echo "[validate-config] warning: duplicate host port mappings detected:" $(echo "$ports" | uniq -d)
fi
# unbound configuration syntax check
if command -v docker >/dev/null 2>&1; then
echo "[validate-config] checking unbound configuration in runtime image"
UNBOUND_IMAGE="$(awk '/image: mvance\/unbound@sha256:/{print $2; exit}' "$TEMP_FILE")"
if [ -z "$UNBOUND_IMAGE" ]; then
echo "[validate-config] error: failed to resolve unbound runtime image from rendered compose config" >&2
exit 1
fi
docker run --rm --entrypoint /bin/sh \
-v "${PWD}/unbound/unbound.conf:/opt/unbound/etc/unbound/unbound.conf:ro" \
-v "${PWD}/unbound/forward-records.conf:/opt/unbound/etc/unbound/forward-records.conf:ro" \
-v "${PWD}/unbound/a-records.conf:/opt/unbound/etc/unbound/a-records.conf:ro" \
-v "${PWD}/unbound/srv-records.conf:/opt/unbound/etc/unbound/srv-records.conf:ro" \
"$UNBOUND_IMAGE" -lc \
'getent passwd _unbound >/dev/null || useradd -r -s /usr/sbin/nologin _unbound; mkdir -p /opt/unbound/etc/unbound/dev /opt/unbound/etc/unbound/var/log; : > /opt/unbound/etc/unbound/dev/null; [ -s /opt/unbound/etc/unbound/var/root.key ] || /opt/unbound/sbin/unbound-anchor -a /opt/unbound/etc/unbound/var/root.key >/dev/null 2>&1 || :; [ -e /opt/unbound/etc/unbound/var/root.key ] || : > /opt/unbound/etc/unbound/var/root.key; /opt/unbound/sbin/unbound-checkconf /opt/unbound/etc/unbound/unbound.conf'
elif command -v unbound-checkconf >/dev/null 2>&1; then
echo "[validate-config] checking unbound configuration locally"
unbound-checkconf unbound/unbound.conf
else
echo "[validate-config] cannot check unbound config (no unbound-checkconf or docker)"
fi
# very basic 3proxy config sanity
for cfg in 3proxy/first-instanse.cfg 3proxy/second-instanse.cfg; do
if [ ! -f "$cfg" ]; then
echo "[validate-config] error: missing 3proxy config $cfg" >&2
exit 1
fi
if ! grep -qE 'socks|proxy' "$cfg"; then
echo "[validate-config] warning: $cfg does not contain socks/proxy directives"
fi
done
if grep -q '^auth none$' 3proxy/first-instanse.cfg; then
echo "[validate-config] error: dark3proxy must not expose a public proxy with 'auth none'" >&2
exit 1
fi
if grep -q 'CHANGE_ME_NAMECOIN_RPC_PASSWORD' docker-compose.yml; then
echo "[validate-config] error: replace inline Namecoin RPC password placeholders with Docker secrets" >&2
exit 1
fi
if grep -q 'tskey-YOUR-AUTH-KEY-HERE' docker-compose.yml; then
echo "[validate-config] error: tailscale placeholder auth key is still present in docker-compose.yml" >&2
exit 1
fi
if ! grep -q 'profiles: \["tailscale"\]' docker-compose.yml; then
echo "[validate-config] error: tailscale should be opt-in via a compose profile" >&2
exit 1
fi
# simple sanity checks
if grep -q "container_name: darkproxy" docker-compose.yml; then
echo "[validate-config] warning: 'container_name: darkproxy' appears in compose;" \
"this may conflict with network or project name."
fi
# ensure Pi-hole DNS environment is correctly formatted
if grep -q "PIHOLE_DNS_:" docker-compose.yml; then
echo "[validate-config] warning: PIHOLE_DNS_ variable ends with underscore;" \
"consider using PIHOLE_DNS_1, PIHOLE_DNS_2 etc."
fi
# ensure critical services have healthchecks in the rendered compose config
if [ -f "$TEMP_FILE" ]; then
for service in dark3proxy i2pd_yggdrasil i2pdns unbound pihole ensdns zildns lokinet namecoind namecoindns status_dashboard; do
if ! awk -v svc="$service" '
$0 ~ "^ " svc ":" { in_svc=1; next }
in_svc && $0 ~ "^ [^ ]" { exit !found }
in_svc && $0 ~ "^ healthcheck:" { found=1 }
END { if (in_svc) exit !found; exit 0 }
' "$TEMP_FILE"; then
echo "[validate-config] error: service $service is missing a healthcheck in rendered compose config" >&2
exit 1
fi
done
fi
echo "[validate-config] all validations passed"