From 4e68f45c5dae4e043b422389bdecf68461be1c22 Mon Sep 17 00:00:00 2001 From: kazeyukiro Date: Mon, 28 Sep 2026 16:36:57 +0000 Subject: [PATCH] =?UTF-8?q?fix(sub):=20reduce=20SFA=20lag=20=E2=80=94=20lo?= =?UTF-8?q?cal=20domain=20resolver=20+=20private=20direct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. fix(sub): reduce SFA lag — local domain resolver + private direct Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS, hijack DNS with port 53, route private IPs direct, drop auto_redirect, and use DoH remote DNS after the tunnel is up. --- backend/internal/converter/singbox.go | 57 +++++++++++-------- .../internal/converter/singbox_tun_test.go | 10 +++- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/backend/internal/converter/singbox.go b/backend/internal/converter/singbox.go index 188ac76..8bf1966 100644 --- a/backend/internal/converter/singbox.go +++ b/backend/internal/converter/singbox.go @@ -90,57 +90,67 @@ func buildSingboxSubscriptionDoc(outbounds []map[string]interface{}) map[string] "log": map[string]interface{}{ "level": "warn", }, - // DNS is required for TUN profiles: without it, hijack-dns has nowhere to - // resolve and browsers often load HTML but fail CSS/JS CDNs → broken pages. - // https://sing-box.sagernet.org/configuration/dns/ "dns": defaultSingboxDNS(), "inbounds": []map[string]interface{}{ defaultSingboxTUNInbound(), }, "outbounds": outbounds, - // sing-box 1.11+: sniff / domain_strategy moved from inbound to route rule actions. - // https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions + // Route: private traffic stays local; DNS is hijacked into the dns module; + // everything else uses the proxy selector. Domain names of outbounds are + // resolved via local DNS so we do not create a DNS↔proxy dependency loop + // (a common cause of "works but extremely laggy" on SFA). "route": map[string]interface{}{ "rules": []map[string]interface{}{ {"action": "sniff"}, - {"protocol": "dns", "action": "hijack-dns"}, + { + "type": "logical", + "mode": "or", + "action": "hijack-dns", + "rules": []map[string]interface{}{ + {"protocol": "dns"}, + {"port": 53}, + }, + }, + {"ip_is_private": true, "action": "route", "outbound": "direct"}, }, "final": "proxy", "auto_detect_interface": true, + "default_domain_resolver": map[string]interface{}{ + "server": "local", + }, }, } } -// defaultSingboxDNS sends queries through the proxy selector so domain -// resolution matches the tunneled path (avoids split-horizon / poisoned local DNS). +// defaultSingboxDNS: resolve outbound server names via local; general queries via +// DoH through the proxy selector (after the outbound domain is known). func defaultSingboxDNS() map[string]interface{} { return map[string]interface{}{ "servers": []map[string]interface{}{ - { - "type": "udp", - "tag": "remote", - "server": "8.8.8.8", - "server_port": 53, - "detour": "proxy", - }, { "type": "local", "tag": "local", "detour": "direct", }, + { + "type": "https", + "tag": "remote", + "server": "8.8.8.8", + "detour": "proxy", + }, + }, + "rules": []map[string]interface{}{ + // Prefer local for private reverse lookups / plain local names. + {"ip_is_private": true, "action": "route", "server": "local"}, }, "final": "remote", "strategy": "prefer_ipv4", } } -// defaultSingboxTUNInbound is a conservative TUN profile accepted by recent -// sing-box builds used by SFI/SFM. Operators can still edit the profile on device. +// defaultSingboxTUNInbound mirrors common SFA templates without stack/auto_redirect +// (auto_redirect is Linux-oriented and often hurts mobile clients). func defaultSingboxTUNInbound() map[string]interface{} { - // Tun fields aligned with common SFA-ready templates (e.g. sing-box_v1.14): - // https://github.com/LongLights/sing-box_template_merge_sub-store - // No "stack" — leave stack to the client default for better device compatibility. - // No legacy inbound sniff (use route rule actions instead). return map[string]interface{}{ "type": "tun", "tag": "tun-in", @@ -156,9 +166,8 @@ func defaultSingboxTUNInbound() map[string]interface{} { "192.0.0.0/24", "192.168.0.0/16", }, - "strict_route": true, - "dns_mode": "hijack", - "auto_redirect": true, + "strict_route": true, + "dns_mode": "hijack", } } diff --git a/backend/internal/converter/singbox_tun_test.go b/backend/internal/converter/singbox_tun_test.go index 20686dc..a0086cc 100644 --- a/backend/internal/converter/singbox_tun_test.go +++ b/backend/internal/converter/singbox_tun_test.go @@ -41,14 +41,20 @@ func TestBuildSingboxSubscriptionDocIncludesTUN(t *testing.T) { if !ok || dns["final"] != "remote" { t.Fatalf("dns: %#v", parsed["dns"]) } + route, ok := parsed["route"].(map[string]interface{}) + if !ok { + t.Fatalf("missing route") + } + if _, ok := route["default_domain_resolver"]; !ok { + t.Fatalf("missing default_domain_resolver: %#v", route) + } if first["auto_route"] != true { t.Fatalf("auto_route: %#v", first["auto_route"]) } if _, has := first["sniff"]; has { t.Fatalf("legacy inbound sniff must not be set (removed in sing-box 1.13): %#v", first) } - route, ok := parsed["route"].(map[string]interface{}) - if !ok || route["final"] != "proxy" { + if route["final"] != "proxy" { t.Fatalf("route.final: %#v", route) } rules, ok := route["rules"].([]interface{})