mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
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
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
Every script under /usr/local/bin must be a repo file. install_cert_sync wrote /usr/local/bin/sync-dot-cert.sh from a heredoc with the domain, Caddy container and cert dir baked in. The script now lives at linux_configuration/scripts/sync-dot-cert.sh, takes those three as arguments (defaults = the installer's constants), and is installed with install -m 755. dot-cert-sync.service and the first sync pass the arguments, so --domain reaches the timer too. Test asserts the installed file is the tracked one byte for byte and runs the script itself against a docker shim (reload only on change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014yhTkYV6ZeuAbQYvykFGFc Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
244 lines
11 KiB
Bash
Executable File
244 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Tests for dot_resolver_install.sh — the DoT resolver's install half.
|
|
#
|
|
# Every function here sudo-writes a systemd unit, an stunnel config or an
|
|
# nftables rule against LIVE infrastructure (dns.kuhy.duckdns.org:853). They
|
|
# are executed for real, not mocked, because the generated CONTENT is the
|
|
# thing worth asserting -- a unit file with the wrong ExecStart is exactly the
|
|
# bug a presence check would miss.
|
|
#
|
|
# This is only safe under meta/scripts/shell_coverage_jail.sh, which runs the
|
|
# suite inside a user+mount namespace with /etc, /usr/local/bin and /var/lib
|
|
# bind-mounted to a throwaway dir. Run directly on a real host it would
|
|
# rewrite the resolver, so it refuses to start unless the jail is in place.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=./features_harness.sh
|
|
. "$SCRIPT_DIR/features_harness.sh"
|
|
|
|
# --- Refuse to run outside the jail -----------------------------------------
|
|
# The jail bind-mounts /etc, so the real resolver's config is absent inside it.
|
|
# Seeing the host's own stunnel config means we are NOT contained.
|
|
if [[ -f /etc/stunnel/dot-resolver.conf ]] && [[ ! -w /etc ]]; then
|
|
printf 'REFUSING: this suite must run under shell_coverage_jail.sh\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
_t_setup_env
|
|
trap _t_teardown EXIT
|
|
|
|
# The values the entry script defines above its source line.
|
|
SCRIPT_NAME="setup_dot_resolver.sh"
|
|
CERT_DIR="/etc/stunnel/dot"
|
|
STUNNEL_CONF="/etc/stunnel/dot-resolver.conf"
|
|
SYNC_SCRIPT="/usr/local/bin/sync-dot-cert.sh"
|
|
DOT_PORT=853
|
|
DOMAIN="dns.kuhy.duckdns.org"
|
|
BIND_ADDR="10.8.0.1"
|
|
CADDY_CONTAINER="caddy"
|
|
|
|
# install_cert_sync installs this tracked file verbatim (no heredoc): every
|
|
# script under /usr/local/bin must be a file in a repo.
|
|
SYNC_SCRIPT_SRC="$(cd "$SCRIPT_DIR/../../../scripts" && pwd)/sync-dot-cert.sh"
|
|
die() {
|
|
printf 'die: %s\n' "$1" >&2
|
|
return 1
|
|
}
|
|
|
|
# shellcheck source=../dot_resolver_install.sh
|
|
. "$FEATURES_LIB_DIR/dot_resolver_install.sh"
|
|
|
|
# Fixture self-check: a typo in one of these names would turn every assertion
|
|
# below into a silent no-op rather than a failure.
|
|
[[ -n $CERT_DIR && -n $STUNNEL_CONF && -n $SYNC_SCRIPT ]] ||
|
|
{
|
|
printf 'fixture globals are not populated\n' >&2
|
|
exit 1
|
|
}
|
|
|
|
mkdir -p /etc/stunnel /etc/systemd/system /usr/local/bin
|
|
|
|
# --- install_cert_sync ------------------------------------------------------
|
|
install_cert_sync
|
|
if cmp -s "$SYNC_SCRIPT_SRC" "$SYNC_SCRIPT"; then
|
|
_t_pass "the installed sync script is the tracked file, byte for byte"
|
|
else
|
|
_t_fail "the installed sync script is the tracked file, byte for byte"
|
|
fi
|
|
if grep -q 'Generated by' "$SYNC_SCRIPT"; then
|
|
_t_fail "the sync script is not generated"
|
|
else
|
|
_t_pass "the sync script is not generated"
|
|
fi
|
|
if [[ -d $CERT_DIR ]]; then
|
|
_t_pass "cert dir created"
|
|
else
|
|
_t_fail "cert dir created"
|
|
fi
|
|
_t_eq "700" "$(stat -c '%a' "$CERT_DIR")" "cert dir is mode 700 (it holds a private key)"
|
|
_t_eq "755" "$(stat -c '%a' "$SYNC_SCRIPT")" "sync script is executable"
|
|
# Reinstall over a stale generated copy: install replaces it (idempotent).
|
|
printf '#!/bin/bash\n# Generated by setup_dot_resolver.sh\n' >"$SYNC_SCRIPT"
|
|
install_cert_sync
|
|
_t_eq "$(sha256sum <"$SYNC_SCRIPT_SRC")" "$(sha256sum <"$SYNC_SCRIPT")" "a stale generated copy is replaced"
|
|
out="$(SYNC_SCRIPT_SRC="$TEST_TMPDIR/absent.sh" install_cert_sync 2>&1 || true)"
|
|
_t_has "$out" 'tracked sync script missing' "a missing tracked file stops the install"
|
|
|
|
# --- the tracked sync script itself -----------------------------------------
|
|
# docker cp is stubbed to copy from a fixture tree, so the script's own logic
|
|
# (assemble, compare, install, reload-only-on-change) runs for real.
|
|
SYNC_CERTS="$TEST_TMPDIR/caddy"
|
|
mkdir -p "$SYNC_CERTS"
|
|
cat >"$TEST_TMPDIR/bin/docker" <<DOCKER
|
|
#!/usr/bin/env bash
|
|
printf 'docker %s\n' "\$*" >>"$TEST_TMPDIR/calls.log"
|
|
cp "$SYNC_CERTS/\$(basename "\${2#*:}")" "\$3"
|
|
DOCKER
|
|
chmod +x "$TEST_TMPDIR/bin/docker"
|
|
_t_stub systemctl
|
|
sync_dir="$TEST_TMPDIR/stunnel-dot"
|
|
mkdir -p "$sync_dir"
|
|
printf 'CERT1\n' >"$SYNC_CERTS/d.example.crt"
|
|
printf 'KEY1\n' >"$SYNC_CERTS/d.example.key"
|
|
out="$("$SYNC_SCRIPT_SRC" d.example box "$sync_dir")"
|
|
_t_eq "$(printf 'CERT1\nKEY1')" "$(cat "$sync_dir/combined.pem")" "the sync assembles cert + key"
|
|
_t_eq "600" "$(stat -c '%a' "$sync_dir/combined.pem")" "combined.pem is mode 600"
|
|
_t_has "$out" 'certificate updated for d.example' "a new cert is announced"
|
|
_t_called 'docker cp box:/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/d.example/d.example.crt' \
|
|
"the cert is read from the domain's Caddy dir in the named container"
|
|
_t_called 'systemctl reload-or-restart stunnel-dot.service' "a new cert reloads stunnel"
|
|
: >"$TEST_TMPDIR/calls.log"
|
|
out="$("$SYNC_SCRIPT_SRC" d.example box "$sync_dir")"
|
|
_t_eq "" "$out" "an unchanged cert is silent"
|
|
if grep -q reload "$TEST_TMPDIR/calls.log"; then
|
|
_t_fail "an unchanged cert does not reload stunnel"
|
|
else
|
|
_t_pass "an unchanged cert does not reload stunnel"
|
|
fi
|
|
_t_unstub docker
|
|
_t_unstub systemctl
|
|
|
|
# --- install_stunnel_conf ---------------------------------------------------
|
|
install_stunnel_conf
|
|
_t_file_has "$STUNNEL_CONF" "accept = $BIND_ADDR:$DOT_PORT" "stunnel accepts on the bind address"
|
|
_t_file_has "$STUNNEL_CONF" 'connect = 127.0.0.1:53' "stunnel forwards to local dnsmasq"
|
|
_t_file_has "$STUNNEL_CONF" 'sslVersionMin = TLSv1.2' "stunnel refuses pre-TLS1.2 (Android requires it)"
|
|
_t_file_has "$STUNNEL_CONF" "cert = $CERT_DIR/combined.pem" "stunnel reads the synced cert"
|
|
|
|
# --- install_units ----------------------------------------------------------
|
|
_t_stub systemctl
|
|
install_units
|
|
_t_file_has /etc/systemd/system/stunnel-dot.service 'stunnel' "stunnel unit installed"
|
|
# Requires= turned one failed dnsmasq start at boot into a DoT outage that
|
|
# outlived dnsmasq's own 5 s recovery; Wants= lets stunnel start regardless.
|
|
_t_file_has /etc/systemd/system/stunnel-dot.service '^Wants=dnsmasq\.service$' "stunnel only Wants dnsmasq"
|
|
if grep -q '^Requires=dnsmasq' /etc/systemd/system/stunnel-dot.service; then
|
|
_t_fail "Requires=dnsmasq.service would fail stunnel whenever dnsmasq's first start fails"
|
|
else
|
|
_t_pass "no Requires=dnsmasq.service"
|
|
fi
|
|
_t_file_has /etc/systemd/system/dot-cert-sync.service "^ExecStart=$SYNC_SCRIPT $DOMAIN $CADDY_CONTAINER $CERT_DIR\$" \
|
|
"cert-sync service passes domain, container and cert dir"
|
|
_t_file_has /etc/systemd/system/dot-cert-sync.timer 'OnCalendar' "cert-sync timer is scheduled"
|
|
_t_called 'daemon-reload' "units are followed by a daemon-reload"
|
|
|
|
# --- open_firewall: the WireGuard-only branch -------------------------------
|
|
# nft absent entirely: the function must still return cleanly rather than
|
|
# aborting the install on a host without nftables.
|
|
out="$(open_firewall 2>&1)"
|
|
_t_has "$out" 'WireGuard-only' "wg-bound install needs no router forward"
|
|
|
|
# nft present: the rule goes through the firewall owner's allow-dot, which
|
|
# persists ALLOW_DOT so a later re-apply keeps it (a bare 'nft add rule' was
|
|
# silently dropped by the next one).
|
|
_t_stub nft
|
|
WG_SCRIPT="$TEST_TMPDIR/bin/wg_owner"
|
|
printf '#!/usr/bin/env bash\nprintf "wg_owner %%s\\n" "$*" >>"%s/calls.log"\nexit 0\n' \
|
|
"$TEST_TMPDIR" >"$WG_SCRIPT"
|
|
chmod +x "$WG_SCRIPT"
|
|
out="$(open_firewall 2>&1)"
|
|
_t_has "$out" 'wg0 only (persisted)' "the wg0 rule is opened through the firewall owner"
|
|
_t_called 'wg_owner allow-dot' "open_firewall runs setup_wireguard_ssh.sh allow-dot"
|
|
if grep -q 'add rule inet filter input iifname wg0' "$TEST_TMPDIR/calls.log"; then
|
|
_t_fail "no unpersisted runtime 'nft add rule' for the wg0 branch"
|
|
else
|
|
_t_pass "no unpersisted runtime 'nft add rule' for the wg0 branch"
|
|
fi
|
|
|
|
# allow-dot fails (e.g. sshd check rolled it back): warn and carry on.
|
|
printf '#!/usr/bin/env bash\nexit 1\n' >"$WG_SCRIPT"
|
|
out="$(open_firewall 2>&1)"
|
|
_t_has "$out" 'WARNING: could not open' "a failed allow-dot warns instead of aborting"
|
|
unset WG_SCRIPT
|
|
|
|
# The public branch still adds its rule at runtime; its add-failure path
|
|
# needs an nft whose 'list' succeeds and 'add' fails.
|
|
cat >"$TEST_TMPDIR/bin/nft" <<'NFT'
|
|
#!/usr/bin/env bash
|
|
printf 'nft %s\n' "$*" >>"$TEST_TMPDIR/calls.log"
|
|
[[ $* == *add* ]] && exit 1
|
|
exit 0
|
|
NFT
|
|
chmod +x "$TEST_TMPDIR/bin/nft"
|
|
|
|
# --- open_firewall: the publicly-bound branch -------------------------------
|
|
# A public bind is an open resolver, so the rule MUST carry a rate limit.
|
|
BIND_ADDR="0.0.0.0"
|
|
out="$(open_firewall 2>&1)"
|
|
_t_has "$out" 'rate limited' "a public bind is rate limited"
|
|
_t_called 'limit rate 25/second burst 50' "the public rule carries the documented limit"
|
|
_t_has "$out" 'router forwards' "a public bind asks for a router forward"
|
|
|
|
# The same add-failure path on the public branch.
|
|
out="$(open_firewall 2>&1)"
|
|
_t_has "$out" 'WARNING: could not add rate-limited nftables rule' \
|
|
"a failed public rule add warns instead of aborting"
|
|
BIND_ADDR="10.8.0.1"
|
|
|
|
# --- run_check --------------------------------------------------------------
|
|
# Everything healthy: stunnel active, port listening, cert present.
|
|
printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_TMPDIR/bin/systemctl"
|
|
chmod +x "$TEST_TMPDIR/bin/systemctl"
|
|
printf '#!/usr/bin/env bash\nprintf "LISTEN 0 0 10.8.0.1:%s\\n"\n' "$DOT_PORT" >"$TEST_TMPDIR/bin/ss"
|
|
chmod +x "$TEST_TMPDIR/bin/ss"
|
|
install -m 600 /dev/null "$CERT_DIR/combined.pem"
|
|
printf 'cert\n' >"$CERT_DIR/combined.pem"
|
|
if run_check >/dev/null 2>&1; then
|
|
_t_pass "run_check passes on a healthy install"
|
|
else
|
|
_t_fail "run_check passes on a healthy install"
|
|
fi
|
|
|
|
# stunnel down: must fail and say which check failed.
|
|
printf '#!/usr/bin/env bash\n[[ $* == *is-active* ]] && exit 1\nexit 0\n' >"$TEST_TMPDIR/bin/systemctl"
|
|
chmod +x "$TEST_TMPDIR/bin/systemctl"
|
|
out="$(run_check 2>&1 || true)"
|
|
_t_has "$out" 'FAIL stunnel-dot.service not active' "a stopped stunnel is reported"
|
|
|
|
# Certificate missing: the check that must be run as root to be correct.
|
|
rm -f "$CERT_DIR/combined.pem"
|
|
printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_TMPDIR/bin/systemctl"
|
|
chmod +x "$TEST_TMPDIR/bin/systemctl"
|
|
out="$(run_check 2>&1 || true)"
|
|
_t_has "$out" 'FAIL certificate missing' "a missing certificate is reported"
|
|
|
|
# Nothing listening on the port.
|
|
printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_TMPDIR/bin/ss"
|
|
chmod +x "$TEST_TMPDIR/bin/ss"
|
|
out="$(run_check 2>&1 || true)"
|
|
_t_has "$out" 'FAIL nothing listening' "a silent port is reported"
|
|
|
|
# kdig present and the DoT query failing. The check is skipped entirely when
|
|
# kdig is absent, so this arm only exists on a host that has knot-utils --
|
|
# which makes it exactly the arm most likely to rot unnoticed.
|
|
printf '#!/usr/bin/env bash\nexit 1\n' >"$TEST_TMPDIR/bin/kdig"
|
|
chmod +x "$TEST_TMPDIR/bin/kdig"
|
|
printf 'cert\n' >"$CERT_DIR/combined.pem"
|
|
printf '#!/usr/bin/env bash\nprintf "LISTEN 0 0 10.8.0.1:%s\\n"\n' "$DOT_PORT" >"$TEST_TMPDIR/bin/ss"
|
|
chmod +x "$TEST_TMPDIR/bin/ss"
|
|
out="$(run_check 2>&1 || true)"
|
|
_t_has "$out" 'FAIL DoT query did not answer' "an unanswered DoT query is reported"
|
|
|
|
_t_report "test_dot_resolver_install.sh"
|