mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
publish: 1.0.0 snapshot (bad2ec1)
channel: master version: 1.0.0 source-ref: master published-at-utc: 2026-03-13T15:19:28Z
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user