465 lines
11 KiB
Go
465 lines
11 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 fp := tlsFingerprint(conf.UTLS); fp != "" {
|
|
proxy["client-fingerprint"] = fp
|
|
}
|
|
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 fp := tlsFingerprint(conf.UTLS); fp != "" {
|
|
proxy["client-fingerprint"] = fp
|
|
}
|
|
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"]),
|
|
}
|
|
}
|
|
}
|
|
proxy["network"] = "tcp"
|
|
if settings, ok := conf.NetworkSettings.(map[string]any); ok {
|
|
switch toClashString(conf.Network) {
|
|
case "", "tcp":
|
|
if header, ok := settings["header"].(map[string]any); ok && toClashString(header["type"]) == "http" {
|
|
proxy["network"] = "http"
|
|
httpOpts := map[string]any{}
|
|
if request, ok := header["request"].(map[string]any); ok {
|
|
if headers, ok := request["headers"].(map[string]any); ok && len(headers) > 0 {
|
|
httpOpts["headers"] = headers
|
|
}
|
|
if paths := clashPathList(request["path"]); len(paths) > 0 {
|
|
httpOpts["path"] = paths
|
|
}
|
|
}
|
|
if len(httpOpts) > 0 {
|
|
proxy["http-opts"] = httpOpts
|
|
}
|
|
}
|
|
case "ws":
|
|
proxy["network"] = "ws"
|
|
wsOpts := map[string]any{}
|
|
if path := toClashString(settings["path"]); path != "" {
|
|
wsOpts["path"] = path
|
|
}
|
|
if headers, ok := settings["headers"].(map[string]any); ok {
|
|
if host := toClashString(headers["Host"]); host != "" {
|
|
wsOpts["headers"] = map[string]any{"Host": host}
|
|
}
|
|
}
|
|
if len(wsOpts) > 0 {
|
|
proxy["ws-opts"] = wsOpts
|
|
}
|
|
case "grpc":
|
|
proxy["network"] = "grpc"
|
|
if serviceName := toClashString(settings["serviceName"]); serviceName != "" {
|
|
proxy["grpc-opts"] = map[string]any{"grpc-service-name": serviceName}
|
|
}
|
|
case "h2":
|
|
proxy["network"] = "h2"
|
|
h2Opts := map[string]any{}
|
|
if path := toClashString(settings["path"]); path != "" {
|
|
h2Opts["path"] = path
|
|
}
|
|
if hosts := clashHostList(settings["host"]); len(hosts) > 0 {
|
|
h2Opts["host"] = hosts
|
|
}
|
|
if len(h2Opts) > 0 {
|
|
proxy["h2-opts"] = h2Opts
|
|
}
|
|
case "httpupgrade":
|
|
proxy["network"] = "ws"
|
|
wsOpts := map[string]any{
|
|
"v2ray-http-upgrade": true,
|
|
}
|
|
if path := toClashString(settings["path"]); path != "" {
|
|
wsOpts["path"] = path
|
|
}
|
|
host := toClashString(settings["host"])
|
|
if host == "" {
|
|
host = conf.RawHost
|
|
}
|
|
if host != "" {
|
|
wsOpts["headers"] = map[string]any{"Host": host}
|
|
}
|
|
proxy["ws-opts"] = wsOpts
|
|
default:
|
|
if network := toClashString(conf.Network); network != "" {
|
|
proxy["network"] = network
|
|
}
|
|
}
|
|
} else if network := toClashString(conf.Network); network != "" {
|
|
proxy["network"] = network
|
|
}
|
|
if smux := buildClashSmux(conf.Multiplex); smux != nil {
|
|
proxy["smux"] = smux
|
|
}
|
|
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 clashPathList(value any) []string {
|
|
switch typed := value.(type) {
|
|
case []string:
|
|
return append([]string{}, typed...)
|
|
case []any:
|
|
result := make([]string, 0, len(typed))
|
|
for _, item := range typed {
|
|
text := toClashString(item)
|
|
if text != "" {
|
|
result = append(result, text)
|
|
}
|
|
}
|
|
return result
|
|
case string:
|
|
if typed == "" {
|
|
return nil
|
|
}
|
|
return []string{typed}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func clashHostList(value any) []string {
|
|
switch typed := value.(type) {
|
|
case []string:
|
|
return append([]string{}, typed...)
|
|
case []any:
|
|
result := make([]string, 0, len(typed))
|
|
for _, item := range typed {
|
|
text := toClashString(item)
|
|
if text != "" {
|
|
result = append(result, text)
|
|
}
|
|
}
|
|
return result
|
|
case string:
|
|
if typed == "" {
|
|
return nil
|
|
}
|
|
return []string{typed}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func buildClashSmux(value any) map[string]any {
|
|
settings, ok := value.(map[string]any)
|
|
if !ok || !toClashBool(settings["enabled"]) {
|
|
return nil
|
|
}
|
|
|
|
smux := map[string]any{
|
|
"enabled": true,
|
|
}
|
|
if protocol := toClashString(settings["protocol"]); protocol != "" {
|
|
smux["protocol"] = protocol
|
|
}
|
|
if maxConnections := toClashInt(settings["max_connections"]); maxConnections > 0 {
|
|
smux["max-connections"] = maxConnections
|
|
}
|
|
if toClashBool(settings["padding"]) {
|
|
smux["padding"] = true
|
|
}
|
|
if brutal, ok := settings["brutal"].(map[string]any); ok && toClashBool(brutal["enabled"]) {
|
|
smux["brutal-opts"] = map[string]any{
|
|
"enabled": true,
|
|
"up": toClashInt(brutal["up_mbps"]),
|
|
"down": toClashInt(brutal["down_mbps"]),
|
|
}
|
|
}
|
|
return smux
|
|
}
|
|
|
|
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()
|
|
}
|