- 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
161 lines
5.3 KiB
Bash
161 lines
5.3 KiB
Bash
#!/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'
|