Files
Ant-Browser/backend/internal/proxy/singbox_parser.go
T
Ant Browser Release Bot 6f58a6c19a publish: 1.0.0 snapshot (bad2ec1)
channel: master
version: 1.0.0
source-ref: master
published-at-utc: 2026-03-13T15:19:28Z
2026-03-13 23:19:29 +08:00

249 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package proxy
import (
"fmt"
"net/url"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
// IsSingBoxProtocol 判断是否为 sing-box 支持的协议(hysteria2/tuic
func IsSingBoxProtocol(proxyConfig string) bool {
l := strings.ToLower(strings.TrimSpace(proxyConfig))
if strings.HasPrefix(l, "hysteria2://") || strings.HasPrefix(l, "hysteria://") {
return true
}
// Clash YAML 格式
if strings.Contains(l, "type: hysteria2") || strings.Contains(l, "type:hysteria2") ||
strings.Contains(l, "type: hysteria") || strings.Contains(l, "type:hysteria") ||
strings.Contains(l, "type: tuic") || strings.Contains(l, "type:tuic") {
return true
}
return false
}
// BuildSingBoxOutbound 解析节点配置,返回 sing-box outbound map
func BuildSingBoxOutbound(node string) (map[string]interface{}, error) {
src := strings.TrimSpace(node)
l := strings.ToLower(src)
if strings.HasPrefix(l, "hysteria2://") || strings.HasPrefix(l, "hysteria://") {
return parseHysteria2URI(src)
}
// Clash YAML 格式
if strings.Contains(l, "type:") || strings.Contains(l, "proxies:") {
return parseClashSingBoxNode(src)
}
return nil, fmt.Errorf("不支持的 sing-box 节点格式")
}
// parseHysteria2URI 解析 hysteria2:// URI
// 格式: hysteria2://password@host:port?sni=xxx&insecure=1
func parseHysteria2URI(node string) (map[string]interface{}, error) {
// 统一为 hysteria2://
if strings.HasPrefix(strings.ToLower(node), "hysteria://") {
node = "hysteria2://" + node[len("hysteria://"):]
}
u, err := url.Parse(node)
if err != nil {
return nil, fmt.Errorf("hysteria2 URI 解析失败: %v", err)
}
host := u.Hostname()
portStr := u.Port()
port, _ := strconv.Atoi(portStr)
password := u.User.Username()
if password == "" {
// 有些格式把密码放在 userinfo 里不带 @
password = strings.TrimPrefix(u.Host, "@")
}
q := u.Query()
sni := q.Get("sni")
if sni == "" {
sni = q.Get("peer")
}
insecure := q.Get("insecure") == "1" || strings.ToLower(q.Get("insecure")) == "true"
obfsPassword := q.Get("obfs-password")
if host == "" || port == 0 {
return nil, fmt.Errorf("hysteria2 节点信息不完整: host=%s port=%d", host, port)
}
out := map[string]interface{}{
"type": "hysteria2",
"tag": "proxy-out",
"server": host,
"server_port": port,
"password": password,
"tls": map[string]interface{}{
"enabled": true,
"insecure": insecure,
},
}
if sni != "" {
out["tls"].(map[string]interface{})["server_name"] = sni
}
if obfsPassword != "" {
out["obfs"] = map[string]interface{}{
"type": "salamander",
"password": obfsPassword,
}
}
return out, nil
}
// parseClashSingBoxNode 解析 Clash YAML 格式的 sing-box 节点
func parseClashSingBoxNode(src string) (map[string]interface{}, error) {
// 复用已有的 YAML 解析基础设施
var payload interface{}
if err := yaml.Unmarshal([]byte(src), &payload); err != nil {
return nil, fmt.Errorf("YAML 解析失败: %v", err)
}
nodeMap := pickClashNode(payload)
if nodeMap == nil {
return nil, fmt.Errorf("节点解析失败")
}
nodeType := strings.ToLower(getMapString(nodeMap, "type"))
switch nodeType {
case "hysteria2", "hysteria":
return buildSingBoxHysteria2FromClash(nodeMap)
case "tuic":
return buildSingBoxTUICFromClash(nodeMap)
default:
return nil, fmt.Errorf("不支持的 sing-box 节点类型: %s", nodeType)
}
}
func buildSingBoxHysteria2FromClash(node map[string]interface{}) (map[string]interface{}, error) {
host := getMapString(node, "server")
port := getMapInt(node, "port")
password := getMapString(node, "password")
sni := getMapString(node, "sni")
if sni == "" {
sni = getMapString(node, "servername")
}
skipVerify := getMapBool(node, "skip-cert-verify")
if host == "" || port == 0 {
return nil, fmt.Errorf("hysteria2 节点信息不完整")
}
tls := map[string]interface{}{
"enabled": true,
"insecure": skipVerify,
}
if sni != "" {
tls["server_name"] = sni
}
out := map[string]interface{}{
"type": "hysteria2",
"tag": "proxy-out",
"server": host,
"server_port": port,
"password": password,
"tls": tls,
}
// 带宽限制(可选)
if up := getMapString(node, "up"); up != "" {
out["up_mbps"] = parseBandwidthMbps(up)
}
if down := getMapString(node, "down"); down != "" {
out["down_mbps"] = parseBandwidthMbps(down)
}
// obfs
if obfsPassword := getMapString(node, "obfs-password"); obfsPassword != "" {
out["obfs"] = map[string]interface{}{
"type": "salamander",
"password": obfsPassword,
}
}
return out, nil
}
func buildSingBoxTUICFromClash(node map[string]interface{}) (map[string]interface{}, error) {
host := getMapString(node, "server")
port := getMapInt(node, "port")
uuid := getMapString(node, "uuid")
password := getMapString(node, "password")
sni := getMapString(node, "sni")
skipVerify := getMapBool(node, "skip-cert-verify")
if host == "" || port == 0 {
return nil, fmt.Errorf("tuic 节点信息不完整")
}
tls := map[string]interface{}{
"enabled": true,
"insecure": skipVerify,
}
if sni != "" {
tls["server_name"] = sni
}
// alpn
if alpnRaw, ok := node["alpn"]; ok {
if alpnList := toStringSlice(alpnRaw); len(alpnList) > 0 {
tls["alpn"] = alpnList
}
}
return map[string]interface{}{
"type": "tuic",
"tag": "proxy-out",
"server": host,
"server_port": port,
"uuid": uuid,
"password": password,
"congestion_control": "bbr",
"tls": tls,
}, nil
}
// parseBandwidthMbps 解析带宽字符串,返回 Mbps 整数
// 支持: "100 Mbps", "100", "100M"
func parseBandwidthMbps(s string) int {
s = strings.TrimSpace(s)
s = strings.ToUpper(s)
s = strings.ReplaceAll(s, " ", "")
s = strings.TrimSuffix(s, "BPS")
s = strings.TrimSuffix(s, "B")
s = strings.TrimSuffix(s, "M")
n, _ := strconv.Atoi(s)
return n
}
// toStringSlice 将 interface{} 转为 []string
func toStringSlice(v interface{}) []string {
if v == nil {
return nil
}
if arr, ok := v.([]interface{}); ok {
result := make([]string, 0, len(arr))
for _, item := range arr {
if s, ok := item.(string); ok {
result = append(result, s)
}
}
return result
}
if s, ok := v.(string); ok && s != "" {
return []string{s}
}
return nil
}