first commit
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
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
This commit is contained in:
653
internal/generator/clash.go
Normal file
653
internal/generator/clash.go
Normal file
@@ -0,0 +1,653 @@
|
||||
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
|
||||
}
|
||||
106
internal/generator/group.go
Normal file
106
internal/generator/group.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GroupDefinition captures parsed custom proxy group metadata.
|
||||
type GroupDefinition struct {
|
||||
Name string
|
||||
Type string
|
||||
Proxies []string
|
||||
UseProviders []string
|
||||
URL string
|
||||
Interval *int
|
||||
Timeout *int
|
||||
Tolerance *int
|
||||
Extras map[string]string
|
||||
}
|
||||
|
||||
// ParseGroupDefinition converts a custom group definition string into a GroupDefinition.
|
||||
func ParseGroupDefinition(definition string) (*GroupDefinition, error) {
|
||||
values, err := url.ParseQuery(definition)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid group definition: %v", err)
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(values.Get("name"))
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("custom group missing name")
|
||||
}
|
||||
|
||||
typeVal := strings.TrimSpace(values.Get("type"))
|
||||
if typeVal == "" {
|
||||
typeVal = "select"
|
||||
}
|
||||
|
||||
group := &GroupDefinition{
|
||||
Name: name,
|
||||
Type: typeVal,
|
||||
Extras: make(map[string]string),
|
||||
}
|
||||
|
||||
if proxyStr := strings.TrimSpace(values.Get("proxies")); proxyStr != "" {
|
||||
parts := strings.Split(proxyStr, ",")
|
||||
for _, entry := range parts {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry != "" {
|
||||
group.Proxies = append(group.Proxies, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if useStr := strings.TrimSpace(values.Get("use")); useStr != "" {
|
||||
parts := strings.Split(useStr, ",")
|
||||
for _, entry := range parts {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry != "" {
|
||||
group.UseProviders = append(group.UseProviders, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if raw := strings.TrimSpace(values.Get("url")); raw != "" {
|
||||
group.URL = raw
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("interval")); raw != "" {
|
||||
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
|
||||
group.Interval = &n
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid interval for group %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("timeout")); raw != "" {
|
||||
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
|
||||
group.Timeout = &n
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid timeout for group %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("tolerance")); raw != "" {
|
||||
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
|
||||
group.Tolerance = &n
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid tolerance for group %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for key, vals := range values {
|
||||
if key == "name" || key == "type" || key == "proxies" || key == "use" || key == "url" || key == "interval" || key == "timeout" || key == "tolerance" {
|
||||
continue
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(vals[len(vals)-1])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
group.Extras[strings.ReplaceAll(key, "_", "-")] = value
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
352
internal/generator/loon.go
Normal file
352
internal/generator/loon.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
"github.com/subconverter-go/internal/parser"
|
||||
)
|
||||
|
||||
// LoonGenerator Loon格式生成器
|
||||
// 实现Loon代理配置的生成功能
|
||||
type LoonGenerator struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewLoonGenerator 创建新的Loon生成器
|
||||
// 返回初始化好的LoonGenerator实例
|
||||
func NewLoonGenerator(logger *logging.Logger) *LoonGenerator {
|
||||
return &LoonGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成Loon配置
|
||||
func (g *LoonGenerator) Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error) {
|
||||
g.logger.Debugf("Generating Loon configuration")
|
||||
|
||||
// 生成Loon配置文本
|
||||
var builder strings.Builder
|
||||
|
||||
// 生成头部信息
|
||||
g.generateHeader(&builder, options)
|
||||
|
||||
// 生成代理配置
|
||||
g.generateProxies(&builder, configs)
|
||||
|
||||
// 生成代理组
|
||||
g.generateProxyGroups(&builder, configs, options)
|
||||
|
||||
// 生成规则
|
||||
g.generateRules(&builder, options)
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// ValidateOptions 验证Loon生成选项
|
||||
func (g *LoonGenerator) ValidateOptions(options *GenerationOptions) error {
|
||||
g.logger.Debugf("Validating Loon generation options")
|
||||
|
||||
// 验证基本信息
|
||||
if options.Name == "" {
|
||||
return fmt.Errorf("configuration name is required")
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
if options.MixedPort < 0 || options.MixedPort > 65535 {
|
||||
return fmt.Errorf("invalid mixed port: %d", options.MixedPort)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSupportedFormats 获取支持的格式
|
||||
func (g *LoonGenerator) GetSupportedFormats() []string {
|
||||
return []string{"loon"}
|
||||
}
|
||||
|
||||
// generateHeader 生成配置头部信息
|
||||
func (g *LoonGenerator) generateHeader(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("# Loon Configuration\n")
|
||||
builder.WriteString("# Generated by SubConverter-Go\n")
|
||||
builder.WriteString(fmt.Sprintf("# Name: %s\n\n", options.Name))
|
||||
|
||||
// 基本配置
|
||||
builder.WriteString("[General]\n")
|
||||
builder.WriteString("bypass-system = true\n")
|
||||
builder.WriteString("bypass-tun = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12\n")
|
||||
builder.WriteString("dns-server = 119.29.29.29, 223.5.5.5, 1.1.1.1\n")
|
||||
|
||||
if options.MixedPort > 0 {
|
||||
builder.WriteString(fmt.Sprintf("http-port = %d\n", options.MixedPort))
|
||||
builder.WriteString(fmt.Sprintf("socks5-port = %d\n", options.MixedPort+1))
|
||||
}
|
||||
|
||||
builder.WriteString("allow-wifi-access = false\n")
|
||||
builder.WriteString("wifi-access-http-port = 6152\n")
|
||||
builder.WriteString("wifi-access-socks5-port = 6153\n")
|
||||
|
||||
if options.IPv6 {
|
||||
builder.WriteString("ipv6 = true\n")
|
||||
} else {
|
||||
builder.WriteString("ipv6 = false\n")
|
||||
}
|
||||
|
||||
builder.WriteString("prefer-ipv6 = false\n")
|
||||
builder.WriteString("enhanced-mode-by-rule = false\n")
|
||||
builder.WriteString("fallback-dns-server = 223.5.5.5\n")
|
||||
builder.WriteString("loglevel = info\n")
|
||||
builder.WriteString("skip-proxy = 127.0.0.1, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12\n")
|
||||
|
||||
if options.EnableLan {
|
||||
builder.WriteString("enhanced-mode-by-rule = true\n")
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// generateProxies 生成代理配置
|
||||
func (g *LoonGenerator) generateProxies(builder *strings.Builder, configs []*parser.ProxyConfig) {
|
||||
builder.WriteString("[Proxy]\n")
|
||||
|
||||
for i, config := range configs {
|
||||
proxy := g.convertProxyConfig(config)
|
||||
builder.WriteString(proxy)
|
||||
|
||||
if i < len(configs)-1 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// convertProxyConfig 转换代理配置为Loon格式
|
||||
func (g *LoonGenerator) convertProxyConfig(proxyConfig *parser.ProxyConfig) string {
|
||||
switch proxyConfig.Protocol {
|
||||
case "ss":
|
||||
return g.generateSSProxy(proxyConfig)
|
||||
case "ssr":
|
||||
return g.generateSSRProxy(proxyConfig)
|
||||
case "vmess":
|
||||
return g.generateVMessProxy(proxyConfig)
|
||||
case "trojan":
|
||||
return g.generateTrojanProxy(proxyConfig)
|
||||
case "http", "https":
|
||||
return g.generateHTTPProxy(proxyConfig)
|
||||
case "socks5":
|
||||
return g.generateSocks5Proxy(proxyConfig)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// generateSSProxy 生成Shadowsocks代理配置
|
||||
func (g *LoonGenerator) generateSSProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
plugin, _ := config.Settings["plugin"].(string)
|
||||
|
||||
if plugin != "" {
|
||||
return fmt.Sprintf("%s = Shadowsocks, %s, %d, %s, %s, %s, %s",
|
||||
config.Name, config.Server, config.Port, method, password, plugin, g.generatePluginOpts(config))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = Shadowsocks, %s, %d, %s, %s",
|
||||
config.Name, config.Server, config.Port, method, password)
|
||||
}
|
||||
|
||||
// generateSSRProxy 生成ShadowsocksR代理配置
|
||||
func (g *LoonGenerator) generateSSRProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
protocol, _ := config.Settings["protocol"].(string)
|
||||
protocolParam, _ := config.Settings["protocol-param"].(string)
|
||||
obfs, _ := config.Settings["obfs"].(string)
|
||||
obfsParam, _ := config.Settings["obfs-param"].(string)
|
||||
|
||||
return fmt.Sprintf("%s = ShadowsocksR, %s, %d, %s, %s, %s, %s, %s, %s",
|
||||
config.Name, config.Server, config.Port, method, password, protocol, protocolParam, obfs, obfsParam)
|
||||
}
|
||||
|
||||
// generateVMessProxy 生成VMess代理配置
|
||||
func (g *LoonGenerator) generateVMessProxy(config *parser.ProxyConfig) string {
|
||||
uuid, _ := config.Settings["uuid"].(string)
|
||||
alterId, _ := config.Settings["alterId"].(int)
|
||||
network, _ := config.Settings["network"].(string)
|
||||
tls, _ := config.Settings["tls"].(string)
|
||||
host, _ := config.Settings["host"].(string)
|
||||
path, _ := config.Settings["path"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("%s", config.Server))
|
||||
opts = append(opts, fmt.Sprintf("%d", config.Port))
|
||||
opts = append(opts, fmt.Sprintf("%s", "auto")) // Loon会自动选择加密方式
|
||||
opts = append(opts, fmt.Sprintf("%s", uuid))
|
||||
opts = append(opts, fmt.Sprintf("%d", alterId))
|
||||
|
||||
if network != "" {
|
||||
opts = append(opts, fmt.Sprintf("%s", network))
|
||||
}
|
||||
|
||||
if tls != "" {
|
||||
opts = append(opts, fmt.Sprintf("tls"))
|
||||
}
|
||||
|
||||
if host != "" {
|
||||
opts = append(opts, fmt.Sprintf("%s", host))
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
opts = append(opts, fmt.Sprintf("%s", path))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = VMess, %s",
|
||||
config.Name, strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateTrojanProxy 生成Trojan代理配置
|
||||
func (g *LoonGenerator) generateTrojanProxy(config *parser.ProxyConfig) string {
|
||||
password, _ := config.Settings["password"].(string)
|
||||
sni, _ := config.Settings["sni"].(string)
|
||||
network, _ := config.Settings["network"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("%s", config.Server))
|
||||
opts = append(opts, fmt.Sprintf("%d", config.Port))
|
||||
opts = append(opts, fmt.Sprintf("%s", password))
|
||||
|
||||
if sni != "" {
|
||||
opts = append(opts, fmt.Sprintf("%s", sni))
|
||||
}
|
||||
|
||||
if network != "" {
|
||||
opts = append(opts, fmt.Sprintf("%s", network))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = Trojan, %s",
|
||||
config.Name, strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateHTTPProxy 生成HTTP代理配置
|
||||
func (g *LoonGenerator) generateHTTPProxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
tls, _ := config.Settings["tls"].(bool)
|
||||
|
||||
if username != "" && password != "" {
|
||||
if tls {
|
||||
return fmt.Sprintf("%s = HTTPS, %s, %d, %s, %s",
|
||||
config.Name, config.Server, config.Port, username, password)
|
||||
}
|
||||
return fmt.Sprintf("%s = HTTP, %s, %d, %s, %s",
|
||||
config.Name, config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
if tls {
|
||||
return fmt.Sprintf("%s = HTTPS, %s, %d",
|
||||
config.Name, config.Server, config.Port)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = HTTP, %s, %d",
|
||||
config.Name, config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generateSocks5Proxy 生成Socks5代理配置
|
||||
func (g *LoonGenerator) generateSocks5Proxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
|
||||
if username != "" && password != "" {
|
||||
return fmt.Sprintf("%s = SOCKS5, %s, %d, %s, %s",
|
||||
config.Name, config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = SOCKS5, %s, %d",
|
||||
config.Name, config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generatePluginOpts 生成插件选项
|
||||
func (g *LoonGenerator) generatePluginOpts(config *parser.ProxyConfig) string {
|
||||
// 这里可以根据具体插件生成相应的选项
|
||||
return ""
|
||||
}
|
||||
|
||||
// generateProxyGroups 生成代理组
|
||||
func (g *LoonGenerator) generateProxyGroups(builder *strings.Builder, configs []*parser.ProxyConfig, options *GenerationOptions) {
|
||||
builder.WriteString("[Proxy Group]\n")
|
||||
|
||||
// 创建代理名称列表
|
||||
proxyNames := make([]string, 0)
|
||||
for _, config := range configs {
|
||||
proxyNames = append(proxyNames, config.Name)
|
||||
}
|
||||
|
||||
// 选择代理组
|
||||
builder.WriteString(fmt.Sprintf("Proxy = select, %s\n", strings.Join(proxyNames, ", ")))
|
||||
|
||||
// URL测试代理组
|
||||
if options.ProxyTest {
|
||||
testURL := options.ProxyURL
|
||||
if testURL == "" {
|
||||
testURL = "http://www.gstatic.com/generate_204"
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("URL-Test = url-test, %s, url=%s, interval=300\n",
|
||||
strings.Join(proxyNames, ", "), testURL))
|
||||
}
|
||||
|
||||
// 故障转移代理组
|
||||
builder.WriteString(fmt.Sprintf("Fallback = fallback, %s, url=http://www.gstatic.com/generate_204, interval=300\n",
|
||||
strings.Join(proxyNames, ", ")))
|
||||
|
||||
// 直连和拒绝代理组
|
||||
builder.WriteString("Direct = select, DIRECT\n")
|
||||
builder.WriteString("Reject = select, REJECT\n\n")
|
||||
}
|
||||
|
||||
// generateRules 生成规则
|
||||
func (g *LoonGenerator) generateRules(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("[Rule]\n")
|
||||
|
||||
// 添加默认规则
|
||||
if len(options.Rules) > 0 {
|
||||
builder.WriteString(strings.Join(options.Rules, "\n"))
|
||||
} 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",
|
||||
"FINAL,Proxy",
|
||||
}
|
||||
builder.WriteString(strings.Join(defaultRules, "\n"))
|
||||
}
|
||||
|
||||
// 启用IPv6支持
|
||||
if options.IPv6 {
|
||||
builder.WriteString("\n# IPv6 Rules\n")
|
||||
builder.WriteString("IP-CIDR6,::1/128,DIRECT\n")
|
||||
builder.WriteString("IP-CIDR6,fc00::/7,DIRECT\n")
|
||||
}
|
||||
|
||||
// 启用局域网支持
|
||||
if options.EnableLan {
|
||||
builder.WriteString("\n# LAN Rules\n")
|
||||
builder.WriteString("SRC-IP-CIDR,192.168.0.0/16,DIRECT\n")
|
||||
builder.WriteString("SRC-IP-CIDR,10.0.0.0/8,DIRECT\n")
|
||||
builder.WriteString("SRC-IP-CIDR,172.16.0.0/12,DIRECT\n")
|
||||
}
|
||||
}
|
||||
236
internal/generator/manager.go
Normal file
236
internal/generator/manager.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
"github.com/subconverter-go/internal/parser"
|
||||
)
|
||||
|
||||
// Generator 生成器接口
|
||||
// 定义了所有代理配置生成器必须实现的方法
|
||||
type Generator interface {
|
||||
// Generate 生成代理配置
|
||||
// config: 代理配置列表
|
||||
// options: 生成选项
|
||||
// 返回生成的配置字符串和错误信息
|
||||
Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error)
|
||||
|
||||
// ValidateOptions 验证生成选项
|
||||
// options: 需要验证的生成选项
|
||||
// 返回验证结果和错误信息
|
||||
ValidateOptions(options *GenerationOptions) error
|
||||
|
||||
// GetSupportedFormats 获取支持的格式
|
||||
// 返回该生成器支持的格式列表
|
||||
GetSupportedFormats() []string
|
||||
}
|
||||
|
||||
// GenerationOptions 生成选项结构体
|
||||
// 包含所有生成格式的通用配置选项
|
||||
type GenerationOptions struct {
|
||||
// 基本信息
|
||||
Name string `json:"name"` // 配置名称
|
||||
Group string `json:"group"` // 代理组名称
|
||||
Location string `json:"location"` // 地理位置
|
||||
|
||||
// 代理设置
|
||||
ProxyTest bool `json:"proxyTest"` // 是否启用代理测试
|
||||
ProxyURL string `json:"proxyURL"` // 测试代理URL
|
||||
|
||||
// 规则设置
|
||||
Rules []string `json:"rules"` // 规则列表
|
||||
|
||||
// 性能设置
|
||||
EnableLan bool `json:"enableLan"` // 是否启用局域网连接
|
||||
IPv6 bool `json:"ipv6"` // 是否启用IPv6
|
||||
|
||||
// 高级设置
|
||||
MixedPort int `json:"mixedPort"` // 混合端口
|
||||
AllowLan bool `json:"allowLan"` // 是否允许局域网连接
|
||||
Mode string `json:"mode"` // 模式 (global/rule/direct)
|
||||
LogLevel string `json:"logLevel"` // 日志级别
|
||||
ExternalController string `json:"externalController"` // 外部控制器地址
|
||||
Secret string `json:"secret"` // 密钥
|
||||
|
||||
// 兼容扩展设置
|
||||
AppendType bool `json:"appendType"`
|
||||
TFO bool `json:"tfo"`
|
||||
Script bool `json:"script"`
|
||||
SkipCert bool `json:"skipCertVerify"`
|
||||
FilterDeprecated bool `json:"filterDeprecated"`
|
||||
ExpandRules bool `json:"expandRules"`
|
||||
AppendInfo bool `json:"appendInfo"`
|
||||
Prepend bool `json:"prepend"`
|
||||
Classic bool `json:"classic"`
|
||||
TLS13 bool `json:"tls13"`
|
||||
AddEmoji bool `json:"addEmoji"`
|
||||
RemoveEmoji bool `json:"removeEmoji"`
|
||||
EmojiRules []string `json:"emojiRules"`
|
||||
RenameRules []string `json:"renameRules"`
|
||||
CustomGroups []string `json:"customGroups"`
|
||||
CustomRulesets []string `json:"customRulesets"`
|
||||
CustomProviders []string `json:"customProviders"`
|
||||
GroupDefinitions []*GroupDefinition `json:"-"`
|
||||
Providers []*ProviderDefinition `json:"-"`
|
||||
FilterScript string `json:"filterScript"`
|
||||
Upload bool `json:"upload"`
|
||||
UploadPath string `json:"uploadPath"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
Interval string `json:"interval"`
|
||||
BasePath string `json:"-"`
|
||||
}
|
||||
|
||||
// GeneratorManager 生成器管理器
|
||||
// 管理所有代理配置生成器,提供统一的生成接口
|
||||
type GeneratorManager struct {
|
||||
logger *logging.Logger
|
||||
generators map[string]Generator // 格式名称到生成器的映射
|
||||
}
|
||||
|
||||
// NewGeneratorManager 创建新的生成器管理器
|
||||
// 返回初始化好的GeneratorManager实例
|
||||
func NewGeneratorManager(logger *logging.Logger) *GeneratorManager {
|
||||
gm := &GeneratorManager{
|
||||
logger: logger,
|
||||
generators: make(map[string]Generator),
|
||||
}
|
||||
|
||||
// 注册所有生成器
|
||||
gm.registerGenerators()
|
||||
|
||||
return gm
|
||||
}
|
||||
|
||||
// registerGenerators 注册所有生成器
|
||||
// 将各种代理配置生成器注册到管理器中
|
||||
func (gm *GeneratorManager) registerGenerators() {
|
||||
// 注册Clash生成器
|
||||
clashGen := NewClashGenerator(gm.logger)
|
||||
gm.generators["clash"] = clashGen
|
||||
gm.generators["clashr"] = clashGen
|
||||
|
||||
// 注册Surge生成器
|
||||
gm.generators["surge"] = NewSurgeGenerator(gm.logger)
|
||||
|
||||
// 注册Quantumult X生成器
|
||||
quantumultxGen := NewQuantumultXGenerator(gm.logger)
|
||||
gm.generators["quantumultx"] = quantumultxGen
|
||||
gm.generators["quanx"] = quantumultxGen
|
||||
|
||||
// 注册Loon生成器
|
||||
gm.generators["loon"] = NewLoonGenerator(gm.logger)
|
||||
|
||||
// 注册Surfboard生成器
|
||||
gm.generators["surfboard"] = NewSurfboardGenerator(gm.logger)
|
||||
|
||||
// 注册V2Ray生成器
|
||||
gm.generators["v2ray"] = NewV2RayGenerator(gm.logger)
|
||||
|
||||
gm.logger.Info("Registered generators: clash, clashr, surge, quantumultx, loon, surfboard, v2ray")
|
||||
}
|
||||
|
||||
// Generate 生成代理配置
|
||||
// 自动识别格式并调用相应的生成器
|
||||
func (gm *GeneratorManager) Generate(configs []*parser.ProxyConfig, format string, options *GenerationOptions) (string, error) {
|
||||
gm.logger.Debugf("Generating proxy configuration for format: %s with %d proxies", format, len(configs))
|
||||
|
||||
// 验证输入参数
|
||||
if len(configs) == 0 {
|
||||
return "", fmt.Errorf("no proxy configurations provided")
|
||||
}
|
||||
|
||||
if format == "" {
|
||||
return "", fmt.Errorf("format is required")
|
||||
}
|
||||
|
||||
// 获取对应的生成器
|
||||
generator, exists := gm.generators[format]
|
||||
if !exists {
|
||||
gm.logger.Errorf("No generator found for format: %s", format)
|
||||
return "", fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
|
||||
// 验证生成选项
|
||||
if options == nil {
|
||||
options = &GenerationOptions{
|
||||
Name: "SubConverter-Go",
|
||||
Group: "proxy",
|
||||
IPv6: true,
|
||||
Mode: "rule",
|
||||
BasePath: "",
|
||||
}
|
||||
}
|
||||
|
||||
if err := generator.ValidateOptions(options); err != nil {
|
||||
gm.logger.WithError(err).Errorf("Invalid generation options for format: %s", format)
|
||||
return "", fmt.Errorf("invalid generation options: %v", err)
|
||||
}
|
||||
|
||||
// 调用生成器进行生成
|
||||
result, err := generator.Generate(configs, options)
|
||||
if err != nil {
|
||||
gm.logger.WithError(err).Errorf("Failed to generate %s configuration", format)
|
||||
return "", fmt.Errorf("failed to generate %s configuration: %v", format, err)
|
||||
}
|
||||
|
||||
gm.logger.Infof("Successfully generated %s configuration with %d proxies", format, len(configs))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetSupportedFormats 获取所有支持的格式
|
||||
// 返回管理器支持的所有生成格式列表
|
||||
func (gm *GeneratorManager) GetSupportedFormats() []string {
|
||||
formats := make([]string, 0, len(gm.generators))
|
||||
for format := range gm.generators {
|
||||
formats = append(formats, format)
|
||||
}
|
||||
|
||||
// 按字母顺序排序
|
||||
for i := range formats {
|
||||
for j := i + 1; j < len(formats); j++ {
|
||||
if formats[i] > formats[j] {
|
||||
formats[i], formats[j] = formats[j], formats[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return formats
|
||||
}
|
||||
|
||||
// GetGenerator 获取指定格式的生成器
|
||||
// 返回对应格式的生成器实例
|
||||
func (gm *GeneratorManager) GetGenerator(format string) (Generator, error) {
|
||||
generator, exists := gm.generators[format]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("generator not found for format: %s", format)
|
||||
}
|
||||
return generator, nil
|
||||
}
|
||||
|
||||
// ValidateOptions 验证生成选项
|
||||
// 使用对应的生成器验证选项的有效性
|
||||
func (gm *GeneratorManager) ValidateOptions(format string, options *GenerationOptions) error {
|
||||
generator, err := gm.GetGenerator(format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return generator.ValidateOptions(options)
|
||||
}
|
||||
|
||||
// GetDefaultOptions 获取默认生成选项
|
||||
// 返回指定格式的默认生成选项
|
||||
func (gm *GeneratorManager) GetDefaultOptions(format string) (*GenerationOptions, error) {
|
||||
_, err := gm.GetGenerator(format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回通用的默认选项
|
||||
return &GenerationOptions{
|
||||
Name: "SubConverter-Go",
|
||||
Group: "proxy",
|
||||
IPv6: true,
|
||||
Mode: "rule",
|
||||
}, nil
|
||||
}
|
||||
183
internal/generator/provider.go
Normal file
183
internal/generator/provider.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProviderHealthDefinition captures optional health-check settings for providers.
|
||||
type ProviderHealthDefinition struct {
|
||||
Enable *bool
|
||||
URL string
|
||||
Interval *int
|
||||
Lazy *bool
|
||||
Tolerance *int
|
||||
Timeout *int
|
||||
Method string
|
||||
Headers string
|
||||
}
|
||||
|
||||
// ProviderDefinition represents a parsed proxy provider entry.
|
||||
type ProviderDefinition struct {
|
||||
Name string
|
||||
Type string
|
||||
URL string
|
||||
Path string
|
||||
Interval *int
|
||||
Flags map[string]bool
|
||||
Fields map[string]string
|
||||
Health *ProviderHealthDefinition
|
||||
}
|
||||
|
||||
// ParseProviderDefinition converts an encoded provider definition (query string style)
|
||||
// into a structured ProviderDefinition instance.
|
||||
func ParseProviderDefinition(definition string) (*ProviderDefinition, error) {
|
||||
values, err := url.ParseQuery(definition)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid provider definition: %v", err)
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(values.Get("name"))
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("custom provider missing name")
|
||||
}
|
||||
|
||||
providerType := strings.TrimSpace(values.Get("type"))
|
||||
if providerType == "" {
|
||||
providerType = "http"
|
||||
}
|
||||
|
||||
urlValue := strings.TrimSpace(values.Get("url"))
|
||||
if urlValue == "" {
|
||||
return nil, fmt.Errorf("custom provider %s missing url", name)
|
||||
}
|
||||
|
||||
pathValue := strings.TrimSpace(values.Get("path"))
|
||||
if pathValue == "" {
|
||||
return nil, fmt.Errorf("custom provider %s missing path", name)
|
||||
}
|
||||
|
||||
def := &ProviderDefinition{
|
||||
Name: name,
|
||||
Type: providerType,
|
||||
URL: urlValue,
|
||||
Path: pathValue,
|
||||
Flags: make(map[string]bool),
|
||||
Fields: make(map[string]string),
|
||||
}
|
||||
|
||||
if raw := strings.TrimSpace(values.Get("interval")); raw != "" {
|
||||
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
|
||||
def.Interval = &n
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid interval for provider %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
boolFields := []struct {
|
||||
Query string
|
||||
Key string
|
||||
}{
|
||||
{"lazy", "lazy"},
|
||||
{"skip_cert_verify", "skip-cert-verify"},
|
||||
{"override", "override"},
|
||||
{"failover", "failover"},
|
||||
}
|
||||
|
||||
for _, field := range boolFields {
|
||||
if raw := strings.TrimSpace(values.Get(field.Query)); raw != "" {
|
||||
val, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid %s for provider %s: %v", field.Query, name, err)
|
||||
}
|
||||
def.Flags[field.Key] = val
|
||||
}
|
||||
}
|
||||
|
||||
stringFields := []string{"header", "headers", "filter", "format", "path_backup"}
|
||||
for _, key := range stringFields {
|
||||
if raw := strings.TrimSpace(values.Get(key)); raw != "" {
|
||||
def.Fields[strings.ReplaceAll(key, "_", "-")] = raw
|
||||
}
|
||||
}
|
||||
|
||||
health := &ProviderHealthDefinition{}
|
||||
setHealth := false
|
||||
if raw := strings.TrimSpace(values.Get("health_enable")); raw != "" {
|
||||
val, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid health_enable for provider %s: %v", name, err)
|
||||
}
|
||||
health.Enable = &val
|
||||
setHealth = true
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("health_url")); raw != "" {
|
||||
health.URL = raw
|
||||
setHealth = true
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("health_interval")); raw != "" {
|
||||
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
|
||||
health.Interval = &n
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid health_interval for provider %s: %v", name, err)
|
||||
}
|
||||
setHealth = true
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("health_lazy")); raw != "" {
|
||||
val, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid health_lazy for provider %s: %v", name, err)
|
||||
}
|
||||
health.Lazy = &val
|
||||
setHealth = true
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("health_tolerance")); raw != "" {
|
||||
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
|
||||
health.Tolerance = &n
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid health_tolerance for provider %s: %v", name, err)
|
||||
}
|
||||
setHealth = true
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("health_timeout")); raw != "" {
|
||||
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
|
||||
health.Timeout = &n
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid health_timeout for provider %s: %v", name, err)
|
||||
}
|
||||
setHealth = true
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("health_method")); raw != "" {
|
||||
health.Method = raw
|
||||
setHealth = true
|
||||
}
|
||||
if raw := strings.TrimSpace(values.Get("health_headers")); raw != "" {
|
||||
health.Headers = raw
|
||||
setHealth = true
|
||||
}
|
||||
if setHealth {
|
||||
def.Health = health
|
||||
}
|
||||
|
||||
// Capture any remaining custom fields not explicitly handled.
|
||||
for key, vals := range values {
|
||||
if key == "name" || key == "type" || key == "url" || key == "path" || key == "interval" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(key, "health_") || key == "lazy" || key == "skip_cert_verify" || key == "override" || key == "failover" || key == "header" || key == "headers" || key == "filter" || key == "format" || key == "path_backup" {
|
||||
continue
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(vals[len(vals)-1])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
def.Fields[strings.ReplaceAll(key, "_", "-")] = value
|
||||
}
|
||||
|
||||
return def, nil
|
||||
}
|
||||
481
internal/generator/quantumultx.go
Normal file
481
internal/generator/quantumultx.go
Normal file
@@ -0,0 +1,481 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
"github.com/subconverter-go/internal/parser"
|
||||
)
|
||||
|
||||
// QuantumultXGenerator Quantumult X格式生成器
|
||||
// 实现Quantumult X代理配置的生成功能
|
||||
type QuantumultXGenerator struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewQuantumultXGenerator 创建新的Quantumult X生成器
|
||||
// 返回初始化好的QuantumultXGenerator实例
|
||||
func NewQuantumultXGenerator(logger *logging.Logger) *QuantumultXGenerator {
|
||||
return &QuantumultXGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成Quantumult X配置
|
||||
func (g *QuantumultXGenerator) Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error) {
|
||||
g.logger.Debugf("Generating Quantumult X configuration")
|
||||
|
||||
// 生成Quantumult X配置文本
|
||||
var builder strings.Builder
|
||||
|
||||
// 生成头部信息
|
||||
g.generateHeader(&builder, options)
|
||||
|
||||
// 生成服务器配置
|
||||
g.generateServers(&builder, configs)
|
||||
|
||||
// 生成远程服务器提供者
|
||||
g.generateRemoteServers(&builder, options)
|
||||
|
||||
// 生成策略组
|
||||
g.generatePolicyGroups(&builder, configs, options)
|
||||
|
||||
// 生成规则
|
||||
g.generateRules(&builder, options)
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// ValidateOptions 验证Quantumult X生成选项
|
||||
func (g *QuantumultXGenerator) ValidateOptions(options *GenerationOptions) error {
|
||||
g.logger.Debugf("Validating Quantumult X generation options")
|
||||
|
||||
// 验证基本信息
|
||||
if options.Name == "" {
|
||||
return fmt.Errorf("configuration name is required")
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
if options.MixedPort < 0 || options.MixedPort > 65535 {
|
||||
return fmt.Errorf("invalid mixed port: %d", options.MixedPort)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSupportedFormats 获取支持的格式
|
||||
func (g *QuantumultXGenerator) GetSupportedFormats() []string {
|
||||
return []string{"quantumultx"}
|
||||
}
|
||||
|
||||
// generateHeader 生成配置头部信息
|
||||
func (g *QuantumultXGenerator) generateHeader(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("[General]\n")
|
||||
builder.WriteString("network_check_url = http://www.gstatic.com/generate_204\n")
|
||||
builder.WriteString("ipv6 = false\n")
|
||||
builder.WriteString("prefer_ipv6 = false\n")
|
||||
builder.WriteString("wifi_assist = false\n")
|
||||
builder.WriteString("enable_policy = true\n")
|
||||
builder.WriteString("exclude_simple_hostnames = true\n")
|
||||
builder.WriteString("all_proxy_available = false\n")
|
||||
builder.WriteString("dns_server = 119.29.29.29, 223.5.5.5, 1.1.1.1, 8.8.8.8\n")
|
||||
builder.WriteString("fallback_dns_server = 223.5.5.5, 119.29.29.29\n")
|
||||
|
||||
if options.IPv6 {
|
||||
builder.WriteString("ipv6 = true\n")
|
||||
}
|
||||
|
||||
if options.MixedPort > 0 {
|
||||
builder.WriteString(fmt.Sprintf("http_port = %d\n", options.MixedPort))
|
||||
builder.WriteString(fmt.Sprintf("socks5_port = %d\n", options.MixedPort+1))
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// generateServers 生成服务器配置
|
||||
func (g *QuantumultXGenerator) generateServers(builder *strings.Builder, configs []*parser.ProxyConfig) {
|
||||
builder.WriteString("[Server]\n")
|
||||
|
||||
for i, config := range configs {
|
||||
server := g.convertProxyConfig(config)
|
||||
builder.WriteString(server)
|
||||
|
||||
if i < len(configs)-1 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) generateRemoteServers(builder *strings.Builder, options *GenerationOptions) {
|
||||
providers := g.resolveProviders(options)
|
||||
if len(providers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
builder.WriteString("[Server Remote]\n")
|
||||
for _, def := range providers {
|
||||
builder.WriteString(g.formatRemoteServer(def))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// convertProxyConfig 转换代理配置为Quantumult X格式
|
||||
func (g *QuantumultXGenerator) convertProxyConfig(proxyConfig *parser.ProxyConfig) string {
|
||||
switch proxyConfig.Protocol {
|
||||
case "ss":
|
||||
return g.generateSSProxy(proxyConfig)
|
||||
case "ssr":
|
||||
return g.generateSSRProxy(proxyConfig)
|
||||
case "vmess":
|
||||
return g.generateVMessProxy(proxyConfig)
|
||||
case "trojan":
|
||||
return g.generateTrojanProxy(proxyConfig)
|
||||
case "http", "https":
|
||||
return g.generateHTTPProxy(proxyConfig)
|
||||
case "socks5":
|
||||
return g.generateSocks5Proxy(proxyConfig)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// generateSSProxy 生成Shadowsocks代理配置
|
||||
func (g *QuantumultXGenerator) generateSSProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
plugin, _ := config.Settings["plugin"].(string)
|
||||
|
||||
if plugin != "" {
|
||||
return fmt.Sprintf("shadowsocks=%s:%d, method=%s, password=%s, plugin=%s, %s",
|
||||
config.Server, config.Port, method, password, plugin, g.generatePluginOpts(config))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("shadowsocks=%s:%d, method=%s, password=%s",
|
||||
config.Server, config.Port, method, password)
|
||||
}
|
||||
|
||||
// generateSSRProxy 生成ShadowsocksR代理配置
|
||||
func (g *QuantumultXGenerator) generateSSRProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
protocol, _ := config.Settings["protocol"].(string)
|
||||
protocolParam, _ := config.Settings["protocol-param"].(string)
|
||||
obfs, _ := config.Settings["obfs"].(string)
|
||||
obfsParam, _ := config.Settings["obfs-param"].(string)
|
||||
|
||||
return fmt.Sprintf("shadowsocks=%s:%d, method=%s, password=%s, ssr-protocol=%s, ssr-protocol-param=%s, obfs=%s, obfs-host=%s",
|
||||
config.Server, config.Port, method, password, protocol, protocolParam, obfs, obfsParam)
|
||||
}
|
||||
|
||||
// generateVMessProxy 生成VMess代理配置
|
||||
func (g *QuantumultXGenerator) generateVMessProxy(config *parser.ProxyConfig) string {
|
||||
uuid, _ := config.Settings["uuid"].(string)
|
||||
_, _ = config.Settings["alterId"].(int)
|
||||
network, _ := config.Settings["network"].(string)
|
||||
tls, _ := config.Settings["tls"].(string)
|
||||
host, _ := config.Settings["host"].(string)
|
||||
path, _ := config.Settings["path"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("%s:%d", config.Server, config.Port))
|
||||
opts = append(opts, fmt.Sprintf("method=%s", "chacha20-ietf-poly1305")) // Quantumult X默认加密方式
|
||||
opts = append(opts, fmt.Sprintf("password=%s", uuid))
|
||||
|
||||
if network == "ws" {
|
||||
opts = append(opts, fmt.Sprintf("obfs=ws"))
|
||||
if host != "" {
|
||||
opts = append(opts, fmt.Sprintf("obfs-host=%s", host))
|
||||
}
|
||||
if path != "" {
|
||||
opts = append(opts, fmt.Sprintf("obfs-uri=%s", path))
|
||||
}
|
||||
}
|
||||
|
||||
if tls != "" {
|
||||
opts = append(opts, fmt.Sprintf("tls-host=%s", host))
|
||||
opts = append(opts, fmt.Sprintf("over-tls=true"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("vmess=%s", strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateTrojanProxy 生成Trojan代理配置
|
||||
func (g *QuantumultXGenerator) generateTrojanProxy(config *parser.ProxyConfig) string {
|
||||
password, _ := config.Settings["password"].(string)
|
||||
sni, _ := config.Settings["sni"].(string)
|
||||
network, _ := config.Settings["network"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("%s:%d", config.Server, config.Port))
|
||||
opts = append(opts, fmt.Sprintf("password=%s", password))
|
||||
|
||||
if network == "ws" {
|
||||
opts = append(opts, fmt.Sprintf("obfs=ws"))
|
||||
if sni != "" {
|
||||
opts = append(opts, fmt.Sprintf("obfs-host=%s", sni))
|
||||
}
|
||||
}
|
||||
|
||||
if sni != "" {
|
||||
opts = append(opts, fmt.Sprintf("tls-host=%s", sni))
|
||||
opts = append(opts, fmt.Sprintf("over-tls=true"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("trojan=%s", strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateHTTPProxy 生成HTTP代理配置
|
||||
func (g *QuantumultXGenerator) generateHTTPProxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
tls, _ := config.Settings["tls"].(bool)
|
||||
|
||||
if username != "" && password != "" {
|
||||
if tls {
|
||||
return fmt.Sprintf("https=%s:%d, username=%s, password=%s",
|
||||
config.Server, config.Port, username, password)
|
||||
}
|
||||
return fmt.Sprintf("http=%s:%d, username=%s, password=%s",
|
||||
config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
if tls {
|
||||
return fmt.Sprintf("https=%s:%d",
|
||||
config.Server, config.Port)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http=%s:%d",
|
||||
config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generateSocks5Proxy 生成Socks5代理配置
|
||||
func (g *QuantumultXGenerator) generateSocks5Proxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
|
||||
if username != "" && password != "" {
|
||||
return fmt.Sprintf("socks5=%s:%d, username=%s, password=%s",
|
||||
config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("socks5=%s:%d",
|
||||
config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generatePluginOpts 生成插件选项
|
||||
func (g *QuantumultXGenerator) generatePluginOpts(config *parser.ProxyConfig) string {
|
||||
// 这里可以根据具体插件生成相应的选项
|
||||
return ""
|
||||
}
|
||||
|
||||
// generatePolicyGroups 生成策略组
|
||||
func (g *QuantumultXGenerator) generatePolicyGroups(builder *strings.Builder, configs []*parser.ProxyConfig, options *GenerationOptions) {
|
||||
builder.WriteString("[Policy]\n")
|
||||
|
||||
// 创建代理名称列表
|
||||
proxyNames := make([]string, 0)
|
||||
for _, config := range configs {
|
||||
proxyNames = append(proxyNames, config.Name)
|
||||
}
|
||||
|
||||
// 直连策略
|
||||
builder.WriteString("static=Direct, direct\n")
|
||||
|
||||
// 拒绝策略
|
||||
builder.WriteString("static=Reject, reject\n")
|
||||
|
||||
// 选择策略
|
||||
builder.WriteString(fmt.Sprintf("static=Proxy, %s\n", strings.Join(proxyNames, ", ")))
|
||||
|
||||
// URL测试策略
|
||||
if options.ProxyTest {
|
||||
testURL := options.ProxyURL
|
||||
if testURL == "" {
|
||||
testURL = "http://www.gstatic.com/generate_204"
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("url-test=URL-Test, %s, url=%s, interval=300\n",
|
||||
strings.Join(proxyNames, ", "), testURL))
|
||||
}
|
||||
|
||||
// 故障转移策略
|
||||
builder.WriteString(fmt.Sprintf("fallback=Fallback, %s, url=http://www.gstatic.com/generate_204, interval=300\n",
|
||||
strings.Join(proxyNames, ", ")))
|
||||
|
||||
if len(options.GroupDefinitions) > 0 {
|
||||
for _, def := range options.GroupDefinitions {
|
||||
builder.WriteString(g.formatPolicyGroup(def, proxyNames))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// generateRules 生成规则
|
||||
func (g *QuantumultXGenerator) generateRules(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("[Rule]\n")
|
||||
|
||||
// 添加默认规则
|
||||
if len(options.Rules) > 0 {
|
||||
builder.WriteString(strings.Join(options.Rules, "\n"))
|
||||
} 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",
|
||||
"FINAL,Proxy",
|
||||
}
|
||||
builder.WriteString(strings.Join(defaultRules, "\n"))
|
||||
}
|
||||
|
||||
// 启用IPv6支持
|
||||
if options.IPv6 {
|
||||
builder.WriteString("\n# IPv6 Rules\n")
|
||||
builder.WriteString("IP-CIDR6,::1/128,Direct\n")
|
||||
builder.WriteString("IP-CIDR6,fc00::/7,Direct\n")
|
||||
}
|
||||
|
||||
// 启用局域网支持
|
||||
if options.EnableLan {
|
||||
builder.WriteString("\n# LAN Rules\n")
|
||||
builder.WriteString("SRC-IP-CIDR,192.168.0.0/16,Direct\n")
|
||||
builder.WriteString("SRC-IP-CIDR,10.0.0.0/8,Direct\n")
|
||||
builder.WriteString("SRC-IP-CIDR,172.16.0.0/12,Direct\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) resolveProviders(options *GenerationOptions) []*ProviderDefinition {
|
||||
if len(options.Providers) > 0 {
|
||||
return options.Providers
|
||||
}
|
||||
if len(options.CustomProviders) == 0 {
|
||||
return nil
|
||||
}
|
||||
providers := make([]*ProviderDefinition, 0, len(options.CustomProviders))
|
||||
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 = append(providers, parsed)
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) formatRemoteServer(def *ProviderDefinition) string {
|
||||
parts := []string{def.URL, fmt.Sprintf("tag=%s", def.Name)}
|
||||
parts = append(parts, fmt.Sprintf("path=%s", def.Path))
|
||||
if def.Interval != nil {
|
||||
parts = append(parts, fmt.Sprintf("update-interval=%d", *def.Interval))
|
||||
}
|
||||
parts = append(parts, "opt-parser=true", "enabled=true")
|
||||
|
||||
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 {
|
||||
parts = append(parts, fmt.Sprintf("%s=%t", k, def.Flags[k]))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, def.Fields[k]))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = %s", def.Name, strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) formatPolicyGroup(def *GroupDefinition, defaultProxies []string) string {
|
||||
proxies := make([]string, 0, len(def.Proxies))
|
||||
proxies = append(proxies, def.Proxies...)
|
||||
if len(proxies) == 0 && len(def.UseProviders) == 0 {
|
||||
proxies = append(proxies, defaultProxies...)
|
||||
}
|
||||
|
||||
typeToken := g.policyTypeToken(def.Type)
|
||||
items := make([]string, 0, len(proxies))
|
||||
items = append(items, proxies...)
|
||||
|
||||
if len(def.UseProviders) > 0 {
|
||||
items = append(items, fmt.Sprintf("server-tag-regex=%s", g.providerRegex(def.UseProviders)))
|
||||
}
|
||||
if def.URL != "" {
|
||||
items = append(items, fmt.Sprintf("url=%s", def.URL))
|
||||
}
|
||||
if def.Interval != nil {
|
||||
items = append(items, fmt.Sprintf("interval=%d", *def.Interval))
|
||||
}
|
||||
if def.Timeout != nil {
|
||||
items = append(items, fmt.Sprintf("timeout=%d", *def.Timeout))
|
||||
}
|
||||
if def.Tolerance != nil {
|
||||
items = append(items, fmt.Sprintf("tolerance=%d", *def.Tolerance))
|
||||
}
|
||||
if len(def.Extras) > 0 {
|
||||
keys := make([]string, 0, len(def.Extras))
|
||||
for k := range def.Extras {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
items = append(items, fmt.Sprintf("%s=%s", k, def.Extras[k]))
|
||||
}
|
||||
}
|
||||
|
||||
joined := strings.Join(items, ", ")
|
||||
if joined != "" {
|
||||
joined = ", " + joined
|
||||
}
|
||||
return fmt.Sprintf("%s=%s%s", typeToken, def.Name, joined)
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) policyTypeToken(groupType string) string {
|
||||
switch strings.ToLower(groupType) {
|
||||
case "select":
|
||||
return "static"
|
||||
case "url-test":
|
||||
return "url-test"
|
||||
case "fallback":
|
||||
return "fallback"
|
||||
case "load-balance":
|
||||
return "available"
|
||||
default:
|
||||
return "static"
|
||||
}
|
||||
}
|
||||
|
||||
func (g *QuantumultXGenerator) providerRegex(providers []string) string {
|
||||
if len(providers) == 1 {
|
||||
return fmt.Sprintf("^%s$", providers[0])
|
||||
}
|
||||
return fmt.Sprintf("^(%s)$", strings.Join(providers, "|"))
|
||||
}
|
||||
321
internal/generator/surfboard.go
Normal file
321
internal/generator/surfboard.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
"github.com/subconverter-go/internal/parser"
|
||||
)
|
||||
|
||||
// SurfboardGenerator Surfboard格式生成器
|
||||
// 实现Surfboard代理配置的生成功能
|
||||
type SurfboardGenerator struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewSurfboardGenerator 创建新的Surfboard生成器
|
||||
// 返回初始化好的SurfboardGenerator实例
|
||||
func NewSurfboardGenerator(logger *logging.Logger) *SurfboardGenerator {
|
||||
return &SurfboardGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成Surfboard配置
|
||||
func (g *SurfboardGenerator) Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error) {
|
||||
g.logger.Debugf("Generating Surfboard configuration")
|
||||
|
||||
// 生成Surfboard配置文本
|
||||
var builder strings.Builder
|
||||
|
||||
// 生成头部信息
|
||||
g.generateHeader(&builder, options)
|
||||
|
||||
// 生成代理配置
|
||||
g.generateProxies(&builder, configs)
|
||||
|
||||
// 生成代理组
|
||||
g.generateProxyGroups(&builder, configs, options)
|
||||
|
||||
// 生成规则
|
||||
g.generateRules(&builder, options)
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// ValidateOptions 验证Surfboard生成选项
|
||||
func (g *SurfboardGenerator) ValidateOptions(options *GenerationOptions) error {
|
||||
g.logger.Debugf("Validating Surfboard generation options")
|
||||
|
||||
// 验证基本信息
|
||||
if options.Name == "" {
|
||||
return fmt.Errorf("configuration name is required")
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
if options.MixedPort < 0 || options.MixedPort > 65535 {
|
||||
return fmt.Errorf("invalid mixed port: %d", options.MixedPort)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSupportedFormats 获取支持的格式
|
||||
func (g *SurfboardGenerator) GetSupportedFormats() []string {
|
||||
return []string{"surfboard"}
|
||||
}
|
||||
|
||||
// generateHeader 生成配置头部信息
|
||||
func (g *SurfboardGenerator) generateHeader(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("# Surfboard Configuration\n")
|
||||
builder.WriteString("# Generated by SubConverter-Go\n")
|
||||
builder.WriteString(fmt.Sprintf("# Name: %s\n\n", options.Name))
|
||||
|
||||
// 基本配置
|
||||
builder.WriteString("[General]\n")
|
||||
builder.WriteString("fallback-dns-server = 223.5.5.5, 119.29.29.29\n")
|
||||
builder.WriteString("ipv6 = false\n")
|
||||
builder.WriteString("prefer-ipv6 = false\n")
|
||||
builder.WriteString("wifi-assist = false\n")
|
||||
builder.WriteString("skip-proxy = 127.0.0.1, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12\n")
|
||||
builder.WriteString("dns-server = 119.29.29.29, 223.5.5.5, 1.1.1.1\n")
|
||||
builder.WriteString("exclude-simple-hostnames = true\n")
|
||||
builder.WriteString("all-proxy-available = false\n")
|
||||
|
||||
if options.MixedPort > 0 {
|
||||
builder.WriteString(fmt.Sprintf("http-port = %d\n", options.MixedPort))
|
||||
builder.WriteString(fmt.Sprintf("socks5-port = %d\n", options.MixedPort+1))
|
||||
}
|
||||
|
||||
if options.IPv6 {
|
||||
builder.WriteString("ipv6 = true\n")
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// generateProxies 生成代理配置
|
||||
func (g *SurfboardGenerator) generateProxies(builder *strings.Builder, configs []*parser.ProxyConfig) {
|
||||
builder.WriteString("[Proxy]\n")
|
||||
|
||||
for i, config := range configs {
|
||||
proxy := g.convertProxyConfig(config)
|
||||
builder.WriteString(proxy)
|
||||
|
||||
if i < len(configs)-1 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// convertProxyConfig 转换代理配置为Surfboard格式
|
||||
func (g *SurfboardGenerator) convertProxyConfig(proxyConfig *parser.ProxyConfig) string {
|
||||
switch proxyConfig.Protocol {
|
||||
case "ss":
|
||||
return g.generateSSProxy(proxyConfig)
|
||||
case "ssr":
|
||||
return g.generateSSRProxy(proxyConfig)
|
||||
case "vmess":
|
||||
return g.generateVMessProxy(proxyConfig)
|
||||
case "trojan":
|
||||
return g.generateTrojanProxy(proxyConfig)
|
||||
case "http", "https":
|
||||
return g.generateHTTPProxy(proxyConfig)
|
||||
case "socks5":
|
||||
return g.generateSocks5Proxy(proxyConfig)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// generateSSProxy 生成Shadowsocks代理配置
|
||||
func (g *SurfboardGenerator) generateSSProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
|
||||
return fmt.Sprintf("%s = ss, %s, %d, encrypt-method=%s, password=%s",
|
||||
config.Name, config.Server, config.Port, method, password)
|
||||
}
|
||||
|
||||
// generateSSRProxy 生成ShadowsocksR代理配置
|
||||
func (g *SurfboardGenerator) generateSSRProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
protocol, _ := config.Settings["protocol"].(string)
|
||||
protocolParam, _ := config.Settings["protocol-param"].(string)
|
||||
obfs, _ := config.Settings["obfs"].(string)
|
||||
obfsParam, _ := config.Settings["obfs-param"].(string)
|
||||
|
||||
return fmt.Sprintf("%s = ssr, %s, %d, encrypt-method=%s, password=%s, protocol=%s, protocol-param=%s, obfs=%s, obfs-param=%s",
|
||||
config.Name, config.Server, config.Port, method, password, protocol, protocolParam, obfs, obfsParam)
|
||||
}
|
||||
|
||||
// generateVMessProxy 生成VMess代理配置
|
||||
func (g *SurfboardGenerator) generateVMessProxy(config *parser.ProxyConfig) string {
|
||||
uuid, _ := config.Settings["uuid"].(string)
|
||||
alterId, _ := config.Settings["alterId"].(int)
|
||||
network, _ := config.Settings["network"].(string)
|
||||
tls, _ := config.Settings["tls"].(string)
|
||||
host, _ := config.Settings["host"].(string)
|
||||
path, _ := config.Settings["path"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("%s:%d", config.Server, config.Port))
|
||||
opts = append(opts, fmt.Sprintf("%s", uuid))
|
||||
opts = append(opts, fmt.Sprintf("%d", alterId))
|
||||
|
||||
if network != "" {
|
||||
opts = append(opts, fmt.Sprintf("network=%s", network))
|
||||
}
|
||||
|
||||
if tls != "" {
|
||||
opts = append(opts, fmt.Sprintf("tls=%s", tls))
|
||||
}
|
||||
|
||||
if host != "" {
|
||||
opts = append(opts, fmt.Sprintf("ws-host=%s", host))
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
opts = append(opts, fmt.Sprintf("ws-path=%s", path))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = vmess, %s",
|
||||
config.Name, strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateTrojanProxy 生成Trojan代理配置
|
||||
func (g *SurfboardGenerator) generateTrojanProxy(config *parser.ProxyConfig) string {
|
||||
password, _ := config.Settings["password"].(string)
|
||||
sni, _ := config.Settings["sni"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("%s:%d", config.Server, config.Port))
|
||||
opts = append(opts, fmt.Sprintf("%s", password))
|
||||
|
||||
if sni != "" {
|
||||
opts = append(opts, fmt.Sprintf("sni=%s", sni))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = trojan, %s",
|
||||
config.Name, strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateHTTPProxy 生成HTTP代理配置
|
||||
func (g *SurfboardGenerator) generateHTTPProxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
tls, _ := config.Settings["tls"].(bool)
|
||||
|
||||
if username != "" && password != "" {
|
||||
if tls {
|
||||
return fmt.Sprintf("%s = https, %s, %d, username=%s, password=%s",
|
||||
config.Name, config.Server, config.Port, username, password)
|
||||
}
|
||||
return fmt.Sprintf("%s = http, %s, %d, username=%s, password=%s",
|
||||
config.Name, config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
if tls {
|
||||
return fmt.Sprintf("%s = https, %s, %d",
|
||||
config.Name, config.Server, config.Port)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = http, %s, %d",
|
||||
config.Name, config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generateSocks5Proxy 生成Socks5代理配置
|
||||
func (g *SurfboardGenerator) generateSocks5Proxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
|
||||
if username != "" && password != "" {
|
||||
return fmt.Sprintf("%s = socks5, %s, %d, username=%s, password=%s",
|
||||
config.Name, config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = socks5, %s, %d",
|
||||
config.Name, config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generateProxyGroups 生成代理组
|
||||
func (g *SurfboardGenerator) generateProxyGroups(builder *strings.Builder, configs []*parser.ProxyConfig, options *GenerationOptions) {
|
||||
builder.WriteString("[Proxy Group]\n")
|
||||
|
||||
// 创建代理名称列表
|
||||
proxyNames := make([]string, 0)
|
||||
for _, config := range configs {
|
||||
proxyNames = append(proxyNames, config.Name)
|
||||
}
|
||||
|
||||
// 选择代理组
|
||||
builder.WriteString(fmt.Sprintf("Proxy = select, %s\n", strings.Join(proxyNames, ", ")))
|
||||
|
||||
// URL测试代理组
|
||||
if options.ProxyTest {
|
||||
testURL := options.ProxyURL
|
||||
if testURL == "" {
|
||||
testURL = "http://www.gstatic.com/generate_204"
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("URL-Test = url-test, %s, url=%s, interval=300\n",
|
||||
strings.Join(proxyNames, ", "), testURL))
|
||||
}
|
||||
|
||||
// 故障转移代理组
|
||||
builder.WriteString(fmt.Sprintf("Fallback = fallback, %s, url=http://www.gstatic.com/generate_204, interval=300\n",
|
||||
strings.Join(proxyNames, ", ")))
|
||||
|
||||
// 直连和拒绝代理组
|
||||
builder.WriteString("Direct = select, DIRECT\n")
|
||||
builder.WriteString("Reject = select, REJECT\n\n")
|
||||
}
|
||||
|
||||
// generateRules 生成规则
|
||||
func (g *SurfboardGenerator) generateRules(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("[Rule]\n")
|
||||
|
||||
// 添加默认规则
|
||||
if len(options.Rules) > 0 {
|
||||
builder.WriteString(strings.Join(options.Rules, "\n"))
|
||||
} 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",
|
||||
"FINAL,Proxy",
|
||||
}
|
||||
builder.WriteString(strings.Join(defaultRules, "\n"))
|
||||
}
|
||||
|
||||
// 启用IPv6支持
|
||||
if options.IPv6 {
|
||||
builder.WriteString("\n# IPv6 Rules\n")
|
||||
builder.WriteString("IP-CIDR6,::1/128,DIRECT\n")
|
||||
builder.WriteString("IP-CIDR6,fc00::/7,DIRECT\n")
|
||||
}
|
||||
|
||||
// 启用局域网支持
|
||||
if options.EnableLan {
|
||||
builder.WriteString("\n# LAN Rules\n")
|
||||
builder.WriteString("SRC-IP-CIDR,192.168.0.0/16,DIRECT\n")
|
||||
builder.WriteString("SRC-IP-CIDR,10.0.0.0/8,DIRECT\n")
|
||||
builder.WriteString("SRC-IP-CIDR,172.16.0.0/12,DIRECT\n")
|
||||
}
|
||||
}
|
||||
489
internal/generator/surge.go
Normal file
489
internal/generator/surge.go
Normal file
@@ -0,0 +1,489 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
"github.com/subconverter-go/internal/parser"
|
||||
)
|
||||
|
||||
// SurgeGenerator Surge格式生成器
|
||||
// 实现Surge代理配置的生成功能
|
||||
type SurgeGenerator struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewSurgeGenerator 创建新的Surge生成器
|
||||
// 返回初始化好的SurgeGenerator实例
|
||||
func NewSurgeGenerator(logger *logging.Logger) *SurgeGenerator {
|
||||
return &SurgeGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成Surge配置
|
||||
func (g *SurgeGenerator) Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error) {
|
||||
g.logger.Debugf("Generating Surge configuration")
|
||||
|
||||
// 生成Surge配置文本
|
||||
var builder strings.Builder
|
||||
|
||||
// 生成头部信息
|
||||
g.generateHeader(&builder, options)
|
||||
|
||||
// 生成代理提供者
|
||||
g.generateProviders(&builder, options)
|
||||
|
||||
// 生成代理配置
|
||||
g.generateProxies(&builder, configs)
|
||||
|
||||
// 生成代理组
|
||||
g.generateProxyGroups(&builder, configs, options)
|
||||
|
||||
// 生成规则
|
||||
g.generateRules(&builder, options)
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// ValidateOptions 验证Surge生成选项
|
||||
func (g *SurgeGenerator) ValidateOptions(options *GenerationOptions) error {
|
||||
g.logger.Debugf("Validating Surge generation options")
|
||||
|
||||
// 验证基本信息
|
||||
if options.Name == "" {
|
||||
return fmt.Errorf("configuration name is required")
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
if options.MixedPort < 0 || options.MixedPort > 65535 {
|
||||
return fmt.Errorf("invalid mixed port: %d", options.MixedPort)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSupportedFormats 获取支持的格式
|
||||
func (g *SurgeGenerator) GetSupportedFormats() []string {
|
||||
return []string{"surge"}
|
||||
}
|
||||
|
||||
// generateHeader 生成配置头部信息
|
||||
func (g *SurgeGenerator) generateHeader(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("# Surge Configuration\n")
|
||||
builder.WriteString("# Generated by SubConverter-Go\n")
|
||||
builder.WriteString(fmt.Sprintf("# Name: %s\n\n", options.Name))
|
||||
|
||||
// 基本配置
|
||||
builder.WriteString("[General]\n")
|
||||
builder.WriteString("skip-proxy = 127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12\n")
|
||||
|
||||
if options.MixedPort > 0 {
|
||||
builder.WriteString(fmt.Sprintf("http-port = %d\n", options.MixedPort))
|
||||
builder.WriteString(fmt.Sprintf("socks5-port = %d\n", options.MixedPort+1))
|
||||
}
|
||||
|
||||
builder.WriteString("allow-wifi-access = false\n")
|
||||
builder.WriteString("wifi-access-http-port = 6152\n")
|
||||
builder.WriteString("wifi-access-socks5-port = 6153\n")
|
||||
|
||||
if options.IPv6 {
|
||||
builder.WriteString("ipv6 = true\n")
|
||||
} else {
|
||||
builder.WriteString("ipv6 = false\n")
|
||||
}
|
||||
|
||||
builder.WriteString("prefer-ipv6 = false\n")
|
||||
builder.WriteString("dns-server = 119.29.29.29, 223.5.5.5, 1.1.1.1, 8.8.8.8\n")
|
||||
builder.WriteString("fallback-dns-server = 223.5.5.5, 119.29.29.29\n")
|
||||
builder.WriteString("exclude-simple-hostnames = true\n")
|
||||
builder.WriteString("show-error-page-for-reject = true\n")
|
||||
builder.WriteString("always-real-ip = true\n")
|
||||
|
||||
if options.EnableLan {
|
||||
builder.WriteString("enhanced-mode-by-rule = true\n")
|
||||
} else {
|
||||
builder.WriteString("enhanced-mode-by-rule = false\n")
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
func (g *SurgeGenerator) generateProviders(builder *strings.Builder, options *GenerationOptions) {
|
||||
providers := g.resolveProviders(options)
|
||||
if len(providers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
builder.WriteString("[Proxy Provider]\n")
|
||||
for _, def := range providers {
|
||||
builder.WriteString(g.formatProviderLine(def))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// generateProxies 生成代理配置
|
||||
func (g *SurgeGenerator) generateProxies(builder *strings.Builder, configs []*parser.ProxyConfig) {
|
||||
builder.WriteString("[Proxy]\n")
|
||||
|
||||
for i, config := range configs {
|
||||
proxy := g.convertProxyConfig(config)
|
||||
builder.WriteString(proxy)
|
||||
|
||||
if i < len(configs)-1 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// convertProxyConfig 转换代理配置为Surge格式
|
||||
func (g *SurgeGenerator) convertProxyConfig(proxyConfig *parser.ProxyConfig) string {
|
||||
switch proxyConfig.Protocol {
|
||||
case "ss":
|
||||
return g.generateSSProxy(proxyConfig)
|
||||
case "ssr":
|
||||
return g.generateSSRProxy(proxyConfig)
|
||||
case "vmess":
|
||||
return g.generateVMessProxy(proxyConfig)
|
||||
case "trojan":
|
||||
return g.generateTrojanProxy(proxyConfig)
|
||||
case "http", "https":
|
||||
return g.generateHTTPProxy(proxyConfig)
|
||||
case "socks5":
|
||||
return g.generateSocks5Proxy(proxyConfig)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// generateSSProxy 生成Shadowsocks代理配置
|
||||
func (g *SurgeGenerator) generateSSProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
plugin, _ := config.Settings["plugin"].(string)
|
||||
|
||||
if plugin != "" {
|
||||
return fmt.Sprintf("%s = ss, %s, %d, encrypt-method=%s, password=%s, plugin=%s, plugin-opts=%s",
|
||||
config.Name, config.Server, config.Port, method, password, plugin, g.generatePluginOpts(config))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = ss, %s, %d, encrypt-method=%s, password=%s",
|
||||
config.Name, config.Server, config.Port, method, password)
|
||||
}
|
||||
|
||||
// generateSSRProxy 生成ShadowsocksR代理配置
|
||||
func (g *SurgeGenerator) generateSSRProxy(config *parser.ProxyConfig) string {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
protocol, _ := config.Settings["protocol"].(string)
|
||||
protocolParam, _ := config.Settings["protocol-param"].(string)
|
||||
obfs, _ := config.Settings["obfs"].(string)
|
||||
obfsParam, _ := config.Settings["obfs-param"].(string)
|
||||
|
||||
return fmt.Sprintf("%s = ssr, %s, %d, encrypt-method=%s, password=%s, protocol=%s, protocol-param=%s, obfs=%s, obfs-param=%s",
|
||||
config.Name, config.Server, config.Port, method, password, protocol, protocolParam, obfs, obfsParam)
|
||||
}
|
||||
|
||||
// generateVMessProxy 生成VMess代理配置
|
||||
func (g *SurgeGenerator) generateVMessProxy(config *parser.ProxyConfig) string {
|
||||
uuid, _ := config.Settings["uuid"].(string)
|
||||
alterId, _ := config.Settings["alterId"].(int)
|
||||
network, _ := config.Settings["network"].(string)
|
||||
tls, _ := config.Settings["tls"].(string)
|
||||
host, _ := config.Settings["host"].(string)
|
||||
path, _ := config.Settings["path"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("uuid=%s", uuid))
|
||||
opts = append(opts, fmt.Sprintf("alterId=%d", alterId))
|
||||
|
||||
if network != "" {
|
||||
opts = append(opts, fmt.Sprintf("network=%s", network))
|
||||
}
|
||||
|
||||
if tls != "" {
|
||||
opts = append(opts, fmt.Sprintf("tls=%s", tls))
|
||||
}
|
||||
|
||||
if host != "" {
|
||||
opts = append(opts, fmt.Sprintf("ws-host=%s", host))
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
opts = append(opts, fmt.Sprintf("ws-path=%s", path))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = vmess, %s, %d, %s",
|
||||
config.Name, config.Server, config.Port, strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateTrojanProxy 生成Trojan代理配置
|
||||
func (g *SurgeGenerator) generateTrojanProxy(config *parser.ProxyConfig) string {
|
||||
password, _ := config.Settings["password"].(string)
|
||||
sni, _ := config.Settings["sni"].(string)
|
||||
network, _ := config.Settings["network"].(string)
|
||||
|
||||
var opts []string
|
||||
opts = append(opts, fmt.Sprintf("password=%s", password))
|
||||
|
||||
if sni != "" {
|
||||
opts = append(opts, fmt.Sprintf("sni=%s", sni))
|
||||
}
|
||||
|
||||
if network != "" {
|
||||
opts = append(opts, fmt.Sprintf("network=%s", network))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = trojan, %s, %d, %s",
|
||||
config.Name, config.Server, config.Port, strings.Join(opts, ", "))
|
||||
}
|
||||
|
||||
// generateHTTPProxy 生成HTTP代理配置
|
||||
func (g *SurgeGenerator) generateHTTPProxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
tls, _ := config.Settings["tls"].(bool)
|
||||
|
||||
if username != "" && password != "" {
|
||||
if tls {
|
||||
return fmt.Sprintf("%s = https, %s, %d, username=%s, password=%s",
|
||||
config.Name, config.Server, config.Port, username, password)
|
||||
}
|
||||
return fmt.Sprintf("%s = http, %s, %d, username=%s, password=%s",
|
||||
config.Name, config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
if tls {
|
||||
return fmt.Sprintf("%s = https, %s, %d",
|
||||
config.Name, config.Server, config.Port)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = http, %s, %d",
|
||||
config.Name, config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generateSocks5Proxy 生成Socks5代理配置
|
||||
func (g *SurgeGenerator) generateSocks5Proxy(config *parser.ProxyConfig) string {
|
||||
username, _ := config.Settings["username"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
|
||||
if username != "" && password != "" {
|
||||
return fmt.Sprintf("%s = socks5, %s, %d, username=%s, password=%s",
|
||||
config.Name, config.Server, config.Port, username, password)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = socks5, %s, %d",
|
||||
config.Name, config.Server, config.Port)
|
||||
}
|
||||
|
||||
// generatePluginOpts 生成插件选项
|
||||
func (g *SurgeGenerator) generatePluginOpts(config *parser.ProxyConfig) string {
|
||||
// 这里可以根据具体插件生成相应的选项
|
||||
return ""
|
||||
}
|
||||
|
||||
// generateProxyGroups 生成代理组
|
||||
func (g *SurgeGenerator) generateProxyGroups(builder *strings.Builder, configs []*parser.ProxyConfig, options *GenerationOptions) {
|
||||
builder.WriteString("[Proxy Group]\n")
|
||||
|
||||
// 创建代理名称列表
|
||||
proxyNames := make([]string, 0)
|
||||
for _, config := range configs {
|
||||
proxyNames = append(proxyNames, config.Name)
|
||||
}
|
||||
|
||||
// 选择代理组
|
||||
builder.WriteString(fmt.Sprintf("Proxy = select, %s\n", strings.Join(proxyNames, ", ")))
|
||||
|
||||
// URL测试代理组
|
||||
if options.ProxyTest {
|
||||
testURL := options.ProxyURL
|
||||
if testURL == "" {
|
||||
testURL = "http://www.gstatic.com/generate_204"
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("URL-Test = url-test, %s, url=%s, interval=300\n",
|
||||
strings.Join(proxyNames, ", "), testURL))
|
||||
}
|
||||
|
||||
// 故障转移代理组
|
||||
builder.WriteString(fmt.Sprintf("Fallback = fallback, %s, url=http://www.gstatic.com/generate_204, interval=300\n",
|
||||
strings.Join(proxyNames, ", ")))
|
||||
|
||||
// 直连和拒绝代理组
|
||||
builder.WriteString("Direct = select, DIRECT\n")
|
||||
builder.WriteString("Reject = select, REJECT\n")
|
||||
|
||||
if len(options.GroupDefinitions) > 0 {
|
||||
for _, def := range options.GroupDefinitions {
|
||||
builder.WriteString(g.formatCustomGroup(def, proxyNames))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
// generateRules 生成规则
|
||||
func (g *SurgeGenerator) generateRules(builder *strings.Builder, options *GenerationOptions) {
|
||||
builder.WriteString("[Rule]\n")
|
||||
|
||||
// 添加默认规则
|
||||
if len(options.Rules) > 0 {
|
||||
builder.WriteString(strings.Join(options.Rules, "\n"))
|
||||
} 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",
|
||||
"FINAL,Proxy",
|
||||
}
|
||||
builder.WriteString(strings.Join(defaultRules, "\n"))
|
||||
}
|
||||
|
||||
// 启用IPv6支持
|
||||
if options.IPv6 {
|
||||
builder.WriteString("\n# IPv6 Rules\n")
|
||||
builder.WriteString("IP-CIDR6,::1/128,DIRECT\n")
|
||||
builder.WriteString("IP-CIDR6,fc00::/7,DIRECT\n")
|
||||
}
|
||||
|
||||
// 启用局域网支持
|
||||
if options.EnableLan {
|
||||
builder.WriteString("\n# LAN Rules\n")
|
||||
builder.WriteString("SRC-IP-CIDR,192.168.0.0/16,DIRECT\n")
|
||||
builder.WriteString("SRC-IP-CIDR,10.0.0.0/8,DIRECT\n")
|
||||
builder.WriteString("SRC-IP-CIDR,172.16.0.0/12,DIRECT\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (g *SurgeGenerator) resolveProviders(options *GenerationOptions) []*ProviderDefinition {
|
||||
if len(options.Providers) > 0 {
|
||||
return options.Providers
|
||||
}
|
||||
if len(options.CustomProviders) == 0 {
|
||||
return nil
|
||||
}
|
||||
providers := make([]*ProviderDefinition, 0, len(options.CustomProviders))
|
||||
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 = append(providers, parsed)
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
func (g *SurgeGenerator) formatProviderLine(def *ProviderDefinition) string {
|
||||
parts := []string{strings.ToLower(def.Type), def.URL}
|
||||
parts = append(parts, fmt.Sprintf("path=%s", def.Path))
|
||||
if def.Interval != nil {
|
||||
parts = append(parts, fmt.Sprintf("interval=%d", *def.Interval))
|
||||
}
|
||||
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 {
|
||||
parts = append(parts, fmt.Sprintf("%s=%t", k, def.Flags[k]))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, def.Fields[k]))
|
||||
}
|
||||
}
|
||||
if def.Health != nil {
|
||||
if def.Health.Enable != nil {
|
||||
parts = append(parts, fmt.Sprintf("health-check=%t", *def.Health.Enable))
|
||||
}
|
||||
if def.Health.URL != "" {
|
||||
parts = append(parts, fmt.Sprintf("health-check-url=%s", def.Health.URL))
|
||||
}
|
||||
if def.Health.Interval != nil {
|
||||
parts = append(parts, fmt.Sprintf("health-check-interval=%d", *def.Health.Interval))
|
||||
}
|
||||
if def.Health.Lazy != nil {
|
||||
parts = append(parts, fmt.Sprintf("health-check-lazy=%t", *def.Health.Lazy))
|
||||
}
|
||||
if def.Health.Tolerance != nil {
|
||||
parts = append(parts, fmt.Sprintf("health-check-tolerance=%d", *def.Health.Tolerance))
|
||||
}
|
||||
if def.Health.Timeout != nil {
|
||||
parts = append(parts, fmt.Sprintf("health-check-timeout=%d", *def.Health.Timeout))
|
||||
}
|
||||
if def.Health.Method != "" {
|
||||
parts = append(parts, fmt.Sprintf("health-check-method=%s", def.Health.Method))
|
||||
}
|
||||
if def.Health.Headers != "" {
|
||||
parts = append(parts, fmt.Sprintf("health-check-headers=%s", def.Health.Headers))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = %s", def.Name, strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
func (g *SurgeGenerator) formatCustomGroup(def *GroupDefinition, defaultProxies []string) string {
|
||||
proxies := make([]string, 0, len(def.Proxies))
|
||||
proxies = append(proxies, def.Proxies...)
|
||||
if len(proxies) == 0 && len(def.UseProviders) == 0 {
|
||||
proxies = append(proxies, defaultProxies...)
|
||||
}
|
||||
|
||||
items := make([]string, 0, 1+len(proxies)+len(def.UseProviders))
|
||||
items = append(items, def.Type)
|
||||
items = append(items, proxies...)
|
||||
for _, provider := range def.UseProviders {
|
||||
items = append(items, fmt.Sprintf("use-provider=%s", provider))
|
||||
}
|
||||
if def.URL != "" {
|
||||
items = append(items, fmt.Sprintf("url=%s", def.URL))
|
||||
}
|
||||
if def.Interval != nil {
|
||||
items = append(items, fmt.Sprintf("interval=%d", *def.Interval))
|
||||
}
|
||||
if def.Timeout != nil {
|
||||
items = append(items, fmt.Sprintf("timeout=%d", *def.Timeout))
|
||||
}
|
||||
if def.Tolerance != nil {
|
||||
items = append(items, fmt.Sprintf("tolerance=%d", *def.Tolerance))
|
||||
}
|
||||
if len(def.Extras) > 0 {
|
||||
keys := make([]string, 0, len(def.Extras))
|
||||
for k := range def.Extras {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
items = append(items, fmt.Sprintf("%s=%s", k, def.Extras[k]))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = %s", def.Name, strings.Join(items, ", "))
|
||||
}
|
||||
322
internal/generator/v2ray.go
Normal file
322
internal/generator/v2ray.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/subconverter-go/internal/logging"
|
||||
"github.com/subconverter-go/internal/parser"
|
||||
)
|
||||
|
||||
// V2RayGenerator V2Ray格式生成器
|
||||
// 实现V2Ray代理配置的生成功能
|
||||
type V2RayGenerator struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewV2RayGenerator 创建新的V2Ray生成器
|
||||
// 返回初始化好的V2RayGenerator实例
|
||||
func NewV2RayGenerator(logger *logging.Logger) *V2RayGenerator {
|
||||
return &V2RayGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成V2Ray配置
|
||||
func (g *V2RayGenerator) Generate(configs []*parser.ProxyConfig, options *GenerationOptions) (string, error) {
|
||||
g.logger.Debugf("Generating V2Ray configuration")
|
||||
|
||||
// 创建V2Ray配置结构
|
||||
v2rayConfig := g.createV2RayConfig(configs, options)
|
||||
|
||||
// 转换为JSON
|
||||
jsonData, err := json.MarshalIndent(v2rayConfig, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal V2Ray configuration: %v", err)
|
||||
}
|
||||
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// ValidateOptions 验证V2Ray生成选项
|
||||
func (g *V2RayGenerator) ValidateOptions(options *GenerationOptions) error {
|
||||
g.logger.Debugf("Validating V2Ray generation options")
|
||||
|
||||
// 验证基本信息
|
||||
if options.Name == "" {
|
||||
return fmt.Errorf("configuration name is required")
|
||||
}
|
||||
|
||||
// 验证端口
|
||||
if options.MixedPort < 0 || options.MixedPort > 65535 {
|
||||
return fmt.Errorf("invalid mixed port: %d", options.MixedPort)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSupportedFormats 获取支持的格式
|
||||
func (g *V2RayGenerator) GetSupportedFormats() []string {
|
||||
return []string{"v2ray"}
|
||||
}
|
||||
|
||||
// V2RayConfig V2Ray配置结构体
|
||||
type V2RayConfig struct {
|
||||
Log *V2RayLog `json:"log,omitempty"`
|
||||
Inbound *V2RayInbound `json:"inbounds"`
|
||||
Outbound *V2RayOutbound `json:"outbounds"`
|
||||
Routing *V2RayRouting `json:"routing,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayLog 日志配置
|
||||
type V2RayLog struct {
|
||||
Loglevel string `json:"loglevel,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayInbound 入站配置
|
||||
type V2RayInbound struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Listen string `json:"listen"`
|
||||
Port int `json:"port"`
|
||||
Settings *V2RayInSettings `json:"settings,omitempty"`
|
||||
Sniffing *V2RaySniffing `json:"sniffing,omitempty"`
|
||||
Stream *V2RayStream `json:"streamSettings,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayInSettings 入站设置
|
||||
type V2RayInSettings struct {
|
||||
Auth string `json:"auth,omitempty"`
|
||||
UDP bool `json:"udp,omitempty"`
|
||||
UserLevel int `json:"userLevel,omitempty"`
|
||||
Accounts []V2RayAccount `json:"accounts,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayAccount 账户信息
|
||||
type V2RayAccount struct {
|
||||
User string `json:"user"`
|
||||
Pass string `json:"pass"`
|
||||
}
|
||||
|
||||
// V2RaySniffing 流量嗅探
|
||||
type V2RaySniffing struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DestOverride []string `json:"destOverride,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayStream 流媒体设置
|
||||
type V2RayStream struct {
|
||||
Network string `json:"network,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayOutbound 出站配置
|
||||
type V2RayOutbound struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Settings *V2RayOutSettings `json:"settings,omitempty"`
|
||||
Stream *V2RayStream `json:"streamSettings,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayOutSettings 出站设置
|
||||
type V2RayOutSettings struct {
|
||||
VNext []V2RayVNext `json:"vnext,omitempty"`
|
||||
Servers []V2RayServer `json:"servers,omitempty"`
|
||||
Users []V2RayUser `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayVNext 服务器节点
|
||||
type V2RayVNext struct {
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Users []V2RayUser `json:"users"`
|
||||
}
|
||||
|
||||
// V2RayUser 用户信息
|
||||
type V2RayUser struct {
|
||||
ID string `json:"id"`
|
||||
AlterID int `json:"alterId,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Security string `json:"security,omitempty"`
|
||||
Level int `json:"level,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayServer 服务器信息
|
||||
type V2RayServer struct {
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
OTA bool `json:"ota,omitempty"`
|
||||
Users []V2RayUser `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
// V2RayRouting 路由配置
|
||||
type V2RayRouting struct {
|
||||
Rules []V2RayRule `json:"rules"`
|
||||
}
|
||||
|
||||
// V2RayRule 路由规则
|
||||
type V2RayRule struct {
|
||||
Type string `json:"type"`
|
||||
InboundTag []string `json:"inboundTag,omitempty"`
|
||||
OutboundTag string `json:"outboundTag,omitempty"`
|
||||
Domain []string `json:"domain,omitempty"`
|
||||
IP []string `json:"ip,omitempty"`
|
||||
}
|
||||
|
||||
// createV2RayConfig 创建V2Ray配置结构
|
||||
func (g *V2RayGenerator) createV2RayConfig(configs []*parser.ProxyConfig, options *GenerationOptions) *V2RayConfig {
|
||||
config := &V2RayConfig{
|
||||
Log: &V2RayLog{
|
||||
Loglevel: "info",
|
||||
},
|
||||
Inbound: g.createInbound(options),
|
||||
Outbound: g.createOutbound(configs),
|
||||
Routing: g.createRouting(),
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// createInbound 创建入站配置
|
||||
func (g *V2RayGenerator) createInbound(options *GenerationOptions) *V2RayInbound {
|
||||
port := 10808
|
||||
if options.MixedPort > 0 {
|
||||
port = options.MixedPort
|
||||
}
|
||||
|
||||
return &V2RayInbound{
|
||||
Protocol: "socks",
|
||||
Listen: "127.0.0.1",
|
||||
Port: port,
|
||||
Settings: &V2RayInSettings{
|
||||
Auth: "noauth",
|
||||
UDP: true,
|
||||
UserLevel: 0,
|
||||
},
|
||||
Sniffing: &V2RaySniffing{
|
||||
Enabled: true,
|
||||
DestOverride: []string{"http", "tls"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createOutbound 创建出站配置
|
||||
func (g *V2RayGenerator) createOutbound(configs []*parser.ProxyConfig) *V2RayOutbound {
|
||||
// 代理节点列表
|
||||
vnext := make([]V2RayVNext, 0)
|
||||
servers := make([]V2RayServer, 0)
|
||||
|
||||
for _, config := range configs {
|
||||
switch config.Protocol {
|
||||
case "vmess":
|
||||
vnext = append(vnext, g.createVMessVNext(config))
|
||||
case "ss":
|
||||
servers = append(servers, g.createShadowsocksServer(config))
|
||||
case "trojan":
|
||||
servers = append(servers, g.createTrojanServer(config))
|
||||
}
|
||||
}
|
||||
|
||||
outbound := &V2RayOutbound{
|
||||
Protocol: "freedom",
|
||||
Tag: "direct",
|
||||
}
|
||||
|
||||
// 如果有代理节点,创建代理出站
|
||||
if len(vnext) > 0 || len(servers) > 0 {
|
||||
if len(vnext) > 0 {
|
||||
outbound = &V2RayOutbound{
|
||||
Protocol: "vmess",
|
||||
Tag: "proxy",
|
||||
Settings: &V2RayOutSettings{
|
||||
VNext: vnext,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
outbound = &V2RayOutbound{
|
||||
Protocol: "shadowsocks",
|
||||
Tag: "proxy",
|
||||
Settings: &V2RayOutSettings{
|
||||
Servers: servers,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outbound
|
||||
}
|
||||
|
||||
// createVMessVNext 创建VMess节点配置
|
||||
func (g *V2RayGenerator) createVMessVNext(config *parser.ProxyConfig) V2RayVNext {
|
||||
uuid, _ := config.Settings["uuid"].(string)
|
||||
alterId, _ := config.Settings["alterId"].(int)
|
||||
if alterId == 0 {
|
||||
alterId = 0
|
||||
}
|
||||
|
||||
return V2RayVNext{
|
||||
Address: config.Server,
|
||||
Port: config.Port,
|
||||
Users: []V2RayUser{
|
||||
{
|
||||
ID: uuid,
|
||||
AlterID: alterId,
|
||||
Security: "auto",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createShadowsocksServer 创建Shadowsocks服务器配置
|
||||
func (g *V2RayGenerator) createShadowsocksServer(config *parser.ProxyConfig) V2RayServer {
|
||||
method, _ := config.Settings["method"].(string)
|
||||
password, _ := config.Settings["password"].(string)
|
||||
|
||||
return V2RayServer{
|
||||
Address: config.Server,
|
||||
Port: config.Port,
|
||||
Method: method,
|
||||
Password: password,
|
||||
OTA: false,
|
||||
}
|
||||
}
|
||||
|
||||
// createTrojanServer 创建Trojan服务器配置
|
||||
func (g *V2RayGenerator) createTrojanServer(config *parser.ProxyConfig) V2RayServer {
|
||||
password, _ := config.Settings["password"].(string)
|
||||
|
||||
return V2RayServer{
|
||||
Address: config.Server,
|
||||
Port: config.Port,
|
||||
Method: "chacha20-ietf-poly1305",
|
||||
Password: password,
|
||||
OTA: false,
|
||||
}
|
||||
}
|
||||
|
||||
// createRouting 创建路由配置
|
||||
func (g *V2RayGenerator) createRouting() *V2RayRouting {
|
||||
return &V2RayRouting{
|
||||
Rules: []V2RayRule{
|
||||
{
|
||||
Type: "field",
|
||||
InboundTag: []string{"inbound"},
|
||||
OutboundTag: "proxy",
|
||||
},
|
||||
{
|
||||
Type: "field",
|
||||
Domain: []string{"geosite:cn"},
|
||||
OutboundTag: "direct",
|
||||
},
|
||||
{
|
||||
Type: "field",
|
||||
IP: []string{"geoip:cn", "geoip:private"},
|
||||
OutboundTag: "direct",
|
||||
},
|
||||
{
|
||||
Type: "field",
|
||||
OutboundTag: "proxy",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user