release: 1.3.26 — Watchdog proxy restart protection, WAN monitor debounce, and subscription bootstrap resilience
- Fix #52: Watchdog proxy restart on single URL failure: * Validate failure against secondary independent probe URL before declaring proxy down * Skip restart when direct WAN is also degraded or down (!direct_ok) * Implement 120s cooldown between automated proxy restarts * Fix Telegram notification to accurately state 'Перезапуск sing-box' instead of 'Перезапуск роутера' - Fix #51: Telegram bot spamming WAN up/down notifications: * Filter hotplug events strictly by badwan_monitored_interfaces (defaults to wan) * Add atomic directory lock with debounce delay and real ubus state query * Prevent repeated notifications when interface state has not transitioned - Feat: 2-VPN deadlock elimination & self-download subscription proxy: * Isolate unbootstrapped dependent subscription sections on initial boot (deferred_sections) * Allow self-download proxy target for subscriptions with explicit UI hint '(текущее правило при активности)' * Enable automatic local proxy fallback for subscription refresh when direct WAN download fails while sing-box is active * Add regression test in tests/subscription_bootstrap_deadlock.sh - Fix: Parental control quota enforcement: * Correct route rule filtering when daily quota minutes are exhausted without a fixed time interval - i18n & Version bump: * Bump fe-app-tachyon to 1.3.26 and update Russian PO/POT translations
This commit is contained in:
@@ -118,7 +118,10 @@ if grep -q '"dial_detour"' "$output_v14"; then
|
||||
fail "sing-box 1.14+ must NEVER contain 'dial_detour' in http_clients (causes fatal unknown field crash)"
|
||||
fi
|
||||
if command -v sing-box >/dev/null 2>&1; then
|
||||
sing-box check -c "$output_v14" || fail "sing-box 1.14+ config check failed"
|
||||
INSTALLED_SB_VER="$(sing-box version 2>/dev/null | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1 || echo '0.0')"
|
||||
if [ "$(printf '%s\n1.14\n' "$INSTALLED_SB_VER" | sort -V | head -n1)" = "1.14" ]; then
|
||||
sing-box check -c "$output_v14" || fail "sing-box 1.14+ config check failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
output_detour_v14="$WORK_DIR/out_detour_v14.json"
|
||||
@@ -133,7 +136,10 @@ fi
|
||||
grep -q '"detour": "first_proxy-out"' "$output_detour_v14" || \
|
||||
fail "sing-box 1.14+ http_clients must configure 'detour' field when download_lists_via_proxy is enabled"
|
||||
if command -v sing-box >/dev/null 2>&1; then
|
||||
sing-box check -c "$output_detour_v14" || fail "sing-box 1.14+ detour config check failed"
|
||||
INSTALLED_SB_VER="$(sing-box version 2>/dev/null | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1 || echo '0.0')"
|
||||
if [ "$(printf '%s\n1.14\n' "$INSTALLED_SB_VER" | sort -V | head -n1)" = "1.14" ]; then
|
||||
sing-box check -c "$output_detour_v14" || fail "sing-box 1.14+ detour config check failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. Verify zapret section with community_lists does NOT use zapret-out as download_detour
|
||||
|
||||
@@ -135,7 +135,7 @@ grep -Fq '"success": true' "$WORK_DIR/result.json" ||
|
||||
fail "snapshot-restore must report success"
|
||||
for _ in $(seq 1 30); do
|
||||
grep -Fxq 'restart' "$WORK_DIR/bin.log" 2>/dev/null && break
|
||||
sleep 0.1
|
||||
sleep 1
|
||||
done
|
||||
grep -Fxq 'restart' "$WORK_DIR/bin.log" ||
|
||||
fail "snapshot-restore must restart the service through the backend entrypoint"
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LIB_DIR="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
VALIDATOR="$LIB_DIR/config/validator.uc"
|
||||
GENERATOR="$LIB_DIR/singbox/generator.uc"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local expected="$1"
|
||||
local actual="$2"
|
||||
local message="$3"
|
||||
if [ "$expected" != "$actual" ]; then
|
||||
printf 'FAIL: %s: expected "%s", got "%s"\n' "$message" "$expected" "$actual" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. Test validator accepts a section downloading through itself
|
||||
printf 'Testing validator allows section downloading through itself...\n'
|
||||
cat >"$WORK_DIR/self_download.json" <<'JSON'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"dns_server": ["77.88.8.8"],
|
||||
"bootstrap_dns_server": ["77.88.8.8"]
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "vpn1",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "proxy",
|
||||
"subscription_urls": [ "https://example.com/sub" ],
|
||||
"subscription_url_settings": "{\"https://example.com/sub\":{\"download_via_proxy_section\":\"vpn1\"}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
TACHYON_LIB="$LIB_DIR" ucode -L "$LIB_DIR" "$VALIDATOR" validate-runtime-fixture "$WORK_DIR/self_download.json" "{}" >"$WORK_DIR/val.out" 2>"$WORK_DIR/val.err"
|
||||
|
||||
printf 'Validator successfully accepted self-download configuration.\n'
|
||||
|
||||
# 2. Test connections.uc includes self-target in subscription_download_targets
|
||||
printf 'Testing connections.uc subscription_download_targets includes self-target...\n'
|
||||
ucode -L "$LIB_DIR" -e '
|
||||
let connections = require("config.connections");
|
||||
let sections = [
|
||||
{
|
||||
".name": "vpn1",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "proxy",
|
||||
"subscription_urls": [ "https://example.com/sub" ],
|
||||
"subscription_url_settings": "{\"https://example.com/sub\":{\"download_via_proxy_enabled\":\"1\",\"download_via_proxy_section\":\"vpn1\"}}"
|
||||
}
|
||||
];
|
||||
let targets = connections.subscription_download_targets(sections);
|
||||
for (let t in targets)
|
||||
print(t, "\n");
|
||||
' > "$WORK_DIR/targets.out"
|
||||
|
||||
assert_eq "vpn1" "$(cat "$WORK_DIR/targets.out")" "subscription_download_targets should include vpn1"
|
||||
|
||||
# 3. Test singbox config generator with deferred section
|
||||
printf 'Testing singbox config generator skips deferred section...\n'
|
||||
mkdir -p "$WORK_DIR/runtime"
|
||||
|
||||
cat > "$WORK_DIR/fixture.json" <<'JSON'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"enabled": "1",
|
||||
"dns_server": "1.1.1.1"
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
".name": "vpn1",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "proxy",
|
||||
"selector_proxy_links": [ "socks5://127.0.0.1:1080#node1" ]
|
||||
},
|
||||
{
|
||||
".name": "vpn2",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "proxy",
|
||||
"subscription_urls": [ "https://example.com/sub2" ],
|
||||
"subscription_url_settings": "{\"https://example.com/sub2\":{\"download_via_proxy_enabled\":\"1\",\"download_via_proxy_section\":\"vpn1\"}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
# A: Generating config WITHOUT deferring vpn2 must fail (vpn2 has 0 outbounds)
|
||||
set +e
|
||||
mkdir -p "$WORK_DIR/config_fail.json.section-cache" "$WORK_DIR/config_fail.json.rulesets"
|
||||
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
ucode -L "$LIB_DIR" "$GENERATOR" generate-config-fixture \
|
||||
"$WORK_DIR/fixture.json" "$WORK_DIR/config_fail.json" "127.0.0.1" "0" "1" "" >"$WORK_DIR/gen_fail.out" 2>"$WORK_DIR/gen_fail.err"
|
||||
STATUS_FAIL=$?
|
||||
set -e
|
||||
if [ "$STATUS_FAIL" -eq 0 ]; then
|
||||
fail "generator should have failed when vpn2 has no outbounds and is not deferred"
|
||||
fi
|
||||
if ! grep -Fq "connection section has no usable outbounds" "$WORK_DIR/gen_fail.err"; then
|
||||
printf 'gen_fail.err:\n%s\ngen_fail.out:\n%s\n' "$(cat "$WORK_DIR/gen_fail.err")" "$(cat "$WORK_DIR/gen_fail.out")" >&2
|
||||
fail "expected 'connection section has no usable outbounds' in error output"
|
||||
fi
|
||||
|
||||
# B: Generating config WITH deferred_sections='vpn2' must SUCCEED!
|
||||
mkdir -p "$WORK_DIR/config_pass.json.section-cache" "$WORK_DIR/config_pass.json.rulesets"
|
||||
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime" \
|
||||
ucode -L "$LIB_DIR" "$GENERATOR" generate-config-fixture \
|
||||
"$WORK_DIR/fixture.json" "$WORK_DIR/config_pass.json" "127.0.0.1" "0" "1" "vpn2" >"$WORK_DIR/gen_pass.out" 2>"$WORK_DIR/gen_pass.err"
|
||||
|
||||
if [ ! -s "$WORK_DIR/config_pass.json" ]; then
|
||||
fail "generator failed to produce config when vpn2 is deferred"
|
||||
fi
|
||||
|
||||
# Verify generated config has outbounds for vpn1 and NOT broken references to vpn2
|
||||
node - "$WORK_DIR/config_pass.json" <<'NODE'
|
||||
const fs = require('fs');
|
||||
const cfg = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
const tags = (cfg.outbounds || []).map(o => o.tag);
|
||||
console.log("outbound tags:", JSON.stringify(tags));
|
||||
if (!tags.includes('vpn1-out')) {
|
||||
console.error("Generated config must include vpn1-out outbound");
|
||||
process.exit(1);
|
||||
}
|
||||
if (tags.includes('vpn2-out')) {
|
||||
console.error("Generated config must NOT include deferred vpn2-out outbound");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const inbounds = (cfg.inbounds || []).map(i => i.tag);
|
||||
console.log("inbound tags:", JSON.stringify(inbounds));
|
||||
if (!inbounds.includes('service-subscription-vpn1-in')) {
|
||||
console.error("Generated config must include service-subscription-vpn1-in inbound for subscription download");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("CONFIG_VERIFIED_OK");
|
||||
NODE
|
||||
|
||||
printf 'subscription_bootstrap_deadlock tests passed successfully\n'
|
||||
@@ -81,10 +81,19 @@ echo "$@" >> "${TEST_LOG_FILE}"
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/logger"
|
||||
|
||||
# Mock tachyon init.d script
|
||||
cat > "$BIN_DIR/initd_tachyon" << 'EOF'
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/initd_tachyon"
|
||||
|
||||
# Mock tachyon cli (for send_telegram_notification)
|
||||
cat > "$BIN_DIR/tachyon" << 'EOF'
|
||||
#!/bin/sh
|
||||
echo "$@" >> "${TEST_TELEGRAM_LOG}"
|
||||
if [ "$1" = "telegram" ]; then
|
||||
echo "$@" >> "${TEST_TELEGRAM_LOG}"
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/tachyon"
|
||||
@@ -95,12 +104,14 @@ export TEST_LOG_FILE="$TMP_DIR/logger.log"
|
||||
export TEST_TELEGRAM_LOG="$TMP_DIR/telegram.log"
|
||||
mkdir -p "$TEST_UCI_DIR"
|
||||
|
||||
# Patch monitor script temporarily to use our isolated /var/run/tachyon dir and fast debounce (0.05s instead of 2s)
|
||||
# Patch monitor script temporarily to use our isolated /var/run/tachyon dir and fast debounce (sleep 0 instead of 2s)
|
||||
TEST_MONITOR="$TMP_DIR/monitor_under_test.sh"
|
||||
# shellcheck disable=SC2016
|
||||
sed -e "s|/var/run/tachyon|${VAR_RUN}|g" \
|
||||
-e 's|sleep "\$DELAY_SEC"|sleep 0.05|g' \
|
||||
-e 's|sleep 2|sleep 0.05|g' \
|
||||
-e "s|/usr/bin/tachyon|${BIN_DIR}/tachyon|g" \
|
||||
-e "s|/etc/init.d/tachyon|${BIN_DIR}/initd_tachyon|g" \
|
||||
-e 's|sleep "\$DELAY_SEC"|sleep 0|g' \
|
||||
-e 's|sleep 2|sleep 0|g' \
|
||||
"$MONITOR_SCRIPT" > "$TEST_MONITOR"
|
||||
chmod +x "$TEST_MONITOR"
|
||||
|
||||
@@ -111,14 +122,12 @@ echo "wan" > "$TEST_UCI_DIR/monitored_ifaces"
|
||||
echo "down" > "$TEST_UCI_DIR/ubus_state_wan"
|
||||
|
||||
ACTION="ifdown" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.2
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 1 failed: notification sent when enable_badwan=0"
|
||||
|
||||
# Test 2: Telegram disabled (telegram_enabled = 0)
|
||||
echo "1" > "$TEST_UCI_DIR/enable_badwan"
|
||||
echo "0" > "$TEST_UCI_DIR/telegram_enabled"
|
||||
ACTION="ifdown" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.2
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 2 failed: notification sent when telegram.enabled=0"
|
||||
|
||||
# Test 3: Unmonitored interface (wan6 triggered, only wan is monitored)
|
||||
@@ -127,7 +136,6 @@ echo "1" > "$TEST_UCI_DIR/telegram_enabled"
|
||||
echo "wan" > "$TEST_UCI_DIR/monitored_ifaces"
|
||||
echo "down" > "$TEST_UCI_DIR/ubus_state_wan6"
|
||||
ACTION="ifdown" INTERFACE="wan6" sh "$TEST_MONITOR"
|
||||
sleep 0.2
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 3 failed: notification sent for unmonitored interface wan6"
|
||||
|
||||
# Test 4: Monitored interface drops (wan ifdown, initial up -> down transition)
|
||||
@@ -139,7 +147,7 @@ echo "down" > "$TEST_UCI_DIR/ubus_state_wan"
|
||||
|
||||
ACTION="ifdown" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
# Wait for background verification
|
||||
sleep 0.3
|
||||
sleep 1
|
||||
|
||||
[ -f "$TEST_TELEGRAM_LOG" ] || fail "Test 4 failed: no notification sent on UP -> DOWN transition"
|
||||
grep -Fq "Интерфейс *wan* упал!" "$TEST_TELEGRAM_LOG" || fail "Test 4 failed: expected drop message not found in log"
|
||||
@@ -148,7 +156,7 @@ grep -Fq "Интерфейс *wan* упал!" "$TEST_TELEGRAM_LOG" || fail "Test
|
||||
# Test 5: Duplicate hotplug ifdown while state is already down (NO repeated spam)
|
||||
rm -f "$TEST_TELEGRAM_LOG"
|
||||
ACTION="ifdown" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.3
|
||||
sleep 1
|
||||
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 5 failed: duplicate notification sent on repeated ifdown without state change"
|
||||
|
||||
@@ -157,7 +165,7 @@ rm -f "$TEST_TELEGRAM_LOG"
|
||||
echo "up" > "$TEST_UCI_DIR/ubus_state_wan"
|
||||
|
||||
ACTION="ifup" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.3
|
||||
sleep 1
|
||||
|
||||
[ -f "$TEST_TELEGRAM_LOG" ] || fail "Test 6 failed: no notification sent on DOWN -> UP recovery"
|
||||
grep -Fq "Интерфейс *wan* поднялся!" "$TEST_TELEGRAM_LOG" || fail "Test 6 failed: expected recovery message not found in log"
|
||||
@@ -166,7 +174,7 @@ grep -Fq "Интерфейс *wan* поднялся!" "$TEST_TELEGRAM_LOG" || fa
|
||||
# Test 7: Duplicate hotplug ifup while state is already up (NO duplicate message)
|
||||
rm -f "$TEST_TELEGRAM_LOG"
|
||||
ACTION="ifup" INTERFACE="wan" sh "$TEST_MONITOR"
|
||||
sleep 0.3
|
||||
sleep 1
|
||||
|
||||
[ ! -f "$TEST_TELEGRAM_LOG" ] || fail "Test 7 failed: duplicate notification sent on repeated ifup without state change"
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ fail() {
|
||||
# BusyBox wget in OpenWrt 25.12+ no longer supports -t (retries). Tachyon
|
||||
# handles retries explicitly in a while loop, so the flag is unnecessary and
|
||||
# breaks downloads on recent OpenWrt releases.
|
||||
if grep -rn --exclude='*.json' --fixed-strings 'wget' "$ROOT_DIR/tachyon/files" | grep -E -- '(^|[^-a-z])(-t[^a-z]|-t")' >/dev/null 2>&1; then
|
||||
grep -rn --exclude='*.json' --fixed-strings 'wget' "$ROOT_DIR/tachyon/files" | grep -E -- '(^|[^-a-z])(-t[^a-z]|-t")' >&2
|
||||
if find "$ROOT_DIR/tachyon/files" -type f ! -name '*.json' -exec grep -Hn -F 'wget' {} + 2>/dev/null | grep -E -- '(^|[^-a-z])(-t[^a-z]|-t")' >/dev/null 2>&1; then
|
||||
find "$ROOT_DIR/tachyon/files" -type f ! -name '*.json' -exec grep -Hn -F 'wget' {} + | grep -E -- '(^|[^-a-z])(-t[^a-z]|-t")' >&2
|
||||
fail "wget -t is incompatible with OpenWrt 25.12+ BusyBox; use explicit retry loops instead"
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user