From 1871d0c586f2ac8deeeabd1273cf70900f95dd5e Mon Sep 17 00:00:00 2001 From: kazeyukiro Date: Sun, 4 Oct 2026 05:22:52 +0000 Subject: [PATCH] feat(sub): add http_clients to sing-box sub; keep jsDelivr rule-sets Restore wuziwei-style remote rule-sets (no panel hosting) and add http_clients default (HTTP/2) for rule-set downloads per template. --- backend/internal/converter/singbox.go | 178 +++++++++++--------------- 1 file changed, 77 insertions(+), 101 deletions(-) diff --git a/backend/internal/converter/singbox.go b/backend/internal/converter/singbox.go index 2a4521a..fdbb4b3 100644 --- a/backend/internal/converter/singbox.go +++ b/backend/internal/converter/singbox.go @@ -117,167 +117,143 @@ func GenerateUserSingboxSubscription(db *gorm.DB, pu models.ProxyUser, req *http if len(skipped) > 0 { log.Printf("sing-box subscription: skipped %d node(s) for user %s: %s", len(skipped), pu.Username, strings.Join(skipped, "; ")) } - // Tags match LongLights sing-box_v1.14 template: Default + Direct-Out. + // Outbounds: proxy selector + direct (CN/overseas split via remote rule-sets). outbounds = append(outbounds, - map[string]interface{}{"type": "direct", "tag": "Direct-Out"}, + map[string]interface{}{"type": "direct", "tag": "direct"}, map[string]interface{}{ "type": "selector", - "tag": "Default", + "tag": "proxy", "outbounds": append([]string{}, tagNames...), "default": tagNames[0], }, ) - ruleBase := singboxRuleSetBaseURL(req) - doc := buildSingboxSubscriptionDoc(outbounds, ruleBase) + doc := buildSingboxSubscriptionDoc(outbounds) return json.MarshalIndent(doc, "", " ") } -// buildSingboxSubscriptionDoc follows LongLights sing-box_v1.14 template layout -// for CN / overseas split (DNS + route + rule_set), with rule-sets hosted on -// this panel so SFI does not depend on GitHub. -// Ref: https://github.com/LongLights/sing-box_template_merge_sub-store/blob/main/sing-box_v1.14/singbox.json -func buildSingboxSubscriptionDoc(outbounds []map[string]interface{}, ruleBase string) map[string]interface{} { - doc := map[string]interface{}{ +// buildSingboxSubscriptionDoc follows wuziwei client sb-1.15 bridge template +// for CN/overseas split, using jsDelivr remote rule-sets (no panel hosting). +// Ref: https://github.com/wuziwei/sing-box-config-template (client sb-1.15 bridge) +func buildSingboxSubscriptionDoc(outbounds []map[string]interface{}) map[string]interface{} { + return map[string]interface{}{ "log": map[string]interface{}{ "level": "warn", }, - "dns": defaultSingboxDNS(), - "inbounds": []map[string]interface{}{defaultSingboxTUNInbound()}, - "outbounds": outbounds, - "route": map[string]interface{}{ - "rules": defaultSingboxRouteRules(), - "final": "Default", - "auto_detect_interface": true, - "default_domain_resolver": map[string]interface{}{ - "server": "cn_dns", + "http_clients": []map[string]interface{}{ + { + "tag": "default", + "version": 2, + "stream_receive_window": 0, + "connection_receive_window": 0, }, }, + "dns": defaultSingboxDNS(), + "inbounds": []map[string]interface{}{ + defaultSingboxTUNInbound(), + }, + "outbounds": outbounds, + "route": map[string]interface{}{ + "rules": defaultSingboxRouteRules(), + "rule_set": defaultSingboxRemoteRuleSets(), + "final": "proxy", + "auto_detect_interface": true, + "default_domain_resolver": "ali", + "default_http_client": "default", + }, } - if ruleBase != "" { - doc["route"].(map[string]interface{})["rule_set"] = defaultSingboxCNRuleSets(ruleBase) - } - return doc } -func singboxRuleSetBaseURL(req *http.Request) string { - if req == nil { - return "" - } - scheme := "http" - if req.TLS != nil || strings.EqualFold(req.Header.Get("X-Forwarded-Proto"), "https") { - scheme = "https" - } - host := strings.TrimSpace(req.Host) - if host == "" { - return "" - } - webPath := "" - if config.GlobalConfig != nil { - webPath = strings.TrimSuffix(config.GlobalConfig.Server.WebPath, "/") - } - return scheme + "://" + host + webPath + "/api/v1/client/rule-set" -} - -// defaultSingboxCNRuleSets — cnsite / cnip / gfw (MetaCubeX meta-rules-dat, embedded). -func defaultSingboxCNRuleSets(base string) []map[string]interface{} { - base = strings.TrimRight(base, "/") +// defaultSingboxRemoteRuleSets — MetaCubeX geosite/geoip via jsDelivr (template style). +func defaultSingboxRemoteRuleSets() []map[string]interface{} { return []map[string]interface{}{ { - "tag": "cnsite", "type": "remote", - "format": "binary", - "url": base + "/cnsite.srs", - "download_detour": "Direct-Out", - "update_interval": "24h", + "tag": []string{"apple@cn", "microsoft@cn", "category-games@cn", "cn"}, + "url": "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/{tag}.srs", + "update_interval": "48h", }, { - "tag": "cnip", "type": "remote", + "tag": "cn_ip", "format": "binary", - "url": base + "/cnip.srs", - "download_detour": "Direct-Out", - "update_interval": "24h", - }, - { - "tag": "gfw", - "type": "remote", - "format": "binary", - "url": base + "/gfw.srs", - "download_detour": "Direct-Out", - "update_interval": "24h", + "url": "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs", + "update_interval": "48h", }, } } -// defaultSingboxDNS mirrors the v1.14 template: CN domains → 119.29.29.29, GFW → DoH via proxy. +// defaultSingboxDNS — CN domains → AliDNS; else Google DoH via proxy (template simplified). func defaultSingboxDNS() map[string]interface{} { return map[string]interface{}{ "servers": []map[string]interface{}{ { - "tag": "proxy_dns", - "type": "https", - "server": "8.8.8.8", - "detour": "Default", + "type": "hosts", + "tag": "hosts", + "predefined": map[string]interface{}{ + "dns.alidns.com": []string{"223.5.5.5", "223.6.6.6"}, + "dns.google": []string{"8.8.8.8", "8.8.4.4"}, + }, }, { - "tag": "cn_dns", - "type": "udp", - "server": "119.29.29.29", + "tag": "google", + "type": "https", + "server": "dns.google", + "domain_resolver": "hosts", + "detour": "proxy", + }, + { + "tag": "ali", + "type": "https", + "server": "dns.alidns.com", + "domain_resolver": "hosts", }, }, "rules": []map[string]interface{}{ - {"query_type": "AAAA", "action": "predefined", "rcode": "NOERROR"}, - {"rule_set": "gfw", "action": "route", "server": "proxy_dns"}, - {"domain_suffix": []string{"googleapis.cn"}, "server": "proxy_dns"}, { - "type": "logical", - "mode": "or", - "action": "route", - "rules": []map[string]interface{}{ - {"rule_set": "cnsite"}, - }, - "server": "cn_dns", + "rule_set": []string{"apple@cn", "microsoft@cn", "category-games@cn", "cn"}, + "action": "route", + "server": "ali", }, - {"action": "route", "server": "proxy_dns"}, + {"action": "route", "server": "google"}, }, + "final": "google", } } -// defaultSingboxRouteRules — simplified from the v1.14 template (CN/overseas core path). -// Skips per-app selectors (Telegram/TikTok/…) that need empty groups in a bare sub. +// defaultSingboxRouteRules — CN geosite/geoip → direct; rest → proxy (template core path). func defaultSingboxRouteRules() []map[string]interface{} { return []map[string]interface{}{ - {"network": "icmp", "action": "route", "outbound": "Direct-Out"}, - {"action": "sniff"}, { "type": "logical", "mode": "or", "action": "hijack-dns", "rules": []map[string]interface{}{ - {"protocol": "dns"}, {"port": 53}, + {"protocol": "dns"}, }, }, - {"ip_is_private": true, "action": "route", "outbound": "Direct-Out"}, - {"protocol": "bittorrent", "action": "route", "outbound": "Direct-Out"}, - {"domain_suffix": []string{"googleapis.cn"}, "action": "route", "outbound": "Default"}, - {"rule_set": "gfw", "action": "route", "outbound": "Default"}, + {"ip_is_private": true, "action": "route", "outbound": "direct"}, + {"action": "sniff", "timeout": "100ms"}, + {"domain_suffix": []string{"googleapis.cn"}, "action": "route", "outbound": "proxy"}, + { + "rule_set": []string{"apple@cn", "microsoft@cn", "category-games@cn", "cn"}, + "action": "route", + "outbound": "direct", + }, {"action": "resolve"}, - {"rule_set": "cnip", "action": "route", "outbound": "Direct-Out"}, + {"rule_set": "cn_ip", "action": "route", "outbound": "direct"}, } } -// defaultSingboxTUNInbound — TUN for SFI/SFM (compatible fields for 1.12+). func defaultSingboxTUNInbound() map[string]interface{} { return map[string]interface{}{ - "type": "tun", - "tag": "tun-in", - "address": []string{"172.19.0.1/30"}, - "mtu": 9000, - "auto_route": true, - "strict_route": true, - "auto_redirect": true, - "dns_mode": "hijack", + "type": "tun", + "tag": "tun-in", + "address": []string{"172.19.0.1/30"}, + "mtu": 9000, + "auto_route": true, + "strict_route": true, + "dns_mode": "hijack", } }