Files
Ant-Browser/backend/internal/proxy/xray_bridge_launch.go
T

437 lines
12 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 (
"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("复用桥接进程", 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("复用桥接进程", 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
}
}
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("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("复用已就绪桥接进程", 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 配置预检失败: %w%s", err, 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 15 * time.Second
}
func (m *XrayManager) describeBridgeReadyError(err error, cfgPath string, stderrPath string) string {
parts := []string{err.Error()}
if strings.TrimSpace(cfgPath) != "" {
parts = append(parts, "配置文件: "+cfgPath)
}
if tail := readLogTail(stderrPath, 1200); tail != "" {
parts = append(parts, "stderr: "+tail)
} else if cfgPath != "" {
if tail := readLogTail(filepath.Join(filepath.Dir(cfgPath), "xray-error.log"), 1200); tail != "" {
parts = append(parts, "error.log: "+tail)
}
}
return strings.Join(parts, "")
}
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)))
}
}