Files
2026-05-09 17:30:16 +08:00

140 lines
4.0 KiB
Go

package proxy
import (
"encoding/json"
"fmt"
"net/url"
"strings"
)
const chainSocks5Prefix = "chain+socks5://"
type chainSocks5Hop struct {
Protocol string `json:"protocol"`
Server string `json:"server"`
Port int `json:"port"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
type chainSocks5Config struct {
LocalPort int `json:"localPort,omitempty"`
First chainSocks5Hop `json:"first"`
Second chainSocks5Hop `json:"second"`
}
func IsChainSocks5Proxy(src string) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(src)), chainSocks5Prefix)
}
func ParseChainSocks5Config(src string) (*chainSocks5Config, error) {
raw := strings.TrimSpace(src)
if !IsChainSocks5Proxy(raw) {
return nil, fmt.Errorf("不是链式代理配置")
}
encoded := raw[len(chainSocks5Prefix):]
if strings.TrimSpace(encoded) == "" {
return nil, fmt.Errorf("链式代理配置为空")
}
decoded, err := url.QueryUnescape(encoded)
if err != nil {
return nil, fmt.Errorf("链式代理配置解码失败: %w", err)
}
var cfg chainSocks5Config
if err := json.Unmarshal([]byte(decoded), &cfg); err != nil {
return nil, fmt.Errorf("链式代理配置 JSON 解析失败: %w", err)
}
if err := validateChainSocks5Hop("第一层", cfg.First); err != nil {
return nil, err
}
if err := validateChainSocks5Hop("第二层", cfg.Second); err != nil {
return nil, err
}
if cfg.LocalPort < 0 || cfg.LocalPort > 65535 {
return nil, fmt.Errorf("本地监听端口必须在 1-65535 之间")
}
cfg.First.Protocol = normalizeChainHopProtocol(cfg.First.Protocol)
cfg.Second.Protocol = normalizeChainHopProtocol(cfg.Second.Protocol)
return &cfg, nil
}
func normalizeChainHopProtocol(protocol string) string {
normalized := strings.ToLower(strings.TrimSpace(protocol))
if normalized == "" {
return "socks5"
}
return normalized
}
func validateChainSocks5Hop(label string, hop chainSocks5Hop) error {
if strings.TrimSpace(hop.Server) == "" {
return fmt.Errorf("%s代理地址不能为空", label)
}
if hop.Port < 1 || hop.Port > 65535 {
return fmt.Errorf("%s代理端口必须在 1-65535 之间", label)
}
protocol := strings.ToLower(strings.TrimSpace(hop.Protocol))
if protocol != "" && protocol != "socks5" && protocol != "http" {
return fmt.Errorf("%s协议仅支持 http 或 socks5", label)
}
if strings.TrimSpace(hop.Password) != "" && strings.TrimSpace(hop.Username) == "" {
return fmt.Errorf("%s填写密码时请同时填写账号", label)
}
return nil
}
// 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 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("不支持的节点协议")
}