package custom import ( "encoding/base64" "encoding/json" "fmt" "log" "net" "net/url" "os" "strconv" "strings" "gopkg.in/yaml.v3" ) // ParsedNode 解析后的代理节点 type ParsedNode struct { Name string // 节点名称 Type string // vmess/trojan/ss/vless/hysteria2/http/socks5 等 Server string // 远程服务器地址 Port int // 远程服务器端口 Raw map[string]interface{} // 原始配置字段(用于生成 sing-box 配置) } // NodeKey 节点去重 key func (n *ParsedNode) NodeKey() string { return fmt.Sprintf("%s:%s:%d", n.Type, n.Server, n.Port) } // IsDirect 是否可以直接作为代理使用(不需要 sing-box 转换) func (n *ParsedNode) IsDirect() bool { return n.Type == "http" || n.Type == "socks5" } // DirectAddress 返回直接代理的地址 func (n *ParsedNode) DirectAddress() string { return net.JoinHostPort(n.Server, strconv.Itoa(n.Port)) } // DirectProtocol 返回直接代理的协议名 func (n *ParsedNode) DirectProtocol() string { if n.Type == "socks5" { return "socks5" } return "http" } // Parse 解析订阅内容(全自动检测格式) func Parse(data []byte, format string) ([]ParsedNode, error) { // 无论用户选择什么格式,都走自动检测 return parseAutoDetect(data) } // parseAutoDetect 自动检测订阅格式并解析 func parseAutoDetect(data []byte) ([]ParsedNode, error) { content := strings.TrimSpace(string(data)) log.Printf("[custom] 自动检测格式: 内容长度=%d", len(content)) // 1. 尝试 Clash YAML if looksLikeYAML(content) { log.Println("[custom] 检测到 Clash YAML 格式") nodes, err := parseClash(data) if err == nil && len(nodes) > 0 { return nodes, nil } log.Printf("[custom] YAML 解析无有效节点,继续尝试其他格式...") } // 2. 直接包含协议链接(vmess://、vless:// 等) if looksLikeProxyLinks(content) { log.Println("[custom] 检测到协议链接格式") return parseProxyLinks(content) } // 3. 尝试 Base64 解码 decoded, err := tryBase64Decode(content) if err != nil { return nil, fmt.Errorf("无法识别订阅内容格式(非 YAML / 非协议链接 / 非 Base64)") } decodedStr := strings.TrimSpace(string(decoded)) if decodedStr == "" { return nil, fmt.Errorf("Base64 解码后内容为空") } log.Printf("[custom] Base64 解码成功: %d bytes", len(decoded)) // 解码后是 YAML? if looksLikeYAML(decodedStr) { log.Println("[custom] Base64 解码后为 Clash YAML 格式") return parseClash(decoded) } // 解码后是协议链接? if looksLikeProxyLinks(decodedStr) { log.Println("[custom] Base64 解码后为协议链接格式") return parseProxyLinks(decodedStr) } // 解码后尝试纯文本 nodes, err := parsePlain(decoded) if err == nil && len(nodes) > 0 { return nodes, nil } return nil, fmt.Errorf("无法识别订阅内容格式") } func safePreview(s string, n int) string { if len(s) > n { return s[:n] + "..." } return s } // looksLikeProxyLinks 判断内容是否包含代理协议链接 func looksLikeProxyLinks(s string) bool { return strings.Contains(s, "vmess://") || strings.Contains(s, "vless://") || strings.Contains(s, "trojan://") || strings.Contains(s, "ss://") || strings.Contains(s, "ssr://") || strings.Contains(s, "hysteria2://") || strings.Contains(s, "hy2://") || strings.Contains(s, "tuic://") } // clashConfig Clash YAML 配置结构(兼容新旧格式) type clashConfig struct { Proxies []map[string]interface{} `yaml:"proxies"` ProxyOld []map[string]interface{} `yaml:"Proxy"` // 旧版 Clash 格式 } // getProxies 兼容获取代理列表 func (c *clashConfig) getProxies() []map[string]interface{} { if len(c.Proxies) > 0 { return c.Proxies } return c.ProxyOld } // parseClash 解析 Clash YAML 格式 func parseClash(data []byte) ([]ParsedNode, error) { content := strings.TrimSpace(string(data)) // 打印前 100 字符帮助调试 preview := content if len(preview) > 100 { preview = preview[:100] } log.Printf("[custom] 订阅内容预览: %s...", preview) // 自动检测:如果内容不像 YAML(不以常见 YAML 字段开头),尝试 base64 解码 if !looksLikeYAML(content) { log.Println("[custom] 内容不像 YAML,尝试 Base64 解码...") decoded, err := tryBase64Decode(content) if err == nil && looksLikeYAML(string(decoded)) { log.Println("[custom] Base64 解码成功,使用解码后的 YAML") data = decoded } else { log.Println("[custom] Base64 解码后仍不是 YAML,按原始内容解析") } } // 使用 yaml.Node 解析,精确提取 proxies 列表 var doc yaml.Node if err := yaml.Unmarshal(data, &doc); err != nil { return nil, fmt.Errorf("解析 Clash YAML 失败: %w", err) } var proxies []map[string]interface{} proxies = extractProxiesFromNode(&doc) if len(proxies) == 0 { log.Printf("[custom] ⚠️ YAML 中未找到有效的代理节点(内容长度: %d 字节)", len(data)) } else { log.Printf("[custom] 从 YAML 中提取到 %d 个代理节点", len(proxies)) } var nodes []ParsedNode for _, proxy := range proxies { node, err := parseClashProxy(proxy) if err != nil { log.Printf("[custom] 跳过无效节点: %v", err) continue } nodes = append(nodes, *node) } log.Printf("[custom] Clash YAML 解析完成,共 %d 个节点", len(nodes)) return nodes, nil } // extractProxiesFromNode 从 yaml.Node 树中提取 proxies 列表 func extractProxiesFromNode(doc *yaml.Node) []map[string]interface{} { if doc == nil { return nil } // doc 是 DocumentNode,内容在 Content[0](MappingNode) var root *yaml.Node if doc.Kind == yaml.DocumentNode && len(doc.Content) > 0 { root = doc.Content[0] } else if doc.Kind == yaml.MappingNode { root = doc } else { return nil } // 在 MappingNode 中找 proxies 或 Proxy key for i := 0; i < len(root.Content)-1; i += 2 { keyNode := root.Content[i] valNode := root.Content[i+1] if keyNode.Value == "proxies" || keyNode.Value == "Proxy" { log.Printf("[custom] 找到 %s 字段: kind=%d tag=%s 子节点数=%d", keyNode.Value, valNode.Kind, valNode.Tag, len(valNode.Content)) // 把 proxies 段的原始 YAML 写到临时文件方便调试 debugData, _ := yaml.Marshal(valNode) os.WriteFile("/tmp/goproxy_debug_proxies.yaml", debugData, 0644) log.Printf("[custom] 调试: proxies 原始数据已写入 /tmp/goproxy_debug_proxies.yaml (%d bytes)", len(debugData)) if valNode.Kind != yaml.SequenceNode { log.Printf("[custom] proxies 字段不是列表(kind=%d tag=%s)", valNode.Kind, valNode.Tag) return nil } // 每个 item 是一个 MappingNode,解码为 map var proxies []map[string]interface{} for idx, itemNode := range valNode.Content { var m map[string]interface{} if err := itemNode.Decode(&m); err != nil { log.Printf("[custom] 解码代理节点 #%d 失败: %v (kind=%d tag=%s)", idx, err, itemNode.Kind, itemNode.Tag) continue } proxies = append(proxies, m) } log.Printf("[custom] 成功解码 %d/%d 个代理节点", len(proxies), len(valNode.Content)) return proxies } } log.Printf("[custom] 遍历 %d 个顶级 key 未找到 proxies", len(root.Content)/2) return nil } // looksLikeYAML 判断内容是否看起来像 YAML/Clash 配置 func looksLikeYAML(s string) bool { s = strings.TrimSpace(s) // Clash YAML 通常包含 proxies: 或 port: 或以 # 注释开头 return strings.Contains(s, "proxies:") || strings.Contains(s, "proxy-groups:") || strings.HasPrefix(s, "port:") || strings.HasPrefix(s, "mixed-port:") || strings.HasPrefix(s, "#") || strings.HasPrefix(s, "---") } // tryBase64Decode 尝试多种 Base64 变体解码 func tryBase64Decode(s string) ([]byte, error) { // 去掉所有空白字符(换行、回车、空格)再解码 s = strings.TrimSpace(s) s = strings.ReplaceAll(s, "\r", "") s = strings.ReplaceAll(s, "\n", "") s = strings.ReplaceAll(s, " ", "") // 标准 Base64 if decoded, err := base64.StdEncoding.DecodeString(s); err == nil { return decoded, nil } // URL-safe Base64 if decoded, err := base64.URLEncoding.DecodeString(s); err == nil { return decoded, nil } // 无填充 Base64 if decoded, err := base64.RawStdEncoding.DecodeString(s); err == nil { return decoded, nil } // 无填充 URL-safe if decoded, err := base64.RawURLEncoding.DecodeString(s); err == nil { return decoded, nil } return nil, fmt.Errorf("Base64 解码失败") } // parseClashProxy 解析单个 Clash 代理节点 func parseClashProxy(proxy map[string]interface{}) (*ParsedNode, error) { name, _ := proxy["name"].(string) typ, _ := proxy["type"].(string) server, _ := proxy["server"].(string) if typ == "" || server == "" { return nil, fmt.Errorf("缺少 type 或 server 字段") } port := 0 switch v := proxy["port"].(type) { case int: port = v case float64: port = int(v) case string: p, err := strconv.Atoi(v) if err != nil { return nil, fmt.Errorf("无效端口: %s", v) } port = p default: return nil, fmt.Errorf("缺少 port 字段") } // 标准化类型名 typ = strings.ToLower(typ) switch typ { case "ss": typ = "shadowsocks" case "ssr": typ = "shadowsocksr" } // 支持的类型 supported := map[string]bool{ "vmess": true, "vless": true, "trojan": true, "shadowsocks": true, "shadowsocksr": true, "hysteria": true, "hysteria2": true, "tuic": true, "anytls": true, "http": true, "socks5": true, } if !supported[typ] { return nil, fmt.Errorf("不支持的代理类型: %s", typ) } return &ParsedNode{ Name: name, Type: typ, Server: server, Port: port, Raw: proxy, }, nil } // parseBase64 解析 Base64 编码的纯文本 func parseBase64(data []byte) ([]ParsedNode, error) { // 尝试标准 Base64 解码 decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(data))) if err != nil { // 尝试 URL-safe Base64 decoded, err = base64.URLEncoding.DecodeString(strings.TrimSpace(string(data))) if err != nil { // 尝试无填充的 Base64 decoded, err = base64.RawStdEncoding.DecodeString(strings.TrimSpace(string(data))) if err != nil { return nil, fmt.Errorf("Base64 解码失败: %w", err) } } } return parsePlain(decoded) } // parsePlain 解析纯文本格式(每行一个 IP:PORT) func parsePlain(data []byte) ([]ParsedNode, error) { lines := strings.Split(string(data), "\n") var nodes []ParsedNode for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } protocol := "http" addr := line // 解析协议前缀 if strings.HasPrefix(line, "socks5://") { protocol = "socks5" addr = strings.TrimPrefix(line, "socks5://") } else if strings.HasPrefix(line, "socks4://") { protocol = "socks5" // socks4 当 socks5 处理 addr = strings.TrimPrefix(line, "socks4://") } else if strings.HasPrefix(line, "http://") { protocol = "http" addr = strings.TrimPrefix(line, "http://") } else if strings.HasPrefix(line, "https://") { protocol = "http" addr = strings.TrimPrefix(line, "https://") } host, portStr, err := net.SplitHostPort(addr) if err != nil { continue } port, err := strconv.Atoi(portStr) if err != nil { continue } nodes = append(nodes, ParsedNode{ Name: addr, Type: protocol, Server: host, Port: port, Raw: map[string]interface{}{"type": protocol, "server": host, "port": port}, }) } log.Printf("[custom] 纯文本解析完成,共 %d 个节点", len(nodes)) return nodes, nil } // parseProxyLinks 解析协议链接格式(vmess://, trojan://, ss://, vless:// 等) func parseProxyLinks(content string) ([]ParsedNode, error) { lines := strings.Split(content, "\n") var nodes []ParsedNode for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } node, err := parseProxyLink(line) if err != nil { continue } nodes = append(nodes, *node) } log.Printf("[custom] 协议链接解析完成,共 %d 个节点", len(nodes)) return nodes, nil } // parseProxyLink 解析单个协议链接 func parseProxyLink(link string) (*ParsedNode, error) { link = strings.TrimSpace(link) switch { case strings.HasPrefix(link, "vmess://"): return parseVmessLink(link) case strings.HasPrefix(link, "vless://"): return parseStandardLink(link, "vless") case strings.HasPrefix(link, "trojan://"): return parseStandardLink(link, "trojan") case strings.HasPrefix(link, "ss://"): return parseShadowsocksLink(link) case strings.HasPrefix(link, "hysteria2://"), strings.HasPrefix(link, "hy2://"): return parseStandardLink(link, "hysteria2") case strings.HasPrefix(link, "tuic://"): return parseStandardLink(link, "tuic") default: return nil, fmt.Errorf("不支持的协议链接: %s", link[:min(20, len(link))]) } } // parseVmessLink 解析 vmess:// 链接(V2rayN JSON base64 格式) func parseVmessLink(link string) (*ParsedNode, error) { encoded := strings.TrimPrefix(link, "vmess://") decoded, err := tryBase64Decode(encoded) if err != nil { return nil, fmt.Errorf("vmess base64 解码失败: %w", err) } var info map[string]interface{} if err := json.Unmarshal(decoded, &info); err != nil { return nil, fmt.Errorf("vmess JSON 解析失败: %w", err) } server := fmt.Sprintf("%v", info["add"]) portStr := fmt.Sprintf("%v", info["port"]) port, _ := strconv.Atoi(portStr) name := fmt.Sprintf("%v", info["ps"]) if name == "" || name == "" { name = server } // 构建 Clash 兼容的 raw 配置 raw := map[string]interface{}{ "type": "vmess", "name": name, "server": server, "port": port, "uuid": fmt.Sprintf("%v", info["id"]), "alterId": getInt(info, "aid"), "cipher": getStrDefault(info, "scy", "auto"), } // TLS if fmt.Sprintf("%v", info["tls"]) == "tls" { raw["tls"] = true if sni, ok := info["sni"]; ok { raw["sni"] = sni } } // 传输层 net := getStrDefault(info, "net", "tcp") raw["network"] = net if net == "ws" { wsOpts := map[string]interface{}{} if path := getStr(info, "path"); path != "" { wsOpts["path"] = path } if host := getStr(info, "host"); host != "" { wsOpts["headers"] = map[string]interface{}{"Host": host} } raw["ws-opts"] = wsOpts } else if net == "grpc" { grpcOpts := map[string]interface{}{} if path := getStr(info, "path"); path != "" { grpcOpts["grpc-service-name"] = path } raw["grpc-opts"] = grpcOpts } return &ParsedNode{ Name: name, Type: "vmess", Server: server, Port: port, Raw: raw, }, nil } // parseStandardLink 解析标准 URI 格式链接(vless://, trojan://, hysteria2://, tuic://) // 格式: protocol://userinfo@host:port?params#fragment func parseStandardLink(link string, typ string) (*ParsedNode, error) { // 去除协议前缀,统一处理 u, err := url.Parse(link) if err != nil { return nil, fmt.Errorf("链接解析失败: %w", err) } host := u.Hostname() port, _ := strconv.Atoi(u.Port()) if port == 0 { port = 443 } name := u.Fragment if name == "" { name = host } raw := map[string]interface{}{ "type": typ, "name": name, "server": host, "port": port, } // 用户信息(password/uuid) if u.User != nil { password := u.User.Username() if typ == "trojan" || typ == "hysteria2" { raw["password"] = password } else if typ == "vless" || typ == "tuic" { raw["uuid"] = password if p, ok := u.User.Password(); ok { raw["password"] = p // tuic 的 password } } } // 查询参数 params := u.Query() // TLS security := params.Get("security") if security == "" { security = params.Get("type") // 有些链接用 type 表示 } if security != "none" && security != "" || typ == "trojan" || typ == "hysteria2" { raw["tls"] = true if sni := params.Get("sni"); sni != "" { raw["sni"] = sni } if fp := params.Get("fp"); fp != "" { raw["client-fingerprint"] = fp } if alpn := params.Get("alpn"); alpn != "" { raw["alpn"] = strings.Split(alpn, ",") } if params.Get("allowInsecure") == "1" || params.Get("insecure") == "1" { raw["skip-cert-verify"] = true } } // Reality if security == "reality" { raw["tls"] = true realityOpts := map[string]interface{}{} if pbk := params.Get("pbk"); pbk != "" { realityOpts["public-key"] = pbk } if sid := params.Get("sid"); sid != "" { realityOpts["short-id"] = sid } raw["reality-opts"] = realityOpts } // 传输层 netType := params.Get("type") if netType == "" { netType = "tcp" } raw["network"] = netType if netType == "ws" { wsOpts := map[string]interface{}{} if path := params.Get("path"); path != "" { wsOpts["path"] = path } if host := params.Get("host"); host != "" { wsOpts["headers"] = map[string]interface{}{"Host": host} } raw["ws-opts"] = wsOpts } else if netType == "grpc" { grpcOpts := map[string]interface{}{} if sn := params.Get("serviceName"); sn != "" { grpcOpts["grpc-service-name"] = sn } raw["grpc-opts"] = grpcOpts } // Hysteria2 特有 if typ == "hysteria2" { if obfs := params.Get("obfs"); obfs != "" { raw["obfs"] = obfs raw["obfs-password"] = params.Get("obfs-password") } } // VLESS flow if typ == "vless" { if flow := params.Get("flow"); flow != "" { raw["flow"] = flow } } return &ParsedNode{ Name: name, Type: typ, Server: host, Port: port, Raw: raw, }, nil } // parseShadowsocksLink 解析 ss:// 链接 // 格式1: ss://base64(method:password)@host:port#name // 格式2: ss://base64(method:password@host:port)#name func parseShadowsocksLink(link string) (*ParsedNode, error) { link = strings.TrimPrefix(link, "ss://") // 分离 fragment (节点名) name := "" if idx := strings.Index(link, "#"); idx >= 0 { name, _ = url.QueryUnescape(link[idx+1:]) link = link[:idx] } var server, method, password string var port int // 尝试格式1: base64(method:password)@host:port if idx := strings.LastIndex(link, "@"); idx >= 0 { userInfo := link[:idx] hostPort := link[idx+1:] // 解码 userInfo decoded, err := tryBase64Decode(userInfo) if err != nil { // 可能未编码 decoded = []byte(userInfo) } parts := strings.SplitN(string(decoded), ":", 2) if len(parts) == 2 { method = parts[0] password = parts[1] } // 分离 host:port(去掉查询参数) if qIdx := strings.Index(hostPort, "?"); qIdx >= 0 { hostPort = hostPort[:qIdx] } h, p, err := net.SplitHostPort(hostPort) if err != nil { return nil, fmt.Errorf("ss 地址解析失败: %w", err) } server = h port, _ = strconv.Atoi(p) } else { // 格式2: 整个 base64 编码 if qIdx := strings.Index(link, "?"); qIdx >= 0 { link = link[:qIdx] } decoded, err := tryBase64Decode(link) if err != nil { return nil, fmt.Errorf("ss base64 解码失败: %w", err) } // method:password@host:port s := string(decoded) atIdx := strings.LastIndex(s, "@") if atIdx < 0 { return nil, fmt.Errorf("ss 格式无效") } parts := strings.SplitN(s[:atIdx], ":", 2) if len(parts) == 2 { method = parts[0] password = parts[1] } h, p, err := net.SplitHostPort(s[atIdx+1:]) if err != nil { return nil, fmt.Errorf("ss 地址解析失败: %w", err) } server = h port, _ = strconv.Atoi(p) } if name == "" { name = server } raw := map[string]interface{}{ "type": "ss", "name": name, "server": server, "port": port, "cipher": method, "password": password, } return &ParsedNode{ Name: name, Type: "shadowsocks", Server: server, Port: port, Raw: raw, }, nil }