Files
subconverter-go/internal/generator/clash.go
Rogee 7fcabe0225
Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
first commit
2025-09-28 10:05:07 +08:00

654 lines
17 KiB
Go

package generator
import (
"fmt"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
yaml "gopkg.in/yaml.v3"
"github.com/subconverter-go/internal/logging"
"github.com/subconverter-go/internal/parser"
)
// ClashGenerator Clash格式生成器
// 实现Clash代理配置的生成功能
type ClashGenerator struct {
logger *logging.Logger
}
// NewClashGenerator 创建新的Clash生成器
// 返回初始化好的ClashGenerator实例
func NewClashGenerator(logger *logging.Logger) *ClashGenerator {
return &ClashGenerator{
logger: logger,
}
}
// Generate 生成Clash配置
func (g *ClashGenerator) Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error) {
g.logger.Debugf("Generating Clash configuration")
// 创建Clash配置结构
clashConfig := g.createClashConfig(configs, options)
// 转换为YAML
yamlData, err := yaml.Marshal(clashConfig)
if err != nil {
return "", fmt.Errorf("failed to marshal Clash configuration: %v", err)
}
return string(yamlData), nil
}
// ValidateOptions 验证Clash生成选项
func (g *ClashGenerator) ValidateOptions(options *GenerationOptions) error {
g.logger.Debugf("Validating Clash generation options")
// 验证基本信息
if options.Name == "" {
return fmt.Errorf("configuration name is required")
}
// 验证模式
validModes := []string{"global", "rule", "direct"}
if options.Mode != "" {
valid := false
for _, mode := range validModes {
if options.Mode == mode {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid mode: %s, must be one of: %s", options.Mode, strings.Join(validModes, ", "))
}
}
// 验证端口
if options.MixedPort < 0 || options.MixedPort > 65535 {
return fmt.Errorf("invalid mixed port: %d", options.MixedPort)
}
// 验证日志级别
validLogLevels := []string{"info", "warning", "error", "debug", "silent"}
if options.LogLevel != "" {
valid := false
for _, level := range validLogLevels {
if options.LogLevel == level {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid log level: %s, must be one of: %s", options.LogLevel, strings.Join(validLogLevels, ", "))
}
}
return nil
}
// GetSupportedFormats 获取支持的格式
func (g *ClashGenerator) GetSupportedFormats() []string {
return []string{"clash"}
}
// ClashConfig Clash配置结构体
type ClashConfig struct {
Port int `yaml:"port" json:"port"`
SocksPort int `yaml:"socks-port" json:"socks-port"`
AllowLan bool `yaml:"allow-lan" json:"allow-lan"`
Mode string `yaml:"mode" json:"mode"`
LogLevel string `yaml:"log-level" json:"log-level"`
ExternalController string `yaml:"external-controller" json:"external-controller"`
Secret string `yaml:"secret,omitempty" json:"secret,omitempty"`
Proxies []map[string]interface{} `yaml:"proxies" json:"proxies"`
ProxyGroup []map[string]interface{} `yaml:"proxy-groups" json:"proxy-groups"`
Rules []string `yaml:"rules" json:"rules"`
ProxyProviders map[string]map[string]interface{} `yaml:"proxy-providers,omitempty" json:"proxy-providers,omitempty"`
}
// createClashConfig 创建Clash配置结构
func (g *ClashGenerator) createClashConfig(configs []*parser.ProxyConfig, options *GenerationOptions) *ClashConfig {
config := &ClashConfig{
Port: 7890,
SocksPort: 7891,
AllowLan: options.AllowLan,
Mode: options.Mode,
LogLevel: options.LogLevel,
ExternalController: options.ExternalController,
Secret: options.Secret,
Proxies: make([]map[string]interface{}, 0),
ProxyGroup: make([]map[string]interface{}, 0),
Rules: make([]string, 0),
}
// 设置默认值
if config.Port == 0 {
config.Port = 7890
}
if config.SocksPort == 0 {
config.SocksPort = 7891
}
if config.Mode == "" {
config.Mode = "rule"
}
if config.LogLevel == "" {
config.LogLevel = "info"
}
// 生成代理配置
for _, proxyConfig := range configs {
proxy := g.convertProxyConfig(proxyConfig)
config.Proxies = append(config.Proxies, proxy)
}
// 生成代理组
config.ProxyGroup = g.createProxyGroups(configs, options)
if len(options.GroupDefinitions) > 0 {
for _, def := range options.GroupDefinitions {
config.ProxyGroup = append(config.ProxyGroup, g.buildClashGroup(def, configs))
}
} else if len(options.CustomGroups) > 0 {
for _, entry := range options.CustomGroups {
group, err := g.parseCustomGroup(entry, configs)
if err != nil {
g.logger.WithError(err).Warnf("Failed to parse custom group: %s", entry)
continue
}
config.ProxyGroup = append(config.ProxyGroup, group)
}
}
if len(options.Providers) > 0 {
providers := make(map[string]map[string]interface{}, len(options.Providers))
for _, def := range options.Providers {
providers[def.Name] = g.buildClashProvider(def)
}
config.ProxyProviders = providers
} else if len(options.CustomProviders) > 0 {
providers := make(map[string]map[string]interface{})
for _, entry := range options.CustomProviders {
parsed, err := ParseProviderDefinition(entry)
if err != nil {
g.logger.WithError(err).Warnf("Failed to parse custom provider: %s", entry)
continue
}
providers[parsed.Name] = g.buildClashProvider(parsed)
}
if len(providers) > 0 {
config.ProxyProviders = providers
}
}
// 生成规则
rules := g.createRules(options)
config.Rules = g.applyCustomRulesets(rules, options)
return config
}
// convertProxyConfig 转换代理配置为Clash格式
func (g *ClashGenerator) convertProxyConfig(proxyConfig *parser.ProxyConfig) map[string]interface{} {
proxy := map[string]interface{}{
"name": proxyConfig.Name,
"type": proxyConfig.Type,
"server": proxyConfig.Server,
"port": proxyConfig.Port,
"udp": proxyConfig.UDP,
}
// 根据协议类型添加特定配置
switch proxyConfig.Protocol {
case "ss":
if method, exists := proxyConfig.Settings["method"]; exists {
proxy["cipher"] = method
}
if password, exists := proxyConfig.Settings["password"]; exists {
proxy["password"] = password
}
if plugin, exists := proxyConfig.Settings["plugin"]; exists {
proxy["plugin"] = plugin
}
case "ssr":
if method, exists := proxyConfig.Settings["method"]; exists {
proxy["cipher"] = method
}
if password, exists := proxyConfig.Settings["password"]; exists {
proxy["password"] = password
}
if protocol, exists := proxyConfig.Settings["protocol"]; exists {
proxy["protocol"] = protocol
}
if obfs, exists := proxyConfig.Settings["obfs"]; exists {
proxy["obfs"] = obfs
}
case "vmess":
if uuid, exists := proxyConfig.Settings["uuid"]; exists {
proxy["uuid"] = uuid
}
if alterId, exists := proxyConfig.Settings["alterId"]; exists {
proxy["alterId"] = alterId
}
if network, exists := proxyConfig.Settings["network"]; exists {
proxy["network"] = network
}
if tls, exists := proxyConfig.Settings["tls"]; exists {
proxy["tls"] = tls
}
if host, exists := proxyConfig.Settings["host"]; exists {
proxy["servername"] = host
}
if path, exists := proxyConfig.Settings["path"]; exists {
proxy["ws-path"] = path
}
case "trojan":
if password, exists := proxyConfig.Settings["password"]; exists {
proxy["password"] = password
}
if sni, exists := proxyConfig.Settings["sni"]; exists {
proxy["sni"] = sni
}
if network, exists := proxyConfig.Settings["network"]; exists {
proxy["network"] = network
}
case "http", "https":
if username, exists := proxyConfig.Settings["username"]; exists {
proxy["username"] = username
}
if password, exists := proxyConfig.Settings["password"]; exists {
proxy["password"] = password
}
if tls, exists := proxyConfig.Settings["tls"]; exists {
proxy["tls"] = tls
}
case "socks5":
if username, exists := proxyConfig.Settings["username"]; exists {
proxy["username"] = username
}
if password, exists := proxyConfig.Settings["password"]; exists {
proxy["password"] = password
}
}
return proxy
}
// createProxyGroups 创建代理组
func (g *ClashGenerator) createProxyGroups(configs []*parser.ProxyConfig, options *GenerationOptions) []map[string]interface{} {
groups := make([]map[string]interface{}, 0)
// 创建代理名称列表
proxyNames := make([]string, 0)
for _, config := range configs {
proxyNames = append(proxyNames, config.Name)
}
// 创建选择代理组
selectGroup := map[string]interface{}{
"name": "PROXY",
"type": "select",
"proxies": proxyNames,
}
groups = append(groups, selectGroup)
// 创建自动选择代理组(如果启用了代理测试)
if options.ProxyTest {
urlTestGroup := map[string]interface{}{
"name": "URL-TEST",
"type": "url-test",
"proxies": proxyNames,
"url": options.ProxyURL,
"interval": 300,
"tolerance": 50,
}
if urlTestGroup["url"] == "" {
urlTestGroup["url"] = "http://www.gstatic.com/generate_204"
}
groups = append(groups, urlTestGroup)
}
// 创建故障转移代理组
fallbackGroup := map[string]interface{}{
"name": "FALLBACK",
"type": "fallback",
"proxies": proxyNames,
"url": "http://www.gstatic.com/generate_204",
"interval": 300,
}
groups = append(groups, fallbackGroup)
// 创建直连和拒绝代理组
groups = append(groups, map[string]interface{}{
"name": "DIRECT",
"type": "select",
"proxies": []string{"DIRECT"},
})
groups = append(groups, map[string]interface{}{
"name": "REJECT",
"type": "select",
"proxies": []string{"REJECT"},
})
return groups
}
func (g *ClashGenerator) buildClashGroup(def *GroupDefinition, configs []*parser.ProxyConfig) map[string]interface{} {
group := map[string]interface{}{
"name": def.Name,
"type": def.Type,
}
proxies := make([]string, 0, len(def.Proxies))
proxies = append(proxies, def.Proxies...)
if len(proxies) == 0 && len(def.UseProviders) == 0 {
for _, cfg := range configs {
proxies = append(proxies, cfg.Name)
}
}
if len(proxies) > 0 {
group["proxies"] = proxies
}
if len(def.UseProviders) > 0 {
group["use"] = def.UseProviders
}
if def.URL != "" {
group["url"] = def.URL
}
if def.Interval != nil {
group["interval"] = *def.Interval
}
if def.Timeout != nil {
group["timeout"] = *def.Timeout
}
if def.Tolerance != nil {
group["tolerance"] = *def.Tolerance
}
for k, v := range def.Extras {
group[k] = v
}
return group
}
func (g *ClashGenerator) buildClashProvider(def *ProviderDefinition) map[string]interface{} {
provider := map[string]interface{}{
"type": def.Type,
"url": def.URL,
"path": def.Path,
}
if def.Interval != nil {
provider["interval"] = *def.Interval
}
if len(def.Fields) > 0 {
keys := make([]string, 0, len(def.Fields))
for k := range def.Fields {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
provider[k] = def.Fields[k]
}
}
if len(def.Flags) > 0 {
keys := make([]string, 0, len(def.Flags))
for k := range def.Flags {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
provider[k] = def.Flags[k]
}
}
if def.Health != nil {
health := make(map[string]interface{})
if def.Health.Enable != nil {
health["enable"] = *def.Health.Enable
}
if def.Health.URL != "" {
health["url"] = def.Health.URL
}
if def.Health.Interval != nil {
health["interval"] = *def.Health.Interval
}
if def.Health.Lazy != nil {
health["lazy"] = *def.Health.Lazy
}
if def.Health.Tolerance != nil {
health["tolerance"] = *def.Health.Tolerance
}
if def.Health.Timeout != nil {
health["timeout"] = *def.Health.Timeout
}
if def.Health.Method != "" {
health["method"] = def.Health.Method
}
if def.Health.Headers != "" {
health["headers"] = def.Health.Headers
}
if len(health) > 0 {
provider["health-check"] = health
}
}
return provider
}
// createRules 创建规则
func (g *ClashGenerator) createRules(options *GenerationOptions) []string {
rules := make([]string, 0)
// 添加默认规则
if len(options.Rules) > 0 {
rules = append(rules, options.Rules...)
} else {
// 添加默认规则集
defaultRules := []string{
"DOMAIN-SUFFIX,bilibili.com,DIRECT",
"DOMAIN-SUFFIX,baidu.com,DIRECT",
"DOMAIN-SUFFIX,cn,DIRECT",
"DOMAIN-KEYWORD,google,PROXY",
"DOMAIN-KEYWORD,github,PROXY",
"DOMAIN-KEYWORD,twitter,PROXY",
"DOMAIN-KEYWORD,facebook,PROXY",
"DOMAIN-KEYWORD,youtube,PROXY",
"DOMAIN-KEYWORD,instagram,PROXY",
"DOMAIN-KEYWORD,telegram,PROXY",
"GEOIP,CN,DIRECT",
"IP-CIDR,127.0.0.0/8,DIRECT",
"IP-CIDR,192.168.0.0/16,DIRECT",
"IP-CIDR,10.0.0.0/8,DIRECT",
"IP-CIDR,172.16.0.0/12,DIRECT",
"MATCH,PROXY",
}
rules = append(rules, defaultRules...)
}
// 启用IPv6支持
if options.IPv6 {
ipv6Rules := []string{
"IP-CIDR6,::1/128,DIRECT",
"IP-CIDR6,fc00::/7,DIRECT",
}
rules = append(ipv6Rules, rules...)
}
// 启用局域网支持
if options.EnableLan {
lanRules := []string{
"SRC-IP-CIDR,192.168.0.0/16,DIRECT",
"SRC-IP-CIDR,10.0.0.0/8,DIRECT",
"SRC-IP-CIDR,172.16.0.0/12,DIRECT",
}
rules = append(lanRules, rules...)
}
return rules
}
func (g *ClashGenerator) parseCustomGroup(definition string, configs []*parser.ProxyConfig) (map[string]interface{}, error) {
values, err := url.ParseQuery(definition)
if err != nil {
return nil, fmt.Errorf("invalid group definition: %v", err)
}
name := values.Get("name")
if name == "" {
return nil, fmt.Errorf("custom group missing name")
}
groupType := values.Get("type")
if groupType == "" {
groupType = "select"
}
useProviders := make([]string, 0)
if useStr := values.Get("use"); useStr != "" {
for _, entry := range strings.Split(useStr, ",") {
entry = strings.TrimSpace(entry)
if entry != "" {
useProviders = append(useProviders, entry)
}
}
}
proxies := make([]string, 0)
if proxyStr := values.Get("proxies"); proxyStr != "" {
for _, entry := range strings.Split(proxyStr, ",") {
entry = strings.TrimSpace(entry)
if entry != "" {
proxies = append(proxies, entry)
}
}
}
if len(proxies) == 0 && len(useProviders) == 0 {
for _, cfg := range configs {
proxies = append(proxies, cfg.Name)
}
}
group := map[string]interface{}{
"name": name,
"type": groupType,
"proxies": proxies,
}
if len(useProviders) > 0 {
group["use"] = useProviders
}
if urlValue := values.Get("url"); urlValue != "" {
group["url"] = urlValue
}
if interval := values.Get("interval"); interval != "" {
if n, err := strconv.Atoi(interval); err == nil && n > 0 {
group["interval"] = n
}
}
if tolerance := values.Get("tolerance"); tolerance != "" {
if n, err := strconv.Atoi(tolerance); err == nil && n >= 0 {
group["tolerance"] = n
}
}
if timeout := values.Get("timeout"); timeout != "" {
if n, err := strconv.Atoi(timeout); err == nil && n >= 0 {
group["timeout"] = n
}
}
return group, nil
}
func (g *ClashGenerator) applyCustomRulesets(existing []string, options *GenerationOptions) []string {
rules := make([]string, 0, len(existing))
for _, rule := range existing {
rule = strings.TrimSpace(rule)
if rule == "" {
continue
}
rules = append(rules, rule)
}
for _, entry := range options.CustomRulesets {
entry = strings.TrimSpace(entry)
if entry == "" {
continue
}
lower := strings.ToLower(entry)
switch {
case strings.HasPrefix(lower, "ruleset,"):
payload := strings.TrimSpace(entry[len("ruleset,"):])
if payload == "" {
continue
}
lines, err := g.loadRulesetLines(options.BasePath, payload)
if err != nil {
g.logger.WithError(err).Warnf("Failed to load ruleset %s", payload)
continue
}
rules = append(rules, lines...)
case strings.HasPrefix(lower, "rule,"):
statement := strings.TrimSpace(entry[len("rule,"):])
if statement != "" {
rules = append(rules, statement)
}
default:
rules = append(rules, entry)
}
}
return normalizeRules(rules)
}
func (g *ClashGenerator) loadRulesetLines(basePath, relative string) ([]string, error) {
path := relative
if basePath != "" && !filepath.IsAbs(relative) {
path = filepath.Join(basePath, relative)
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
content := strings.ReplaceAll(string(data), "\r\n", "\n")
lines := make([]string, 0)
for _, line := range strings.Split(content, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//") {
continue
}
lines = append(lines, trimmed)
}
return lines, nil
}
func normalizeRules(rules []string) []string {
seen := make(map[string]struct{})
result := make([]string, 0, len(rules))
for _, rule := range rules {
rule = strings.TrimSpace(rule)
if rule == "" {
continue
}
if _, ok := seen[rule]; ok {
continue
}
seen[rule] = struct{}{}
result = append(result, rule)
}
return result
}