feat(proxy): enhance proxy and group handling with new configuration options

This commit is contained in:
Chang lue Tsen 2025-07-17 10:13:04 -04:00 committed by Leif Draven
parent 82e447c55e
commit 224365ce79
9 changed files with 315 additions and 171 deletions

8
go.mod
View File

@ -56,6 +56,7 @@ require (
) )
require ( require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/goccy/go-json v0.10.4 github.com/goccy/go-json v0.10.4
github.com/golang-migrate/migrate/v4 v4.18.2 github.com/golang-migrate/migrate/v4 v4.18.2
@ -66,7 +67,10 @@ require (
require ( require (
cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect
dario.cat/mergo v1.0.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
@ -99,6 +103,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
@ -107,12 +112,15 @@ require (
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/smartwalle/ncrypto v1.0.4 // indirect github.com/smartwalle/ncrypto v1.0.4 // indirect
github.com/smartwalle/ngx v1.0.9 // indirect github.com/smartwalle/ngx v1.0.9 // indirect
github.com/smartwalle/nsign v1.0.9 // indirect github.com/smartwalle/nsign v1.0.9 // indirect

16
go.sum
View File

@ -1,6 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
@ -8,6 +10,12 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f h1:RDkg3pyE1qGbBpRWmvSN9RNZC5nUrOaEPiEpEb8y2f0= github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f h1:RDkg3pyE1qGbBpRWmvSN9RNZC5nUrOaEPiEpEb8y2f0=
github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f/go.mod h1:zA7AF9RTfpluCfz0omI4t5KCMaWHUMicsZoMccnaT44= github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f/go.mod h1:zA7AF9RTfpluCfz0omI4t5KCMaWHUMicsZoMccnaT44=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
@ -208,6 +216,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw= github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw=
github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts= github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
@ -249,6 +259,10 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
@ -287,6 +301,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/smartwalle/alipay/v3 v3.2.23 h1:i1VwJeu70EmwpsXXz6GZZnMAtRx5MTfn2dPoql/L3zE= github.com/smartwalle/alipay/v3 v3.2.23 h1:i1VwJeu70EmwpsXXz6GZZnMAtRx5MTfn2dPoql/L3zE=
github.com/smartwalle/alipay/v3 v3.2.23/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= github.com/smartwalle/alipay/v3 v3.2.23/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE=
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=

View File

@ -9,11 +9,12 @@ import (
) )
const ( const (
RelayModeNone = "none" RelayModeNone = "none"
RelayModeAll = "all" RelayModeAll = "all"
RelayModeRandom = "random" RelayModeRandom = "random"
RuleGroupTypeBan = "ban" RuleGroupTypeReject = "reject"
RuleGroupTypeAuto = "auto" RuleGroupTypeDefault = "default"
RuleGroupTypeDirect = "direct"
) )
type ServerFilter struct { type ServerFilter struct {

View File

@ -1,6 +1,8 @@
package adapter package adapter
import ( import (
"embed"
"github.com/perfect-panel/server/internal/model/server" "github.com/perfect-panel/server/internal/model/server"
"github.com/perfect-panel/server/pkg/adapter/clash" "github.com/perfect-panel/server/pkg/adapter/clash"
"github.com/perfect-panel/server/pkg/adapter/general" "github.com/perfect-panel/server/pkg/adapter/general"
@ -13,9 +15,11 @@ import (
"github.com/perfect-panel/server/pkg/adapter/v2rayn" "github.com/perfect-panel/server/pkg/adapter/v2rayn"
) )
//go:embed template/*
var TemplateFS embed.FS
var ( var (
AutoSelect = "Auto - UrlTest" AutoSelect = "Auto - UrlTest"
Selection = "Selection"
) )
type Config struct { type Config struct {
@ -30,69 +34,63 @@ type Adapter struct {
func NewAdapter(cfg *Config) *Adapter { func NewAdapter(cfg *Config) *Adapter {
// 转换服务器列表 // 转换服务器列表
proxies := adapterProxies(cfg.Nodes) proxies, nodes, tags := adapterProxies(cfg.Nodes)
defaultGroup := FindDefaultGroup(cfg.Rules)
// 转换规则组 // 转换规则组
g, r := adapterRules(cfg.Rules) g, r, d := adapterRules(cfg.Rules)
if d == "" {
// 生成代理组 d = AutoSelect
proxyGroup, nodes := generateProxyGroup(proxies)
// 加入兜底节点
for i, group := range g {
if group.Direct {
continue
}
if len(group.Proxies) == 0 {
p := append([]string{AutoSelect, Selection}, nodes...)
g[i].Proxies = append(p, "DIRECT")
}
} }
// 生成默认代理组
proxyGroup := append(generateDefaultGroup(), g...)
// 合并代理组 // 合并代理组
proxyGroup = RemoveEmptyGroup(append(proxyGroup, g...)) proxyGroup = SortGroups(proxyGroup, nodes, tags, d)
// 处理标签
proxyGroup = adapterTags(cfg.Tags, proxyGroup)
return &Adapter{ return &Adapter{
Adapter: proxy.Adapter{ Adapter: proxy.Adapter{
Proxies: proxies, Proxies: proxies,
Group: SortGroups(proxyGroup, defaultGroup), Group: proxyGroup,
Rules: r, Rules: r,
Nodes: nodes, Nodes: nodes,
Default: defaultGroup, Default: d,
TemplateFS: &TemplateFS,
}, },
} }
} }
// BuildClash generates a Clash configuration for the given UUID.
func (m *Adapter) BuildClash(uuid string) ([]byte, error) { func (m *Adapter) BuildClash(uuid string) ([]byte, error) {
client := clash.NewClash(m.Adapter) client := clash.NewClash(m.Adapter)
return client.Build(uuid) return client.Build(uuid)
} }
// BuildGeneral generates a general configuration for the given UUID.
func (m *Adapter) BuildGeneral(uuid string) []byte { func (m *Adapter) BuildGeneral(uuid string) []byte {
return general.GenerateBase64General(m.Proxies, uuid) return general.GenerateBase64General(m.Proxies, uuid)
} }
// BuildLoon generates a Loon configuration for the given UUID.
func (m *Adapter) BuildLoon(uuid string) []byte { func (m *Adapter) BuildLoon(uuid string) []byte {
return loon.BuildLoon(m.Proxies, uuid) return loon.BuildLoon(m.Proxies, uuid)
} }
// BuildQuantumultX generates a Quantumult X configuration for the given UUID.
func (m *Adapter) BuildQuantumultX(uuid string) string { func (m *Adapter) BuildQuantumultX(uuid string) string {
return quantumultx.BuildQuantumultX(m.Proxies, uuid) return quantumultx.BuildQuantumultX(m.Proxies, uuid)
} }
// BuildSingbox generates a Singbox configuration for the given UUID.
func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) { func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) {
return singbox.BuildSingbox(m.Adapter, uuid) return singbox.BuildSingbox(m.Adapter, uuid)
} }
func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte { func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte {
return shadowrocket.BuildShadowrocket(m.Proxies, uuid, userInfo) return shadowrocket.BuildShadowrocket(m.Proxies, uuid, userInfo)
} }
// BuildSurfboard generates a Surfboard configuration for the given site name and user info.
func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte { func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte {
return surfboard.BuildSurfboard(m.Adapter, siteName, user) return surfboard.BuildSurfboard(m.Adapter, siteName, user)
} }
// BuildV2rayN generates a V2rayN configuration for the given UUID.
func (m *Adapter) BuildV2rayN(uuid string) []byte { func (m *Adapter) BuildV2rayN(uuid string) []byte {
return v2rayn.NewV2rayN(m.Adapter).Build(uuid) return v2rayn.NewV2rayN(m.Adapter).Build(uuid)
} }

View File

@ -1,8 +1,11 @@
package clash package clash
import ( import (
"bytes"
"fmt" "fmt"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/perfect-panel/server/pkg/adapter/proxy" "github.com/perfect-panel/server/pkg/adapter/proxy"
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -20,22 +23,16 @@ func NewClash(adapter proxy.Adapter) *Clash {
func (c *Clash) Build(uuid string) ([]byte, error) { func (c *Clash) Build(uuid string) ([]byte, error) {
var proxies []Proxy var proxies []Proxy
for _, v := range c.Proxies { for _, proxied := range c.Adapter.Proxies {
p, err := c.parseProxy(v, uuid) p, err := c.parseProxy(proxied, uuid)
if err != nil { if err != nil {
logger.Errorf("Failed to parse proxy for %s: %s", v.Name, err.Error()) logger.Errorw("Failed to parse proxy", logger.Field("error", err), logger.Field("proxy", p.Name))
continue continue
} }
proxies = append(proxies, *p) proxies = append(proxies, *p)
} }
var rawConfig RawConfig
if err := yaml.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil {
return nil, fmt.Errorf("failed to unmarshal template: %w", err)
}
rawConfig.Proxies = proxies
// generate proxy groups
var groups []ProxyGroup var groups []ProxyGroup
for _, group := range c.Group { for _, group := range c.Adapter.Group {
groups = append(groups, ProxyGroup{ groups = append(groups, ProxyGroup{
Name: group.Name, Name: group.Name,
Type: string(group.Type), Type: string(group.Type),
@ -44,9 +41,38 @@ func (c *Clash) Build(uuid string) ([]byte, error) {
Interval: group.Interval, Interval: group.Interval,
}) })
} }
rawConfig.ProxyGroups = groups var rules = append(c.Rules, fmt.Sprintf("MATCH,%s", c.Default))
rawConfig.Rules = append(c.Rules, fmt.Sprintf("MATCH,%s", c.Default))
return yaml.Marshal(&rawConfig) tmplBytes, err := c.TemplateFS.ReadFile("template/clash.tpl")
if err != nil {
logger.Errorw("Failed to read template file", logger.Field("error", err))
return nil, fmt.Errorf("failed to read template file: %w", err)
}
tpl, err := template.New("clash.yaml").Funcs(sprig.FuncMap()).Funcs(template.FuncMap{
"toYaml": func(v interface{}) string {
out, err := yaml.Marshal(v)
if err != nil {
return fmt.Sprintf("# YAML encode error: %v", err.Error())
}
return string(out)
},
}).Parse(string(tmplBytes))
if err != nil {
logger.Errorw("[Clash] Failed to parse template", logger.Field("error", err))
return nil, fmt.Errorf("failed to parse template: %w", err)
}
var buf bytes.Buffer
err = tpl.Execute(&buf, map[string]interface{}{
"Proxies": proxies,
"ProxyGroups": groups,
"Rules": rules,
})
if err != nil {
logger.Errorw("[Clash] Failed to execute template", logger.Field("error", err))
return nil, fmt.Errorf("failed to execute template: %w", err)
}
return buf.Bytes(), nil
} }
func (c *Clash) parseProxy(p proxy.Proxy, uuid string) (*Proxy, error) { func (c *Clash) parseProxy(p proxy.Proxy, uuid string) (*Proxy, error) {

View File

@ -1,31 +1,50 @@
package clash package clash
const DefaultTemplate = ` const DefaultTemplate = `
mixed-port: 7890 mode: rule
ipv6: true
allow-lan: true allow-lan: true
bind-address: "*" bind-address: "*"
mode: rule mixed-port: 7890
log-level: info log-level: error
external-controller: 127.0.0.1:9090
global-client-fingerprint: chrome
unified-delay: true unified-delay: true
geox-url: tcp-concurrent: true
mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb" external-controller: 0.0.0.0:9090
tun:
enable: true
stack: system
auto-route: true
dns: dns:
enable: true enable: true
cache-algorithm: arc
listen: 0.0.0.0:1053
ipv6: true ipv6: true
enhanced-mode: fake-ip enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16 fake-ip-range: 198.18.0.1/16
use-hosts: true fake-ip-filter:
- "*.lan"
- "lens.l.google.com"
- "*.srv.nintendo.net"
- "*.stun.playstation.net"
- "xbox.*.*.microsoft.com"
- "*.xboxlive.com"
- "*.msftncsi.com"
- "*.msftconnecttest.com"
default-nameserver: default-nameserver:
- 120.53.53.53 - 119.29.29.29
- 1.12.12.12 - 223.5.5.5
nameserver: nameserver:
- https://120.53.53.53/dns-query#skip-cert-verify=true - system
- tls://1.12.12.12#skip-cert-verify=true - 119.29.29.29
proxy-server-nameserver: - 223.5.5.5
- https://120.53.53.53/dns-query#skip-cert-verify=true fallback:
- tls://1.12.12.12#skip-cert-verify=true - 8.8.8.8
- 1.1.1.1
fallback-filter:
geoip: true
geoip-code: CN
proxies: proxies:

View File

@ -1,22 +1,26 @@
package proxy package proxy
import "embed"
// Adapter represents a proxy adapter // Adapter represents a proxy adapter
type Adapter struct { type Adapter struct {
Proxies []Proxy Proxies []Proxy
Group []Group Group []Group
Rules []string // rule Rules []string // rule
Nodes []string // all node Nodes []string // all node
Default string // Default Node Default string // Default Node
TemplateFS *embed.FS // Template file system
} }
// Proxy represents a proxy server // Proxy represents a proxy server
type Proxy struct { type Proxy struct {
Name string Name string // Name of the proxy
Server string Server string // Server address of the proxy
Port int Port int // Port of the proxy server
Protocol string Protocol string // Protocol type (e.g., shadowsocks, vless, vmess, trojan, hysteria2, tuic, anytls)
Country string Country string // Country of the proxy
Option any Tags []string // Tags for the proxy
Option any // Additional options for the proxy configuration
} }
// Group represents a group of proxies // Group represents a group of proxies
@ -26,7 +30,10 @@ type Group struct {
Proxies []string Proxies []string
URL string URL string
Interval int Interval int
Direct bool Reject bool // Reject group
Direct bool // Direct group
Tags []string // Tags for the group
Default bool // Default group
} }
type GroupType string type GroupType string

View File

@ -0,0 +1,51 @@
mode: rule
ipv6: true
allow-lan: true
bind-address: "*"
mixed-port: 7890
log-level: error
unified-delay: true
tcp-concurrent: true
external-controller: 0.0.0.0:9090
tun:
enable: true
stack: system
auto-route: true
dns:
enable: true
cache-algorithm: arc
listen: 0.0.0.0:1053
ipv6: true
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter:
- "*.lan"
- "lens.l.google.com"
- "*.srv.nintendo.net"
- "*.stun.playstation.net"
- "xbox.*.*.microsoft.com"
- "*.xboxlive.com"
- "*.msftncsi.com"
- "*.msftconnecttest.com"
default-nameserver:
- 119.29.29.29
- 223.5.5.5
nameserver:
- system
- 119.29.29.29
- 223.5.5.5
fallback:
- 8.8.8.8
- 1.1.1.1
fallback-filter:
geoip: true
geoip-code: CN
proxies:
{{.Proxies | toYaml | indent 2}}
proxy-groups:
{{.ProxyGroups | toYaml | indent 2}}
rules:
{{.Rules | toYaml | indent 2}}

View File

@ -2,6 +2,7 @@ package adapter
import ( import (
"encoding/json" "encoding/json"
"log"
"strings" "strings"
"github.com/perfect-panel/server/internal/model/server" "github.com/perfect-panel/server/internal/model/server"
@ -11,14 +12,21 @@ import (
"github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/tool"
) )
// addNode creates a new proxy node based on the provided server data and host/port.
func addNode(data *server.Server, host string, port int) *proxy.Proxy { func addNode(data *server.Server, host string, port int) *proxy.Proxy {
var option any var option any
tags := strings.Split(data.Tags, ",")
if len(tags) > 0 {
tags = tool.RemoveDuplicateElements(tags...)
}
node := proxy.Proxy{ node := proxy.Proxy{
Name: data.Name, Name: data.Name,
Server: host, Server: host,
Port: port, Port: port,
Country: data.Country, Country: data.Country,
Protocol: data.Protocol, Protocol: data.Protocol,
Tags: tags,
} }
switch data.Protocol { switch data.Protocol {
case "shadowsocks": case "shadowsocks":
@ -82,64 +90,45 @@ func addNode(data *server.Server, host string, port int) *proxy.Proxy {
return &node return &node
} }
func addProxyToGroup(proxyName, groupName string, groups []proxy.Group) []proxy.Group { func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string, defaultGroup string) {
for i, group := range groups {
if group.Name == groupName {
groups[i].Proxies = tool.RemoveDuplicateElements(append(group.Proxies, proxyName)...)
return groups
}
}
groups = append(groups, proxy.Group{
Name: groupName,
Type: proxy.GroupTypeSelect,
Proxies: []string{proxyName},
})
return groups
}
func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string) {
for _, group := range groups { for _, group := range groups {
if group.Default {
log.Printf("[Debug] 规则组 %s 是默认组", group.Name)
defaultGroup = group.Name
}
switch group.Type { switch group.Type {
case server.RuleGroupTypeBan: case server.RuleGroupTypeReject:
proxyGroup = append(proxyGroup, proxy.Group{ proxyGroup = append(proxyGroup, proxy.Group{
Name: group.Name, Name: group.Name,
Type: proxy.GroupTypeSelect, Type: proxy.GroupTypeSelect,
Proxies: []string{"REJECT", "DIRECT"}, Proxies: []string{"REJECT", "DIRECT", AutoSelect},
Direct: true, Reject: true,
}) })
case server.RuleGroupTypeAuto: case server.RuleGroupTypeDirect:
proxyGroup = append(proxyGroup, proxy.Group{ proxyGroup = append(proxyGroup, proxy.Group{
Name: group.Name, Name: group.Name,
Type: proxy.GroupTypeURLTest, Type: proxy.GroupTypeSelect,
URL: "https://www.gstatic.com/generate_204", Proxies: []string{"DIRECT", AutoSelect},
Proxies: RemoveEmptyString(strings.Split(group.Tags, ",")), Direct: true,
}) })
default: default:
proxyGroup = append(proxyGroup, proxy.Group{ proxyGroup = append(proxyGroup, proxy.Group{
Name: group.Name, Name: group.Name,
Type: proxy.GroupTypeSelect, Type: proxy.GroupTypeSelect,
Proxies: RemoveEmptyString(strings.Split(group.Tags, ",")), Proxies: []string{},
Tags: RemoveEmptyString(strings.Split(group.Tags, ",")),
Default: group.Default,
}) })
} }
rules = append(rules, strings.Split(group.Rules, "\n")...) rules = append(rules, strings.Split(group.Rules, "\n")...)
} }
return log.Printf("[Dapter] 生成规则组: %d", len(proxyGroup))
return proxyGroup, tool.RemoveDuplicateElements(rules...), defaultGroup
} }
func adapterTags(tags map[string][]*server.Server, group []proxy.Group) (proxyGroup []proxy.Group) { // generateDefaultGroup generates a default proxy group with auto-selection and manual selection options.
for tag, servers := range tags { func generateDefaultGroup() (proxyGroup []proxy.Group) {
proxies := adapterProxies(servers)
if len(proxies) != 0 {
for _, p := range proxies {
group = addProxyToGroup(p.Name, tag, group)
}
}
}
return group
}
func generateProxyGroup(servers []proxy.Proxy) (proxyGroup []proxy.Group, nodes []string) {
proxyGroup = append(proxyGroup, proxy.Group{ proxyGroup = append(proxyGroup, proxy.Group{
Name: AutoSelect, Name: AutoSelect,
Type: proxy.GroupTypeURLTest, Type: proxy.GroupTypeURLTest,
@ -148,23 +137,12 @@ func generateProxyGroup(servers []proxy.Proxy) (proxyGroup []proxy.Group, nodes
Interval: 300, Interval: 300,
}) })
// 设置手动选择分组 return proxyGroup
proxyGroup = append(proxyGroup, proxy.Group{
Name: Selection,
Type: proxy.GroupTypeSelect,
Proxies: []string{AutoSelect},
})
for _, node := range servers {
proxyGroup = addProxyToGroup(node.Name, AutoSelect, proxyGroup)
proxyGroup = addProxyToGroup(node.Name, Selection, proxyGroup)
nodes = append(nodes, node.Name)
}
return proxyGroup, tool.RemoveDuplicateElements(nodes...)
} }
func adapterProxies(servers []*server.Server) []proxy.Proxy { func adapterProxies(servers []*server.Server) ([]proxy.Proxy, []string, map[string][]string) {
var proxies []proxy.Proxy var proxies []proxy.Proxy
var tags = make(map[string][]string)
for _, node := range servers { for _, node := range servers {
switch node.RelayMode { switch node.RelayMode {
case server.RelayModeAll: case server.RelayModeAll:
@ -179,8 +157,20 @@ func adapterProxies(servers []*server.Server) []proxy.Proxy {
continue continue
} }
if relay.Prefix != "" { if relay.Prefix != "" {
n.Name = relay.Prefix + "-" + n.Name n.Name = relay.Prefix + n.Name
} }
if node.Tags != "" {
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
for _, tag := range t {
if tag != "" {
if _, ok := tags[tag]; !ok {
tags[tag] = []string{}
}
tags[tag] = append(tags[tag], n.Name)
}
}
}
proxies = append(proxies, *n) proxies = append(proxies, *n)
} }
case server.RelayModeRandom: case server.RelayModeRandom:
@ -196,18 +186,46 @@ func adapterProxies(servers []*server.Server) []proxy.Proxy {
continue continue
} }
if relay.Prefix != "" { if relay.Prefix != "" {
n.Name = relay.Prefix + " - " + node.Name n.Name = relay.Prefix + node.Name
}
if node.Tags != "" {
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
for _, tag := range t {
if tag != "" {
if _, ok := tags[tag]; !ok {
tags[tag] = []string{}
}
tags[tag] = append(tags[tag], n.Name)
}
}
} }
proxies = append(proxies, *n) proxies = append(proxies, *n)
default: default:
logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode)) logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode))
n := addNode(node, node.ServerAddr, 0) n := addNode(node, node.ServerAddr, 0)
if n != nil { if n != nil {
if node.Tags != "" {
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
for _, tag := range t {
if tag != "" {
if _, ok := tags[tag]; !ok {
tags[tag] = []string{}
}
tags[tag] = append(tags[tag], n.Name)
}
}
}
proxies = append(proxies, *n) proxies = append(proxies, *n)
} }
} }
} }
return proxies
var nodes []string
for _, p := range proxies {
nodes = append(nodes, p.Name)
}
return proxies, tool.RemoveDuplicateElements(nodes...), tags
} }
// RemoveEmptyString 切片去除空值 // RemoveEmptyString 切片去除空值
@ -221,61 +239,61 @@ func RemoveEmptyString(arr []string) []string {
return result return result
} }
// RemoveEmptyGroup removes empty groups from the provided slice of proxy groups.
func RemoveEmptyGroup(arr []proxy.Group) []proxy.Group {
var result []proxy.Group
var removeNames []string
for _, group := range arr {
if group.Name == "手动选择" {
group.Proxies = tool.RemoveStringElement(group.Proxies, removeNames...)
}
if len(group.Proxies) > 0 {
result = append(result, group)
} else {
removeNames = append(removeNames, group.Name)
}
}
return result
}
// FindDefaultGroup finds the default rule group from a list of rule groups.
func FindDefaultGroup(groups []*server.RuleGroup) string {
for _, group := range groups {
if group.Default {
return group.Name
}
}
return AutoSelect
}
// SortGroups sorts the provided slice of proxy groups by their names. // SortGroups sorts the provided slice of proxy groups by their names.
func SortGroups(groups []proxy.Group, defaultName string) []proxy.Group { func SortGroups(groups []proxy.Group, nodes []string, tags map[string][]string, defaultName string) []proxy.Group {
var sortedGroups []proxy.Group var sortedGroups []proxy.Group
var selectedGroup proxy.Group var defaultGroup, autoSelectGroup proxy.Group
// 在所有分组找到默认分组并将他放到第一个 // 在所有分组找到默认分组并将他放到第一个
for _, group := range groups { for _, group := range groups {
if group.Name == "" || group.Name == "DIRECT" || group.Name == "REJECT" { if group.Name == "" || group.Name == "DIRECT" || group.Name == "REJECT" {
continue continue
} }
if group.Name == defaultName { // 如果是默认分组
group.Proxies = tool.RemoveStringElement(group.Proxies, defaultName, "REJECT") if group.Default {
sortedGroups = append([]proxy.Group{group}, sortedGroups...) group.Proxies = append([]string{AutoSelect}, nodes...)
continue group.Proxies = append(group.Proxies, "DIRECT")
} else if group.Name == Selection { defaultGroup = group
group.Proxies = tool.RemoveStringElement(group.Proxies, defaultName)
selectedGroup = group
continue
} else if group.Name == AutoSelect {
group.Proxies = tool.RemoveStringElement(group.Proxies, defaultName, group.Name)
sortedGroups = append([]proxy.Group{group}, sortedGroups...)
continue continue
} }
if group.Reject || group.Direct {
if defaultName != AutoSelect {
group.Proxies = append(group.Proxies, defaultName)
}
sortedGroups = append(sortedGroups, group)
continue
}
if group.Name == AutoSelect {
group.Proxies = nodes
autoSelectGroup = group
continue
}
// Tags 分组
if len(group.Tags) > 0 {
var proxies []string
for _, tag := range group.Tags {
if node, ok := tags[tag]; ok {
proxies = append(proxies, node...)
}
}
group.Proxies = append(tool.RemoveDuplicateElements(proxies...), AutoSelect, "DIRECT")
sortedGroups = append(sortedGroups, group)
continue
}
group.Proxies = append([]string{AutoSelect}, nodes...)
group.Proxies = append(group.Proxies, "DIRECT")
group.Proxies = tool.RemoveElementBySlice(group.Proxies, group.Name)
sortedGroups = append(sortedGroups, group) sortedGroups = append(sortedGroups, group)
} }
// 将手动选择分组放到最后
if selectedGroup.Name != "" { if defaultGroup.Name != "" {
sortedGroups = append(sortedGroups, selectedGroup) sortedGroups = append([]proxy.Group{defaultGroup}, sortedGroups...)
} }
if autoSelectGroup.Name != "" && autoSelectGroup.Name != defaultGroup.Name {
sortedGroups = append(sortedGroups, autoSelectGroup)
}
return sortedGroups return sortedGroups
} }