fix(dns): preserve 127.0.0.42 during restart and add local DNS cache support

- 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
This commit is contained in:
Dushnilin
2026-09-17 08:45:24 +03:00
parent 44a1777c0d
commit 0efa47abdc
14 changed files with 1119 additions and 750 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TACHYON\n" "Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 08:13+0300\n" "POT-Creation-Date: 2026-09-17 08:45+0300\n"
"PO-Revision-Date: 2026-09-17 08:13+0300\n" "PO-Revision-Date: 2026-09-17 08:45+0300\n"
"Last-Translator: Dushnilin\n" "Last-Translator: Dushnilin\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -1439,6 +1439,9 @@ msgstr "Автоподбор и бенчмарк DNS"
msgid "DNS Blocking" msgid "DNS Blocking"
msgstr "Блокировка DNS" msgstr "Блокировка DNS"
msgid "DNS Cache Size"
msgstr ""
msgid "DNS Check Interval" msgid "DNS Check Interval"
msgstr "Интервал проверки DNS" msgstr "Интервал проверки DNS"
@@ -1793,6 +1796,9 @@ msgstr "Включено"
msgid "Enabled (always prioritize)" msgid "Enabled (always prioritize)"
msgstr "Включено (всегда повышать приоритет)" msgstr "Включено (всегда повышать приоритет)"
msgid "Enables high-capacity local DNS caching in dnsmasq to accelerate DNS queries and absorb transient latency during service restarts."
msgstr ""
msgid "Enables the background daemon that polls Telegram for commands and sends notifications." msgid "Enables the background daemon that polls Telegram for commands and sends notifications."
msgstr "Включает фоновый демон, который опрашивает Telegram на наличие команд и отправляет уведомления." msgstr "Включает фоновый демон, который опрашивает Telegram на наличие команд и отправляет уведомления."
@@ -3095,6 +3101,9 @@ msgstr "Загрузка..."
msgid "Local Address" msgid "Local Address"
msgstr "Локальный адрес" msgstr "Локальный адрес"
msgid "Local DNS Cache"
msgstr ""
msgid "Local SOCKS proxy desynchronizer (ciadpi)" msgid "Local SOCKS proxy desynchronizer (ciadpi)"
msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)" msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)"
@@ -3470,6 +3479,9 @@ msgstr "Уведомлять о перезапусках сервиса"
msgid "Notify on Subscription Updates" msgid "Notify on Subscription Updates"
msgstr "Уведомлять об обновлениях подписок" msgstr "Уведомлять об обновлениях подписок"
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
msgstr ""
msgid "Number of consecutive failed checks required before switching DNS servers." msgid "Number of consecutive failed checks required before switching DNS servers."
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов." msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
+2
View File
@@ -476,6 +476,8 @@ export namespace Tachyon {
smart_detect_section?: string; smart_detect_section?: string;
smart_detect_sections?: string | string[]; smart_detect_sections?: string | string[];
dns_turbo_cache?: '0' | '1'; dns_turbo_cache?: '0' | '1';
dns_local_cache?: '0' | '1';
dns_cache_size?: string;
// AI Watchdog settings (introduced in AI Watchdog feature) // AI Watchdog settings (introduced in AI Watchdog feature)
ai_proxy_health_enabled?: '0' | '1'; ai_proxy_health_enabled?: '0' | '1';
ai_proxy_health_interval?: string; ai_proxy_health_interval?: string;
@@ -1853,6 +1853,30 @@ function createSettingsContent(section, capabilities) {
o.default = "0"; o.default = "0";
o.rmempty = false; o.rmempty = false;
o = section.taboption(
"dns",
form.Flag,
"dns_local_cache",
_("Local DNS Cache"),
_(
"Enables high-capacity local DNS caching in dnsmasq to accelerate DNS queries and absorb transient latency during service restarts.",
),
);
o.default = "0";
o.rmempty = false;
o = section.taboption(
"dns",
form.Value,
"dns_cache_size",
_("DNS Cache Size"),
_("Number of cached DNS domain entries in dnsmasq (default: 10000)."),
);
o.depends("dns_local_cache", "1");
o.default = "10000";
o.datatype = "uinteger";
o.rmempty = true;
// ─── DNS Strategy ──────────────────────────────────────────────────────── // ─── DNS Strategy ────────────────────────────────────────────────────────
o = section.taboption( o = section.taboption(
+14 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TACHYON\n" "Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 08:13+0300\n" "POT-Creation-Date: 2026-09-17 08:45+0300\n"
"PO-Revision-Date: 2026-09-17 08:13+0300\n" "PO-Revision-Date: 2026-09-17 08:45+0300\n"
"Last-Translator: Dushnilin\n" "Last-Translator: Dushnilin\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: ru\n" "Language: ru\n"
@@ -1439,6 +1439,9 @@ msgstr "Автоподбор и бенчмарк DNS"
msgid "DNS Blocking" msgid "DNS Blocking"
msgstr "Блокировка DNS" msgstr "Блокировка DNS"
msgid "DNS Cache Size"
msgstr ""
msgid "DNS Check Interval" msgid "DNS Check Interval"
msgstr "Интервал проверки DNS" msgstr "Интервал проверки DNS"
@@ -1793,6 +1796,9 @@ msgstr "Включено"
msgid "Enabled (always prioritize)" msgid "Enabled (always prioritize)"
msgstr "Включено (всегда повышать приоритет)" msgstr "Включено (всегда повышать приоритет)"
msgid "Enables high-capacity local DNS caching in dnsmasq to accelerate DNS queries and absorb transient latency during service restarts."
msgstr ""
msgid "Enables the background daemon that polls Telegram for commands and sends notifications." msgid "Enables the background daemon that polls Telegram for commands and sends notifications."
msgstr "Включает фоновый демон, который опрашивает Telegram на наличие команд и отправляет уведомления." msgstr "Включает фоновый демон, который опрашивает Telegram на наличие команд и отправляет уведомления."
@@ -3095,6 +3101,9 @@ msgstr "Загрузка..."
msgid "Local Address" msgid "Local Address"
msgstr "Локальный адрес" msgstr "Локальный адрес"
msgid "Local DNS Cache"
msgstr ""
msgid "Local SOCKS proxy desynchronizer (ciadpi)" msgid "Local SOCKS proxy desynchronizer (ciadpi)"
msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)" msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)"
@@ -3470,6 +3479,9 @@ msgstr "Уведомлять о перезапусках сервиса"
msgid "Notify on Subscription Updates" msgid "Notify on Subscription Updates"
msgstr "Уведомлять об обновлениях подписок" msgstr "Уведомлять об обновлениях подписок"
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
msgstr ""
msgid "Number of consecutive failed checks required before switching DNS servers." msgid "Number of consecutive failed checks required before switching DNS servers."
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов." msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
File diff suppressed because it is too large Load Diff
+2
View File
@@ -48,6 +48,8 @@ config settings 'settings'
#option p2p_ports 'tcp:6881,udp:6881-6889' #option p2p_ports 'tcp:6881,udp:6881-6889'
option config_path '/etc/sing-box/config.json' option config_path '/etc/sing-box/config.json'
option cache_path '/tmp/sing-box/cache.db' option cache_path '/tmp/sing-box/cache.db'
option dns_local_cache '0'
option dns_cache_size '10000'
option log_level 'warn' option log_level 'warn'
option exclude_ntp '0' option exclude_ntp '0'
option shutdown_correctly '0' option shutdown_correctly '0'
+10
View File
@@ -46,6 +46,16 @@ stop_service() {
trap - INT TERM trap - INT TERM
} }
restart_service() {
trap 'exit 1' INT TERM
initd_ucode restart-service "$$"
trap - INT TERM
}
restart() {
restart_service "$@"
}
reload_service() { reload_service() {
local reason="$1" local reason="$1"
local result status local result status
+10 -2
View File
@@ -120,10 +120,18 @@ function dnsmasq_management_disabled() {
return truthy(uci_get(CONFIG_NAME + ".settings.dont_touch_dhcp")); return truthy(uci_get(CONFIG_NAME + ".settings.dont_touch_dhcp"));
} }
function dnsmasq_target_cachesize() {
if (truthy(uci_get(CONFIG_NAME + ".settings.dns_local_cache"))) {
let size = uci_get(CONFIG_NAME + ".settings.dns_cache_size");
return (size != null && size != "") ? as_string(size) : "10000";
}
return "0";
}
function dnsmasq_default_config_is_complete() { function dnsmasq_default_config_is_complete() {
return dnsmasq_default_has_tachyon_dns() && return dnsmasq_default_has_tachyon_dns() &&
uci_get("dhcp.@dnsmasq[0].noresolv") == "1" && uci_get("dhcp.@dnsmasq[0].noresolv") == "1" &&
uci_get("dhcp.@dnsmasq[0].cachesize") == "0" && uci_get("dhcp.@dnsmasq[0].cachesize") == dnsmasq_target_cachesize() &&
!dnsmasq_legacy_instance_exists(); !dnsmasq_legacy_instance_exists();
} }
@@ -209,7 +217,7 @@ function dnsmasq_configure_default_instance() {
uci_delete("dhcp.@dnsmasq[0].server"); uci_delete("dhcp.@dnsmasq[0].server");
uci_add_list("dhcp.@dnsmasq[0].server", SB_DNS_INBOUND_ADDRESS); uci_add_list("dhcp.@dnsmasq[0].server", SB_DNS_INBOUND_ADDRESS);
uci_set("dhcp.@dnsmasq[0].noresolv", "1"); uci_set("dhcp.@dnsmasq[0].noresolv", "1");
uci_set("dhcp.@dnsmasq[0].cachesize", "0"); uci_set("dhcp.@dnsmasq[0].cachesize", dnsmasq_target_cachesize());
} }
function dnsmasq_restore_default_instance() { function dnsmasq_restore_default_instance() {
+22
View File
@@ -647,6 +647,26 @@ function stop_service(owner_pid) {
return stop_finish(job_id, status); return stop_finish(job_id, status);
} }
function restart_service(owner_pid) {
clear_start_retry(START_RETRY_FILE);
cancel_scheduled_start_retry(START_RETRY_PID_FILE);
let job_id = begin_external_service_action("restart", "initd", owner_pid);
if (!file_executable(BIN_PATH)) {
restore_dnsmasq_failsafe();
finish_external_service_action("restart", job_id, 1);
return 1;
}
let status = command_status_from_args([ BIN_PATH, "restart" ]);
if (status != 0) {
mark_start_retry(START_RETRY_FILE, "restart_failed");
schedule_start_retry(START_RETRY_PID_FILE, START_RETRY_DELAY_SECONDS);
command_success_from_args([ "logger", "-t", SERVICE_NAME, "[warn] Tachyon restart failed; scheduled an automatic retry" ]);
}
finish_external_service_action("restart", job_id, status);
return status;
}
function reload_begin_value(reason, owner_pid, runtime_running_value, service_enabled_value, active_service_action) { function reload_begin_value(reason, owner_pid, runtime_running_value, service_enabled_value, active_service_action) {
reason = as_string(reason); reason = as_string(reason);
@@ -817,6 +837,8 @@ else if (mode == "stop-finish")
exit(stop_finish(ARGV[1], ARGV[2])); exit(stop_finish(ARGV[1], ARGV[2]));
else if (mode == "stop-service") else if (mode == "stop-service")
exit(stop_service(ARGV[1])); exit(stop_service(ARGV[1]));
else if (mode == "restart-service")
exit(restart_service(ARGV[1]));
else if (mode == "reload-begin") else if (mode == "reload-begin")
exit(reload_begin(ARGV[1], ARGV[2], null, null)); exit(reload_begin(ARGV[1], ARGV[2], null, null));
else if (mode == "reload-begin-fixture") { else if (mode == "reload-begin-fixture") {
+14 -1
View File
@@ -1627,7 +1627,9 @@ function restart() {
log_message("Restarting Tachyon", "info"); log_message("Restarting Tachyon", "info");
let selector_state = capture_selector_state(); let selector_state = capture_selector_state();
let status = stop_impl(); invalidate_reload_hash();
let status = stop_main();
if (status != 0) if (status != 0)
return status; return status;
@@ -1637,6 +1639,17 @@ function restart() {
return status; return status;
} }
status = module_status(STATE_UC, [
"wait-sing-box-service-stable",
as_string(SING_BOX_START_STABLE_MIN_AGE),
as_string(SING_BOX_START_VERIFY_TIMEOUT)
]);
if (status != 0) {
log_message("Restart verification failed after Tachyon was started; rolling back DNS changes", "fatal");
cleanup_failed_runtime();
return status;
}
if (module_success(STATE_UC, [ if (module_success(STATE_UC, [
"tachyon-stably-running", "tachyon-stably-running",
RT_TABLE_NAME, RT_TABLE_NAME,
+3 -1
View File
@@ -453,7 +453,9 @@ function base_config(settings, service_address, runtime_context) {
let log_level = option(settings, "log_level", "warn"); let log_level = option(settings, "log_level", "warn");
let rewrite_ttl = int_option(settings, "dns_rewrite_ttl", "60"); let rewrite_ttl = int_option(settings, "dns_rewrite_ttl", "60");
let turbo_cache = bool_option(settings, "dns_turbo_cache", false); let turbo_cache = bool_option(settings, "dns_turbo_cache", false);
let cache_path = option(settings, "cache_path", turbo_cache ? "/etc/sing-box/cache.db" : "/tmp/sing-box/cache.db"); let cache_path = option(settings, "cache_path", "/tmp/sing-box/cache.db");
if (turbo_cache && (cache_path == "/tmp/sing-box/cache.db" || cache_path == ""))
cache_path = "/etc/sing-box/cache.db";
let cache_dir = replace(cache_path, /\/[^\/]+$/, ""); let cache_dir = replace(cache_path, /\/[^\/]+$/, "");
if (cache_dir != "" && cache_dir != cache_path) { if (cache_dir != "" && cache_dir != cache_path) {
try { fs.mkdir(cache_dir, 0755); } catch(e) {} try { fs.mkdir(cache_dir, 0755); } catch(e) {}
+202
View File
@@ -0,0 +1,202 @@
#!/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'