Files
Ant-Browser/backend/internal/proxy/singbox_bridge_runtime.go
T
2026-05-09 17:30:16 +08:00

221 lines
5.7 KiB
Go

package proxy
import (
"ant-chrome/backend/internal/config"
"ant-chrome/backend/internal/logger"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
)
// EnsureBridge 确保 sing-box 桥接进程运行,返回 socks5://127.0.0.1:port
func (m *SingBoxManager) EnsureBridge(proxyConfig string, proxies []config.BrowserProxy, proxyId 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); reused {
log.Info("复用 sing-box 桥接", logger.F("key", key[:8]), logger.F("socks_url", socksURL))
return socksURL, 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 = 3
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
port, err := nextAvailablePort()
if err != nil {
lastErr = err
continue
}
cfgPath, err := m.buildConfig(key, outbound, port)
if err != nil {
return "", fmt.Errorf("sing-box 配置生成失败: %w", err)
}
cmd := exec.Command(binaryPath, "run", "-c", cfgPath)
hideWindow(cmd)
cmd.Dir = filepath.Dir(cfgPath)
stderrPath := filepath.Join(filepath.Dir(cfgPath), "singbox-stderr.log")
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))
lastErr = err
continue
}
bridge := &SingBoxBridge{
NodeKey: key,
Port: port,
Cmd: cmd,
Pid: cmd.Process.Pid,
Running: true,
}
log.Info("sing-box 启动", logger.F("key", key[:8]), logger.F("pid", bridge.Pid), logger.F("port", port))
if err := waitPortReady("127.0.0.1", port, 10*time.Second); err != nil {
if stderrFile != nil {
stderrFile.Close()
}
if content, readErr := os.ReadFile(stderrPath); readErr == nil && len(content) > 0 {
log.Error("sing-box stderr", logger.F("output", string(content)))
}
bridge.Stopping = true
m.stopBridgeProcess(bridge)
bridge.Running = false
bridge.Pid = 0
bridge.LastError = err.Error()
log.Error("sing-box 端口不可用,重试", logger.F("error", err), logger.F("attempt", attempt))
lastErr = err
time.Sleep(200 * time.Millisecond)
continue
}
if stderrFile != nil {
stderrFile.Close()
}
if socksURL, reused := m.registerBridge(key, bridge); reused {
log.Info("复用已就绪 sing-box 桥接", logger.F("key", key[:8]), logger.F("socks_url", socksURL))
bridge.Stopping = true
m.stopBridgeProcess(bridge)
return socksURL, nil
}
go m.watchBridge(bridge, key)
return fmt.Sprintf("socks5://127.0.0.1:%d", port), nil
}
return "", fmt.Errorf("sing-box 启动失败(已重试 %d 次): %w", maxRetries, lastErr)
}
// StopAll 关闭所有 sing-box 桥接进程
func (m *SingBoxManager) StopAll() {
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) (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 && waitPortReady("127.0.0.1", bridge.Port, 800*time.Millisecond) == nil {
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) (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 && waitPortReady("127.0.0.1", existing.Port, 800*time.Millisecond) == nil {
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
}
existing.Stopping = true
delete(m.Bridges, key)
duplicate = existing
}
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.Cmd.Wait()
m.mu.Lock()
if current, ok := m.Bridges[key]; ok && current == bridge {
delete(m.Bridges, key)
}
bridge.Running = false
stopping := bridge.Stopping
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()
}