mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
channel: master version: 1.0.0 source-ref: master published-at-utc: 2026-03-13T15:19:28Z
249 lines
6.2 KiB
Go
249 lines
6.2 KiB
Go
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
|
||
}
|