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:
+274
-246
File diff suppressed because it is too large
Load Diff
+264
-248
File diff suppressed because it is too large
Load Diff
@@ -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-серверов."
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
Reference in New Issue
Block a user