diff --git a/fe-app-tachyon/src/tachyon/tabs/monitoring/initController.ts b/fe-app-tachyon/src/tachyon/tabs/monitoring/initController.ts index f92f8b7c..fb4c9221 100644 --- a/fe-app-tachyon/src/tachyon/tabs/monitoring/initController.ts +++ b/fe-app-tachyon/src/tachyon/tabs/monitoring/initController.ts @@ -189,6 +189,7 @@ function buildRouteDisplayNames(sections: Tachyon.ConfigSection[]) { const map: Record = { 'bypass-out': 'Bypass', 'direct-out': 'direct', + 'tachyon-failover': 'Failover', }; const serverMap: Record = {}; 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 }; diff --git a/fe-app-tachyon/src/tachyon/tabs/monitoring/tests/initController.test.ts b/fe-app-tachyon/src/tachyon/tabs/monitoring/tests/initController.test.ts index 7d0bf311..ee66f1d1 100644 --- a/fe-app-tachyon/src/tachyon/tabs/monitoring/tests/initController.test.ts +++ b/fe-app-tachyon/src/tachyon/tabs/monitoring/tests/initController.test.ts @@ -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'); + }); }); diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index 6430030f..b413cccd 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -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 || ""; } diff --git a/tachyon/files/usr/lib/service/state.uc b/tachyon/files/usr/lib/service/state.uc index ac767cef..fa5cf250 100644 --- a/tachyon/files/usr/lib/service/state.uc +++ b/tachyon/files/usr/lib/service/state.uc @@ -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")); diff --git a/tachyon/files/usr/lib/singbox/generator_routes.uc b/tachyon/files/usr/lib/singbox/generator_routes.uc index 18227e93..b7d235ee 100644 --- a/tachyon/files/usr/lib/singbox/generator_routes.uc +++ b/tachyon/files/usr/lib/singbox/generator_routes.uc @@ -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; } } diff --git a/tachyon/files/usr/lib/singbox/route.uc b/tachyon/files/usr/lib/singbox/route.uc index 5435bd23..b774c2f9 100644 --- a/tachyon/files/usr/lib/singbox/route.uc +++ b/tachyon/files/usr/lib/singbox/route.uc @@ -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)); diff --git a/tests/installer_rollback_detection.sh b/tests/installer_rollback_detection.sh index d27b7c23..aab2a57f 100644 --- a/tests/installer_rollback_detection.sh +++ b/tests/installer_rollback_detection.sh @@ -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" ] || diff --git a/tests/route_rules_coexistence.sh b/tests/route_rules_coexistence.sh index c71f366e..08691a40 100644 --- a/tests/route_rules_coexistence.sh +++ b/tests/route_rules_coexistence.sh @@ -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" diff --git a/tests/ruleset_download_detour.sh b/tests/ruleset_download_detour.sh index 933030fd..784dfbcc 100644 --- a/tests/ruleset_download_detour.sh +++ b/tests/ruleset_download_detour.sh @@ -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"