Files
SingBox-Gopanel/internal/protocol/clash.go
CN-JS-HuiBai 4ba835b93f
Some checks failed
build / build (api, amd64, linux) (push) Failing after -46s
build / build (api, arm64, linux) (push) Failing after -51s
build / build (api.exe, amd64, windows) (push) Failing after -51s
进一步查看前半部分对不对
2026-04-17 23:45:44 +08:00

313 lines
7.4 KiB
Go

package protocol
import (
"fmt"
"strings"
"xboard-go/internal/model"
"xboard-go/internal/service"
"github.com/goccy/go-yaml"
)
func GenerateClash(servers []model.Server, user model.User) (string, error) {
return GenerateClashWithTemplate("clash", servers, user)
}
func GenerateClashWithTemplate(templateName string, servers []model.Server, user model.User) (string, error) {
template := strings.TrimSpace(service.GetSubscribeTemplate(templateName))
if template == "" {
return generateClashFallback(servers, user), nil
}
var config map[string]any
if err := yaml.Unmarshal([]byte(template), &config); err != nil {
return generateClashFallback(servers, user), nil
}
proxies := make([]any, 0, len(servers))
proxyNames := make([]string, 0, len(servers))
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
password := service.GenerateServerPassword(&s, &user)
proxy := buildClashProxy(templateName, conf, password)
if proxy == nil {
continue
}
proxies = append(proxies, proxy)
proxyNames = append(proxyNames, conf.Name)
}
config["proxies"] = append(anySlice(config["proxies"]), proxies...)
config["proxy-groups"] = mergeClashProxyGroups(anySlice(config["proxy-groups"]), proxyNames)
if _, ok := config["rules"]; !ok {
config["rules"] = []any{"MATCH,Proxy"}
}
data, err := yaml.Marshal(config)
if err != nil {
return "", err
}
output := strings.ReplaceAll(string(data), "$app_name", service.MustGetString("app_name", "XBoard"))
return output, nil
}
func buildClashProxy(templateName string, conf service.NodeServerConfig, password string) map[string]any {
switch conf.Protocol {
case "shadowsocks":
cipher, _ := conf.Cipher.(string)
if templateName == "clash" {
switch cipher {
case "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305":
default:
return nil
}
}
return map[string]any{
"name": conf.Name,
"type": "ss",
"server": conf.RawHost,
"port": conf.Port,
"cipher": cipher,
"password": password,
"udp": true,
}
case "vmess":
return map[string]any{
"name": conf.Name,
"type": "vmess",
"server": conf.RawHost,
"port": conf.Port,
"uuid": password,
"alterId": 0,
"cipher": "auto",
"udp": true,
}
case "vless":
if templateName != "clashmeta" {
return nil
}
proxy := map[string]any{
"name": conf.Name,
"type": "vless",
"server": conf.RawHost,
"port": conf.Port,
"uuid": password,
"alterId": 0,
"cipher": "auto",
"udp": true,
"flow": toClashString(conf.Flow),
"encryption": "none",
"tls": false,
}
switch toClashInt(conf.Tls) {
case 1:
proxy["tls"] = true
if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok {
proxy["skip-cert-verify"] = toClashBool(tlsSettings["allow_insecure"])
if serverName := toClashString(tlsSettings["server_name"]); serverName != "" {
proxy["servername"] = serverName
}
}
case 2:
proxy["tls"] = true
if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok {
proxy["skip-cert-verify"] = toClashBool(tlsSettings["allow_insecure"])
if serverName := toClashString(tlsSettings["server_name"]); serverName != "" {
proxy["servername"] = serverName
}
proxy["reality-opts"] = map[string]any{
"public-key": toClashString(tlsSettings["public_key"]),
"short-id": toClashString(tlsSettings["short_id"]),
}
}
}
network := toClashString(conf.Network)
if network == "" {
network = "tcp"
}
proxy["network"] = network
return proxy
case "trojan":
return map[string]any{
"name": conf.Name,
"type": "trojan",
"server": conf.RawHost,
"port": conf.Port,
"password": password,
"udp": true,
}
default:
return nil
}
}
func mergeClashProxyGroups(groups []any, proxyNames []string) []any {
if len(groups) == 0 {
return []any{
map[string]any{
"name": "Proxy",
"type": "select",
"proxies": append([]any{"DIRECT"}, stringsToAny(proxyNames)...),
},
}
}
for index, item := range groups {
group, ok := item.(map[string]any)
if !ok {
continue
}
group["proxies"] = appendUniqueAny(anySlice(group["proxies"]), stringsToAny(proxyNames)...)
groups[index] = group
}
return groups
}
func anySlice(value any) []any {
switch typed := value.(type) {
case nil:
return []any{}
case []any:
return append([]any{}, typed...)
case []string:
result := make([]any, 0, len(typed))
for _, item := range typed {
result = append(result, item)
}
return result
default:
return []any{}
}
}
func stringsToAny(values []string) []any {
result := make([]any, 0, len(values))
for _, value := range values {
result = append(result, value)
}
return result
}
func appendUniqueAny(base []any, values ...any) []any {
existing := make(map[string]struct{}, len(base))
for _, item := range base {
existing[fmt.Sprint(item)] = struct{}{}
}
for _, item := range values {
key := fmt.Sprint(item)
if _, ok := existing[key]; ok {
continue
}
base = append(base, item)
existing[key] = struct{}{}
}
return base
}
func toClashString(value any) string {
switch typed := value.(type) {
case string:
return typed
case nil:
return ""
default:
return fmt.Sprint(typed)
}
}
func toClashInt(value any) int {
switch typed := value.(type) {
case int:
return typed
case int64:
return int(typed)
case float64:
return int(typed)
case string:
if typed == "" {
return 0
}
var result int
fmt.Sscanf(typed, "%d", &result)
return result
default:
return 0
}
}
func toClashBool(value any) bool {
switch typed := value.(type) {
case bool:
return typed
case int:
return typed != 0
case int64:
return typed != 0
case float64:
return typed != 0
case string:
return typed == "1" || strings.EqualFold(typed, "true")
default:
return false
}
}
func generateClashFallback(servers []model.Server, user model.User) string {
var builder strings.Builder
builder.WriteString("proxies:\n")
var proxyNames []string
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
password := service.GenerateServerPassword(&s, &user)
proxy := buildClashProxy("clash", conf, password)
if proxy == nil {
continue
}
switch proxy["type"] {
case "ss":
builder.WriteString(fmt.Sprintf(
" - name: \"%s\"\n type: ss\n server: %s\n port: %d\n cipher: %s\n password: %s\n",
conf.Name,
conf.RawHost,
conf.ServerPort,
fmt.Sprint(proxy["cipher"]),
fmt.Sprint(proxy["password"]),
))
case "vmess":
builder.WriteString(fmt.Sprintf(
" - name: \"%s\"\n type: vmess\n server: %s\n port: %d\n uuid: %s\n alterId: 0\n cipher: auto\n udp: true\n",
conf.Name,
conf.RawHost,
conf.ServerPort,
fmt.Sprint(proxy["uuid"]),
))
case "trojan":
builder.WriteString(fmt.Sprintf(
" - name: \"%s\"\n type: trojan\n server: %s\n port: %d\n password: %s\n udp: true\n",
conf.Name,
conf.RawHost,
conf.ServerPort,
fmt.Sprint(proxy["password"]),
))
}
proxyNames = append(proxyNames, fmt.Sprintf("\"%s\"", conf.Name))
}
builder.WriteString("\nproxy-groups:\n")
builder.WriteString(" - name: Proxy\n type: select\n proxies:\n - DIRECT\n")
for _, name := range proxyNames {
builder.WriteString(" - " + name + "\n")
}
builder.WriteString("\nrules:\n")
builder.WriteString(" - MATCH,Proxy\n")
return builder.String()
}