diff --git a/backend/internal/converter/ruleset/cnip.srs b/backend/internal/converter/ruleset/cnip.srs deleted file mode 100644 index 09674d6..0000000 Binary files a/backend/internal/converter/ruleset/cnip.srs and /dev/null differ diff --git a/backend/internal/converter/ruleset/cnsite.srs b/backend/internal/converter/ruleset/cnsite.srs deleted file mode 100644 index 1d685b1..0000000 Binary files a/backend/internal/converter/ruleset/cnsite.srs and /dev/null differ diff --git a/backend/internal/converter/ruleset/embed.go b/backend/internal/converter/ruleset/embed.go deleted file mode 100644 index c6d0397..0000000 --- a/backend/internal/converter/ruleset/embed.go +++ /dev/null @@ -1,12 +0,0 @@ -package ruleset - -import _ "embed" - -//go:embed cnsite.srs -var CNSite []byte - -//go:embed cnip.srs -var CNIP []byte - -//go:embed gfw.srs -var GFW []byte diff --git a/backend/internal/converter/ruleset/gfw.srs b/backend/internal/converter/ruleset/gfw.srs deleted file mode 100644 index 97c9d7e..0000000 Binary files a/backend/internal/converter/ruleset/gfw.srs and /dev/null differ diff --git a/backend/internal/converter/singbox.go b/backend/internal/converter/singbox.go index 2a4521a..fc8bf69 100644 --- a/backend/internal/converter/singbox.go +++ b/backend/internal/converter/singbox.go @@ -117,167 +117,134 @@ 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()}, + "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", - }, + "rules": defaultSingboxRouteRules(), + "rule_set": defaultSingboxRemoteRuleSets(), + "final": "proxy", + "auto_detect_interface": true, + "default_domain_resolver": "ali", }, } - 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", } } diff --git a/backend/internal/router/subscription.go b/backend/internal/router/subscription.go index 7a8f25b..51dbe06 100644 --- a/backend/internal/router/subscription.go +++ b/backend/internal/router/subscription.go @@ -13,7 +13,6 @@ import ( "github.com/gin-gonic/gin" "github.com/kazeyukiro/3m-ui/backend/internal/config" "github.com/kazeyukiro/3m-ui/backend/internal/converter" - "github.com/kazeyukiro/3m-ui/backend/internal/converter/ruleset" "github.com/kazeyukiro/3m-ui/backend/internal/database/models" "github.com/kazeyukiro/3m-ui/backend/internal/node" "github.com/kazeyukiro/3m-ui/backend/internal/subpage" @@ -261,9 +260,6 @@ func RegisterPublicSubscriptionRoutes(api *gin.RouterGroup, db *gorm.DB, cfg *co handler := subscriptionHandler(db, cfg) api.GET("/client/sub/:token", handler) api.GET("/client/sub/:token/", handler) - api.GET("/client/rule-set/cnsite.srs", serveSingboxRuleSet("cnsite")) - api.GET("/client/rule-set/cnip.srs", serveSingboxRuleSet("cnip")) - api.GET("/client/rule-set/gfw.srs", serveSingboxRuleSet("gfw")) // Path-based formats (docs.sanaei.dev style /sub /json /clash). api.GET("/client/json/:token", forcedTargetHandler(db, cfg, "singbox")) api.GET("/client/json/:token/", forcedTargetHandler(db, cfg, "singbox")) @@ -439,26 +435,3 @@ func requestScheme(c *gin.Context) string { } return "http" } - -func serveSingboxRuleSet(name string) gin.HandlerFunc { - return func(c *gin.Context) { - var body []byte - switch name { - case "cnsite": - body = ruleset.CNSite - case "cnip": - body = ruleset.CNIP - case "gfw": - body = ruleset.GFW - default: - c.Status(http.StatusNotFound) - return - } - if len(body) == 0 { - c.Status(http.StatusNotFound) - return - } - c.Header("Cache-Control", "public, max-age=86400") - c.Data(http.StatusOK, "application/octet-stream", body) - } -}