testsAndMisc/linux_configuration/features/lib/dot_resolver_install.sh
Krzysztof kuhy Rudnicki 038017d9eb
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
wg firewall: replace only inet filter (no docker restart), persist DoT 853 via ALLOW_DOT
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cqv4ckcaStUFpUkcJKmmNz
2026-10-04 21:16:16 +02:00

161 lines
5.5 KiB
Bash
Executable File

#!/bin/bash
# Certificate sync, stunnel and unit installation for the DoT resolver.
#
# Sourced by setup_dot_resolver.sh; split out to keep it under the 250-line cap.
# Sourced rather than run, so it inherits the caller's strict mode and
# the helper functions and variables defined above the source line.
install_cert_sync() {
# Caddy renews on its own schedule; stunnel reads files from disk and must
# be told. A timer copies the current cert out of the container and
# reloads stunnel only when the content actually changed.
sudo install -d -m 700 "$CERT_DIR"
sudo tee "$SYNC_SCRIPT" >/dev/null <<EOF
#!/bin/bash
# Copy the Caddy-managed certificate for $DOMAIN out to stunnel.
# Generated by $SCRIPT_NAME - edit that instead.
set -euo pipefail
src="$(cert_source_dir)"
tmp="\$(mktemp -d)"
trap 'rm -rf "\$tmp"' EXIT
docker cp "$CADDY_CONTAINER:\$src/$DOMAIN.crt" "\$tmp/cert.pem"
docker cp "$CADDY_CONTAINER:\$src/$DOMAIN.key" "\$tmp/key.pem"
cat "\$tmp/cert.pem" "\$tmp/key.pem" >"\$tmp/combined.pem"
if ! cmp -s "\$tmp/combined.pem" "$CERT_DIR/combined.pem"; then
install -m 600 "\$tmp/combined.pem" "$CERT_DIR/combined.pem"
systemctl reload-or-restart stunnel-dot.service
echo "certificate updated for $DOMAIN"
fi
EOF
sudo chmod 755 "$SYNC_SCRIPT"
}
install_stunnel_conf() {
sudo tee "$STUNNEL_CONF" >/dev/null <<EOF
; DNS-over-TLS front end for the local dnsmasq blocklist.
; Generated by $SCRIPT_NAME - edit that instead.
foreground = yes
syslog = yes
[dns-over-tls]
accept = $BIND_ADDR:$DOT_PORT
connect = 127.0.0.1:53
cert = $CERT_DIR/combined.pem
; DoT is TLS 1.2+ only in practice; Android requires a publicly trusted cert.
sslVersionMin = TLSv1.2
EOF
}
install_units() {
sudo tee /etc/systemd/system/stunnel-dot.service >/dev/null <<EOF
[Unit]
Description=DNS-over-TLS front end for dnsmasq blocklist
After=network-online.target dnsmasq.service wg-quick@wg0.service
Wants=network-online.target
; Wants, not Requires: stunnel only forwards to 127.0.0.1:53 per connection,
; and a Requires= turned one failed dnsmasq start at boot into a DoT outage
; that lasted until someone restarted stunnel by hand.
Wants=dnsmasq.service
; Binding to the WireGuard address fails if the interface is not up yet.
After=sys-devices-virtual-net-wg0.device
[Service]
ExecStart=/usr/bin/stunnel $STUNNEL_CONF
ExecReload=/bin/kill -HUP \$MAINPID
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo tee /etc/systemd/system/dot-cert-sync.service >/dev/null <<EOF
[Unit]
Description=Copy the Caddy certificate for $DOMAIN to stunnel
[Service]
Type=oneshot
ExecStart=$SYNC_SCRIPT
EOF
sudo tee /etc/systemd/system/dot-cert-sync.timer >/dev/null <<EOF
[Unit]
Description=Daily certificate sync for the DoT resolver
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
}
open_firewall() {
# Binding to the WireGuard address is NOT sufficient on its own. The
# input chain has `policy drop` and admits only specific ports over wg0
# (ssh), so a DoT connection from the phone is dropped even though the
# tunnel itself is up and ICMP replies fine. That combination is
# confusing to debug -- ping works, the port does not -- so the rule is
# added explicitly, scoped to the tunnel interface.
if [[ "$BIND_ADDR" != "0.0.0.0" ]]; then
if command -v nft >/dev/null 2>&1 && sudo nft list table inet filter >/dev/null 2>&1; then
# Through the firewall's owner, which persists ALLOW_DOT and renders
# the rule: an 'nft add rule' here vanished at the next re-apply.
if sudo bash "${WG_SCRIPT:-${SCRIPT_DIR}/setup_wireguard_ssh.sh}" allow-dot; then
log "Permitted TCP $DOT_PORT on wg0 only (persisted)"
else
log "WARNING: could not open $DOT_PORT on wg0 (setup_wireguard_ssh.sh allow-dot)"
fi
fi
log "No router port-forward needed - $BIND_ADDR is WireGuard-only."
return
fi
if command -v nft >/dev/null 2>&1 && sudo nft list table inet filter >/dev/null 2>&1; then
if ! sudo nft list table inet filter | grep -q "dport $DOT_PORT"; then
# Rate limited, because a publicly bound 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.
# A phone issues a handful of lookups a second at worst; 25/second
# with a burst of 50 is far above real use and far below abuse.
sudo nft add rule inet filter input tcp dport "$DOT_PORT" \
limit rate 25/second burst 50 packets accept ||
log "WARNING: could not add rate-limited nftables rule for $DOT_PORT"
log "Permitted TCP $DOT_PORT (rate limited 25/s, burst 50)"
fi
fi
log "Ensure your router forwards TCP $DOT_PORT to this host."
}
run_check() {
local ok=0
systemctl is-active --quiet stunnel-dot.service ||
{
log "FAIL stunnel-dot.service not active"
ok=1
}
ss -lnt 2>/dev/null | grep -qE ":$DOT_PORT\b" ||
{
log "FAIL nothing listening on $DOT_PORT"
ok=1
}
# $CERT_DIR is mode 700 and owned by root because it holds a private key,
# so an unprivileged [[ -s ]] reports "missing" for a file that is
# present. Test as root, or --check reports a false failure on a healthy
# install.
sudo test -s "$CERT_DIR/combined.pem" ||
{
log "FAIL certificate missing at $CERT_DIR/combined.pem"
ok=1
}
if command -v kdig >/dev/null 2>&1; then
kdig +tls "@$BIND_ADDR" -p "$DOT_PORT" +short example.com >/dev/null 2>&1 ||
{
log "FAIL DoT query did not answer"
ok=1
}
fi
[[ $ok -eq 0 ]] && log "OK - DoT resolver healthy on $DOMAIN:$DOT_PORT"
return "$ok"
}