Files
Ant-Browser/backend/internal/proxy/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

686 lines
18 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 (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
// ParseProxyNode 解析代理节点
func ParseProxyNode(node string) (string, map[string]interface{}, error) {
src := strings.TrimSpace(node)
if src == "" {
return "", nil, fmt.Errorf("代理节点为空")
}
l := strings.ToLower(src)
if strings.HasPrefix(l, "http://") || strings.HasPrefix(l, "https://") || strings.HasPrefix(l, "socks5://") {
return src, nil, nil
}
if strings.HasPrefix(l, "clash://") || strings.Contains(l, "type:") || strings.Contains(l, "proxies:") {
outbound, standard, err := parseClashNode(src)
if err != nil {
return "", nil, err
}
if standard != "" {
return standard, nil, nil
}
if outbound != nil {
return "", outbound, nil
}
}
outbound, err := buildXrayOutbound(src)
if err != nil {
return "", nil, err
}
return "", outbound, nil
}
func parseClashNode(src string) (map[string]interface{}, string, error) {
data := strings.TrimSpace(src)
if strings.HasPrefix(strings.ToLower(data), "clash://") {
raw := strings.TrimPrefix(data, "clash://")
raw, _ = url.QueryUnescape(raw)
decoded, err := decodeBase64String(raw)
if err != nil {
return nil, "", err
}
data = string(decoded)
}
var payload interface{}
if err := yaml.Unmarshal([]byte(data), &payload); err != nil {
return nil, "", err
}
nodeMap := pickClashNode(payload)
if nodeMap == nil {
return nil, "", fmt.Errorf("clash 节点解析失败")
}
nodeType := strings.ToLower(getMapString(nodeMap, "type"))
switch nodeType {
case "socks5", "http", "https":
return nil, buildStandardProxyFromClash(nodeMap, nodeType), nil
case "vmess":
return buildOutboundFromClashVmess(nodeMap)
case "vless":
return buildOutboundFromClashVless(nodeMap)
case "trojan":
return buildOutboundFromClashTrojan(nodeMap)
case "ss", "shadowsocks":
return buildOutboundFromClashSS(nodeMap)
case "ssr":
return nil, "", fmt.Errorf("不支持 ShadowsocksR 协议,Xray 不支持 SSR,请使用 SS/vmess/vless/trojan")
case "hysteria2", "hysteria":
return buildOutboundFromClashHysteria2(nodeMap)
}
return nil, "", fmt.Errorf("不支持的节点类型")
}
func pickClashNode(payload interface{}) map[string]interface{} {
if m := toStringMap(payload); m != nil {
if proxies, ok := m["proxies"]; ok {
if arr, ok := proxies.([]interface{}); ok && len(arr) > 0 {
return toStringMap(arr[0])
}
}
if proxyItem, ok := m["proxy"]; ok {
if node := toStringMap(proxyItem); node != nil {
return node
}
}
return m
}
if arr, ok := payload.([]interface{}); ok && len(arr) > 0 {
return toStringMap(arr[0])
}
return nil
}
func buildStandardProxyFromClash(node map[string]interface{}, scheme string) string {
host := getMapString(node, "server")
port := getMapInt(node, "port")
username := getMapString(node, "username")
password := getMapString(node, "password")
if host == "" || port == 0 {
return ""
}
address := fmt.Sprintf("%s:%d", host, port)
if username != "" {
user := url.UserPassword(username, password)
return fmt.Sprintf("%s://%s@%s", scheme, user.String(), address)
}
return fmt.Sprintf("%s://%s", scheme, address)
}
func buildOutboundFromClashVless(node map[string]interface{}) (map[string]interface{}, string, error) {
host := getMapString(node, "server")
port := getMapInt(node, "port")
id := getMapString(node, "uuid")
flow := getMapString(node, "flow")
// sni 和 servername 都要读
sni := getMapString(node, "sni")
if sni == "" {
sni = getMapString(node, "servername")
}
network := getMapString(node, "network")
out := map[string]interface{}{
"protocol": "vless",
"tag": "proxy-out",
"settings": map[string]interface{}{
"vnext": []interface{}{
map[string]interface{}{
"address": host,
"port": port,
"users": []interface{}{
map[string]interface{}{
"id": id,
"flow": flow,
"encryption": "none",
},
},
},
},
},
}
stream := map[string]interface{}{}
tlsVal := strings.ToLower(getMapString(node, "tls"))
_, hasRealityOpts := node["reality-opts"]
if hasRealityOpts {
// Reality 模式:network 必须显式为 tcp,否则 xray 校验失败
stream["network"] = "tcp"
realityOpts := map[string]interface{}{
"spiderX": "",
}
if sni != "" {
realityOpts["serverName"] = sni
}
fingerprint := getMapString(node, "client-fingerprint")
if fingerprint == "" {
fingerprint = "chrome"
}
realityOpts["fingerprint"] = fingerprint
if rm := toStringMap(node["reality-opts"]); rm != nil {
if pbk := getMapString(rm, "public-key"); pbk != "" {
realityOpts["publicKey"] = pbk
}
if sid := getMapString(rm, "short-id"); sid != "" {
realityOpts["shortId"] = sid
}
}
stream["security"] = "reality"
stream["realitySettings"] = realityOpts
} else if getMapBool(node, "tls") || tlsVal == "true" || tlsVal == "tls" {
// 普通 TLS 模式
tlsSettings := map[string]interface{}{}
if sni != "" {
tlsSettings["serverName"] = sni
}
tlsSettings["allowInsecure"] = getMapBool(node, "skip-cert-verify")
stream["security"] = "tls"
stream["tlsSettings"] = tlsSettings
}
if network == "ws" {
stream["network"] = "ws"
ws := map[string]interface{}{}
if wsOpts, ok := node["ws-opts"]; ok {
if wsMap := toStringMap(wsOpts); wsMap != nil {
path := getMapString(wsMap, "path")
// path 为 "/" 也要设置
if path != "" {
ws["path"] = path
}
if headers, ok := wsMap["headers"]; ok {
if headerMap := toStringMap(headers); headerMap != nil {
if hostH := getMapString(headerMap, "Host"); hostH != "" {
ws["headers"] = map[string]interface{}{"Host": hostH}
}
}
}
}
}
stream["wsSettings"] = ws
}
if network == "grpc" {
stream["network"] = "grpc"
if grpcOpts, ok := node["grpc-opts"]; ok {
if grpcMap := toStringMap(grpcOpts); grpcMap != nil {
serviceName := getMapString(grpcMap, "grpc-service-name")
if serviceName != "" {
stream["grpcSettings"] = map[string]interface{}{"serviceName": serviceName}
}
}
}
}
if len(stream) > 0 {
out["streamSettings"] = stream
}
return out, "", nil
}
func buildOutboundFromClashVmess(node map[string]interface{}) (map[string]interface{}, string, error) {
host := getMapString(node, "server")
port := getMapInt(node, "port")
id := getMapString(node, "uuid")
cipher := getMapString(node, "cipher")
if cipher == "" {
cipher = "auto"
}
network := getMapString(node, "network")
// sni 和 servername 都要读
sni := getMapString(node, "sni")
if sni == "" {
sni = getMapString(node, "servername")
}
out := map[string]interface{}{
"protocol": "vmess",
"tag": "proxy-out",
"settings": map[string]interface{}{
"vnext": []interface{}{
map[string]interface{}{
"address": host,
"port": port,
"users": []interface{}{
map[string]interface{}{
"id": id,
"security": cipher,
},
},
},
},
},
}
stream := map[string]interface{}{}
if getMapBool(node, "tls") || strings.ToLower(getMapString(node, "tls")) == "true" {
tlsSettings := map[string]interface{}{}
if sni != "" {
tlsSettings["serverName"] = sni
}
skipVerify := getMapBool(node, "skip-cert-verify")
tlsSettings["allowInsecure"] = skipVerify
stream["security"] = "tls"
stream["tlsSettings"] = tlsSettings
}
if network == "ws" {
stream["network"] = "ws"
ws := map[string]interface{}{}
if wsOpts, ok := node["ws-opts"]; ok {
if wsMap := toStringMap(wsOpts); wsMap != nil {
path := getMapString(wsMap, "path")
// path 为 "/" 也要设置
if path != "" {
ws["path"] = path
}
if headers, ok := wsMap["headers"]; ok {
if headerMap := toStringMap(headers); headerMap != nil {
if hostH := getMapString(headerMap, "Host"); hostH != "" {
ws["headers"] = map[string]interface{}{"Host": hostH}
}
}
}
}
}
stream["wsSettings"] = ws
}
if network == "grpc" {
stream["network"] = "grpc"
if grpcOpts, ok := node["grpc-opts"]; ok {
if grpcMap := toStringMap(grpcOpts); grpcMap != nil {
serviceName := getMapString(grpcMap, "grpc-service-name")
if serviceName != "" {
stream["grpcSettings"] = map[string]interface{}{"serviceName": serviceName}
}
}
}
}
if len(stream) > 0 {
out["streamSettings"] = stream
}
return out, "", nil
}
func buildOutboundFromClashTrojan(node map[string]interface{}) (map[string]interface{}, string, error) {
host := getMapString(node, "server")
port := getMapInt(node, "port")
password := getMapString(node, "password")
sni := getMapString(node, "sni")
if sni == "" {
sni = getMapString(node, "servername")
}
network := getMapString(node, "network")
skipVerify := getMapBool(node, "skip-cert-verify")
out := map[string]interface{}{
"protocol": "trojan",
"tag": "proxy-out",
"settings": map[string]interface{}{
"address": host,
"port": port,
"password": password,
},
}
stream := map[string]interface{}{
"security": "tls",
"tlsSettings": map[string]interface{}{
"serverName": sni,
"allowInsecure": skipVerify,
},
}
if network == "ws" {
stream["network"] = "ws"
ws := map[string]interface{}{}
if wsOpts, ok := node["ws-opts"]; ok {
if wsMap := toStringMap(wsOpts); wsMap != nil {
if path := getMapString(wsMap, "path"); path != "" {
ws["path"] = path
}
if headers := toStringMap(wsMap["headers"]); headers != nil {
if h := getMapString(headers, "Host"); h != "" {
ws["headers"] = map[string]interface{}{"Host": h}
}
}
}
}
stream["wsSettings"] = ws
} else if network == "grpc" {
stream["network"] = "grpc"
if grpcOpts, ok := node["grpc-opts"]; ok {
if grpcMap := toStringMap(grpcOpts); grpcMap != nil {
if svcName := getMapString(grpcMap, "grpc-service-name"); svcName != "" {
stream["grpcSettings"] = map[string]interface{}{"serviceName": svcName}
}
}
}
}
out["streamSettings"] = stream
return out, "", nil
}
func buildOutboundFromClashHysteria2(node map[string]interface{}) (map[string]interface{}, string, error) {
// 支持的协议: vless, vmess, trojan, shadowsocks, socks, http, wireguard
// hysteria2 需要使用 Hysteria 客户端或 sing-box
return nil, "", fmt.Errorf("Xray 不支持 hysteria2 协议,请使用 vless/vmess/socks5/http 格式的代理")
}
func buildXrayOutbound(node string) (map[string]interface{}, error) {
l := strings.ToLower(node)
if strings.HasPrefix(l, "vmess://") {
return buildOutboundVmess(node)
}
if strings.HasPrefix(l, "vless://") {
return buildOutboundVless(node)
}
if strings.HasPrefix(l, "trojan://") {
return buildOutboundTrojan(node)
}
if strings.HasPrefix(l, "ss://") {
return buildOutboundSS(node)
}
if strings.HasPrefix(l, "ssr://") {
return nil, fmt.Errorf("不支持 ShadowsocksR 协议,Xray 不支持 SSR,请使用 SS/vmess/vless/trojan")
}
if strings.HasPrefix(l, "hysteria2://") || strings.HasPrefix(l, "hysteria://") {
return buildOutboundHysteria2(node)
}
return nil, fmt.Errorf("不支持的节点协议")
}
func buildOutboundVmess(node string) (map[string]interface{}, error) {
raw := strings.TrimPrefix(node, "vmess://")
decoded, err := decodeBase64String(strings.TrimSpace(raw))
if err != nil {
return nil, fmt.Errorf("vmess 解析失败: %v", err)
}
var v struct {
Add string `json:"add"`
Port string `json:"port"`
ID string `json:"id"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
Path string `json:"path"`
TLS string `json:"tls"`
Sni string `json:"sni"`
Alpn string `json:"alpn"`
}
if err := json.Unmarshal(decoded, &v); err != nil {
return nil, fmt.Errorf("vmess 配置解析失败: %v", err)
}
p, _ := strconv.Atoi(v.Port)
out := map[string]interface{}{
"protocol": "vmess",
"tag": "proxy-out",
"settings": map[string]interface{}{
"vnext": []interface{}{
map[string]interface{}{
"address": v.Add,
"port": p,
"users": []interface{}{
map[string]interface{}{
"id": v.ID,
"security": "auto",
},
},
},
},
},
}
stream := map[string]interface{}{}
if v.TLS == "tls" {
stream["security"] = "tls"
if v.Sni != "" {
stream["tlsSettings"] = map[string]interface{}{"serverName": v.Sni}
}
}
if v.Net == "ws" {
stream["network"] = "ws"
ws := map[string]interface{}{}
if v.Path != "" {
ws["path"] = v.Path
}
if v.Host != "" {
ws["headers"] = map[string]interface{}{"Host": v.Host}
}
if len(ws) > 0 {
stream["wsSettings"] = ws
}
}
if len(stream) > 0 {
out["streamSettings"] = stream
}
return out, nil
}
func buildOutboundVless(node string) (map[string]interface{}, error) {
u, err := url.Parse(node)
if err != nil {
return nil, fmt.Errorf("vless 解析失败: %v", err)
}
host := u.Hostname()
portStr := u.Port()
p, _ := strconv.Atoi(portStr)
id := u.User.Username()
q := u.Query()
flow := q.Get("flow")
sec := strings.ToLower(q.Get("security"))
sni := q.Get("sni")
out := map[string]interface{}{
"protocol": "vless",
"tag": "proxy-out",
"settings": map[string]interface{}{
"vnext": []interface{}{
map[string]interface{}{
"address": host,
"port": p,
"users": []interface{}{
map[string]interface{}{
"id": id,
"flow": flow,
"encryption": "none",
},
},
},
},
},
}
stream := map[string]interface{}{}
if sec == "tls" || sec == "reality" {
stream["security"] = "tls"
if sni != "" {
stream["tlsSettings"] = map[string]interface{}{"serverName": sni}
}
}
network := q.Get("type")
if network == "" {
network = q.Get("network")
}
if network == "ws" {
stream["network"] = "ws"
ws := map[string]interface{}{}
if pth := q.Get("path"); pth != "" {
ws["path"] = pth
}
hostH := q.Get("host")
if hostH == "" {
hostH = u.Hostname()
}
if hostH != "" {
ws["headers"] = map[string]interface{}{"Host": hostH}
}
stream["wsSettings"] = ws
}
if len(stream) > 0 {
out["streamSettings"] = stream
}
return out, nil
}
func buildOutboundHysteria2(node string) (map[string]interface{}, error) {
// Xray 不支持 hysteria2 作为 outbound 协议
// 支持的协议: vless, vmess, trojan, shadowsocks, socks, http, wireguard
// hysteria2 需要使用 Hysteria 客户端或 sing-box
return nil, fmt.Errorf("Xray 不支持 hysteria2 协议,请使用 vless/vmess/socks5/http 格式的代理")
}
// buildOutboundTrojan 解析 trojan:// URI 格式
func buildOutboundTrojan(node string) (map[string]interface{}, error) {
u, err := url.Parse(node)
if err != nil {
return nil, fmt.Errorf("trojan 解析失败: %v", err)
}
host := u.Hostname()
portStr := u.Port()
p, _ := strconv.Atoi(portStr)
password := u.User.Username()
q := u.Query()
sni := q.Get("sni")
if sni == "" {
sni = q.Get("peer")
}
skipVerify := q.Get("allowInsecure") == "1" || strings.ToLower(q.Get("allowInsecure")) == "true"
network := q.Get("type")
out := map[string]interface{}{
"protocol": "trojan",
"tag": "proxy-out",
"settings": map[string]interface{}{
"address": host,
"port": p,
"password": password,
},
}
stream := map[string]interface{}{
"security": "tls",
"tlsSettings": map[string]interface{}{
"serverName": sni,
"allowInsecure": skipVerify,
},
}
if network == "ws" {
stream["network"] = "ws"
ws := map[string]interface{}{}
if pth := q.Get("path"); pth != "" {
ws["path"] = pth
}
if h := q.Get("host"); h != "" {
ws["headers"] = map[string]interface{}{"Host": h}
}
stream["wsSettings"] = ws
}
out["streamSettings"] = stream
return out, nil
}
// buildOutboundFromClashSS 从 Clash YAML 格式解析 Shadowsocks outbound
func buildOutboundFromClashSS(node map[string]interface{}) (map[string]interface{}, string, error) {
host := getMapString(node, "server")
port := getMapInt(node, "port")
password := getMapString(node, "password")
cipher := getMapString(node, "cipher")
if cipher == "" {
cipher = getMapString(node, "method")
}
if cipher == "" {
cipher = "aes-256-gcm"
}
out := map[string]interface{}{
"protocol": "shadowsocks",
"tag": "proxy-out",
"settings": map[string]interface{}{
"address": host,
"port": port,
"method": cipher,
"password": password,
},
}
// plugin 支持(obfs/v2ray-plugin
if plugin := getMapString(node, "plugin"); plugin != "" {
pluginOpts := getMapString(node, "plugin-opts")
_ = pluginOpts // xray 原生不支持 plugin,忽略
}
return out, "", nil
}
// buildOutboundSS 解析 ss:// URI 格式
// 支持两种格式:
// 1. ss://BASE64(method:password)@host:port
// 2. ss://BASE64(method:password@host:port)
func buildOutboundSS(node string) (map[string]interface{}, error) {
raw := strings.TrimPrefix(node, "ss://")
// 去掉 fragment#备注)
if idx := strings.Index(raw, "#"); idx >= 0 {
raw = raw[:idx]
}
raw = strings.TrimSpace(raw)
var host, method, password string
var port int
// 格式1method:password@host:portSIP002
if strings.Contains(raw, "@") {
u, err := url.Parse("ss://" + raw)
if err != nil {
return nil, fmt.Errorf("ss 解析失败: %v", err)
}
host = u.Hostname()
port, _ = strconv.Atoi(u.Port())
userInfo := u.User.String()
// userInfo 可能是 base64 编码的 method:password
if decoded, err := decodeBase64String(userInfo); err == nil {
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) == 2 {
method = parts[0]
password = parts[1]
}
} else {
// 明文 method:password
parts := strings.SplitN(userInfo, ":", 2)
if len(parts) == 2 {
method = parts[0]
password = parts[1]
}
}
} else {
// 格式2:整体 base64
decoded, err := decodeBase64String(raw)
if err != nil {
return nil, fmt.Errorf("ss base64 解析失败: %v", err)
}
// method:password@host:port
s := string(decoded)
atIdx := strings.LastIndex(s, "@")
if atIdx < 0 {
return nil, fmt.Errorf("ss 格式错误")
}
userPart := s[:atIdx]
hostPart := s[atIdx+1:]
parts := strings.SplitN(userPart, ":", 2)
if len(parts) == 2 {
method = parts[0]
password = parts[1]
}
hostPort := strings.Split(hostPart, ":")
if len(hostPort) == 2 {
host = hostPort[0]
port, _ = strconv.Atoi(hostPort[1])
}
}
if host == "" || port == 0 || method == "" {
return nil, fmt.Errorf("ss 节点信息不完整")
}
return map[string]interface{}{
"protocol": "shadowsocks",
"tag": "proxy-out",
"settings": map[string]interface{}{
"address": host,
"port": port,
"method": method,
"password": password,
},
}, nil
}