From 224365ce79024be0e793aa89f273b25c534be848 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Thu, 17 Jul 2025 10:13:04 -0400 Subject: [PATCH] feat(proxy): enhance proxy and group handling with new configuration options --- go.mod | 8 ++ go.sum | 16 +++ internal/model/server/server.go | 11 +- pkg/adapter/adapter.go | 54 ++++---- pkg/adapter/clash/clash.go | 52 ++++++-- pkg/adapter/clash/default.go | 49 +++++--- pkg/adapter/proxy/proxy.go | 31 +++-- pkg/adapter/template/clash.tpl | 51 ++++++++ pkg/adapter/uilts.go | 214 +++++++++++++++++--------------- 9 files changed, 315 insertions(+), 171 deletions(-) create mode 100644 pkg/adapter/template/clash.tpl diff --git a/go.mod b/go.mod index 6891fb7..c0145a7 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( ) require ( + github.com/Masterminds/sprig/v3 v3.3.0 github.com/fatih/color v1.18.0 github.com/goccy/go-json v0.10.4 github.com/golang-migrate/migrate/v4 v4.18.2 @@ -66,7 +67,10 @@ require ( require ( cloud.google.com/go/compute/metadata v0.6.0 // indirect + dario.cat/mergo v1.0.1 // 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/debug v1.0.1 // 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/hashicorp/errwrap v1.1.0 // 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/jinzhu/inflection v1.0.0 // 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/mattn/go-colorable v0.1.13 // 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/reflect2 v1.0.2 // indirect github.com/openzipkin/zipkin-go v0.4.2 // 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/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/ngx v1.0.9 // indirect github.com/smartwalle/nsign v1.0.9 // indirect diff --git a/go.sum b/go.sum index a0a75b1..cc90adf 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ 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/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/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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/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/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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw= 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/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 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/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 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/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 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/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= diff --git a/internal/model/server/server.go b/internal/model/server/server.go index dff001d..c185503 100644 --- a/internal/model/server/server.go +++ b/internal/model/server/server.go @@ -9,11 +9,12 @@ import ( ) const ( - RelayModeNone = "none" - RelayModeAll = "all" - RelayModeRandom = "random" - RuleGroupTypeBan = "ban" - RuleGroupTypeAuto = "auto" + RelayModeNone = "none" + RelayModeAll = "all" + RelayModeRandom = "random" + RuleGroupTypeReject = "reject" + RuleGroupTypeDefault = "default" + RuleGroupTypeDirect = "direct" ) type ServerFilter struct { diff --git a/pkg/adapter/adapter.go b/pkg/adapter/adapter.go index cd4f1b8..ced0b6f 100644 --- a/pkg/adapter/adapter.go +++ b/pkg/adapter/adapter.go @@ -1,6 +1,8 @@ package adapter import ( + "embed" + "github.com/perfect-panel/server/internal/model/server" "github.com/perfect-panel/server/pkg/adapter/clash" "github.com/perfect-panel/server/pkg/adapter/general" @@ -13,9 +15,11 @@ import ( "github.com/perfect-panel/server/pkg/adapter/v2rayn" ) +//go:embed template/* +var TemplateFS embed.FS + var ( AutoSelect = "Auto - UrlTest" - Selection = "Selection" ) type Config struct { @@ -30,69 +34,63 @@ type Adapter struct { func NewAdapter(cfg *Config) *Adapter { // 转换服务器列表 - proxies := adapterProxies(cfg.Nodes) - defaultGroup := FindDefaultGroup(cfg.Rules) + proxies, nodes, tags := adapterProxies(cfg.Nodes) // 转换规则组 - g, r := adapterRules(cfg.Rules) - - // 生成代理组 - 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") - } + g, r, d := adapterRules(cfg.Rules) + if d == "" { + d = AutoSelect } - + // 生成默认代理组 + proxyGroup := append(generateDefaultGroup(), g...) // 合并代理组 - proxyGroup = RemoveEmptyGroup(append(proxyGroup, g...)) - // 处理标签 - proxyGroup = adapterTags(cfg.Tags, proxyGroup) - + proxyGroup = SortGroups(proxyGroup, nodes, tags, d) return &Adapter{ Adapter: proxy.Adapter{ - Proxies: proxies, - Group: SortGroups(proxyGroup, defaultGroup), - Rules: r, - Nodes: nodes, - Default: defaultGroup, + Proxies: proxies, + Group: proxyGroup, + Rules: r, + Nodes: nodes, + Default: d, + TemplateFS: &TemplateFS, }, } } +// BuildClash generates a Clash configuration for the given UUID. func (m *Adapter) BuildClash(uuid string) ([]byte, error) { client := clash.NewClash(m.Adapter) return client.Build(uuid) } +// BuildGeneral generates a general configuration for the given UUID. func (m *Adapter) BuildGeneral(uuid string) []byte { return general.GenerateBase64General(m.Proxies, uuid) } +// BuildLoon generates a Loon configuration for the given UUID. func (m *Adapter) BuildLoon(uuid string) []byte { return loon.BuildLoon(m.Proxies, uuid) } +// BuildQuantumultX generates a Quantumult X configuration for the given UUID. func (m *Adapter) BuildQuantumultX(uuid string) string { return quantumultx.BuildQuantumultX(m.Proxies, uuid) } +// BuildSingbox generates a Singbox configuration for the given UUID. func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) { return singbox.BuildSingbox(m.Adapter, uuid) } - func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte { 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 { return surfboard.BuildSurfboard(m.Adapter, siteName, user) } + +// BuildV2rayN generates a V2rayN configuration for the given UUID. func (m *Adapter) BuildV2rayN(uuid string) []byte { return v2rayn.NewV2rayN(m.Adapter).Build(uuid) } diff --git a/pkg/adapter/clash/clash.go b/pkg/adapter/clash/clash.go index e4c59dd..0f9f35c 100644 --- a/pkg/adapter/clash/clash.go +++ b/pkg/adapter/clash/clash.go @@ -1,8 +1,11 @@ package clash import ( + "bytes" "fmt" + "text/template" + "github.com/Masterminds/sprig/v3" "github.com/perfect-panel/server/pkg/adapter/proxy" "github.com/perfect-panel/server/pkg/logger" "gopkg.in/yaml.v3" @@ -20,22 +23,16 @@ func NewClash(adapter proxy.Adapter) *Clash { func (c *Clash) Build(uuid string) ([]byte, error) { var proxies []Proxy - for _, v := range c.Proxies { - p, err := c.parseProxy(v, uuid) + for _, proxied := range c.Adapter.Proxies { + p, err := c.parseProxy(proxied, uuid) 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 } 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 - for _, group := range c.Group { + for _, group := range c.Adapter.Group { groups = append(groups, ProxyGroup{ Name: group.Name, Type: string(group.Type), @@ -44,9 +41,38 @@ func (c *Clash) Build(uuid string) ([]byte, error) { Interval: group.Interval, }) } - rawConfig.ProxyGroups = groups - rawConfig.Rules = append(c.Rules, fmt.Sprintf("MATCH,%s", c.Default)) - return yaml.Marshal(&rawConfig) + var rules = append(c.Rules, fmt.Sprintf("MATCH,%s", c.Default)) + + 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) { diff --git a/pkg/adapter/clash/default.go b/pkg/adapter/clash/default.go index 4766053..2c2ceb0 100644 --- a/pkg/adapter/clash/default.go +++ b/pkg/adapter/clash/default.go @@ -1,31 +1,50 @@ package clash const DefaultTemplate = ` -mixed-port: 7890 +mode: rule +ipv6: true allow-lan: true bind-address: "*" -mode: rule -log-level: info -external-controller: 127.0.0.1:9090 -global-client-fingerprint: chrome +mixed-port: 7890 +log-level: error unified-delay: true -geox-url: - mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb" +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 - 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: - - 120.53.53.53 - - 1.12.12.12 + - 119.29.29.29 + - 223.5.5.5 nameserver: - - https://120.53.53.53/dns-query#skip-cert-verify=true - - tls://1.12.12.12#skip-cert-verify=true - proxy-server-nameserver: - - https://120.53.53.53/dns-query#skip-cert-verify=true - - tls://1.12.12.12#skip-cert-verify=true + - 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: diff --git a/pkg/adapter/proxy/proxy.go b/pkg/adapter/proxy/proxy.go index 22f1303..75760ff 100644 --- a/pkg/adapter/proxy/proxy.go +++ b/pkg/adapter/proxy/proxy.go @@ -1,22 +1,26 @@ package proxy +import "embed" + // Adapter represents a proxy adapter type Adapter struct { - Proxies []Proxy - Group []Group - Rules []string // rule - Nodes []string // all node - Default string // Default Node + Proxies []Proxy + Group []Group + Rules []string // rule + Nodes []string // all node + Default string // Default Node + TemplateFS *embed.FS // Template file system } // Proxy represents a proxy server type Proxy struct { - Name string - Server string - Port int - Protocol string - Country string - Option any + Name string // Name of the proxy + Server string // Server address of the proxy + Port int // Port of the proxy server + Protocol string // Protocol type (e.g., shadowsocks, vless, vmess, trojan, hysteria2, tuic, anytls) + Country string // Country of the proxy + Tags []string // Tags for the proxy + Option any // Additional options for the proxy configuration } // Group represents a group of proxies @@ -26,7 +30,10 @@ type Group struct { Proxies []string URL string 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 diff --git a/pkg/adapter/template/clash.tpl b/pkg/adapter/template/clash.tpl new file mode 100644 index 0000000..6d8dd36 --- /dev/null +++ b/pkg/adapter/template/clash.tpl @@ -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}} \ No newline at end of file diff --git a/pkg/adapter/uilts.go b/pkg/adapter/uilts.go index ae89df6..04d9cde 100644 --- a/pkg/adapter/uilts.go +++ b/pkg/adapter/uilts.go @@ -2,6 +2,7 @@ package adapter import ( "encoding/json" + "log" "strings" "github.com/perfect-panel/server/internal/model/server" @@ -11,14 +12,21 @@ import ( "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 { var option any + tags := strings.Split(data.Tags, ",") + if len(tags) > 0 { + tags = tool.RemoveDuplicateElements(tags...) + } + node := proxy.Proxy{ Name: data.Name, Server: host, Port: port, Country: data.Country, Protocol: data.Protocol, + Tags: tags, } switch data.Protocol { case "shadowsocks": @@ -82,64 +90,45 @@ func addNode(data *server.Server, host string, port int) *proxy.Proxy { return &node } -func addProxyToGroup(proxyName, groupName string, groups []proxy.Group) []proxy.Group { - 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) { +func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string, defaultGroup string) { for _, group := range groups { + if group.Default { + log.Printf("[Debug] 规则组 %s 是默认组", group.Name) + defaultGroup = group.Name + } switch group.Type { - case server.RuleGroupTypeBan: + case server.RuleGroupTypeReject: proxyGroup = append(proxyGroup, proxy.Group{ Name: group.Name, Type: proxy.GroupTypeSelect, - Proxies: []string{"REJECT", "DIRECT"}, - Direct: true, + Proxies: []string{"REJECT", "DIRECT", AutoSelect}, + Reject: true, }) - case server.RuleGroupTypeAuto: + case server.RuleGroupTypeDirect: proxyGroup = append(proxyGroup, proxy.Group{ Name: group.Name, - Type: proxy.GroupTypeURLTest, - URL: "https://www.gstatic.com/generate_204", - Proxies: RemoveEmptyString(strings.Split(group.Tags, ",")), + Type: proxy.GroupTypeSelect, + Proxies: []string{"DIRECT", AutoSelect}, + Direct: true, }) default: proxyGroup = append(proxyGroup, proxy.Group{ Name: group.Name, 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")...) } - 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) { - for tag, servers := range tags { - 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) { +// generateDefaultGroup generates a default proxy group with auto-selection and manual selection options. +func generateDefaultGroup() (proxyGroup []proxy.Group) { proxyGroup = append(proxyGroup, proxy.Group{ Name: AutoSelect, Type: proxy.GroupTypeURLTest, @@ -148,23 +137,12 @@ func generateProxyGroup(servers []proxy.Proxy) (proxyGroup []proxy.Group, nodes Interval: 300, }) - // 设置手动选择分组 - 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...) + return proxyGroup } -func adapterProxies(servers []*server.Server) []proxy.Proxy { +func adapterProxies(servers []*server.Server) ([]proxy.Proxy, []string, map[string][]string) { var proxies []proxy.Proxy + var tags = make(map[string][]string) for _, node := range servers { switch node.RelayMode { case server.RelayModeAll: @@ -179,8 +157,20 @@ func adapterProxies(servers []*server.Server) []proxy.Proxy { continue } 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) } case server.RelayModeRandom: @@ -196,18 +186,46 @@ func adapterProxies(servers []*server.Server) []proxy.Proxy { continue } 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) default: logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode)) n := addNode(node, node.ServerAddr, 0) 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) } } } - return proxies + + var nodes []string + for _, p := range proxies { + nodes = append(nodes, p.Name) + } + + return proxies, tool.RemoveDuplicateElements(nodes...), tags } // RemoveEmptyString 切片去除空值 @@ -221,61 +239,61 @@ func RemoveEmptyString(arr []string) []string { 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. -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 selectedGroup proxy.Group + var defaultGroup, autoSelectGroup proxy.Group // 在所有分组找到默认分组并将他放到第一个 for _, group := range groups { if group.Name == "" || group.Name == "DIRECT" || group.Name == "REJECT" { continue } - if group.Name == defaultName { - group.Proxies = tool.RemoveStringElement(group.Proxies, defaultName, "REJECT") - sortedGroups = append([]proxy.Group{group}, sortedGroups...) - continue - } else if group.Name == Selection { - 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...) + // 如果是默认分组 + if group.Default { + group.Proxies = append([]string{AutoSelect}, nodes...) + group.Proxies = append(group.Proxies, "DIRECT") + defaultGroup = group 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) } - // 将手动选择分组放到最后 - if selectedGroup.Name != "" { - sortedGroups = append(sortedGroups, selectedGroup) + + if defaultGroup.Name != "" { + sortedGroups = append([]proxy.Group{defaultGroup}, sortedGroups...) } + if autoSelectGroup.Name != "" && autoSelectGroup.Name != defaultGroup.Name { + sortedGroups = append(sortedGroups, autoSelectGroup) + } + return sortedGroups }