From b83ce5090abda3e558bc90340bb4618c3c2fd349 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Fri, 18 Jul 2025 02:52:39 -0400 Subject: [PATCH] feat(surge): add Surge adapter support and enhance subscription URL handling --- internal/logic/subscribe/subscribeLogic.go | 23 ++++- pkg/adapter/adapter.go | 6 ++ pkg/adapter/surge/default.tpl | 106 ++++++++++++--------- pkg/adapter/surge/surge.go | 57 ++++------- 4 files changed, 105 insertions(+), 87 deletions(-) diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index d2beeed..edcca23 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -9,6 +9,7 @@ import ( "github.com/perfect-panel/server/pkg/adapter" "github.com/perfect-panel/server/pkg/adapter/shadowrocket" "github.com/perfect-panel/server/pkg/adapter/surfboard" + "github.com/perfect-panel/server/pkg/adapter/surge" "github.com/perfect-panel/server/internal/model/server" @@ -236,7 +237,7 @@ func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub case "loon": resp = proxyManager.BuildLoon(userSub.UUID) case "surfboard": - subsURL := l.getSubscribeURL(userSub.Token) + subsURL := l.getSubscribeURL(userSub.Token, "surfboard") resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{ Upload: userSub.Upload, Download: userSub.Download, @@ -248,6 +249,17 @@ func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub l.setSurfboardHeaders() case "v2rayn": resp = proxyManager.BuildV2rayN(userSub.UUID) + case "surge": + subsURL := l.getSubscribeURL(userSub.Token, "surge") + resp = proxyManager.BuildSurge(l.svc.Config.Site.SiteName, surge.UserInfo{ + UUID: userSub.UUID, + Upload: userSub.Upload, + Download: userSub.Download, + TotalTraffic: userSub.Traffic, + ExpiredDate: userSub.ExpireTime, + SubscribeURL: subsURL, + }) + l.setSurgeHeaders() default: resp = proxyManager.BuildGeneral(userSub.UUID) } @@ -269,14 +281,19 @@ func (l *SubscribeLogic) setSurfboardHeaders() { l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") } -func (l *SubscribeLogic) getSubscribeURL(token string) string { +func (l *SubscribeLogic) setSurgeHeaders() { + l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", url.QueryEscape(l.svc.Config.Site.SiteName))) + l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") +} + +func (l *SubscribeLogic) getSubscribeURL(token, flag string) string { if l.svc.Config.Subscribe.PanDomain { return fmt.Sprintf("https://%s", l.ctx.Request.Host) } if l.svc.Config.Subscribe.SubscribeDomain != "" { domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n") - return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", domains[0], l.svc.Config.Subscribe.SubscribePath, token) + return fmt.Sprintf("https://%s%s?token=%s&flag=%s", domains[0], l.svc.Config.Subscribe.SubscribePath, token, flag) } return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token) diff --git a/pkg/adapter/adapter.go b/pkg/adapter/adapter.go index ced0b6f..be121b9 100644 --- a/pkg/adapter/adapter.go +++ b/pkg/adapter/adapter.go @@ -12,6 +12,7 @@ import ( "github.com/perfect-panel/server/pkg/adapter/shadowrocket" "github.com/perfect-panel/server/pkg/adapter/singbox" "github.com/perfect-panel/server/pkg/adapter/surfboard" + "github.com/perfect-panel/server/pkg/adapter/surge" "github.com/perfect-panel/server/pkg/adapter/v2rayn" ) @@ -94,3 +95,8 @@ func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byt func (m *Adapter) BuildV2rayN(uuid string) []byte { return v2rayn.NewV2rayN(m.Adapter).Build(uuid) } + +// BuildSurge generates a Surge configuration for the given UUID and site name. +func (m *Adapter) BuildSurge(siteName string, user surge.UserInfo) []byte { + return surge.NewSurge(m.Adapter).Build(siteName, user) +} diff --git a/pkg/adapter/surge/default.tpl b/pkg/adapter/surge/default.tpl index 7375b37..6b70844 100644 --- a/pkg/adapter/surge/default.tpl +++ b/pkg/adapter/surge/default.tpl @@ -1,61 +1,79 @@ -#!MANAGED-CONFIG {{ .SubscribeURL }} interval=43200 strict=true -# Surge 的规则配置手册: https://manual.nssurge.com/ +#!MANAGED-CONFIG {{.SubscribeURL}} interval=43200 strict=true [General] loglevel = notify -# 从 Surge iOS 4 / Surge Mac 3.3.0 起,工具开始支持 DoH -doh-server = https://doh.pub/dns-query -# https://dns.alidns.com/dns-query, https://13800000000.rubyfish.cn/, https://dns.google/dns-query -dns-server = 223.5.5.5, 114.114.114.114 -tun-excluded-routes = 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 255.255.255.255/32 -skip-proxy = localhost, *.local, injections.adguard.org, local.adguard.org, captive.apple.com, guzzoni.apple.com, 0.0.0.0/8, 10.0.0.0/8, 17.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32 - -wifi-assist = true -allow-wifi-access = true -wifi-access-http-port = 6152 -wifi-access-socks5-port = 6153 -http-listen = 0.0.0.0:6152 -socks5-listen = 0.0.0.0:6153 - -external-controller-access = surgepasswd@0.0.0.0:6170 -replica = false - -tls-provider = openssl -network-framework = false +external-controller-access = purinio@0.0.0.0:6170 exclude-simple-hostnames = true +show-error-page-for-reject = true +udp-priority = true +udp-policy-not-supported-behaviour = reject ipv6 = true - -test-timeout = 4 +ipv6-vif = auto proxy-test-url = http://www.gstatic.com/generate_204 -geoip-maxmind-url = https://unpkg.zhimg.com/rulestatic@1.0.1/Country.mmdb +internet-test-url = http://connectivitycheck.platform.hicloud.com/generate_204 +test-timeout = 5 +dns-server = system, 119.29.29.29, 223.5.5.5 +hijack-dns = 8.8.8.8:53, 8.8.4.4:53, 1.1.1.1:53, 1.0.0.1:53 +skip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 127.0.0.0/8, localhost, *.local +always-real-ip = *.lan, lens.l.google.com, *.srv.nintendo.net, *.stun.playstation.net, *.xboxlive.com, xbox.*.*.microsoft.com, *.msftncsi.com, *.msftconnecttest.com -[Replica] -hide-apple-request = true -hide-crashlytics-request = true -use-keyword-filter = false -hide-udp = false +# > Surge Mac Parameters +http-listen = 0.0.0.0:6088 +socks5-listen = 0.0.0.0:6089 + +# > Surge iOS Parameters +allow-wifi-access = true +allow-hotspot-access = true +wifi-access-http-port = 6088 +wifi-access-socks5-port = 6089 [Panel] -SubscribeInfo = {{ .SubscribeInfo }}, style=info - -# ----------------------------- -# Surge 的几种策略配置规范,请参考 https://manual.nssurge.com/policy/proxy.html -# 不同的代理策略有*很多*可选参数,请参考上方连接的 Parameters 一段,根据需求自行添加参数。 -# -# Surge 现已支持 UDP 转发功能,请参考: https://trello.com/c/ugOMxD3u/53-udp-%E8%BD%AC%E5%8F%91 -# Surge 现已支持 TCP-Fast-Open 技术,请参考: https://trello.com/c/ij65BU6Q/48-tcp-fast-open-troubleshooting-guide -# Surge 现已支持 ss-libev 的全部加密方式和混淆,请参考: https://trello.com/c/BTr0vG1O/47-ss-libev-%E7%9A%84%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5 -# ----------------------------- +SubscribeInfo = {{.SubscribeInfo}}, style=info [Proxy] -{{ .Proxies }} +{{.Proxies}} [Proxy Group] -# 代理组列表 -{{ .ProxyGroup }} +🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes +🍎 Apple = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes +🔍 Google = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes +🪟 Microsoft = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes +📺 GlobalMedia = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes +🤖 AI = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes +🪙 Crypto = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes +🎮 Game = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes +📟 Telegram = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes +🇨🇳 China = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes +🐠 Final = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes +🌏 Auto = smart, include-other-group=🇺🇳 Nodes +🎯 Direct = select, DIRECT, hidden=1 +🇺🇳 Nodes = select, {{.ProxyGroup}}, hidden=1 [Rule] -{{ .Rules }} +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Google/Google.list, 🔍 Google +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GitHub/GitHub.list, 🪟 Microsoft +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Microsoft/Microsoft.list, 🪟 Microsoft +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/HBO/HBO.list, 📺 GlobalMedia +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Disney/Disney.list, 📺 GlobalMedia +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/TikTok/TikTok.list, 📺 GlobalMedia +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Netflix/Netflix.list, 📺 GlobalMedia +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GlobalMedia/GlobalMedia_All_No_Resolve.list, 📺 GlobalMedia +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Telegram/Telegram.list, 📟 Telegram +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/OpenAI/OpenAI.list, 🤖 AI +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Gemini/Gemini.list, 🤖 AI +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Copilot/Copilot.list, 🤖 AI +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Claude/Claude.list, 🤖 AI +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Crypto/Crypto.list, 🪙 Crypto +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Cryptocurrency/Cryptocurrency.list, 🪙 Crypto +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Game/Game.list, 🎮 Game +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Global/Global_All_No_Resolve.list, 🚀 Proxy +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/ChinaMax/ChinaMax_All_No_Resolve.list, 🇨🇳 China +RULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Lan/Lan.list, 🎯 Direct + +GEOIP, CN, 🇨🇳 China +FINAL, 🐠 Final, dns-failed [URL Rewrite] -^https?://(www.)?(g|google).cn https://www.google.com 302 \ No newline at end of file +^https?:\/\/(www.)?g\.cn https://www.google.com 302 +^https?:\/\/(www.)?google\.cn https://www.google.com 302 \ No newline at end of file diff --git a/pkg/adapter/surge/surge.go b/pkg/adapter/surge/surge.go index 4c075be..5215389 100644 --- a/pkg/adapter/surge/surge.go +++ b/pkg/adapter/surge/surge.go @@ -4,14 +4,13 @@ import ( "bytes" "embed" "fmt" - "github.com/perfect-panel/server/pkg/tool" - "net/url" "strings" "text/template" "time" "github.com/perfect-panel/server/pkg/adapter/proxy" "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/traffic" ) @@ -39,46 +38,26 @@ func NewSurge(adapter proxy.Adapter) *Surge { } } -func (m *Surge) Build(uuid, siteName string, user UserInfo) []byte { - var proxies, proxyGroup, rules string +func (m *Surge) Build(siteName string, user UserInfo) []byte { + var proxies, proxyGroup string var removed []string + var ps []string for _, p := range m.Adapter.Proxies { switch p.Protocol { case "shadowsocks": - proxies += buildShadowsocks(p, uuid) + proxies += buildShadowsocks(p, user.UUID) case "trojan": - proxies += buildTrojan(p, uuid) + proxies += buildTrojan(p, user.UUID) case "hysteria2": - proxies += buildHysteria2(p, uuid) + proxies += buildHysteria2(p, user.UUID) case "vmess": - proxies += buildVMess(p, uuid) + proxies += buildVMess(p, user.UUID) default: removed = append(removed, p.Name) } + ps = append(ps, p.Name) } - for _, group := range m.Adapter.Group { - if len(removed) > 0 { - group.Proxies = tool.RemoveStringElement(group.Proxies, removed...) - } - if group.Type == proxy.GroupTypeSelect { - proxyGroup += fmt.Sprintf("%s = select, %s", group.Name, strings.Join(group.Proxies, ", ")) + "\r\n" - } else if group.Type == proxy.GroupTypeURLTest { - proxyGroup += fmt.Sprintf("%s = url-test, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" - } else if group.Type == proxy.GroupTypeFallback { - proxyGroup += fmt.Sprintf("%s = fallback, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" - } else { - logger.Errorf("[BuildSurfboard] unknown group type: %s", group.Type) - } - } - for _, rule := range m.Adapter.Rules { - if rule == "" { - continue - } - rules += rule + "\r\n" - } - //final rule - rules += "\r\n" + "FINAL,手动选择,dns-failed" file, err := configFiles.ReadFile("default.tpl") if err != nil { @@ -99,23 +78,21 @@ func (m *Surge) Build(uuid, siteName string, user UserInfo) []byte { } else { expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05") } + + ps = tool.RemoveStringElement(ps, removed...) + proxyGroup = strings.Join(ps, ",") + // convert traffic upload := traffic.AutoConvert(user.Upload, false) download := traffic.AutoConvert(user.Download, false) total := traffic.AutoConvert(user.TotalTraffic, false) unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false) // query Host - urlParse, err := url.Parse(user.SubscribeURL) - if err != nil { - return nil - } if err := tpl.Execute(&buf, map[string]interface{}{ - "Proxies": proxies, - "ProxyGroup": proxyGroup, - "SubscribeURL": user.SubscribeURL, - "SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt), - "SubscribeDomain": urlParse.Host, - "Rules": rules, + "Proxies": proxies, + "ProxyGroup": proxyGroup, + "SubscribeURL": user.SubscribeURL, + "SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt), }); err != nil { logger.Errorf("build Surge config error: %v", err.Error()) return nil