init: 1.0.0
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
#!MANAGED-CONFIG {{ .SubscribeURL }} interval=43200 strict=true
|
||||
# Surge 的规则配置手册: https://manual.nssurge.com/
|
||||
|
||||
[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
|
||||
exclude-simple-hostnames = true
|
||||
ipv6 = true
|
||||
|
||||
test-timeout = 4
|
||||
proxy-test-url = http://www.gstatic.com/generate_204
|
||||
geoip-maxmind-url = https://unpkg.zhimg.com/rulestatic@1.0.1/Country.mmdb
|
||||
|
||||
[Replica]
|
||||
hide-apple-request = true
|
||||
hide-crashlytics-request = true
|
||||
use-keyword-filter = false
|
||||
hide-udp = false
|
||||
|
||||
[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
|
||||
# -----------------------------
|
||||
|
||||
[Proxy]
|
||||
{{ .Proxies }}
|
||||
|
||||
[Proxy Group]
|
||||
# 代理组列表
|
||||
{{ .ProxyGroup }}
|
||||
|
||||
[Rule]
|
||||
{{ .Rules }}
|
||||
|
||||
[URL Rewrite]
|
||||
^https?://(www.)?(g|google).cn https://www.google.com 302
|
||||
@@ -0,0 +1,43 @@
|
||||
package surge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
func buildHysteria2(data proxy.Proxy, uuid string) string {
|
||||
hysteria2, ok := data.Option.(proxy.Hysteria2)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
var port int
|
||||
if hysteria2.HopPorts != "" {
|
||||
ports := strings.Split(hysteria2.HopPorts, ",")
|
||||
p := ports[0]
|
||||
if len(strings.Split(p, "-")) > 1 {
|
||||
p = strings.Split(p, "-")[0]
|
||||
}
|
||||
port, _ = strconv.Atoi(p)
|
||||
} else {
|
||||
port = data.Port
|
||||
}
|
||||
|
||||
config := []string{
|
||||
fmt.Sprintf("%s=hysteria2,%s,%d", data.Name, data.Server, port),
|
||||
"password=" + uuid,
|
||||
"udp-relay=true",
|
||||
}
|
||||
if hysteria2.SecurityConfig.SNI != "" {
|
||||
config = append(config, "sni="+hysteria2.SecurityConfig.SNI)
|
||||
}
|
||||
if hysteria2.SecurityConfig.AllowInsecure {
|
||||
config = append(config, "skip-cert-verify=true")
|
||||
} else {
|
||||
config = append(config, "skip-cert-verify=false")
|
||||
}
|
||||
return strings.Join(config, ",") + "\r\n"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package surge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
func TestBuildHysteria2(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data proxy.Proxy
|
||||
uuid string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Valid Hysteria2 with HopPorts",
|
||||
data: proxy.Proxy{
|
||||
Name: "test",
|
||||
Server: "server.com",
|
||||
Port: 443,
|
||||
Option: proxy.Hysteria2{
|
||||
HopPorts: "1000-2000",
|
||||
SecurityConfig: proxy.SecurityConfig{
|
||||
SNI: "example.com",
|
||||
AllowInsecure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
uuid: "test-uuid",
|
||||
expected: "test=hysteria2,server.com,1000,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=true\r\n",
|
||||
},
|
||||
{
|
||||
name: "Valid Hysteria2 without HopPorts",
|
||||
data: proxy.Proxy{
|
||||
Name: "test",
|
||||
Server: "server.com",
|
||||
Port: 443,
|
||||
Option: proxy.Hysteria2{
|
||||
SecurityConfig: proxy.SecurityConfig{
|
||||
SNI: "example.com",
|
||||
AllowInsecure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
uuid: "test-uuid",
|
||||
expected: "test=hysteria2,server.com,443,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=false\r\n",
|
||||
},
|
||||
{
|
||||
name: "Invalid Hysteria2 Option",
|
||||
data: proxy.Proxy{
|
||||
Name: "test",
|
||||
Server: "server.com",
|
||||
Port: 443,
|
||||
Option: nil,
|
||||
},
|
||||
uuid: "test-uuid",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := buildHysteria2(tt.data, tt.uuid)
|
||||
if result != tt.expected {
|
||||
t.Errorf("expected %s, got %s", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package surge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
func buildShadowsocks(data proxy.Proxy, uuid string) string {
|
||||
ss, ok := data.Option.(proxy.Shadowsocks)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port)
|
||||
config := []string{
|
||||
addr,
|
||||
fmt.Sprintf("encrypt-method=%s", ss.Method),
|
||||
fmt.Sprintf("password=%s", uuid),
|
||||
"tfo=true",
|
||||
"udp-relay=true",
|
||||
}
|
||||
return strings.Join(config, ",") + "\r\n"
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package surge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/logger"
|
||||
"github.com/perfect-panel/ppanel-server/pkg/traffic"
|
||||
)
|
||||
|
||||
//go:embed *.tpl
|
||||
var configFiles embed.FS
|
||||
|
||||
type UserInfo struct {
|
||||
UUID string
|
||||
Upload int64
|
||||
Download int64
|
||||
TotalTraffic int64
|
||||
ExpiredDate time.Time
|
||||
SubscribeURL string
|
||||
}
|
||||
|
||||
type Surge struct {
|
||||
Adapter proxy.Adapter
|
||||
UUID string
|
||||
User UserInfo
|
||||
}
|
||||
|
||||
func NewSurge(adapter proxy.Adapter) *Surge {
|
||||
return &Surge{
|
||||
Adapter: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Surge) Build(uuid, siteName string, user UserInfo) []byte {
|
||||
var proxies, proxyGroup, rules string
|
||||
|
||||
for _, p := range m.Adapter.Proxies {
|
||||
switch p.Protocol {
|
||||
case "shadowsocks":
|
||||
proxies += buildShadowsocks(p, uuid)
|
||||
case "trojan":
|
||||
proxies += buildTrojan(p, uuid)
|
||||
case "hysteria2":
|
||||
proxies += buildHysteria2(p, uuid)
|
||||
case "vmess":
|
||||
proxies += buildVMess(p, uuid)
|
||||
}
|
||||
}
|
||||
for _, group := range m.Adapter.Group {
|
||||
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 {
|
||||
logger.Errorf("read default surfboard config error: %v", err.Error())
|
||||
return nil
|
||||
}
|
||||
// replace template
|
||||
tpl, err := template.New("default").Parse(string(file))
|
||||
if err != nil {
|
||||
logger.Errorf("read default surfboard config error: %v", err.Error())
|
||||
return nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
|
||||
var expiredAt string
|
||||
if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||
expiredAt = "长期有效"
|
||||
} else {
|
||||
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
// 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,
|
||||
}); err != nil {
|
||||
logger.Errorf("build Surge config error: %v", err.Error())
|
||||
return nil
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package surge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
func TestSurgeBuild(t *testing.T) {
|
||||
adapter := proxy.Adapter{
|
||||
Proxies: []proxy.Proxy{
|
||||
{
|
||||
Name: "test-shadowsocks",
|
||||
Protocol: "shadowsocks",
|
||||
Server: "1.2.3.4",
|
||||
Port: 8388,
|
||||
Option: proxy.Shadowsocks{
|
||||
Method: "aes-256-gcm",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "test-trojan",
|
||||
Protocol: "trojan",
|
||||
Server: "5.6.7.8",
|
||||
Port: 443,
|
||||
Option: proxy.Trojan{
|
||||
SecurityConfig: proxy.SecurityConfig{
|
||||
SNI: "example.com",
|
||||
AllowInsecure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "test-hysteria",
|
||||
Protocol: "hysteria2",
|
||||
Server: "1.1.1.1",
|
||||
Port: 443,
|
||||
Option: proxy.Hysteria2{
|
||||
HopPorts: "8080-8090",
|
||||
HopInterval: 320,
|
||||
SecurityConfig: proxy.SecurityConfig{
|
||||
SNI: "example.com",
|
||||
AllowInsecure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Group: []proxy.Group{
|
||||
{
|
||||
Name: "test-group",
|
||||
Type: proxy.GroupTypeSelect,
|
||||
Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"},
|
||||
},
|
||||
{
|
||||
Name: "手动选择",
|
||||
Type: proxy.GroupTypeSelect,
|
||||
Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"},
|
||||
},
|
||||
},
|
||||
Rules: []string{
|
||||
"DOMAIN-SUFFIX,example.com,DIRECT",
|
||||
},
|
||||
}
|
||||
|
||||
user := UserInfo{
|
||||
UUID: "test-uuid",
|
||||
Upload: 1024,
|
||||
Download: 2048,
|
||||
TotalTraffic: 4096,
|
||||
ExpiredDate: time.Now().Add(24 * time.Hour),
|
||||
SubscribeURL: "http://example.com/subscribe",
|
||||
}
|
||||
|
||||
surge := NewSurge(adapter)
|
||||
config := surge.Build("test-uuid", "TestSite", user)
|
||||
|
||||
if config == nil {
|
||||
t.Fatal("Expected non-nil config")
|
||||
}
|
||||
|
||||
configStr := string(config)
|
||||
t.Logf("configStr: %v", configStr)
|
||||
if !strings.Contains(configStr, "test-shadowsocks=ss") {
|
||||
t.Errorf("Expected config to contain test-shadowsocks proxy")
|
||||
}
|
||||
if !strings.Contains(configStr, "test-trojan=trojan") {
|
||||
t.Errorf("Expected config to contain test-trojan proxy")
|
||||
}
|
||||
if !strings.Contains(configStr, "test-group = select") {
|
||||
t.Errorf("Expected config to contain test-group proxy group")
|
||||
}
|
||||
if !strings.Contains(configStr, "DOMAIN-SUFFIX,example.com,DIRECT") {
|
||||
t.Errorf("Expected config to contain rule for example.com")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package surge
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
func buildTrojan(data proxy.Proxy, uuid string) string {
|
||||
trojan, ok := data.Option.(proxy.Trojan)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
config := []string{
|
||||
data.Name + "=trojan",
|
||||
data.Server,
|
||||
strconv.Itoa(data.Port),
|
||||
"password=" + uuid,
|
||||
"tfo=true",
|
||||
"udp-relay=true",
|
||||
}
|
||||
if trojan.SecurityConfig.SNI != "" {
|
||||
config = append(config, "sni="+trojan.SecurityConfig.SNI)
|
||||
}
|
||||
if trojan.SecurityConfig.AllowInsecure {
|
||||
config = append(config, "skip-cert-verify=true")
|
||||
} else {
|
||||
config = append(config, "skip-cert-verify=false")
|
||||
}
|
||||
return strings.Join(config, ",") + "\r\n"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package surge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
|
||||
)
|
||||
|
||||
func buildVMess(data proxy.Proxy, uuid string) string {
|
||||
vmess, ok := data.Option.(proxy.Vmess)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port)
|
||||
uriConfig := []string{
|
||||
addr,
|
||||
fmt.Sprintf("username=%s", uuid),
|
||||
"vmess-aead=true",
|
||||
"tfo=true",
|
||||
"udp-relay=true",
|
||||
}
|
||||
if vmess.Security == "tls" {
|
||||
uriConfig = append(uriConfig, "tls=true")
|
||||
if vmess.SecurityConfig.AllowInsecure {
|
||||
uriConfig = append(uriConfig, "skip-cert-verify=true")
|
||||
} else {
|
||||
uriConfig = append(uriConfig, "skip-cert-verify=false")
|
||||
}
|
||||
if vmess.SecurityConfig.SNI != "" {
|
||||
uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI))
|
||||
}
|
||||
}
|
||||
if vmess.Transport == "websocket" {
|
||||
uriConfig = append(uriConfig, "ws=true")
|
||||
if vmess.TransportConfig.Path != "" {
|
||||
uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path))
|
||||
}
|
||||
if vmess.TransportConfig.Host != "" {
|
||||
uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host))
|
||||
}
|
||||
}
|
||||
return strings.Join(uriConfig, ",") + "\r\n"
|
||||
}
|
||||
Reference in New Issue
Block a user