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

310 lines
8.8 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/base64"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"ant-chrome/backend/internal/config"
xproxy "golang.org/x/net/proxy"
"gopkg.in/yaml.v3"
)
// TestResult 代理测试结果
type TestResult struct {
ProxyId string
Ok bool
LatencyMs int64
Error string
}
// proxyEndpoint 从代理配置中提取 server:port,用于 TCP ping
func proxyEndpoint(src string) (string, error) {
src = strings.TrimSpace(src)
l := strings.ToLower(src)
// 标准 URL 格式: socks5://host:port, http://host:port
if strings.HasPrefix(l, "socks5://") || strings.HasPrefix(l, "http://") || strings.HasPrefix(l, "https://") {
hostport := src[strings.Index(src, "//")+2:]
hostport = strings.SplitN(hostport, "/", 2)[0]
return hostport, nil
}
// vmess:// URL (base64 encoded JSON)
if strings.HasPrefix(l, "vmess://") {
raw := strings.TrimPrefix(src, "vmess://")
decoded, err := decodeBase64String(strings.TrimSpace(raw))
if err == nil {
var v struct {
Add string `json:"add"`
Port interface{} `json:"port"`
}
if jsonErr := json.Unmarshal(decoded, &v); jsonErr == nil && v.Add != "" {
return fmt.Sprintf("%s:%v", v.Add, v.Port), nil
}
}
}
// vless:// URL: vless://uuid@host:port?...
if strings.HasPrefix(l, "vless://") {
rest := src[len("vless://"):]
if at := strings.LastIndex(rest, "@"); at >= 0 {
hostport := strings.SplitN(rest[at+1:], "?", 2)[0]
hostport = strings.SplitN(hostport, "#", 2)[0]
return hostport, nil
}
}
// Clash YAML 格式
var payload interface{}
if err := yaml.Unmarshal([]byte(src), &payload); err == nil {
node := pickClashNode(payload)
if node != nil {
server := getMapString(node, "server")
port := getMapInt(node, "port")
if server != "" && port > 0 {
return fmt.Sprintf("%s:%d", server, port), nil
}
}
}
return "", fmt.Errorf("无法解析代理地址")
}
// TestConnectivity 通过 TCP 握手测试代理服务器的可达性和延迟
// 直接对 server:port 建立 TCP 连接测量 RTT,无需启动外部进程
func TestConnectivity(proxyId string, proxyConfig string, proxies []config.BrowserProxy, _ interface{}) TestResult {
src := strings.TrimSpace(proxyConfig)
if proxyId != "" {
for _, item := range proxies {
if strings.EqualFold(item.ProxyId, proxyId) {
src = strings.TrimSpace(item.ProxyConfig)
break
}
}
}
if src == "" {
return TestResult{ProxyId: proxyId, Ok: false, Error: "代理配置为空"}
}
endpoint, err := proxyEndpoint(src)
if err != nil {
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("地址解析失败: %v", err)}
}
start := time.Now()
conn, err := net.DialTimeout("tcp", endpoint, 10*time.Second)
latency := time.Since(start).Milliseconds()
if err != nil {
return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: latency, Error: err.Error()}
}
conn.Close()
return TestResult{ProxyId: proxyId, Ok: true, LatencyMs: latency}
}
func toStringMap(input interface{}) map[string]interface{} {
switch v := input.(type) {
case map[string]interface{}:
return v
case map[interface{}]interface{}:
out := map[string]interface{}{}
for k, val := range v {
out[fmt.Sprint(k)] = val
}
return out
}
return nil
}
func getMapString(m map[string]interface{}, key string) string {
v, ok := m[key]
if !ok {
return ""
}
switch s := v.(type) {
case string:
return strings.TrimSpace(s)
case int:
return strconv.Itoa(s)
case int64:
return strconv.FormatInt(s, 10)
case float64:
return strconv.Itoa(int(s))
case bool:
if s {
return "true"
}
return "false"
}
return strings.TrimSpace(fmt.Sprint(v))
}
func getMapInt(m map[string]interface{}, key string) int {
v, ok := m[key]
if !ok {
return 0
}
switch s := v.(type) {
case int:
return s
case int64:
return int(s)
case float64:
return int(s)
case string:
value, _ := strconv.Atoi(s)
return value
}
return 0
}
func getMapBool(m map[string]interface{}, key string) bool {
v, ok := m[key]
if !ok {
return false
}
switch s := v.(type) {
case bool:
return s
case string:
return strings.ToLower(s) == "true"
case int:
return s != 0
case float64:
return int(s) != 0
}
return false
}
func decodeBase64String(raw string) ([]byte, error) {
if raw == "" {
return nil, fmt.Errorf("base64 内容为空")
}
if data, err := base64.StdEncoding.DecodeString(raw); err == nil {
return data, nil
}
if data, err := base64.RawStdEncoding.DecodeString(raw); err == nil {
return data, nil
}
if data, err := base64.URLEncoding.DecodeString(raw); err == nil {
return data, nil
}
if data, err := base64.RawURLEncoding.DecodeString(raw); err == nil {
return data, nil
}
return nil, fmt.Errorf("base64 解析失败")
}
// isUnsupportedProtocol 判断是否为不支持的协议(hysteria/hysteria2
func isUnsupportedProtocol(src string) bool {
l := strings.ToLower(strings.TrimSpace(src))
return strings.HasPrefix(l, "hysteria://") || strings.HasPrefix(l, "hysteria2://")
}
// TestRealConnectivity 通过代理链路发起真实 HTTP 请求测量端到端延迟。
// - DirectProxy (http/https/socks5):直接通过该代理发送请求
// - BridgeProxy (vmess/vless/Clash):调用 EnsureBridge 获取 socks5 地址后发送请求
// - SingBoxProxy (hysteria2/tuic):调用 SingBoxManager.EnsureBridge 后发送请求
func TestRealConnectivity(
proxyId string,
proxies []config.BrowserProxy,
xrayMgr *XrayManager,
) TestResult {
return TestRealConnectivityWithSingBox(proxyId, proxies, xrayMgr, nil)
}
// TestRealConnectivityWithSingBox 支持 sing-box 的真实连通性测试
func TestRealConnectivityWithSingBox(
proxyId string,
proxies []config.BrowserProxy,
xrayMgr *XrayManager,
singboxMgr *SingBoxManager,
) TestResult {
src := ""
for _, item := range proxies {
if strings.EqualFold(item.ProxyId, proxyId) {
src = strings.TrimSpace(item.ProxyConfig)
break
}
}
if src == "" {
return TestResult{ProxyId: proxyId, Ok: false, Error: "代理配置为空"}
}
const targetURL = "http://www.gstatic.com/generate_204"
const timeout = 15 * time.Second
var client *http.Client
if IsSingBoxProtocol(src) {
// hysteria2/tuic → sing-box 桥接
if singboxMgr == nil {
return TestResult{ProxyId: proxyId, Ok: false, Error: "sing-box 管理器未初始化,无法测试 hysteria2"}
}
socks5Addr, err := singboxMgr.EnsureBridge(src, proxies, proxyId)
if err != nil {
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("sing-box 桥接启动失败: %v", err)}
}
socks5Host := strings.TrimPrefix(socks5Addr, "socks5://")
dialer, err := xproxy.SOCKS5("tcp", socks5Host, nil, xproxy.Direct)
if err != nil {
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("SOCKS5 dialer 创建失败: %v", err)}
}
contextDialer, ok := dialer.(xproxy.ContextDialer)
if !ok {
return TestResult{ProxyId: proxyId, Ok: false, Error: "SOCKS5 dialer 不支持 ContextDialer"}
}
transport := &http.Transport{DialContext: contextDialer.DialContext}
client = &http.Client{Transport: transport, Timeout: timeout}
} else if RequiresBridge(src, proxies, proxyId) {
// BridgeProxy:通过 xray socks5 桥接
if xrayMgr == nil {
return TestResult{ProxyId: proxyId, Ok: false, Error: "xray 管理器未初始化"}
}
socks5Addr, err := xrayMgr.EnsureBridge(src, proxies, proxyId)
if err != nil {
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("桥接启动失败: %v", err)}
}
// 解析 socks5://127.0.0.1:port
socks5Host := strings.TrimPrefix(socks5Addr, "socks5://")
dialer, err := xproxy.SOCKS5("tcp", socks5Host, nil, xproxy.Direct)
if err != nil {
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("SOCKS5 dialer 创建失败: %v", err)}
}
contextDialer, ok := dialer.(xproxy.ContextDialer)
if !ok {
return TestResult{ProxyId: proxyId, Ok: false, Error: "SOCKS5 dialer 不支持 ContextDialer"}
}
transport := &http.Transport{DialContext: contextDialer.DialContext}
client = &http.Client{Transport: transport, Timeout: timeout}
} else {
// DirectProxyhttp/https/socks5 直接代理
proxyURL, err := url.Parse(src)
if err != nil {
return TestResult{ProxyId: proxyId, Ok: false, Error: fmt.Sprintf("代理地址解析失败: %v", err)}
}
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
client = &http.Client{Transport: transport, Timeout: timeout}
}
start := time.Now()
resp, err := client.Get(targetURL)
latency := time.Since(start).Milliseconds()
if err != nil {
return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: latency, Error: err.Error()}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return TestResult{ProxyId: proxyId, Ok: false, LatencyMs: latency, Error: fmt.Sprintf("HTTP %d", resp.StatusCode)}
}
return TestResult{ProxyId: proxyId, Ok: true, LatencyMs: latency}
}