- decouple restart() from stop_impl() in lifecycle.uc to avoid premature dnsmasq_restore - keep dhcp.@dnsmasq[0].server locked to 127.0.0.42 throughout service restarts - add atomic restart_service hooks in init.d/tachyon and initd.uc - fix dns_turbo_cache path resolution to promote default cache to /etc/sing-box/cache.db - add Local DNS Cache support in dnsmasq (dns_local_cache, dns_cache_size) - add comprehensive behavioral regression test tests/service_restart_lifecycle.sh
203 lines
7.3 KiB
Bash
203 lines
7.3 KiB
Bash
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
|
TACHYON_INIT="$ROOT_DIR/tachyon/files/etc/init.d/tachyon"
|
|
LIFECYCLE_UC="$ROOT_DIR/tachyon/files/usr/lib/service/lifecycle.uc"
|
|
INITD_UC="$ROOT_DIR/tachyon/files/usr/lib/service/initd.uc"
|
|
APPLY_UC="$ROOT_DIR/tachyon/files/usr/lib/dns/apply.uc"
|
|
GENERATOR_UC="$ROOT_DIR/tachyon/files/usr/lib/singbox/generator.uc"
|
|
|
|
fail() {
|
|
printf 'FAIL: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# --- 1. Static assertions on lifecycle.uc restart() -------------------------
|
|
awk '
|
|
/function restart\(\)/ { in_restart = 1 }
|
|
in_restart && /stop_impl\(\)/ { called_stop_impl = 1 }
|
|
in_restart && /stop_main\(\)/ { called_stop_main = 1 }
|
|
in_restart && /start_impl\(\)/ { called_start_impl = 1 }
|
|
in_restart && /capture_selector_state\(\)/ { captured_selector = 1 }
|
|
in_restart && /restore_selector_state\(selector_state\)/ { restored_selector = 1 }
|
|
in_restart && /cleanup_failed_runtime\(\)/ { cleaned_on_failure = 1 }
|
|
in_restart && /^}/ { done = 1; exit }
|
|
END {
|
|
if (!done) exit 1
|
|
if (called_stop_impl) { print "restart() must not call stop_impl()"; exit 2 }
|
|
if (!called_stop_main) { print "restart() must call stop_main()"; exit 3 }
|
|
if (!called_start_impl) { print "restart() must call start_impl()"; exit 4 }
|
|
if (!captured_selector) { print "restart() must capture selector state"; exit 5 }
|
|
if (!restored_selector) { print "restart() must restore selector state"; exit 6 }
|
|
if (!cleaned_on_failure) { print "restart() must call cleanup_failed_runtime on failure"; exit 7 }
|
|
exit 0
|
|
}
|
|
' "$LIFECYCLE_UC" || fail "restart() in lifecycle.uc failed structural verification"
|
|
|
|
# Verify stop() still calls stop_impl()
|
|
awk '
|
|
/function stop\(\)/ { in_stop = 1 }
|
|
in_stop && /return stop_impl\(\);/ { called_stop_impl = 1 }
|
|
in_stop && /^}/ { done = 1; exit }
|
|
END {
|
|
if (!done || !called_stop_impl) exit 1
|
|
exit 0
|
|
}
|
|
' "$LIFECYCLE_UC" || fail "stop() in lifecycle.uc must still call stop_impl()"
|
|
|
|
# --- 2. Static assertions on init.d script -----------------------------------
|
|
grep -Fq 'restart_service()' "$TACHYON_INIT" ||
|
|
fail "/etc/init.d/tachyon must define restart_service()"
|
|
|
|
grep -Fq 'initd_ucode restart-service "$$"' "$TACHYON_INIT" ||
|
|
fail "/etc/init.d/tachyon restart_service() must invoke initd_ucode restart-service"
|
|
|
|
awk '
|
|
/restart\(\)/ { in_restart = 1 }
|
|
in_restart && /restart_service "\$@"/ { delegates = 1 }
|
|
in_restart && /^}/ { done = 1; exit }
|
|
END { exit (done && delegates) ? 0 : 1 }
|
|
' "$TACHYON_INIT" || fail "/etc/init.d/tachyon restart() must delegate to restart_service"
|
|
|
|
# --- 3. Static assertions on initd.uc ----------------------------------------
|
|
grep -Fq 'function restart_service(owner_pid)' "$INITD_UC" ||
|
|
fail "initd.uc must define restart_service(owner_pid)"
|
|
|
|
grep -Fq 'else if (mode == "restart-service")' "$INITD_UC" ||
|
|
fail "initd.uc must handle restart-service command"
|
|
|
|
awk '
|
|
/function restart_service\(owner_pid\)/ { in_func = 1 }
|
|
in_func && /BIN_PATH, "restart"/ { called_bin_restart = 1 }
|
|
in_func && /^}/ { done = 1; exit }
|
|
END { exit (done && called_bin_restart) ? 0 : 1 }
|
|
' "$INITD_UC" || fail "initd.uc restart_service must invoke [ BIN_PATH, 'restart' ]"
|
|
|
|
# --- 4. Static assertions on generator.uc turbo cache ------------------------
|
|
awk '
|
|
/let turbo_cache = bool_option\(settings, "dns_turbo_cache", false\);/ { in_turbo = 1 }
|
|
in_turbo && index($0, "cache_path = \"/etc/sing-box/cache.db\"") { uses_etc = 1 }
|
|
in_turbo && index($0, "turbo_cache &&") { promotes = 1 }
|
|
in_turbo && /cache_dir/ { done = 1; exit }
|
|
END { exit (promotes && uses_etc) ? 0 : 1 }
|
|
' "$GENERATOR_UC" || fail "generator.uc must promote default cache_path to /etc/sing-box/cache.db when dns_turbo_cache is enabled"
|
|
|
|
# --- 5. Behavioral test: dnsmasq server remains 127.0.0.42 during restart ---
|
|
WORK_DIR="$(mktemp -d)"
|
|
STATE="$WORK_DIR/uci.state"
|
|
LOG="$WORK_DIR/dnsmasq_mutations.log"
|
|
|
|
cleanup() {
|
|
rm -rf "$WORK_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# Initial UCI state: Tachyon running, dnsmasq points to 127.0.0.42
|
|
cat >"$STATE" <<'EOF_STATE'
|
|
dhcp.@dnsmasq[0].server=127.0.0.42
|
|
dhcp.@dnsmasq[0].noresolv=1
|
|
dhcp.@dnsmasq[0].cachesize=0
|
|
dhcp.@dnsmasq[0].tachyon_server=1.1.1.1 8.8.8.8
|
|
dhcp.@dnsmasq[0].tachyon_noresolv=0
|
|
dhcp.@dnsmasq[0].tachyon_cachesize=150
|
|
tachyon.settings.shutdown_correctly=0
|
|
tachyon.settings.dont_touch_dhcp=0
|
|
EOF_STATE
|
|
|
|
mkdir -p "$WORK_DIR/bin"
|
|
cat >"$WORK_DIR/bin/uci" <<'UCI'
|
|
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
while [ "${1:-}" = "-q" ]; do shift; done
|
|
cmd="${1:-}"
|
|
shift || true
|
|
state="${UCI_STATE:?}"
|
|
log="${MUTATION_LOG:?}"
|
|
|
|
do_set() {
|
|
local key="$1"
|
|
local val="${key#*=}"
|
|
local var="${key%%=*}"
|
|
printf 'SET %s=%s\n' "$var" "$val" >> "$log"
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
awk -F= -v k="$var" '$1 != k' "$state" > "$tmp"
|
|
printf '%s=%s\n' "$var" "$val" >> "$tmp"
|
|
mv "$tmp" "$state"
|
|
}
|
|
|
|
do_del() {
|
|
printf 'DELETE %s\n' "$1" >> "$log"
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
awk -F= -v k="$1" '$1 != k' "$state" > "$tmp"
|
|
mv "$tmp" "$state"
|
|
}
|
|
|
|
case "$cmd" in
|
|
get)
|
|
awk -F= -v key="$1" '$1 == key { print substr($0, length($1) + 2); found = 1 } END { exit found ? 0 : 1 }' "$state"
|
|
;;
|
|
set)
|
|
do_set "$1"
|
|
;;
|
|
delete)
|
|
do_del "$1"
|
|
;;
|
|
add_list)
|
|
printf 'ADD_LIST %s %s\n' "$1" "$2" >> "$log"
|
|
;;
|
|
commit)
|
|
printf 'COMMIT %s\n' "$1" >> "$log"
|
|
;;
|
|
esac
|
|
UCI
|
|
chmod +x "$WORK_DIR/bin/uci"
|
|
|
|
cat >"$WORK_DIR/bin/dnsmasq_init" <<'SH'
|
|
#!/usr/bin/env bash
|
|
exit 0
|
|
SH
|
|
chmod +x "$WORK_DIR/bin/dnsmasq_init"
|
|
export DNSMASQ_INIT="$WORK_DIR/bin/dnsmasq_init"
|
|
|
|
# Run dnsmasq_configure while service is active (as called by restart -> start_impl)
|
|
export UCI_STATE="$STATE"
|
|
export MUTATION_LOG="$LOG"
|
|
export PATH="$WORK_DIR/bin:$PATH"
|
|
: > "$LOG"
|
|
|
|
ucode -L "$ROOT_DIR/tachyon/files/usr/lib" "$APPLY_UC" configure ||
|
|
fail "dnsmasq_configure should succeed when dnsmasq already points to sing-box"
|
|
|
|
# Verify that no deletion of 127.0.0.42 or restoration of 1.1.1.1 occurred
|
|
if grep -Eq 'SET dhcp\.@dnsmasq\[0\]\.server=1\.1\.1\.1|ADD_LIST dhcp\.@dnsmasq\[0\]\.server 1\.1\.1\.1' "$LOG"; then
|
|
fail "dnsmasq_configure reverted server to ISP DNS during restart!"
|
|
fi
|
|
|
|
current_server="$(awk -F= '$1 == "dhcp.@dnsmasq[0].server" { print $2 }' "$STATE")"
|
|
[ "$current_server" = "127.0.0.42" ] ||
|
|
fail "dhcp.@dnsmasq[0].server must remain 127.0.0.42 throughout restart (was: $current_server)"
|
|
|
|
# --- 6. Behavioral test: Local DNS Cache enables 10000 entries in dnsmasq ---
|
|
printf 'tachyon.settings.dns_local_cache=1\n' >> "$STATE"
|
|
ucode -L "$ROOT_DIR/tachyon/files/usr/lib" "$APPLY_UC" configure force ||
|
|
fail "dnsmasq_configure force should apply local cache"
|
|
|
|
cachesize="$(awk -F= '$1 == "dhcp.@dnsmasq[0].cachesize" { print $2 }' "$STATE")"
|
|
[ "$cachesize" = "10000" ] ||
|
|
fail "dhcp.@dnsmasq[0].cachesize should be 10000 when dns_local_cache is enabled (was: $cachesize)"
|
|
|
|
# --- 7. Behavioral test: Stop restores ISP DNS --------------------------------
|
|
ucode -L "$ROOT_DIR/tachyon/files/usr/lib" "$APPLY_UC" restore force ||
|
|
fail "dnsmasq_restore force should succeed on service stop"
|
|
|
|
restored_server="$(awk -F= '$1 == "dhcp.@dnsmasq[0].server" { print $2 }' "$STATE")"
|
|
[ "$restored_server" = "1.1.1.1 8.8.8.8" ] ||
|
|
fail "dhcp.@dnsmasq[0].server must be restored to ISP DNS on stop (was: $restored_server)"
|
|
|
|
printf 'service restart lifecycle checks passed\n'
|
|
|