testsAndMisc/linux_configuration/scripts/single_use/features/setup_dot_resolver.sh
Krzysztof kuhy Rudnicki 7e967de79b
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Split seven more linux_configuration scripts
10-recon.sh, install_exercism.sh, fix_arch_performance.sh,
format_sd_card.sh, setup_dot_resolver.sh, fix_unity.sh and
20-dump-stock.sh, all now under the cap.

Splitting a file twice is the risky move: the second cut on
20-dump-stock.sh dropped the first cut's source line, leaving
verify_dump called but never defined, and the repair then landed the
line inside the lib instead of the entry script. Both fixed - the entry
sources both libs and no lib sources another. Grep the source lines
after any double split.

fix_unity.sh's desktop_dir is read across the seam; renamed to
DESKTOP_DIR to match the file's own convention for globals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 18:56:05 +02:00

163 lines
5.3 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Expose the existing dnsmasq blocklist over DNS-over-TLS (port 853).
#
# Why this exists:
# setup_dns_blocker.sh serves the blocklist over plain UDP/53, which only
# works on the LAN -- and Android's default "Automatic" Private DNS uses DoH
# and bypasses it entirely. A phone off the LAN gets no filtering at all.
# Terminating DoT here lets the phone pin Private DNS to a resolver that
# applies the blocklist, anywhere, with no root and no VPN slot consumed.
#
# Why stunnel rather than Caddy:
# DoT is a raw TLS stream, not HTTPS. Terminating it in Caddy needs the
# `layer4` module, which the stock `caddy:2.8` image does not ship -- that
# would mean an xcaddy rebuild of a container currently serving five live
# sites. stunnel does exactly this one job, and it can reuse the Let's
# Encrypt certificate Caddy already renews, so there is no second ACME
# client competing for port 80.
#
# Usage:
# ./setup_dot_resolver.sh [--domain <fqdn>] [--check]
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
readonly DEFAULT_DOMAIN="dns.kuhy.duckdns.org"
readonly CADDY_CONTAINER="gitea-caddy"
readonly CERT_DIR="/etc/stunnel/dot"
readonly STUNNEL_CONF="/etc/stunnel/dot-resolver.conf"
readonly SYNC_SCRIPT="/usr/local/bin/sync-dot-cert.sh"
readonly DOT_PORT=853
readonly DEFAULT_BIND="10.8.0.1"
DOMAIN="$DEFAULT_DOMAIN"
# Bind to the WireGuard interface, not 0.0.0.0. An internet-facing DoT
# endpoint is an open resolver: TCP-only so not usable for classic UDP
# amplification, but still someone else's free DNS and a standing attack
# surface. Reaching it over the existing tunnel needs no port-forward and no
# rate limiting, because only configured peers can route to it at all.
BIND_ADDR="$DEFAULT_BIND"
CHECK_ONLY=0
log() { printf '[dot-resolver] %s\n' "$1"; }
die() {
printf '[dot-resolver] ERROR: %s\n' "$1" >&2
exit 1
}
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS]
--domain <fqdn> DoT hostname (default: $DEFAULT_DOMAIN)
--bind <addr> Address to listen on (default: $DEFAULT_BIND, the
WireGuard interface). Use 0.0.0.0 only if you intend
to expose an open resolver to the internet.
--check Verify an existing install, change nothing
-h, --help Show this help
EOF
exit 0
}
require_tools() {
# Per repo policy a script installs what it needs rather than telling the
# user to. pacman is non-interactive here; if that fails we stop rather
# than silently skipping the dependency.
local missing=()
command -v stunnel >/dev/null 2>&1 || missing+=(stunnel)
command -v openssl >/dev/null 2>&1 || missing+=(openssl)
if [[ ${#missing[@]} -gt 0 ]]; then
log "Installing: ${missing[*]}"
sudo pacman -S --needed --noconfirm "${missing[@]}" ||
die "could not install ${missing[*]}; run: sudo pacman -S ${missing[*]}"
fi
}
cert_source_dir() {
# Caddy stores certs per-domain under its data volume.
printf '/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/%s' \
"$DOMAIN"
}
verify_prerequisites() {
# A DoT endpoint in front of a resolver that is not running would answer
# every query with a connection error, which looks exactly like a network
# fault on the phone. Check first.
ss -lnu 2>/dev/null | grep -qE '127\.0\.0\.1:53' ||
die "dnsmasq is not listening on 127.0.0.1:53 - run setup_dns_blocker.sh first"
docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$CADDY_CONTAINER" ||
die "container '$CADDY_CONTAINER' is not running; it owns the certificates"
local resolved
resolved="$(getent hosts "$DOMAIN" | awk '{print $1; exit}')" ||
die "$DOMAIN does not resolve - DuckDNS wildcard missing?"
log "$DOMAIN resolves to $resolved"
# Binding to an address that does not exist yet makes stunnel fail at
# start with an opaque error, so check before installing anything.
if [[ "$BIND_ADDR" != "0.0.0.0" ]]; then
ip -brief addr show 2>/dev/null | grep -q "$BIND_ADDR" ||
die "$BIND_ADDR is not assigned to any interface - is wg0 up?"
log "Binding to $BIND_ADDR (reachable over WireGuard only)"
else
log "WARNING: binding to 0.0.0.0 exposes an open resolver publicly"
fi
}
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
# shellcheck source=lib/dot_resolver_install.sh
source "$SCRIPT_DIR/lib/dot_resolver_install.sh"
main() {
if [[ $CHECK_ONLY -eq 1 ]]; then
run_check
exit $?
fi
require_tools
verify_prerequisites
install_cert_sync
install_stunnel_conf
install_units
log "Syncing certificate..."
sudo "$SYNC_SCRIPT" || die "certificate sync failed - has Caddy issued $DOMAIN yet?"
sudo systemctl enable --now stunnel-dot.service
sudo systemctl enable --now dot-cert-sync.timer
open_firewall
log "Done. Point the phone at:"
log " Private DNS hostname: $DOMAIN"
log "And set in phone_focus_mode/config.sh:"
log " DNS_TRUSTED_DOT_HOST=\"$DOMAIN\""
log " DNS_TRUSTED_DOT_IPS=\"\$(getent hosts $DOMAIN | awk '{print \$1}')\""
run_check || true
}
while [[ $# -gt 0 ]]; do
case $1 in
--domain)
DOMAIN="$2"
shift 2
;;
--bind)
BIND_ADDR="$2"
shift 2
;;
--check)
CHECK_ONLY=1
shift
;;
-h | --help) usage ;;
*) die "Unknown option: $1" ;;
esac
done
main "$@"