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:
Ant Browser Release Bot
2026-03-13 23:19:29 +08:00
commit 6f58a6c19a
230 changed files with 44624 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
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}
}