fix(routing): resolve real IP for zapret routes, add IPv6 sniffing, and fix monitoring route display

This commit is contained in:
Dushnilin
2026-09-10 11:02:51 +03:00
parent cbae1a87fa
commit 240a939bbe
9 changed files with 183 additions and 13 deletions
@@ -189,6 +189,7 @@ function buildRouteDisplayNames(sections: Tachyon.ConfigSection[]) {
const map: Record<string, string> = {
'bypass-out': 'Bypass',
'direct-out': 'direct',
'tachyon-failover': 'Failover',
};
const serverMap: Record<string, string> = {};
const routeSectionItems: Array<{ sectionName: string; displayName: string }> =
@@ -261,12 +262,10 @@ function getRouteDisplayNameByTag(tag: string): string {
}
const manualSection = routeSections.find(({ sectionName }) => {
if (!tag.startsWith(`${sectionName}-`) || !tag.endsWith('-out')) {
return false;
if (tag === sectionName) {
return true;
}
const middle = tag.slice(sectionName.length + 1, -4);
return /^\d+(?:-\d+)?$/.test(middle);
return tag.startsWith(`${sectionName}-`) && tag.endsWith('-out');
});
return manualSection?.displayName || '';
@@ -1893,3 +1892,5 @@ export async function initController(
}
});
}
export { buildRouteDisplayNames, getRouteDisplayNameByTag, getRoute };
@@ -55,4 +55,85 @@ describe('monitoring initController', () => {
const { initController } = await import('../initController');
expect(typeof initController).toBe('function');
});
it('should resolve route display names correctly for built-in and section tags', async () => {
const { buildRouteDisplayNames, getRouteDisplayNameByTag, getRoute } =
await import('../initController');
buildRouteDisplayNames([
{
'.name': 'vpn',
'.type': 'section',
label: 'My VPN',
enabled: '1',
} as any,
{
'.name': 'zapret',
'.type': 'section',
label: 'Zapret YouTube',
enabled: '1',
} as any,
]);
// Built-in tags
expect(getRouteDisplayNameByTag('bypass-out')).toBe('Bypass');
expect(getRouteDisplayNameByTag('direct-out')).toBe('direct');
expect(getRouteDisplayNameByTag('tachyon-failover')).toBe('Failover');
// Section root tags
expect(getRouteDisplayNameByTag('vpn-out')).toBe('My VPN');
expect(getRouteDisplayNameByTag('zapret-out')).toBe('Zapret YouTube');
// Section sub-outbounds (numeric, named, priority, json)
expect(getRouteDisplayNameByTag('vpn-1-out')).toBe('My VPN');
expect(getRouteDisplayNameByTag('vpn-priority-1-out')).toBe('My VPN');
expect(getRouteDisplayNameByTag('vpn-json-1-out')).toBe('My VPN');
expect(getRouteDisplayNameByTag('vpn-interface-1-out')).toBe('My VPN');
expect(getRouteDisplayNameByTag('zapret-1-out')).toBe('Zapret YouTube');
// Direct section name matching
expect(getRouteDisplayNameByTag('vpn')).toBe('My VPN');
expect(getRouteDisplayNameByTag('zapret')).toBe('Zapret YouTube');
// Unknown tag returns empty string
expect(getRouteDisplayNameByTag('unknown-out')).toBe('');
// getRoute helper with chains and fallback rule
expect(
getRoute({
id: '1',
metadata: {},
chains: ['tproxy-in', 'vpn-priority-1-out'],
lastSeenAt: Date.now(),
} as any),
).toBe('My VPN');
expect(
getRoute({
id: '2',
metadata: {},
chains: ['tproxy-in', 'zapret-out'],
lastSeenAt: Date.now(),
} as any),
).toBe('Zapret YouTube');
expect(
getRoute({
id: '3',
metadata: {},
chains: [],
rule: 'rule-1 => route("zapret-out")',
lastSeenAt: Date.now(),
} as any),
).toBe('Zapret YouTube');
expect(
getRoute({
id: '4',
metadata: {},
chains: ['tachyon-failover'],
lastSeenAt: Date.now(),
} as any),
).toBe('Failover');
});
});
@@ -17932,7 +17932,8 @@ function getDisplayName2(section) {
function buildRouteDisplayNames(sections) {
const map = {
"bypass-out": "Bypass",
"direct-out": "direct"
"direct-out": "direct",
"tachyon-failover": "Failover"
};
const serverMap = {};
const routeSectionItems = [];
@@ -17983,11 +17984,10 @@ function getRouteDisplayNameByTag(tag) {
return routeDisplayNames[tag];
}
const manualSection = routeSections.find(({ sectionName }) => {
if (!tag.startsWith(`${sectionName}-`) || !tag.endsWith("-out")) {
return false;
if (tag === sectionName) {
return true;
}
const middle = tag.slice(sectionName.length + 1, -4);
return /^\d+(?:-\d+)?$/.test(middle);
return tag.startsWith(`${sectionName}-`) && tag.endsWith("-out");
});
return manualSection?.displayName || "";
}
+1
View File
@@ -1375,6 +1375,7 @@ function append_sing_box_rule_signature_body(body, section, sections) {
}
else if (action == "zapret" || action == "zapret2") {
body = signature_add_mixed_proxy_body(body, section, prefix);
body = signature_add_value(body, prefix + ".resolve_real_ip_for_routing", "1");
}
else if (action == "dns") {
body = signature_add_value(body, prefix + ".dns_type", option(section, "dns_type", "udp"));
@@ -646,6 +646,8 @@ function is_valid_detour(config, tag) {
if (type(out) == "object" && out.tag == tag) {
if (out.tag == "DPI-out" || (out.type == "socks" && (out.server == "127.0.0.1" || out.server == "::1")))
return false;
if (out.routing_mark != null)
return false;
return true;
}
}
+3 -1
View File
@@ -65,6 +65,8 @@ function config(settings, runtime) {
let output_network_interface = option(settings, "output_network_interface", "");
let mwan3_active = type(runtime) == "object" && bool_value(runtime.mwan3_active);
let sniff_inbounds = [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.DNS_INBOUND_TAG ];
if (common.ipv6_supported())
push(sniff_inbounds, runtime_constants.TPROXY_INBOUND6_TAG);
if (type(runtime) == "object" && bool_value(runtime.source_aware_dns))
push(sniff_inbounds, runtime_constants.SOURCE_DNS_INBOUND_TAG);
if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array")
@@ -136,7 +138,7 @@ function has_resolve_matchers(rule) {
function resolve_rule_for_section(section, route_rule) {
let action = option(section, "action", "");
let should_resolve = action == "byedpi" ||
let should_resolve = action == "byedpi" || action == "zapret" || action == "zapret2" ||
(connections.is_connections_action(action) &&
bool_option(section, "resolve_real_ip_for_routing", false));
+2 -2
View File
@@ -122,8 +122,8 @@ grep -Fq '/stat' "$WORK_DIR/lock.uc" ||
# ucode resolves names lexically and does not hoist, so a call placed above the
# declaration silently becomes null and the lock is never released.
lock_decl="$(grep -n '^function installer_release_init_lock() {' "$INSTALLER" | head -1 | cut -d: -f1)"
lock_call="$(grep -n 'installer_release_init_lock();' "$INSTALLER" | head -1 | cut -d: -f1)"
lock_decl="$(grep -m 1 -n '^function installer_release_init_lock() {' "$INSTALLER" | cut -d: -f1)"
lock_call="$(grep -m 1 -n 'installer_release_init_lock();' "$INSTALLER" | cut -d: -f1)"
[ -n "$lock_decl" ] && [ -n "$lock_call" ] ||
fail "installer_release_init_lock must be both declared and called"
[ "$lock_decl" -lt "$lock_call" ] ||
+51
View File
@@ -130,4 +130,55 @@ for (let rs in rulesets) {
}
' || fail "DPI-out must never be set as download_detour"
# 3. Verify zapret section generates resolve action for rulesets and domains
cat >"$WORK_DIR/fixture_zapret_resolve.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"enabled": "1",
"dns_type": "udp",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "zapret",
".type": "section",
"enabled": "1",
"action": "zapret",
"community_lists": [ "youtube" ]
}
]
}
JSON
output_zapret_resolve="$WORK_DIR/out_zapret_resolve.json"
mkdir -p "$output_zapret_resolve.section-cache" "$output_zapret_resolve.rulesets"
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/fixture_zapret_resolve.json" "$output_zapret_resolve" "127.0.0.1" "0" "1"
ucode -e '
let fs = require("fs");
let raw = fs.readfile("'"$output_zapret_resolve"'");
let cfg = json(raw);
let rules = cfg.route.rules || [];
let found_resolve = false;
let found_route = false;
for (let r in rules) {
if (r.action == "resolve" && r.rule_set)
found_resolve = true;
if (r.action == "route" && r.outbound == "zapret-out")
found_route = true;
}
if (!found_resolve) {
print("ERROR: zapret section did not generate action: resolve rule!\n");
exit(1);
}
if (!found_route) {
print("ERROR: zapret section did not generate action: route rule to zapret-out!\n");
exit(2);
}
' || fail "zapret section must generate resolve rule before route rule"
printf "Route rules coexistence and detour checks passed\n"
+32
View File
@@ -115,4 +115,36 @@ grep -q '"http_clients"' "$output_v14" || \
grep -q '"default_http_client": "ruleset-http"' "$output_v14" || \
fail "sing-box 1.14+ must configure default_http_client"
# 4. Verify zapret section with community_lists does NOT use zapret-out as download_detour
cat >"$WORK_DIR/fixture_zapret.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"enabled": "1",
"dns_type": "udp",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "zapret_sec",
".type": "section",
"enabled": "1",
"action": "zapret",
"community_lists": [ "youtube" ]
}
]
}
JSON
output_zapret="$WORK_DIR/out_zapret.json"
mkdir -p "$output_zapret.section-cache" "$output_zapret.rulesets"
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/fixture_zapret.json" "$output_zapret" "127.0.0.1" "0" "1"
if grep -q '"download_detour": "zapret_sec-out"' "$output_zapret"; then
fail "zapret-out must never be selected as download_detour due to routing_mark"
fi
printf "ruleset download detour checks passed\n"