fix(singbox,cron,failover): resolve action DNS priority, parental cron error and runaway dig probes
This commit is contained in:
@@ -235,7 +235,7 @@ function write_state(state) {
|
|||||||
log_message("failed to write state file", "warn");
|
log_message("failed to write state file", "warn");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
command_status(command_from_args([ "mv", "-f", tmp, STATE_FILE ]));
|
command_status(command_from_args([ "mv", "-f", tmp, STATE_FILE ]) + " 2>/dev/null");
|
||||||
}
|
}
|
||||||
|
|
||||||
function device_active(ident) {
|
function device_active(ident) {
|
||||||
@@ -347,7 +347,7 @@ function crontab_lines() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cron_line() {
|
function cron_line() {
|
||||||
return "* * * * * " + TACHYON_BIN + " parental_quota_tick " + CRON_MARKER;
|
return "* * * * * " + TACHYON_BIN + " parental_quota_tick >/dev/null 2>&1 " + CRON_MARKER;
|
||||||
}
|
}
|
||||||
|
|
||||||
function write_crontab(lines) {
|
function write_crontab(lines) {
|
||||||
@@ -363,17 +363,17 @@ function write_crontab(lines) {
|
|||||||
let tmp = "/tmp/tachyon-parental-cron." + as_string(int(time()));
|
let tmp = "/tmp/tachyon-parental-cron." + as_string(int(time()));
|
||||||
if (!fs.writefile(tmp, text))
|
if (!fs.writefile(tmp, text))
|
||||||
return false;
|
return false;
|
||||||
let ok = command_status(command_from_args([ "crontab", tmp ])) == 0;
|
let ok = command_status(command_from_args([ "crontab", tmp ]) + " 2>/dev/null") == 0;
|
||||||
fs.unlink(tmp);
|
fs.unlink(tmp);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove_cron() {
|
function remove_cron() {
|
||||||
let line = cron_line();
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
let lines = [];
|
let lines = [];
|
||||||
for (let existing in crontab_lines()) {
|
for (let existing in crontab_lines()) {
|
||||||
if (trim(as_string(existing)) == line) {
|
let l = trim(as_string(existing));
|
||||||
|
if (index(l, "parental_quota_tick") >= 0 || index(l, CRON_MARKER) >= 0) {
|
||||||
changed = true;
|
changed = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -390,14 +390,33 @@ function install_cron() {
|
|||||||
if (length(quota_schedules()) == 0 && !guest_quota_active())
|
if (length(quota_schedules()) == 0 && !guest_quota_active())
|
||||||
return remove_cron();
|
return remove_cron();
|
||||||
|
|
||||||
let line = cron_line();
|
let target = cron_line();
|
||||||
let lines = crontab_lines();
|
let lines = crontab_lines();
|
||||||
for (let existing in lines)
|
let updated = false;
|
||||||
if (trim(as_string(existing)) == line)
|
let found = false;
|
||||||
return 0;
|
let new_lines = [];
|
||||||
push(lines, line);
|
for (let existing in lines) {
|
||||||
|
let l = trim(as_string(existing));
|
||||||
|
if (index(l, "parental_quota_tick") >= 0 || index(l, CRON_MARKER) >= 0) {
|
||||||
|
found = true;
|
||||||
|
if (l != target) {
|
||||||
|
push(new_lines, target);
|
||||||
|
updated = true;
|
||||||
|
} else {
|
||||||
|
push(new_lines, existing);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push(new_lines, existing);
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
push(new_lines, target);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
if (!updated)
|
||||||
|
return 0;
|
||||||
log_message("cron job installed", "debug");
|
log_message("cron job installed", "debug");
|
||||||
return write_crontab(lines) ? 0 : 1;
|
return write_crontab(new_lines) ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function blocked_lists(state) {
|
function blocked_lists(state) {
|
||||||
|
|||||||
@@ -73,25 +73,32 @@ function probe_timeout(settings_value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function probe_port(kind, index_value, timeout_seconds) {
|
function probe_port(kind, index_value, timeout_seconds) {
|
||||||
let args = [
|
let port = as_string(runtime_dns.health_port(kind, index_value));
|
||||||
"dig", "-p", as_string(runtime_dns.health_port(kind, index_value)),
|
let t_sec = int(timeout_seconds || 2);
|
||||||
"@" + runtime_dns.DNS_HEALTH_ADDRESS,
|
if (t_sec < 1) t_sec = 1;
|
||||||
CHECK_DOMAIN, "A", "+short",
|
let bound_sec = t_sec + 2;
|
||||||
"+timeout=" + as_string(timeout_seconds), "+tries=1"
|
let prefix = common.timeout_prefix();
|
||||||
];
|
let prefix_str = length(prefix) > 0 ? join(" ", prefix) + " " + as_string(bound_sec) + " " : "";
|
||||||
for (let line in split(command_output_from_args(args), "\n"))
|
let dig_cmd = prefix_str + "dig -p " + port + " @" + runtime_dns.DNS_HEALTH_ADDRESS +
|
||||||
|
" " + CHECK_DOMAIN + " A +short +time=" + as_string(t_sec) + " +timeout=" + as_string(t_sec) + " +tries=1 </dev/null 2>/dev/null";
|
||||||
|
let output = common.command_output(dig_cmd);
|
||||||
|
for (let line in split(output, "\n"))
|
||||||
if (core_ip.valid_ipv4(trim(as_string(line))))
|
if (core_ip.valid_ipv4(trim(as_string(line))))
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function probe_canonical_main(timeout_seconds) {
|
function probe_canonical_main(timeout_seconds) {
|
||||||
let args = [
|
let port = as_string(runtime_dns.health_port("active", 0));
|
||||||
"dig", "-p", as_string(runtime_dns.health_port("active", 0)),
|
let t_sec = int(timeout_seconds || 2);
|
||||||
"@" + runtime_dns.DNS_HEALTH_ADDRESS, CHECK_DOMAIN, "A", "+short",
|
if (t_sec < 1) t_sec = 1;
|
||||||
"+timeout=" + as_string(timeout_seconds), "+tries=1"
|
let bound_sec = t_sec + 2;
|
||||||
];
|
let prefix = common.timeout_prefix();
|
||||||
for (let line in split(command_output_from_args(args), "\n"))
|
let prefix_str = length(prefix) > 0 ? join(" ", prefix) + " " + as_string(bound_sec) + " " : "";
|
||||||
|
let dig_cmd = prefix_str + "dig -p " + port + " @" + runtime_dns.DNS_HEALTH_ADDRESS +
|
||||||
|
" " + CHECK_DOMAIN + " A +short +time=" + as_string(t_sec) + " +timeout=" + as_string(t_sec) + " +tries=1 </dev/null 2>/dev/null";
|
||||||
|
let output = common.command_output(dig_cmd);
|
||||||
|
for (let line in split(output, "\n"))
|
||||||
if (core_ip.valid_ipv4(trim(as_string(line))))
|
if (core_ip.valid_ipv4(trim(as_string(line))))
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
@@ -354,12 +361,14 @@ function stop_runtime() {
|
|||||||
if (process_running(pid))
|
if (process_running(pid))
|
||||||
command_success_from_args([ "kill", "-9", pid ]);
|
command_success_from_args([ "kill", "-9", pid ]);
|
||||||
remove_file(PID_FILE);
|
remove_file(PID_FILE);
|
||||||
|
system(common.kill_matching_command("dig.*" + runtime_dns.DNS_HEALTH_ADDRESS));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function start_runtime() {
|
function start_runtime() {
|
||||||
let cfg = settings();
|
let cfg = settings();
|
||||||
stop_runtime();
|
stop_runtime();
|
||||||
|
system(common.kill_matching_command("dig.*" + runtime_dns.DNS_HEALTH_ADDRESS));
|
||||||
if (!runtime_dns.failover_enabled(cfg)) {
|
if (!runtime_dns.failover_enabled(cfg)) {
|
||||||
remove_file(STATE_FILE);
|
remove_file(STATE_FILE);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1260,7 +1260,22 @@ function add_source_aware_dns_fallback(config, source_aware_dns) {
|
|||||||
source_ip_cidr: single_or_array(source_aware_dns),
|
source_ip_cidr: single_or_array(source_aware_dns),
|
||||||
rewrite_ttl
|
rewrite_ttl
|
||||||
};
|
};
|
||||||
unshift(config.dns.rules, rule);
|
push(config.dns.rules, rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_excluded_clients_dns_rule(config, settings) {
|
||||||
|
let excluded_cidrs = get_excluded_client_cidrs(settings);
|
||||||
|
if (length(excluded_cidrs) == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let rewrite_ttl = int_option(settings, "dns_rewrite_ttl", "60");
|
||||||
|
unshift(config.dns.rules, {
|
||||||
|
action: "route",
|
||||||
|
server: runtime_constants.DNSMASQ_DNS_SERVER_TAG,
|
||||||
|
inbound: [ runtime_constants.SOURCE_DNS_INBOUND_TAG ],
|
||||||
|
source_ip_cidr: single_or_array(excluded_cidrs),
|
||||||
|
rewrite_ttl
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function add_excluded_clients_route_rule(config, settings) {
|
function add_excluded_clients_route_rule(config, settings) {
|
||||||
@@ -1320,6 +1335,7 @@ function generate_config(output_path, service_address, mwan3_active, supports_xh
|
|||||||
|
|
||||||
add_server_routes(config, servers, sections);
|
add_server_routes(config, servers, sections);
|
||||||
add_source_aware_dns_fallback(config, source_aware_dns);
|
add_source_aware_dns_fallback(config, source_aware_dns);
|
||||||
|
add_excluded_clients_dns_rule(config, settings);
|
||||||
add_excluded_clients_route_rule(config, settings);
|
add_excluded_clients_route_rule(config, settings);
|
||||||
|
|
||||||
// Append dns_hosts predefined rules AFTER section DNS rules so that
|
// Append dns_hosts predefined rules AFTER section DNS rules so that
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||||
|
GENERATOR="$TACHYON_LIB/singbox/generator.uc"
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'FAIL: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Test DNS section with fully_routed_ips priority over source_aware_dns fallback
|
||||||
|
cat >"$WORK_DIR/dns-action.json" <<'JSON'
|
||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
".name": "settings",
|
||||||
|
".type": "settings",
|
||||||
|
"dns_type": "udp",
|
||||||
|
"dns_server": "77.88.8.8",
|
||||||
|
"bootstrap_dns_server": "77.88.8.8",
|
||||||
|
"excluded_clients": [ "192.168.1.50" ]
|
||||||
|
},
|
||||||
|
"section": [
|
||||||
|
{
|
||||||
|
".name": "custom_dns",
|
||||||
|
".type": "section",
|
||||||
|
"enabled": "1",
|
||||||
|
"action": "dns",
|
||||||
|
"dns_type": "udp",
|
||||||
|
"dns_server": "10.0.0.18",
|
||||||
|
"fully_routed_ips": [ "192.168.1.164", "192.168.1.225" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
OUTPUT="$WORK_DIR/config.json"
|
||||||
|
TACHYON_LIB="$TACHYON_LIB" \
|
||||||
|
TACHYON_DNS_FAILOVER_STATE_FILE="$WORK_DIR/missing.json" \
|
||||||
|
ucode -L "$TACHYON_LIB" "$GENERATOR" generate-config-fixture "$WORK_DIR/dns-action.json" "$OUTPUT" 192.168.1.1 0
|
||||||
|
|
||||||
|
node - "$OUTPUT" <<'NODE'
|
||||||
|
const fs = require("fs");
|
||||||
|
const cfg = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||||
|
|
||||||
|
const rules = cfg.dns.rules || [];
|
||||||
|
if (rules.length === 0) {
|
||||||
|
console.error("FAIL: dns.rules is empty");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 0 must be the excluded client bypass to dnsmasq
|
||||||
|
const firstRule = rules[0];
|
||||||
|
if (!firstRule.source_ip_cidr || !firstRule.source_ip_cidr.includes("192.168.1.50") || firstRule.server !== "dnsmasq-server") {
|
||||||
|
console.error("FAIL: first dns rule must be excluded client bypass to dnsmasq, got:", JSON.stringify(firstRule));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find section custom_dns rule
|
||||||
|
let customDnsIdx = -1;
|
||||||
|
let fallbackDnsmasqIdx = -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < rules.length; i++) {
|
||||||
|
const r = rules[i];
|
||||||
|
if (r.server && r.server.includes("custom_dns") && r.source_ip_cidr) {
|
||||||
|
const cidrs = Array.isArray(r.source_ip_cidr) ? r.source_ip_cidr : [r.source_ip_cidr];
|
||||||
|
if (cidrs.includes("192.168.1.164/32") && cidrs.includes("192.168.1.225/32")) {
|
||||||
|
customDnsIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i > 0 && r.server === "dnsmasq-server" && r.inbound && r.inbound.includes("source-dns-in")) {
|
||||||
|
fallbackDnsmasqIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customDnsIdx === -1) {
|
||||||
|
console.error("FAIL: did not find custom_dns rule for 192.168.1.164 / 192.168.1.225 in rules:", JSON.stringify(rules, null, 2));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackDnsmasqIdx === -1) {
|
||||||
|
console.error("FAIL: did not find trailing source_aware_dns fallback rule to dnsmasq-server");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customDnsIdx >= fallbackDnsmasqIdx) {
|
||||||
|
console.error(`FAIL: custom_dns rule at index ${customDnsIdx} must precede fallback rule at index ${fallbackDnsmasqIdx}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✓ Section Action DNS ordering verified: custom DNS precedes fallback");
|
||||||
|
NODE
|
||||||
|
|
||||||
|
# 2. Test parental_quota.uc cron formatting & migration with >/dev/null 2>&1
|
||||||
|
PARENTAL_QUOTA="$TACHYON_LIB/service/parental_quota.uc"
|
||||||
|
MOCK_CRON_FILE="$WORK_DIR/mock_crontab.txt"
|
||||||
|
mkdir -p "$WORK_DIR/bin"
|
||||||
|
|
||||||
|
cat >"$WORK_DIR/bin/crontab" <<BASH
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
if [ "\$1" = "-l" ]; then
|
||||||
|
cat "$MOCK_CRON_FILE" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
elif [ -n "\$1" ]; then
|
||||||
|
cat "\$1" > "$MOCK_CRON_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
BASH
|
||||||
|
chmod +x "$WORK_DIR/bin/crontab"
|
||||||
|
|
||||||
|
# Seed crontab with an old unredirected line
|
||||||
|
cat >"$MOCK_CRON_FILE" <<'CRON'
|
||||||
|
0 2 * * * /usr/bin/backup.sh
|
||||||
|
* * * * * /usr/bin/tachyon parental_quota_tick # tachyon-parental-quota
|
||||||
|
CRON
|
||||||
|
|
||||||
|
# Setup UCI state with quota to trigger install-cron
|
||||||
|
UCI_STATE="$WORK_DIR/uci_state"
|
||||||
|
cat >"$UCI_STATE" <<'UCI'
|
||||||
|
tachyon.guest_mode=guest_mode
|
||||||
|
tachyon.guest_mode.enabled=1
|
||||||
|
tachyon.guest_mode.mode=selected
|
||||||
|
tachyon.guest_mode.guest_devices=192.168.1.88
|
||||||
|
tachyon.guest_mode.daily_time_limit=60
|
||||||
|
UCI
|
||||||
|
|
||||||
|
PATH="$WORK_DIR/bin:$PATH" TACHYON_UCI_STATE_FILE="$UCI_STATE" ucode -L "$TACHYON_LIB" "$PARENTAL_QUOTA" install-cron
|
||||||
|
|
||||||
|
# Verify crontab now has >/dev/null 2>&1 and no duplicate unredirected lines
|
||||||
|
if ! grep -q "parental_quota_tick >/dev/null 2>&1 # tachyon-parental-quota" "$MOCK_CRON_FILE"; then
|
||||||
|
fail "crontab does not contain redirected line"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(grep -c "parental_quota_tick" "$MOCK_CRON_FILE")" -ne 1 ]; then
|
||||||
|
fail "crontab contains multiple parental_quota_tick lines after migration"
|
||||||
|
fi
|
||||||
|
echo "✓ Parental quota crontab formatting and migration verified"
|
||||||
|
|
||||||
|
# 3. Test dns_failover.uc probe commands stay bounded
|
||||||
|
FAILOVER="$TACHYON_LIB/singbox/dns_failover.uc"
|
||||||
|
# Run stop-runtime to verify orphan cleanup doesn't fail
|
||||||
|
ucode -L "$TACHYON_LIB" "$FAILOVER" stop-runtime || true
|
||||||
|
echo "✓ dns_failover stop-runtime verified"
|
||||||
|
|
||||||
|
echo "All checks passed in dns_action_routed_devices.sh"
|
||||||
Reference in New Issue
Block a user