Files
Ant-Browser/backend/internal/proxy/singbox_bridge_runtime.go

445 lines
13 KiB
Go
Raw Permalink 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"
)
// EnsureBridge 确保 sing-box 桥接进程运行,用于临时请求场景。
func (m *SingBoxManager) EnsureBridge(proxyConfig string, proxies []config.BrowserProxy, proxyId string) (string, error) {
socksURL, _, err := m.ensureBridge(proxyConfig, proxies, proxyId, false)
return socksURL, err
}
// AcquireBridge 获取一个带引用计数的 sing-box 桥接,用于浏览器实例等长生命周期场景。
func (m *SingBoxManager) AcquireBridge(proxyConfig string, proxies []config.BrowserProxy, proxyId string) (string, string, error) {
return m.ensureBridge(proxyConfig, proxies, proxyId, true)
}
// ReleaseBridge 释放一个已占用的桥接引用;空闲桥接会由后台回收协程延迟清理。
func (m *SingBoxManager) ReleaseBridge(key string) {
key = strings.TrimSpace(key)
if key == "" {
return
}
m.mu.Lock()
defer m.mu.Unlock()
bridge, ok := m.Bridges[key]
if !ok || bridge == nil {
return
}
if bridge.RefCount > 0 {
bridge.RefCount--
}
bridge.LastUsedAt = time.Now()
}
func (m *SingBoxManager) ensureBridge(proxyConfig string, proxies []config.BrowserProxy, proxyId string, pin bool) (string, string, error) {
log := logger.New("SingBox")
src := resolveProxyConfig(proxyConfig, proxies, proxyId)
if src == "" {
return "", "", fmt.Errorf("未找到代理节点")
}
src = normalizeNodeScheme(src)
outbound, err := BuildSingBoxOutbound(src)
if err != nil {
log.Error("节点解析失败", logger.F("error", err))
return "", "", err
}
key := computeNodeKey(src)
if socksURL, reused := m.tryReuseBridge(key, pin); reused {
log.Info("复用 sing-box 桥接", logger.F("engine", "sing-box"), logger.F("key", key[:8]), 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("复用 sing-box 桥接", logger.F("engine", "sing-box"), logger.F("key", key[:8]), logger.F("socks_url", socksURL))
return socksURL, key, nil
}
binaryPath, err := m.resolveBinary()
if err != nil {
log.Error("sing-box 不可用", logger.F("error", err), logger.F("appRoot", m.AppRoot))
return "", "", err
}
log.Debug("sing-box binary", logger.F("path", binaryPath))
const maxRetries = 2
var lastErr error
attemptsUsed := 0
for attempt := 1; attempt <= maxRetries; attempt++ {
attemptsUsed = attempt
port, err := nextAvailablePort()
if err != nil {
lastErr = err
continue
}
bridge, err := m.launchBridgeOnPort(log, key, binaryPath, outbound, port, attempt)
if err != nil {
lastErr = err
if !isRetryableSingBoxLaunchError(err) {
break
}
continue
}
if socksURL, reused := m.registerBridge(key, bridge, pin); reused {
log.Info("复用已就绪 sing-box 桥接", logger.F("engine", "sing-box"), logger.F("key", key[:8]), logger.F("socks_url", socksURL))
bridge.Stopping = true
m.stopBridgeProcess(bridge)
return socksURL, key, nil
}
go m.watchBridge(bridge, key)
return fmt.Sprintf("socks5://127.0.0.1:%d", port), key, nil
}
return "", "", fmt.Errorf("sing-box 启动失败(已尝试 %d 次): %w", attemptsUsed, lastErr)
}
func (m *SingBoxManager) launchBridgeOnPort(log *logger.Logger, key string, binaryPath string, outbound map[string]interface{}, port int, attempt int) (*SingBoxBridge, error) {
cfgPath, err := m.buildConfig(key, outbound, port)
if err != nil {
return nil, fmt.Errorf("sing-box 配置生成失败: %w", err)
}
stderrPath := filepath.Join(filepath.Dir(cfgPath), "singbox-stderr.log")
if err := m.testRuntimeConfig(binaryPath, cfgPath, stderrPath); err != nil {
log.Error("sing-box 配置预检失败", 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("sing-box 启动失败", logger.F("error", err), logger.F("attempt", attempt))
return nil, &singBoxLaunchError{err: err, retryable: false}
}
bridge := &SingBoxBridge{
NodeKey: key,
Port: port,
Cmd: cmd,
Pid: cmd.Process.Pid,
Running: true,
Outbound: cloneStringInterfaceMap(outbound),
LastUsedAt: time.Now(),
}
bridge.startExitWatcher()
log.Info("sing-box 内核进程已启动", logger.F("engine", "sing-box"), logger.F("key", key[:8]), logger.F("pid", bridge.Pid), logger.F("port", port))
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 := "sing-box 桥接未就绪"
if retryable {
message = "sing-box 桥接未就绪,重试"
}
log.Error(message, logger.F("error", err), logger.F("attempt", attempt), logger.F("port", port), logger.F("retryable", retryable))
time.Sleep(200 * time.Millisecond)
return nil, &singBoxLaunchError{err: fmt.Errorf("%s", bridge.LastError), retryable: retryable}
}
if stderrFile != nil {
stderrFile.Close()
}
return bridge, nil
}
func (m *SingBoxManager) 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
}
type singBoxLaunchError struct {
err error
retryable bool
}
func (e *singBoxLaunchError) Error() string {
if e == nil || e.err == nil {
return ""
}
return e.err.Error()
}
func (e *singBoxLaunchError) Unwrap() error {
if e == nil {
return nil
}
return e.err
}
func isRetryableSingBoxLaunchError(err error) bool {
if err == nil {
return false
}
var launchErr *singBoxLaunchError
if errors.As(err, &launchErr) {
return launchErr.retryable
}
return true
}
func (m *SingBoxManager) testRuntimeConfig(binaryPath string, cfgPath string, stderrPath string) error {
cmd := exec.Command(binaryPath, "check", "-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 &singBoxLaunchError{
err: fmt.Errorf("sing-box 配置预检失败: %w%s", err, m.describeBridgeReadyError(err, cfgPath, stderrPath)),
retryable: false,
}
}
func (m *SingBoxManager) waitBridgeSocksReady(bridge *SingBoxBridge, timeout time.Duration) error {
if bridge == nil {
return fmt.Errorf("sing-box 桥接进程不存在")
}
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("sing-box 进程提前退出: %w", err)
}
return fmt.Errorf("sing-box 进程提前退出")
case <-deadline.C:
return fmt.Errorf("sing-box socks5 端口 %d 启动超时", bridge.Port)
}
}
func (m *SingBoxManager) 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), "singbox-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 *SingBoxManager) 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)
}
return strings.Join(parts, "")
}
func (m *SingBoxManager) logBridgeStartupError(log *logger.Logger, cfgPath string, stderrPath string) {
if stderrContent, readErr := os.ReadFile(stderrPath); readErr == nil && len(stderrContent) > 0 {
log.Error("sing-box stderr", logger.F("output", string(stderrContent)))
}
}
// StopAll 关闭所有 sing-box 桥接进程
func (m *SingBoxManager) StopAll() {
m.stopOnce.Do(func() {
close(m.stopCh)
})
m.mu.Lock()
bridges := make([]*SingBoxBridge, 0, len(m.Bridges))
for key, bridge := range m.Bridges {
if bridge != nil {
bridge.Stopping = true
bridges = append(bridges, bridge)
}
delete(m.Bridges, key)
}
m.mu.Unlock()
for _, bridge := range bridges {
m.stopBridgeProcess(bridge)
}
}
func (m *SingBoxManager) tryReuseBridge(key string, pin bool) (string, bool) {
var stale *SingBoxBridge
m.mu.Lock()
if bridge, ok := m.Bridges[key]; ok && bridge != nil {
alive := bridge.Running && bridge.Cmd != nil && bridge.Cmd.Process != nil && bridge.Cmd.ProcessState == nil
if alive && waitSocks5Ready("127.0.0.1", bridge.Port, 800*time.Millisecond) == nil {
if pin {
bridge.RefCount++
}
bridge.LastUsedAt = time.Now()
socksURL := fmt.Sprintf("socks5://127.0.0.1:%d", bridge.Port)
m.mu.Unlock()
return socksURL, true
}
bridge.Stopping = true
stale = bridge
delete(m.Bridges, key)
}
m.mu.Unlock()
if stale != nil {
m.stopBridgeProcess(stale)
}
return "", false
}
func (m *SingBoxManager) registerBridge(key string, bridge *SingBoxBridge, pin bool) (string, bool) {
var duplicate *SingBoxBridge
m.mu.Lock()
if existing, ok := m.Bridges[key]; ok && existing != nil {
if existing == bridge {
m.mu.Unlock()
return "", false
}
alive := existing.Running && existing.Cmd != nil && existing.Cmd.Process != nil && existing.Cmd.ProcessState == nil
if alive && waitSocks5Ready("127.0.0.1", existing.Port, 800*time.Millisecond) == nil {
if pin {
existing.RefCount++
}
existing.LastUsedAt = time.Now()
duplicate = bridge
socksURL := fmt.Sprintf("socks5://127.0.0.1:%d", existing.Port)
m.mu.Unlock()
if duplicate != nil {
duplicate.Stopping = true
m.stopBridgeProcess(duplicate)
}
return socksURL, true
}
transferredRefCount := 0
if existing.Restarting && existing.RefCount > 0 {
transferredRefCount = existing.RefCount
}
existing.Stopping = true
delete(m.Bridges, key)
duplicate = existing
if transferredRefCount > 0 && !pin {
bridge.RefCount = transferredRefCount
}
}
if pin {
bridge.RefCount = 1
}
bridge.LastUsedAt = time.Now()
m.Bridges[key] = bridge
m.mu.Unlock()
if duplicate != nil {
m.stopBridgeProcess(duplicate)
}
return "", false
}
func (m *SingBoxManager) watchBridge(bridge *SingBoxBridge, key string) {
if bridge == nil || bridge.Cmd == nil {
return
}
_ = bridge.waitExit()
var shouldRestart bool
var refCount int
m.mu.Lock()
if current, ok := m.Bridges[key]; ok && current == bridge {
refCount = bridge.RefCount
if !bridge.Stopping && refCount > 0 && !bridge.Restarting {
bridge.Restarting = true
shouldRestart = true
} else {
delete(m.Bridges, key)
}
}
bridge.Running = false
stopping := bridge.Stopping
m.mu.Unlock()
if shouldRestart {
log := logger.New("SingBox")
if err := m.restartBridgeOnSamePort(log, key, bridge, refCount); err == nil {
return
} else if errors.Is(err, errSingBoxBridgeRestartNotNeeded) {
return
} else {
log.Error("sing-box 桥接同端口恢复失败", logger.F("key", key[:8]), logger.F("port", bridge.Port), logger.F("error", err.Error()))
m.mu.Lock()
if current, ok := m.Bridges[key]; ok && current == bridge {
delete(m.Bridges, key)
}
m.mu.Unlock()
}
}
if !stopping && m.OnBridgeDied != nil {
m.OnBridgeDied(key, fmt.Errorf("sing-box 桥接进程意外退出"))
}
}
func (m *SingBoxManager) stopBridgeProcess(bridge *SingBoxBridge) {
if bridge == nil || bridge.Cmd == nil || bridge.Cmd.Process == nil {
return
}
_ = bridge.Cmd.Process.Kill()
}