236 lines
9.4 KiB
Bash
236 lines
9.4 KiB
Bash
#!/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"
|
|
FAILOVER="$TACHYON_LIB/singbox/dns_failover.uc"
|
|
WORK_DIR="$(mktemp -d)"
|
|
|
|
cleanup() {
|
|
rm -rf "$WORK_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
fail() {
|
|
printf 'FAIL: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
generate() {
|
|
local fixture="$1"
|
|
local output="$2"
|
|
local state="${3:-$WORK_DIR/missing-state.json}"
|
|
TACHYON_LIB="$TACHYON_LIB" \
|
|
TACHYON_DNS_FAILOVER_STATE_FILE="$state" \
|
|
ucode -L "$TACHYON_LIB" "$GENERATOR" generate-config-fixture "$fixture" "$output" 192.168.1.1 0
|
|
}
|
|
|
|
# ─── Fixture 1: primary DoH + explicit fallback UDP ───────────────────────────
|
|
cat >"$WORK_DIR/with-fallback.json" <<'JSON'
|
|
{
|
|
"settings": {
|
|
".name": "settings",
|
|
".type": "settings",
|
|
"dns_type": "doh",
|
|
"dns_server": "dns.google/dns-query",
|
|
"dns_fallback_server": [ "1.1.1.1", "8.8.8.8" ],
|
|
"bootstrap_dns_server": "77.88.8.8"
|
|
},
|
|
"section": [
|
|
{
|
|
".name": "direct",
|
|
".type": "section",
|
|
"enabled": "1",
|
|
"action": "bypass",
|
|
"domain_suffix": [ "example.org" ]
|
|
}
|
|
]
|
|
}
|
|
JSON
|
|
|
|
# ─── Fixture 2: two primary + two fallback (all four as candidates) ────────────
|
|
cat >"$WORK_DIR/multi-with-fallback.json" <<'JSON'
|
|
{
|
|
"settings": {
|
|
".name": "settings",
|
|
".type": "settings",
|
|
"dns_type": "doh",
|
|
"dns_server": [ "dns.google/dns-query", "cloudflare-dns.com/dns-query" ],
|
|
"dns_fallback_server": [ "1.1.1.1", "8.8.8.8" ],
|
|
"bootstrap_dns_server": "77.88.8.8"
|
|
},
|
|
"section": [
|
|
{
|
|
".name": "direct",
|
|
".type": "section",
|
|
"enabled": "1",
|
|
"action": "bypass",
|
|
"domain_suffix": [ "example.org" ]
|
|
}
|
|
]
|
|
}
|
|
JSON
|
|
|
|
# ─── State: fallback active (index=1 points to first fallback) ────────────────
|
|
cat >"$WORK_DIR/fallback-state.json" <<'JSON'
|
|
{
|
|
"version": 1,
|
|
"dns_type": "doh",
|
|
"dns_detour": "",
|
|
"main_servers": [ "dns.google/dns-query", "1.1.1.1", "8.8.8.8" ],
|
|
"bootstrap_servers": [ "77.88.8.8" ],
|
|
"main_index": 1,
|
|
"bootstrap_index": 0
|
|
}
|
|
JSON
|
|
|
|
# ─── State: second fallback active (index=2) ─────────────────────────────────
|
|
cat >"$WORK_DIR/fallback-state2.json" <<'JSON'
|
|
{
|
|
"version": 1,
|
|
"dns_type": "doh",
|
|
"dns_detour": "",
|
|
"main_servers": [ "dns.google/dns-query", "1.1.1.1", "8.8.8.8" ],
|
|
"bootstrap_servers": [ "77.88.8.8" ],
|
|
"main_index": 2,
|
|
"bootstrap_index": 0
|
|
}
|
|
JSON
|
|
|
|
generate "$WORK_DIR/with-fallback.json" "$WORK_DIR/with-fallback-config.json"
|
|
generate "$WORK_DIR/with-fallback.json" "$WORK_DIR/fallback-active-config.json" "$WORK_DIR/fallback-state.json"
|
|
generate "$WORK_DIR/with-fallback.json" "$WORK_DIR/fallback-active2-config.json" "$WORK_DIR/fallback-state2.json"
|
|
generate "$WORK_DIR/multi-with-fallback.json" "$WORK_DIR/multi-with-fallback-config.json"
|
|
|
|
ucode -e '
|
|
let fs = require("fs");
|
|
|
|
function cfg(path) { return json(fs.readfile(path)); }
|
|
function assert(value, message) {
|
|
if (!value) { warn("FAIL: ", message, "\n"); exit(1); }
|
|
}
|
|
function find_tag(values, tag) {
|
|
for (let value in values || []) if (value.tag == tag) return value;
|
|
return null;
|
|
}
|
|
function count_prefix(values, prefix) {
|
|
let count = 0;
|
|
for (let value in values || []) if (index(value.tag || "", prefix) == 0) count++;
|
|
return count;
|
|
}
|
|
|
|
// ─── Test 1: primary DoH, no failover state — uses primary server ─────────────
|
|
let primary = cfg(ARGV[0]);
|
|
let main_srv = find_tag(primary.dns.servers, "dns-server");
|
|
assert(main_srv != null, "dns-server tag must exist");
|
|
assert(main_srv.type == "https", "primary DNS must be DoH type");
|
|
assert(main_srv.server == "dns.google", "primary DNS server must be dns.google");
|
|
|
|
// ─── Test 2: fallback state active (index=1 = 1.1.1.1) ───────────────────────
|
|
// Explicit fallback servers MUST always be plain UDP regardless of dns_type
|
|
let fallback_active = cfg(ARGV[1]);
|
|
let fb_main = find_tag(fallback_active.dns.servers, "dns-server");
|
|
assert(fb_main != null, "dns-server must exist in fallback-active config");
|
|
assert(fb_main.type == "udp", "explicit fallback DNS must use UDP even when dns_type=doh");
|
|
assert(fb_main.server == "1.1.1.1", "fallback server index 1 must be 1.1.1.1");
|
|
assert(fb_main.detour == null, "explicit fallback must not use proxy detour");
|
|
|
|
// ─── Test 3: second fallback server (index=2 = 8.8.8.8) ──────────────────────
|
|
let fallback_active2 = cfg(ARGV[2]);
|
|
let fb_main2 = find_tag(fallback_active2.dns.servers, "dns-server");
|
|
assert(fb_main2 != null, "dns-server must exist in fallback-active2 config");
|
|
assert(fb_main2.type == "udp", "second explicit fallback must also be UDP");
|
|
assert(fb_main2.server == "8.8.8.8", "fallback server index 2 must be 8.8.8.8");
|
|
|
|
// ─── Test 4: multi-primary with fallback generates health candidates ──────────
|
|
let multi = cfg(ARGV[3]);
|
|
// 2 main health candidates + 2 fallback candidates + 1 active-main inbound = 5 health rules
|
|
let health_rules = 0;
|
|
for (let rule in multi.dns.rules || []) {
|
|
if (index(rule.inbound || "", "dns-health-") == 0) {
|
|
health_rules++;
|
|
assert(rule.disable_cache === true, "health checks must bypass DNS cache");
|
|
}
|
|
}
|
|
// We have 2 primary + 2 fallback = 4 main candidates (all generate health probes)
|
|
// Plus 1 bootstrap (single) = no bootstrap health
|
|
// Plus 1 active health inbound = total 5 DNS health rules
|
|
assert(health_rules == 5, "2 main + 2 fallback candidates + 1 active = 5 health rules, got " + health_rules);
|
|
assert(count_prefix(multi.dns.servers, "dns-health-main-") == 4, "4 main health servers (2 primary + 2 fallback)");
|
|
|
|
' "$WORK_DIR/with-fallback-config.json" \
|
|
"$WORK_DIR/fallback-active-config.json" \
|
|
"$WORK_DIR/fallback-active2-config.json" \
|
|
"$WORK_DIR/multi-with-fallback-config.json"
|
|
|
|
# ─── Test 5: is_explicit_fallback_index logic via dns.uc ─────────────────────
|
|
ucode -L "$TACHYON_LIB" -e '
|
|
let runtime_dns = require("singbox.dns");
|
|
let common = require("core.common");
|
|
|
|
function assert(value, message) {
|
|
if (!value) { warn("FAIL: ", message, "\n"); exit(1); }
|
|
}
|
|
|
|
let settings_one_primary_two_fallback = {
|
|
dns_server: [ "dns.google/dns-query" ],
|
|
dns_fallback_server: [ "1.1.1.1", "8.8.8.8" ]
|
|
};
|
|
// configured_server_count: 1 (primary)
|
|
// explicit_fallback_count: 2
|
|
// index 0 = primary, index 1 = fallback, index 2 = fallback
|
|
assert(runtime_dns.is_explicit_fallback_index(settings_one_primary_two_fallback, 0) == false, "index 0 is primary, not fallback");
|
|
assert(runtime_dns.is_explicit_fallback_index(settings_one_primary_two_fallback, 1) == true, "index 1 is first explicit fallback");
|
|
assert(runtime_dns.is_explicit_fallback_index(settings_one_primary_two_fallback, 2) == true, "index 2 is second explicit fallback");
|
|
assert(runtime_dns.is_wan_fallback_index(settings_one_primary_two_fallback, 1) == false, "index 1 is NOT WAN fallback");
|
|
assert(runtime_dns.is_wan_fallback_index(settings_one_primary_two_fallback, 2) == false, "index 2 is NOT WAN fallback");
|
|
|
|
let settings_with_wan = {
|
|
dns_server: [ "dns.google/dns-query" ],
|
|
dns_fallback_server: [ "1.1.1.1" ],
|
|
fallback_wan_main: "1"
|
|
};
|
|
// index 0 = primary, index 1 = explicit fallback, index 2+ = WAN
|
|
assert(runtime_dns.is_explicit_fallback_index(settings_with_wan, 1) == true, "index 1 is explicit fallback even with WAN enabled");
|
|
assert(runtime_dns.is_wan_fallback_index(settings_with_wan, 1) == false, "index 1 is NOT WAN (is explicit fallback)");
|
|
// WAN starts at index 2 (1 primary + 1 fallback)
|
|
assert(runtime_dns.is_wan_fallback_index(settings_with_wan, 2) == true, "index 2 is WAN fallback");
|
|
|
|
let settings_no_fallback = {
|
|
dns_server: [ "dns.google/dns-query", "cloudflare-dns.com/dns-query" ],
|
|
fallback_wan_main: "1"
|
|
};
|
|
// index 0,1 = primary, index 2+ = WAN (no explicit fallback)
|
|
assert(runtime_dns.is_explicit_fallback_index(settings_no_fallback, 1) == false, "index 1 is primary, no explicit fallback defined");
|
|
assert(runtime_dns.is_wan_fallback_index(settings_no_fallback, 2) == true, "index 2 is WAN fallback when no explicit fallback");
|
|
'
|
|
|
|
# ─── Test 6: server_list includes dns_fallback_server entries ─────────────────
|
|
ucode -L "$TACHYON_LIB" -e '
|
|
let runtime_dns = require("singbox.dns");
|
|
|
|
function assert(value, message) {
|
|
if (!value) { warn("FAIL: ", message, "\n"); exit(1); }
|
|
}
|
|
|
|
let settings = {
|
|
dns_server: [ "dns.google/dns-query", "cloudflare-dns.com/dns-query" ],
|
|
dns_fallback_server: [ "1.1.1.1", "8.8.8.8" ]
|
|
};
|
|
|
|
let servers = runtime_dns.server_list(settings, "dns_server", "77.88.8.8");
|
|
assert(length(servers) == 4, "server_list must include 2 primary + 2 fallback = 4 total");
|
|
assert(servers[0] == "dns.google/dns-query", "first server is primary[0]");
|
|
assert(servers[1] == "cloudflare-dns.com/dns-query", "second server is primary[1]");
|
|
assert(servers[2] == "1.1.1.1", "third server is fallback[0]");
|
|
assert(servers[3] == "8.8.8.8", "fourth server is fallback[1]");
|
|
|
|
// Bootstrap list must NOT include dns_fallback_server entries
|
|
let bootstrap = runtime_dns.server_list(settings, "bootstrap_dns_server", "77.88.8.8");
|
|
assert(length(bootstrap) == 1, "bootstrap list must not include dns_fallback_server entries");
|
|
assert(bootstrap[0] == "77.88.8.8", "bootstrap falls back to default");
|
|
'
|
|
|
|
printf 'DNS fallback server checks passed\n'
|