diff --git a/internal/protocol/clash.go b/internal/protocol/clash.go index 37a8d89..8793741 100644 --- a/internal/protocol/clash.go +++ b/internal/protocol/clash.go @@ -29,7 +29,7 @@ func GenerateClashWithTemplate(templateName string, servers []model.Server, user for _, s := range servers { conf := service.BuildNodeConfig(&s) password := service.GenerateServerPassword(&s, &user) - proxy := buildClashProxy(conf, password) + proxy := buildClashProxy(templateName, conf, password) if proxy == nil { continue } @@ -53,35 +53,88 @@ func GenerateClashWithTemplate(templateName string, servers []model.Server, user return output, nil } -func buildClashProxy(conf service.NodeServerConfig, password string) map[string]any { +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.ServerPort, + "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.ServerPort, + "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.ServerPort, + "port": conf.Port, "password": password, "udp": true, } @@ -155,6 +208,54 @@ func appendUniqueAny(base []any, values ...any) []any { 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 @@ -163,7 +264,7 @@ func generateClashFallback(servers []model.Server, user model.User) string { for _, s := range servers { conf := service.BuildNodeConfig(&s) password := service.GenerateServerPassword(&s, &user) - proxy := buildClashProxy(conf, password) + proxy := buildClashProxy("clash", conf, password) if proxy == nil { continue } diff --git a/internal/protocol/link.go b/internal/protocol/link.go index 43fb5a3..b37a334 100644 --- a/internal/protocol/link.go +++ b/internal/protocol/link.go @@ -52,7 +52,7 @@ func buildShadowsocks(c service.NodeServerConfig, password string) string { userInfo := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", cipher, password))) userInfo = strings.TrimRight(strings.ReplaceAll(strings.ReplaceAll(userInfo, "+", "-"), "/", "_"), "=") - link := fmt.Sprintf("ss://%s@%s:%d", userInfo, wrapIPv6(c.RawHost), c.ServerPort) + link := fmt.Sprintf("ss://%s@%s:%d", userInfo, wrapIPv6(c.RawHost), c.Port) params := url.Values{} if plugin := toString(c.Plugin); plugin != "" { opts := toString(c.PluginOpts) @@ -69,7 +69,7 @@ func buildVmess(c service.NodeServerConfig, password string) string { "v": "2", "ps": c.Name, "add": c.RawHost, - "port": fmt.Sprintf("%d", c.ServerPort), + "port": fmt.Sprintf("%d", c.Port), "id": password, "aid": "0", "net": toString(c.Network), @@ -145,7 +145,7 @@ func buildVless(c service.NodeServerConfig, password string) string { } } - return fmt.Sprintf("vless://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name)) + return fmt.Sprintf("vless://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name)) } func buildTrojan(c service.NodeServerConfig, password string) string { @@ -176,7 +176,7 @@ func buildTrojan(c service.NodeServerConfig, password string) string { } } - return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name)) + return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name)) } func buildHysteria(c service.NodeServerConfig, password string) string { @@ -188,7 +188,10 @@ func buildHysteria(c service.NodeServerConfig, password string) string { params.Set("obfs", "salamander") params.Set("obfs-password", toString(c.ObfsPassword)) } - return fmt.Sprintf("hysteria2://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name)) + if c.Ports != "" { + params.Set("mport", c.Ports) + } + return fmt.Sprintf("hysteria2://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name)) } params.Set("protocol", "udp") @@ -200,7 +203,7 @@ func buildHysteria(c service.NodeServerConfig, password string) string { params.Set("downmbps", fmt.Sprintf("%v", c.DownMbps)) } - return fmt.Sprintf("hysteria://%s:%d?%s#%s", wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name)) + return fmt.Sprintf("hysteria://%s:%d?%s#%s", wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name)) } func buildTuic(c service.NodeServerConfig, password string) string { @@ -209,23 +212,23 @@ func buildTuic(c service.NodeServerConfig, password string) string { params.Set("congestion_control", toString(c.CongestionControl)) params.Set("udp-relay-mode", "native") - return fmt.Sprintf("tuic://%s:%s@%s:%d?%s#%s", password, password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name)) + return fmt.Sprintf("tuic://%s:%s@%s:%d?%s#%s", password, password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name)) } func buildAnyTLS(c service.NodeServerConfig, password string) string { params := url.Values{} params.Set("sni", toString(c.ServerName)) - return fmt.Sprintf("anytls://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name)) + return fmt.Sprintf("anytls://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.Port, params.Encode(), url.PathEscape(c.Name)) } func buildSocks(c service.NodeServerConfig, password string) string { auth := base64.StdEncoding.EncodeToString([]byte(password + ":" + password)) - return fmt.Sprintf("socks://%s@%s:%d#%s", auth, wrapIPv6(c.RawHost), c.ServerPort, url.PathEscape(c.Name)) + return fmt.Sprintf("socks://%s@%s:%d#%s", auth, wrapIPv6(c.RawHost), c.Port, url.PathEscape(c.Name)) } func buildHttp(c service.NodeServerConfig, password string) string { auth := base64.StdEncoding.EncodeToString([]byte(password + ":" + password)) - link := fmt.Sprintf("http://%s@%s:%d", auth, wrapIPv6(c.RawHost), c.ServerPort) + link := fmt.Sprintf("http://%s@%s:%d", auth, wrapIPv6(c.RawHost), c.Port) if toInt(c.Tls) > 0 { params := url.Values{} params.Set("security", "tls") diff --git a/internal/protocol/singbox.go b/internal/protocol/singbox.go index d8f0133..ff53708 100644 --- a/internal/protocol/singbox.go +++ b/internal/protocol/singbox.go @@ -64,7 +64,7 @@ func buildSingBoxOutbound(conf service.NodeServerConfig, password string) map[st outbound := map[string]any{ "tag": conf.Name, "server": conf.RawHost, - "server_port": conf.ServerPort, + "server_port": conf.Port, } switch conf.Protocol { @@ -76,6 +76,19 @@ func buildSingBoxOutbound(conf service.NodeServerConfig, password string) map[st outbound["type"] = "vmess" outbound["uuid"] = password outbound["security"] = "auto" + case "vless": + outbound["type"] = "vless" + outbound["uuid"] = password + outbound["packet_encoding"] = "xudp" + if flow := singBoxString(conf.Flow); flow != "" { + outbound["flow"] = flow + } + if tls := buildSingBoxTLS(conf); tls != nil { + outbound["tls"] = tls + } + if transport := buildSingBoxTransport(conf); transport != nil { + outbound["transport"] = transport + } case "trojan": outbound["type"] = "trojan" outbound["password"] = password @@ -96,7 +109,7 @@ func generateSingBoxFallback(servers []model.Server, user model.User) (string, e outbound := map[string]interface{}{ "tag": conf.Name, "server": conf.RawHost, - "server_port": conf.ServerPort, + "server_port": conf.Port, } switch conf.Protocol { @@ -108,6 +121,19 @@ func generateSingBoxFallback(servers []model.Server, user model.User) (string, e outbound["type"] = "vmess" outbound["uuid"] = password outbound["security"] = "auto" + case "vless": + outbound["type"] = "vless" + outbound["uuid"] = password + outbound["packet_encoding"] = "xudp" + if flow := singBoxString(conf.Flow); flow != "" { + outbound["flow"] = flow + } + if tls := buildSingBoxTLS(conf); tls != nil { + outbound["tls"] = tls + } + if transport := buildSingBoxTransport(conf); transport != nil { + outbound["transport"] = transport + } case "trojan": outbound["type"] = "trojan" outbound["password"] = password @@ -133,3 +159,128 @@ func generateSingBoxFallback(servers []model.Server, user model.User) (string, e data, err := json.MarshalIndent(config, "", " ") return string(data), err } + +func buildSingBoxTLS(conf service.NodeServerConfig) map[string]any { + switch singBoxInt(conf.Tls) { + case 1: + tls := map[string]any{ + "enabled": true, + } + if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok { + tls["insecure"] = singBoxBool(tlsSettings["allow_insecure"]) + if serverName := singBoxString(tlsSettings["server_name"]); serverName != "" { + tls["server_name"] = serverName + } + } + return tls + case 2: + tls := map[string]any{ + "enabled": true, + } + if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok { + tls["insecure"] = singBoxBool(tlsSettings["allow_insecure"]) + if serverName := singBoxString(tlsSettings["server_name"]); serverName != "" { + tls["server_name"] = serverName + } + tls["reality"] = map[string]any{ + "enabled": true, + "public_key": singBoxString(tlsSettings["public_key"]), + "short_id": singBoxString(tlsSettings["short_id"]), + } + } + return tls + default: + return nil + } +} + +func buildSingBoxTransport(conf service.NodeServerConfig) map[string]any { + network := singBoxString(conf.Network) + settings, _ := conf.NetworkSettings.(map[string]any) + switch network { + case "ws": + transport := map[string]any{ + "type": "ws", + } + if settings != nil { + if path := singBoxString(settings["path"]); path != "" { + transport["path"] = path + } + if headers, ok := settings["headers"].(map[string]any); ok { + if host := singBoxString(headers["Host"]); host != "" { + transport["headers"] = map[string]any{"Host": host} + } + } + } + return transport + case "grpc": + transport := map[string]any{ + "type": "grpc", + } + if settings != nil { + if serviceName := singBoxString(settings["serviceName"]); serviceName != "" { + transport["service_name"] = serviceName + } + } + return transport + case "httpupgrade": + transport := map[string]any{ + "type": "httpupgrade", + } + if settings != nil { + if path := singBoxString(settings["path"]); path != "" { + transport["path"] = path + } + if host := singBoxString(settings["host"]); host != "" { + transport["host"] = host + } + if headers, ok := settings["headers"].(map[string]any); ok && len(headers) > 0 { + transport["headers"] = headers + } + } + return transport + default: + return nil + } +} + +func singBoxString(value any) string { + switch typed := value.(type) { + case string: + return typed + case nil: + return "" + default: + return "" + } +} + +func singBoxInt(value any) int { + switch typed := value.(type) { + case int: + return typed + case int64: + return int(typed) + case float64: + return int(typed) + default: + return 0 + } +} + +func singBoxBool(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" || typed == "true" + default: + return false + } +} diff --git a/internal/service/node.go b/internal/service/node.go index 569b2ab..078e3ed 100644 --- a/internal/service/node.go +++ b/internal/service/node.go @@ -68,6 +68,8 @@ type NodeServerConfig struct { Name string `json:"-"` Protocol string `json:"protocol"` RawHost string `json:"-"` + Port int `json:"-"` + Ports string `json:"-"` ListenIP string `json:"listen_ip"` ServerPort int `json:"server_port"` Network any `json:"network"` @@ -230,10 +232,13 @@ func CurrentRate(server *model.Server) float64 { func BuildNodeConfig(node *model.Server) NodeServerConfig { settings := parseObject(node.ProtocolSettings) + clientPort, portRange := resolveClientPort(node.Port, node.ServerPort) response := NodeServerConfig{ Name: node.Name, Protocol: node.Type, RawHost: node.Host, + Port: clientPort, + Ports: portRange, ListenIP: "0.0.0.0", ServerPort: node.ServerPort, Network: getMapAny(settings, "network"), @@ -408,6 +413,31 @@ func uuidPrefixBase64(uuid string, size int) string { return base64.StdEncoding.EncodeToString([]byte(uuid[:size])) } +func resolveClientPort(rawPort string, fallback int) (int, string) { + rawPort = strings.TrimSpace(rawPort) + if rawPort == "" { + return fallback, "" + } + + if strings.Contains(rawPort, "-") { + parts := strings.SplitN(rawPort, "-", 2) + start, errStart := strconv.Atoi(strings.TrimSpace(parts[0])) + end, errEnd := strconv.Atoi(strings.TrimSpace(parts[1])) + if errStart == nil && errEnd == nil && start > 0 && end >= start { + if start == end { + return start, rawPort + } + return start + int(time.Now().UnixNano()%int64(end-start+1)), rawPort + } + } + + if port, err := strconv.Atoi(rawPort); err == nil && port > 0 { + return port, "" + } + + return fallback, "" +} + func parseIntSlice(raw *string) []int { if raw == nil || strings.TrimSpace(*raw) == "" { return nil