Files

475 lines
14 KiB
Go

package proxy
import (
"ant-chrome/backend/internal/config"
"ant-chrome/backend/internal/logger"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func (m *XrayManager) ensureBridge(proxyConfig string, proxies []config.BrowserProxy, proxyId string, pin bool) (string, string, error) {
log := logger.New("Xray")
src := resolveProxyConfig(proxyConfig, proxies, proxyId)
dnsServers := ""
if proxyId != "" {
for _, item := range proxies {
if strings.EqualFold(item.ProxyId, proxyId) {
dnsServers = item.DnsServers
break
}
}
}
if src == "" {
return "", "", fmt.Errorf("未找到代理节点")
}
src = normalizeNodeScheme(src)
var (
outbounds []interface{}
routes []interface{}
preferredPort int
)
if IsChainSocks5Proxy(src) {
chainCfg, err := ParseChainSocks5Config(src)
if err != nil {
log.Error("链式节点解析失败", logger.F("error", err))
return "", "", err
}
outbounds = []interface{}{
chainSocks5Outbound(chainCfg.First, "first-hop", ""),
chainSocks5Outbound(chainCfg.Second, "second-hop", "first-hop"),
}
routes = []interface{}{
map[string]interface{}{
"type": "field",
"inboundTag": []string{"socks-in"},
"outboundTag": "second-hop",
},
}
preferredPort = chainCfg.LocalPort
} else {
directOutbound, shouldBridgeDirectProxy, err := buildDirectProxyBridgeOutbound(src)
if err != nil {
log.Error("直连代理桥接配置解析失败", logger.F("error", err))
return "", "", err
}
if shouldBridgeDirectProxy {
outbounds = []interface{}{directOutbound}
routes = []interface{}{
map[string]interface{}{
"type": "field",
"inboundTag": []string{"socks-in"},
"outboundTag": "proxy-out",
},
}
} else {
standardProxy, outbound, err := ParseProxyNode(src)
if err != nil {
log.Error("节点解析失败", logger.F("error", err))
return "", "", err
}
if standardProxy != "" {
return standardProxy, "", nil
}
if outbound == nil {
return "", "", fmt.Errorf("节点解析失败")
}
outbounds = []interface{}{outbound}
routes = []interface{}{
map[string]interface{}{
"type": "field",
"inboundTag": []string{"socks-in"},
"outboundTag": "proxy-out",
},
}
}
}
key := computeNodeKey(src + "\x00" + dnsServers)
if socksURL, reused := m.tryReuseBridge(key, pin); reused {
log.Info("复用 xray 桥接进程", logger.F("engine", "xray"), logger.F("key", key), logger.F("socks_url", socksURL))
return socksURL, key, nil
}
unlockLaunch := m.lockLaunchForKey(key)
defer unlockLaunch()
if socksURL, reused := m.tryReuseBridge(key, pin); reused {
log.Info("复用 xray 桥接进程", logger.F("engine", "xray"), logger.F("key", key), logger.F("socks_url", socksURL))
return socksURL, key, nil
}
binaryPath, err := m.resolveBinary()
if err != nil {
log.Error("xray 不可用", logger.F("error", err))
return "", "", err
}
maxLaunchRetries := 2
if preferredPort > 0 {
maxLaunchRetries = 1
}
var lastErr error
attemptsUsed := 0
for attempt := 1; attempt <= maxLaunchRetries; attempt++ {
attemptsUsed = attempt
socksURL, bridge, err := m.launchBridgeAttempt(log, key, binaryPath, outbounds, routes, preferredPort, dnsServers, pin, attempt)
if err == nil {
return socksURL, key, nil
}
if bridge != nil && bridge.Running {
go m.watchBridge(bridge, key)
}
lastErr = err
if !isRetryableXrayLaunchError(err) {
break
}
}
if !isRetryableXrayLaunchError(lastErr) {
return "", "", lastErr
}
return "", "", fmt.Errorf("xray 启动失败(已尝试 %d 次): %w", attemptsUsed, lastErr)
}
type xrayLaunchError struct {
err error
retryable bool
}
func (e *xrayLaunchError) Error() string {
if e == nil || e.err == nil {
return ""
}
return e.err.Error()
}
func (e *xrayLaunchError) Unwrap() error {
if e == nil {
return nil
}
return e.err
}
func isRetryableXrayLaunchError(err error) bool {
if err == nil {
return false
}
var launchErr *xrayLaunchError
if errors.As(err, &launchErr) {
return launchErr.retryable
}
return true
}
func (m *XrayManager) launchBridgeAttempt(log *logger.Logger, key string, binaryPath string, outbounds []interface{}, routes []interface{}, preferredPort int, dnsServers string, pin bool, attempt int) (string, *XrayBridge, error) {
port := preferredPort
if port <= 0 {
var err error
port, err = nextAvailablePort()
if err != nil {
log.Error("端口分配失败", logger.F("error", err), logger.F("attempt", attempt))
return "", nil, err
}
}
cfgPath, err := m.buildRuntimeConfigWithRoute(key, outbounds, routes, port, dnsServers)
if err != nil {
log.Error("xray 配置生成失败", logger.F("error", err))
return "", nil, err
}
stderrPath := filepath.Join(filepath.Dir(cfgPath), "xray-stderr.log")
if err := m.testRuntimeConfig(binaryPath, cfgPath, stderrPath); err != nil {
log.Error("xray 配置预检失败", logger.F("error", err), logger.F("attempt", attempt), logger.F("config", cfgPath))
return "", nil, err
}
cmd := exec.Command(binaryPath, "run", "-c", cfgPath)
hideWindow(cmd)
cmd.Dir = filepath.Dir(cfgPath)
stderrFile, _ := os.Create(stderrPath)
if stderrFile != nil {
cmd.Stderr = stderrFile
}
if err := cmd.Start(); err != nil {
if stderrFile != nil {
stderrFile.Close()
}
log.Error("xray 启动失败", logger.F("error", err), logger.F("attempt", attempt))
return "", nil, &xrayLaunchError{err: err, retryable: false}
}
bridge := &XrayBridge{
NodeKey: key,
Port: port,
Cmd: cmd,
Pid: cmd.Process.Pid,
Running: true,
RefCount: 0,
LastUsedAt: time.Now(),
Outbounds: cloneInterfaceSlice(outbounds),
Routes: cloneInterfaceSlice(routes),
DNSServers: dnsServers,
}
bridge.startExitWatcher()
log.Info("xray 内核进程已启动", logger.F("engine", "xray"), logger.F("key", key), logger.F("pid", bridge.Pid), logger.F("port", bridge.Port), logger.F("attempt", attempt))
if err := m.waitBridgeReady(log, bridge, cfgPath, stderrPath, stderrFile, attempt); err != nil {
return "", nil, err
}
if socksURL, reused := m.registerBridge(key, bridge, pin); reused {
log.Info("复用已就绪 xray 桥接进程", logger.F("engine", "xray"), logger.F("key", key), logger.F("socks_url", socksURL))
bridge.Stopping = true
m.stopBridgeProcess(bridge)
return socksURL, nil, nil
}
return fmt.Sprintf("socks5://127.0.0.1:%d", port), bridge, nil
}
func chainSocks5Outbound(hop chainSocks5Hop, tag string, nextTag string) map[string]interface{} {
protocol := normalizeChainHopProtocol(hop.Protocol)
if protocol == "http" {
return chainHTTPOutbound(hop, tag, nextTag)
}
user := map[string]interface{}{}
if strings.TrimSpace(hop.Username) != "" {
user["user"] = strings.TrimSpace(hop.Username)
if strings.TrimSpace(hop.Password) != "" {
user["pass"] = hop.Password
}
}
server := map[string]interface{}{
"address": strings.TrimSpace(hop.Server),
"port": hop.Port,
}
if len(user) > 0 {
server["users"] = []interface{}{user}
}
outbound := map[string]interface{}{
"protocol": "socks",
"tag": tag,
"settings": map[string]interface{}{
"servers": []interface{}{server},
},
}
if strings.TrimSpace(nextTag) != "" {
outbound["proxySettings"] = map[string]interface{}{
"tag": strings.TrimSpace(nextTag),
}
}
return outbound
}
func chainHTTPOutbound(hop chainSocks5Hop, tag string, nextTag string) map[string]interface{} {
user := map[string]interface{}{}
if strings.TrimSpace(hop.Username) != "" {
user["user"] = strings.TrimSpace(hop.Username)
if strings.TrimSpace(hop.Password) != "" {
user["pass"] = hop.Password
}
}
server := map[string]interface{}{
"address": strings.TrimSpace(hop.Server),
"port": hop.Port,
}
if len(user) > 0 {
server["users"] = []interface{}{user}
}
outbound := map[string]interface{}{
"protocol": "http",
"tag": tag,
"settings": map[string]interface{}{
"servers": []interface{}{server},
},
}
if strings.TrimSpace(nextTag) != "" {
outbound["proxySettings"] = map[string]interface{}{
"tag": strings.TrimSpace(nextTag),
}
}
return outbound
}
func (m *XrayManager) waitBridgeReady(log *logger.Logger, bridge *XrayBridge, cfgPath string, stderrPath string, stderrFile *os.File, attempt int) error {
if err := m.waitBridgeSocksReady(bridge, m.bridgeStartTimeout()); err != nil {
if stderrFile != nil {
stderrFile.Close()
}
m.logBridgeStartupError(log, cfgPath, stderrPath)
bridge.Stopping = true
m.stopBridgeProcess(bridge)
bridge.Running = false
bridge.Pid = 0
bridge.LastError = m.describeBridgeReadyError(err, cfgPath, stderrPath)
retryable := m.isRetryableBridgeReadyError(err, cfgPath, stderrPath)
message := "xray 桥接未就绪"
if retryable {
message = "xray 桥接未就绪,重试"
}
log.Error(message, logger.F("key", bridge.NodeKey), logger.F("error", err), logger.F("port", bridge.Port), logger.F("attempt", attempt), logger.F("retryable", retryable))
time.Sleep(200 * time.Millisecond)
return &xrayLaunchError{err: fmt.Errorf("%s", bridge.LastError), retryable: retryable}
}
if stderrFile != nil {
stderrFile.Close()
}
return nil
}
func (m *XrayManager) isRetryableBridgeReadyError(err error, cfgPath string, stderrPath string) bool {
if err == nil {
return false
}
message := strings.ToLower(err.Error())
if !strings.Contains(message, "提前退出") {
return true
}
tail := strings.ToLower(readLogTail(stderrPath, 1200))
if tail == "" && strings.TrimSpace(cfgPath) != "" {
tail = strings.ToLower(readLogTail(filepath.Join(filepath.Dir(cfgPath), "xray-error.log"), 1200))
}
return strings.Contains(tail, "address already in use") ||
strings.Contains(tail, "only one usage of each socket") ||
strings.Contains(tail, "bind:") ||
strings.Contains(tail, "bind ")
}
func (m *XrayManager) testRuntimeConfig(binaryPath string, cfgPath string, stderrPath string) error {
cmd := exec.Command(binaryPath, "run", "-test", "-c", cfgPath)
hideWindow(cmd)
cmd.Dir = filepath.Dir(cfgPath)
stderrFile, _ := os.Create(stderrPath)
if stderrFile != nil {
defer stderrFile.Close()
cmd.Stderr = stderrFile
}
output, err := cmd.Output()
if err == nil {
return nil
}
if len(output) > 0 && stderrFile != nil {
_, _ = stderrFile.Write(output)
}
return &xrayLaunchError{
err: fmt.Errorf("Xray 配置错误:%s", m.describeBridgeReadyError(err, cfgPath, stderrPath)),
retryable: false,
}
}
func (m *XrayManager) waitBridgeSocksReady(bridge *XrayBridge, timeout time.Duration) error {
if bridge == nil {
return fmt.Errorf("xray 桥接进程不存在")
}
deadline := time.NewTimer(timeout)
defer deadline.Stop()
ready := make(chan error, 1)
go func() {
ready <- waitSocks5Ready("127.0.0.1", bridge.Port, timeout)
}()
select {
case err := <-ready:
return err
case <-bridge.ExitDone:
if err := bridge.exitErr(); err != nil {
return fmt.Errorf("xray 进程提前退出: %w", err)
}
return fmt.Errorf("xray 进程提前退出")
case <-deadline.C:
return fmt.Errorf("xray socks5 端口 %d 启动超时", bridge.Port)
}
}
func (m *XrayManager) bridgeStartTimeout() time.Duration {
if m != nil && m.Config != nil && m.Config.ProxyCheck.BridgeStartTimeoutMs > 0 {
return time.Duration(m.Config.ProxyCheck.BridgeStartTimeoutMs) * time.Millisecond
}
return time.Duration(defaultBridgeStartTimeoutMs) * time.Millisecond
}
func (m *XrayManager) describeBridgeReadyError(err error, cfgPath string, stderrPath string) string {
if tail := readLogTail(stderrPath, 1200); tail != "" {
return summarizeXrayError(tail)
} else if cfgPath != "" {
if tail := readLogTail(filepath.Join(filepath.Dir(cfgPath), "xray-error.log"), 1200); tail != "" {
return summarizeXrayError(tail)
}
}
if err != nil {
return summarizeXrayError(err.Error())
}
return "未知错误"
}
func summarizeXrayError(raw string) string {
text := strings.TrimSpace(raw)
if text == "" {
return "未知错误"
}
text = strings.ReplaceAll(text, "\r\n", "\n")
lines := strings.Split(text, "\n")
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line != "" {
text = line
break
}
}
text = strings.TrimSpace(strings.TrimPrefix(text, "Failed to start:"))
if idx := strings.LastIndex(text, " > "); idx >= 0 && idx+3 < len(text) {
text = strings.TrimSpace(text[idx+3:])
}
text = strings.TrimPrefix(text, "infra/conf: ")
text = strings.TrimPrefix(text, "main: ")
if idx := strings.Index(text, "Try "); idx > 0 {
text = strings.TrimSpace(text[:idx])
}
if idx := strings.Index(text, "请检查"); idx > 0 {
text = strings.TrimSpace(text[:idx])
}
text = strings.Trim(text, " .;。")
if strings.Contains(text, "allowInsecure") && strings.Contains(strings.ToLower(text), "removed") {
return "字段 allowInsecure 已被当前 Xray 移除"
}
if text == "" {
return "未知错误"
}
return text
}
func readLogTail(path string, max int) string {
if strings.TrimSpace(path) == "" || max <= 0 {
return ""
}
data, err := os.ReadFile(path)
if err != nil || len(data) == 0 {
return ""
}
text := strings.TrimSpace(string(data))
if len(text) <= max {
return text
}
return text[len(text)-max:]
}
func (m *XrayManager) logBridgeStartupError(log *logger.Logger, cfgPath string, stderrPath string) {
if stderrContent, readErr := os.ReadFile(stderrPath); readErr == nil && len(stderrContent) > 0 {
log.Error("xray stderr", logger.F("output", string(stderrContent)))
return
}
errLogPath := filepath.Join(filepath.Dir(cfgPath), "xray-error.log")
if errContent, readErr := os.ReadFile(errLogPath); readErr == nil && len(errContent) > 0 {
log.Error("xray error.log", logger.F("output", string(errContent)))
}
}