diff --git a/internal/render/singbox.go b/internal/render/singbox.go index 2e70b74..c43a99b 100644 --- a/internal/render/singbox.go +++ b/internal/render/singbox.go @@ -93,6 +93,85 @@ func RenderSingBoxJson(proxies []model.ProxyNode) string { return string(data) } +// clashToSingBoxTransport converts a Clash proxy node's network/transport +// fields into a sing-box transport object. Returns nil for tcp (no transport). +func clashToSingBoxTransport(proxy model.ProxyNode) any { + network := getString(proxy, "network") + switch network { + case "ws": + wsOpts, _ := proxy["ws-opts"].(map[string]any) + path := "/" + if wsOpts != nil { + if p, ok := wsOpts["path"].(string); ok && p != "" { + path = p + } + } + transportMap := map[string]any{ + "type": "ws", + "path": path, + } + if wsOpts != nil { + if h, ok := wsOpts["headers"]; ok && h != nil { + transportMap["headers"] = h + } + } + return transportMap + + case "grpc": + grpcOpts, _ := proxy["grpc-opts"].(map[string]any) + serviceName := "" + if grpcOpts != nil { + if s, ok := grpcOpts["grpc-service-name"].(string); ok { + serviceName = s + } + } + return map[string]any{ + "type": "grpc", + "service_name": serviceName, + } + + case "h2", "http": + h2Opts, _ := proxy["h2-opts"].(map[string]any) + transportMap := map[string]any{ + "type": "http", + } + if h2Opts != nil { + if host, ok := h2Opts["host"].([]any); ok && len(host) > 0 { + hosts := make([]string, 0, len(host)) + for _, h := range host { + if s, ok := h.(string); ok && s != "" { + hosts = append(hosts, s) + } + } + transportMap["host"] = hosts + } + if p, ok := h2Opts["path"].(string); ok && p != "" { + transportMap["path"] = p + } + } + return transportMap + + case "quic": + return map[string]any{ + "type": "quic", + } + + case "httpupgrade": + wsOpts, _ := proxy["ws-opts"].(map[string]any) + path := "/" + if wsOpts != nil { + if p, ok := wsOpts["path"].(string); ok && p != "" { + path = p + } + } + return map[string]any{ + "type": "httpupgrade", + "path": path, + } + } + return nil +} + // ToSingBoxOutbound converts a proxy node to a sing-box outbound map. // Returns nil for unsupported proxy types. func ToSingBoxOutbound(proxy model.ProxyNode) map[string]any { @@ -127,9 +206,9 @@ func ToSingBoxOutbound(proxy model.ProxyNode) map[string]any { "server_port": port, "uuid": proxy["uuid"], "flow": proxy["flow"], - "network": strOr(proxy, "network", "tcp"), "packet_encoding": "xudp", "tls": tls, + "transport": clashToSingBoxTransport(proxy), }) case "hysteria2": @@ -261,26 +340,6 @@ func ToSingBoxOutbound(proxy model.ProxyNode) map[string]any { if getBool(proxy, "tls") { tls = map[string]any{"enabled": true, "server_name": proxy["servername"]} } - var transport any - if getString(proxy, "network") == "ws" { - wsOpts, _ := proxy["ws-opts"].(map[string]any) - path := "/" - if wsOpts != nil { - if p, ok := wsOpts["path"].(string); ok && p != "" { - path = p - } - } - transportMap := map[string]any{ - "type": "ws", - "path": path, - } - if wsOpts != nil { - if h, ok := wsOpts["headers"]; ok && h != nil { - transportMap["headers"] = h - } - } - transport = transportMap - } return util.StripUndefined(map[string]any{ "type": "vmess", "tag": name, @@ -290,7 +349,7 @@ func ToSingBoxOutbound(proxy model.ProxyNode) map[string]any { "security": strOr(proxy, "cipher", "auto"), "alter_id": proxy["alterId"], "tls": tls, - "transport": transport, + "transport": clashToSingBoxTransport(proxy), }) }