mirror of
https://github.com/isboyjc/GoProxy.git
synced 2026-05-06 20:02:54 +08:00
- Added support for importing Clash/V2ray subscriptions, including automatic format detection and integration with sing-box for protocol conversion. - Introduced five proxy usage modes in the configuration, allowing flexible selection between mixed, custom-only, and free-only modes. - Enhanced `.env.example` and `docker-compose.yml` to include new environment variables for custom proxy settings. - Updated `CHANGELOG.md` to document new features and improvements related to subscription management. - Improved WebUI for managing subscriptions and displaying proxy statistics. - Implemented a background process for refreshing subscriptions and probing disabled proxies for reactivation.
531 lines
12 KiB
Go
531 lines
12 KiB
Go
package custom
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// SingBoxProcess 管理 sing-box 子进程
|
||
type SingBoxProcess struct {
|
||
cmd *exec.Cmd
|
||
binPath string
|
||
configDir string
|
||
configFile string
|
||
basePort int
|
||
portMap map[string]int // nodeKey → 本地端口
|
||
nodes []ParsedNode
|
||
mu sync.Mutex
|
||
running bool
|
||
}
|
||
|
||
// NewSingBoxProcess 创建 sing-box 进程管理器
|
||
func NewSingBoxProcess(binPath, dataDir string, basePort int) *SingBoxProcess {
|
||
if dataDir == "" {
|
||
// 没设置 DATA_DIR 时,使用当前工作目录下的 singbox/
|
||
wd, _ := os.Getwd()
|
||
dataDir = wd
|
||
}
|
||
configDir, _ := filepath.Abs(filepath.Join(dataDir, "singbox"))
|
||
os.MkdirAll(configDir, 0755)
|
||
|
||
return &SingBoxProcess{
|
||
binPath: binPath,
|
||
configDir: configDir,
|
||
configFile: filepath.Join(configDir, "config.json"),
|
||
basePort: basePort,
|
||
portMap: make(map[string]int),
|
||
}
|
||
}
|
||
|
||
// Reload 重新加载节点配置并重启 sing-box
|
||
func (s *SingBoxProcess) Reload(nodes []ParsedNode) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
// 过滤出需要 sing-box 转换的节点
|
||
var tunnelNodes []ParsedNode
|
||
for _, n := range nodes {
|
||
if !n.IsDirect() {
|
||
tunnelNodes = append(tunnelNodes, n)
|
||
}
|
||
}
|
||
|
||
if len(tunnelNodes) == 0 {
|
||
log.Println("[custom] 无需 sing-box 转换的节点,停止进程")
|
||
s.stopLocked()
|
||
s.nodes = nil
|
||
s.portMap = make(map[string]int)
|
||
return nil
|
||
}
|
||
|
||
// 生成配置
|
||
if err := s.generateConfig(tunnelNodes); err != nil {
|
||
return fmt.Errorf("生成 sing-box 配置失败: %w", err)
|
||
}
|
||
|
||
// 重启进程
|
||
s.stopLocked()
|
||
if err := s.startLocked(); err != nil {
|
||
return fmt.Errorf("启动 sing-box 失败: %w", err)
|
||
}
|
||
|
||
s.nodes = tunnelNodes
|
||
return nil
|
||
}
|
||
|
||
// generateConfig 生成 sing-box JSON 配置
|
||
func (s *SingBoxProcess) generateConfig(nodes []ParsedNode) error {
|
||
s.portMap = make(map[string]int)
|
||
port := s.basePort
|
||
|
||
var inbounds []map[string]interface{}
|
||
var outbounds []map[string]interface{}
|
||
var rules []map[string]interface{}
|
||
|
||
for i, node := range nodes {
|
||
port++
|
||
key := node.NodeKey()
|
||
s.portMap[key] = port
|
||
tag := fmt.Sprintf("node-%d", i)
|
||
|
||
// 入站:本地 SOCKS5 监听
|
||
inbounds = append(inbounds, map[string]interface{}{
|
||
"type": "socks",
|
||
"tag": fmt.Sprintf("in-%s", tag),
|
||
"listen": "127.0.0.1",
|
||
"listen_port": port,
|
||
})
|
||
|
||
// 出站:根据节点类型生成
|
||
outbound := buildOutbound(node, tag)
|
||
if outbound == nil {
|
||
log.Printf("[custom] 跳过不支持的节点类型: %s (%s)", node.Name, node.Type)
|
||
delete(s.portMap, key)
|
||
continue
|
||
}
|
||
outbounds = append(outbounds, outbound)
|
||
|
||
// 路由规则:入站 → 出站
|
||
rules = append(rules, map[string]interface{}{
|
||
"inbound": []string{fmt.Sprintf("in-%s", tag)},
|
||
"outbound": fmt.Sprintf("out-%s", tag),
|
||
})
|
||
}
|
||
|
||
// 添加 direct 出站作为默认
|
||
outbounds = append(outbounds, map[string]interface{}{
|
||
"type": "direct",
|
||
"tag": "direct",
|
||
})
|
||
|
||
config := map[string]interface{}{
|
||
"log": map[string]interface{}{
|
||
"level": "warn",
|
||
},
|
||
"inbounds": inbounds,
|
||
"outbounds": outbounds,
|
||
"route": map[string]interface{}{
|
||
"rules": rules,
|
||
"final": "direct",
|
||
},
|
||
}
|
||
|
||
data, err := json.MarshalIndent(config, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
return os.WriteFile(s.configFile, data, 0644)
|
||
}
|
||
|
||
// buildOutbound 根据节点类型构建 sing-box 出站配置
|
||
func buildOutbound(node ParsedNode, tag string) map[string]interface{} {
|
||
raw := node.Raw
|
||
out := map[string]interface{}{
|
||
"tag": fmt.Sprintf("out-%s", tag),
|
||
"server": node.Server,
|
||
}
|
||
|
||
// sing-box 使用 server_port 而不是 port
|
||
out["server_port"] = node.Port
|
||
|
||
switch node.Type {
|
||
case "vmess":
|
||
out["type"] = "vmess"
|
||
out["uuid"] = getStr(raw, "uuid")
|
||
out["alter_id"] = getInt(raw, "alterId")
|
||
out["security"] = getStrDefault(raw, "cipher", "auto")
|
||
applyTLS(raw, out)
|
||
applyTransport(raw, out)
|
||
|
||
case "vless":
|
||
out["type"] = "vless"
|
||
out["uuid"] = getStr(raw, "uuid")
|
||
out["flow"] = getStr(raw, "flow")
|
||
applyTLS(raw, out)
|
||
applyTransport(raw, out)
|
||
|
||
case "trojan":
|
||
out["type"] = "trojan"
|
||
out["password"] = getStr(raw, "password")
|
||
applyTLS(raw, out)
|
||
applyTransport(raw, out)
|
||
|
||
case "shadowsocks":
|
||
out["type"] = "shadowsocks"
|
||
out["method"] = getStr(raw, "cipher")
|
||
out["password"] = getStr(raw, "password")
|
||
if plugin := getStr(raw, "plugin"); plugin != "" {
|
||
out["plugin"] = plugin
|
||
if pluginOpts, ok := raw["plugin-opts"].(map[string]interface{}); ok {
|
||
out["plugin_opts"] = convertPluginOpts(plugin, pluginOpts)
|
||
}
|
||
}
|
||
|
||
case "hysteria2":
|
||
out["type"] = "hysteria2"
|
||
out["password"] = getStr(raw, "password")
|
||
applyTLS(raw, out)
|
||
|
||
case "hysteria":
|
||
out["type"] = "hysteria"
|
||
out["auth_str"] = getStr(raw, "auth-str")
|
||
if up := getStr(raw, "up"); up != "" {
|
||
out["up_mbps"] = parseSpeed(up)
|
||
}
|
||
if down := getStr(raw, "down"); down != "" {
|
||
out["down_mbps"] = parseSpeed(down)
|
||
}
|
||
applyTLS(raw, out)
|
||
|
||
case "tuic":
|
||
out["type"] = "tuic"
|
||
out["uuid"] = getStr(raw, "uuid")
|
||
out["password"] = getStr(raw, "password")
|
||
out["congestion_control"] = getStrDefault(raw, "congestion-controller", "bbr")
|
||
applyTLS(raw, out)
|
||
|
||
case "anytls":
|
||
out["type"] = "anytls"
|
||
out["password"] = getStr(raw, "password")
|
||
// anytls 强制启用 TLS
|
||
forceTLS(raw, out)
|
||
|
||
default:
|
||
return nil
|
||
}
|
||
|
||
return out
|
||
}
|
||
|
||
// forceTLS 强制应用 TLS 配置(用于 anytls 等必须 TLS 的协议)
|
||
func forceTLS(raw map[string]interface{}, out map[string]interface{}) {
|
||
raw["tls"] = true
|
||
applyTLS(raw, out)
|
||
}
|
||
|
||
// applyTLS 应用 TLS 配置
|
||
func applyTLS(raw map[string]interface{}, out map[string]interface{}) {
|
||
tls := getBool(raw, "tls")
|
||
// 如果有 sni/alpn/client-fingerprint 也视为需要 TLS
|
||
if !tls && getStr(raw, "sni") == "" && getStr(raw, "client-fingerprint") == "" {
|
||
return
|
||
}
|
||
|
||
tlsConfig := map[string]interface{}{
|
||
"enabled": true,
|
||
}
|
||
|
||
if sni := getStr(raw, "sni"); sni != "" {
|
||
tlsConfig["server_name"] = sni
|
||
} else if servername := getStr(raw, "servername"); servername != "" {
|
||
tlsConfig["server_name"] = servername
|
||
}
|
||
|
||
if getBool(raw, "skip-cert-verify") {
|
||
tlsConfig["insecure"] = true
|
||
}
|
||
|
||
if alpn, ok := raw["alpn"].([]interface{}); ok {
|
||
var alpnStrs []string
|
||
for _, a := range alpn {
|
||
if s, ok := a.(string); ok {
|
||
alpnStrs = append(alpnStrs, s)
|
||
}
|
||
}
|
||
if len(alpnStrs) > 0 {
|
||
tlsConfig["alpn"] = alpnStrs
|
||
}
|
||
}
|
||
|
||
if fp := getStr(raw, "client-fingerprint"); fp != "" {
|
||
tlsConfig["utls"] = map[string]interface{}{
|
||
"enabled": true,
|
||
"fingerprint": fp,
|
||
}
|
||
}
|
||
|
||
// reality 配置
|
||
if realityOpts, ok := raw["reality-opts"].(map[string]interface{}); ok {
|
||
tlsConfig["reality"] = map[string]interface{}{
|
||
"enabled": true,
|
||
"public_key": getStr(realityOpts, "public-key"),
|
||
"short_id": getStr(realityOpts, "short-id"),
|
||
}
|
||
}
|
||
|
||
out["tls"] = tlsConfig
|
||
}
|
||
|
||
// applyTransport 应用传输层配置
|
||
func applyTransport(raw map[string]interface{}, out map[string]interface{}) {
|
||
network := getStrDefault(raw, "network", "tcp")
|
||
|
||
switch network {
|
||
case "ws":
|
||
transport := map[string]interface{}{
|
||
"type": "ws",
|
||
}
|
||
if wsOpts, ok := raw["ws-opts"].(map[string]interface{}); ok {
|
||
if path := getStr(wsOpts, "path"); path != "" {
|
||
transport["path"] = path
|
||
}
|
||
if headers, ok := wsOpts["headers"].(map[string]interface{}); ok {
|
||
transport["headers"] = headers
|
||
}
|
||
}
|
||
out["transport"] = transport
|
||
|
||
case "grpc":
|
||
transport := map[string]interface{}{
|
||
"type": "grpc",
|
||
}
|
||
if grpcOpts, ok := raw["grpc-opts"].(map[string]interface{}); ok {
|
||
if sn := getStr(grpcOpts, "grpc-service-name"); sn != "" {
|
||
transport["service_name"] = sn
|
||
}
|
||
}
|
||
out["transport"] = transport
|
||
|
||
case "h2":
|
||
transport := map[string]interface{}{
|
||
"type": "http",
|
||
}
|
||
if h2Opts, ok := raw["h2-opts"].(map[string]interface{}); ok {
|
||
if path := getStr(h2Opts, "path"); path != "" {
|
||
transport["path"] = path
|
||
}
|
||
if host, ok := h2Opts["host"].([]interface{}); ok && len(host) > 0 {
|
||
if h, ok := host[0].(string); ok {
|
||
transport["host"] = []string{h}
|
||
}
|
||
}
|
||
}
|
||
out["transport"] = transport
|
||
|
||
case "httpupgrade":
|
||
transport := map[string]interface{}{
|
||
"type": "httpupgrade",
|
||
}
|
||
if wsOpts, ok := raw["ws-opts"].(map[string]interface{}); ok {
|
||
if path := getStr(wsOpts, "path"); path != "" {
|
||
transport["path"] = path
|
||
}
|
||
if headers, ok := wsOpts["headers"].(map[string]interface{}); ok {
|
||
if host, ok := headers["Host"].(string); ok {
|
||
transport["host"] = host
|
||
}
|
||
}
|
||
}
|
||
out["transport"] = transport
|
||
}
|
||
}
|
||
|
||
// convertPluginOpts 转换 shadowsocks 插件选项
|
||
func convertPluginOpts(plugin string, opts map[string]interface{}) string {
|
||
var parts []string
|
||
for k, v := range opts {
|
||
parts = append(parts, fmt.Sprintf("%s=%v", k, v))
|
||
}
|
||
return strings.Join(parts, ";")
|
||
}
|
||
|
||
// startLocked 启动 sing-box(需持有锁)
|
||
func (s *SingBoxProcess) startLocked() error {
|
||
if _, err := exec.LookPath(s.binPath); err != nil {
|
||
return fmt.Errorf("sing-box 未找到: %s(请安装 sing-box 或设置 SINGBOX_PATH)", s.binPath)
|
||
}
|
||
|
||
s.cmd = exec.Command(s.binPath, "run", "-c", s.configFile, "-D", s.configDir)
|
||
s.cmd.Stdout = os.Stdout
|
||
s.cmd.Stderr = os.Stderr
|
||
|
||
if err := s.cmd.Start(); err != nil {
|
||
return err
|
||
}
|
||
s.running = true
|
||
|
||
// 监控进程退出
|
||
go func() {
|
||
if s.cmd != nil && s.cmd.Process != nil {
|
||
s.cmd.Wait()
|
||
s.mu.Lock()
|
||
s.running = false
|
||
s.mu.Unlock()
|
||
}
|
||
}()
|
||
|
||
// 等待端口就绪(最多 10 秒)
|
||
log.Printf("[custom] sing-box 启动中,等待端口就绪...")
|
||
ready := false
|
||
for i := 0; i < 20; i++ {
|
||
time.Sleep(500 * time.Millisecond)
|
||
// 检查第一个端口是否可连
|
||
for _, port := range s.portMap {
|
||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Second)
|
||
if err == nil {
|
||
conn.Close()
|
||
ready = true
|
||
break
|
||
}
|
||
}
|
||
if ready {
|
||
break
|
||
}
|
||
}
|
||
|
||
if !ready {
|
||
log.Println("[custom] ⚠️ sing-box 端口未就绪,部分节点可能不可用")
|
||
} else {
|
||
log.Printf("[custom] ✅ sing-box 启动成功,管理 %d 个节点", len(s.portMap))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// stopLocked 停止 sing-box(需持有锁)
|
||
func (s *SingBoxProcess) stopLocked() {
|
||
if s.cmd != nil && s.cmd.Process != nil && s.running {
|
||
log.Println("[custom] 停止 sing-box 进程...")
|
||
s.cmd.Process.Signal(os.Interrupt)
|
||
done := make(chan struct{})
|
||
go func() {
|
||
s.cmd.Wait()
|
||
close(done)
|
||
}()
|
||
select {
|
||
case <-done:
|
||
case <-time.After(5 * time.Second):
|
||
s.cmd.Process.Kill()
|
||
}
|
||
s.running = false
|
||
}
|
||
}
|
||
|
||
// Stop 停止 sing-box
|
||
func (s *SingBoxProcess) Stop() {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.stopLocked()
|
||
}
|
||
|
||
// IsRunning 检查进程是否运行中
|
||
func (s *SingBoxProcess) IsRunning() bool {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
return s.running
|
||
}
|
||
|
||
// GetLocalAddress 获取节点的本地 SOCKS5 地址
|
||
func (s *SingBoxProcess) GetLocalAddress(nodeKey string) string {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
if port, ok := s.portMap[nodeKey]; ok {
|
||
return net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// GetPortMap 获取所有端口映射
|
||
func (s *SingBoxProcess) GetPortMap() map[string]int {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
result := make(map[string]int, len(s.portMap))
|
||
for k, v := range s.portMap {
|
||
result[k] = v
|
||
}
|
||
return result
|
||
}
|
||
|
||
// GetNodeCount 获取管理的节点数
|
||
func (s *SingBoxProcess) GetNodeCount() int {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
return len(s.portMap)
|
||
}
|
||
|
||
// 辅助函数
|
||
|
||
func getStr(m map[string]interface{}, key string) string {
|
||
if v, ok := m[key]; ok {
|
||
if s, ok := v.(string); ok {
|
||
return s
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func getStrDefault(m map[string]interface{}, key, def string) string {
|
||
if s := getStr(m, key); s != "" {
|
||
return s
|
||
}
|
||
return def
|
||
}
|
||
|
||
func getInt(m map[string]interface{}, key string) int {
|
||
if v, ok := m[key]; ok {
|
||
switch val := v.(type) {
|
||
case int:
|
||
return val
|
||
case float64:
|
||
return int(val)
|
||
case string:
|
||
n, _ := strconv.Atoi(val)
|
||
return n
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func getBool(m map[string]interface{}, key string) bool {
|
||
if v, ok := m[key]; ok {
|
||
switch val := v.(type) {
|
||
case bool:
|
||
return val
|
||
case string:
|
||
return val == "true"
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func parseSpeed(s string) int {
|
||
s = strings.TrimSpace(s)
|
||
s = strings.TrimSuffix(s, " Mbps")
|
||
s = strings.TrimSuffix(s, "Mbps")
|
||
n, _ := strconv.Atoi(s)
|
||
if n == 0 {
|
||
n = 100 // 默认 100 Mbps
|
||
}
|
||
return n
|
||
}
|