Files
GoProxy/main.go
isboyjc f03c3300b4 feat: implement custom proxy subscription management and enhance configuration
- 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.
2026-04-04 22:25:54 +08:00

355 lines
10 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 main
import (
"log"
"os"
"sync"
"sync/atomic"
"time"
"goproxy/checker"
"goproxy/config"
"goproxy/custom"
"goproxy/fetcher"
"goproxy/logger"
"goproxy/optimizer"
"goproxy/pool"
"goproxy/proxy"
"goproxy/storage"
"goproxy/validator"
"goproxy/webui"
)
var fetchRunning atomic.Bool
var fetchMu sync.Mutex
func main() {
// 初始化日志收集器
logger.Init()
// 加载配置
cfg := config.Load()
// 提示密码信息
if os.Getenv("WEBUI_PASSWORD") == "" {
log.Printf("[main] WebUI 使用默认密码: %s可通过环境变量 WEBUI_PASSWORD 自定义)", config.DefaultPassword)
} else {
log.Println("[main] WebUI 密码已通过环境变量 WEBUI_PASSWORD 设置")
}
log.Printf("[main] 🎯 智能代理池配置: 容量=%d HTTP=%.0f%% SOCKS5=%.0f%% 延迟标准=%dms",
cfg.PoolMaxSize, cfg.PoolHTTPRatio*100, (1-cfg.PoolHTTPRatio)*100, cfg.MaxLatencyMs)
// 初始化存储
store, err := storage.New(cfg.DBPath)
if err != nil {
log.Fatalf("init storage: %v", err)
}
defer store.Close()
// 初始化限流器
fetcher.InitIPQueryLimiter(cfg.IPQueryRateLimit)
// 初始化核心模块
sourceMgr := fetcher.NewSourceManager(store.GetDB())
fetch := fetcher.New(cfg.HTTPSourceURL, cfg.SOCKS5SourceURL, sourceMgr)
validate := validator.New(cfg.ValidateConcurrency, cfg.ValidateTimeout, cfg.ValidateURL)
poolMgr := pool.NewManager(store, cfg)
healthChecker := checker.NewHealthChecker(store, validate, cfg, poolMgr)
opt := optimizer.NewOptimizer(store, fetch, validate, poolMgr, cfg)
// 清理无效代理(免费代理删除,订阅代理禁用)
totalDeleted := 0
if len(cfg.AllowedCountries) > 0 {
if deleted, err := store.DeleteNotAllowedCountries(cfg.AllowedCountries); err == nil && deleted > 0 {
log.Printf("[main] 🧹 已清理 %d 个非白名单免费代理 (允许: %v)", deleted, cfg.AllowedCountries)
totalDeleted += int(deleted)
}
if disabled, err := store.DisableNotAllowedCountries(cfg.AllowedCountries); err == nil && disabled > 0 {
log.Printf("[main] 🔒 已禁用 %d 个非白名单订阅代理", disabled)
}
} else if len(cfg.BlockedCountries) > 0 {
if deleted, err := store.DeleteBlockedCountries(cfg.BlockedCountries); err == nil && deleted > 0 {
log.Printf("[main] 🧹 已清理 %d 个屏蔽国家免费代理 (屏蔽: %v)", deleted, cfg.BlockedCountries)
totalDeleted += int(deleted)
}
if disabled, err := store.DisableBlockedCountries(cfg.BlockedCountries); err == nil && disabled > 0 {
log.Printf("[main] 🔒 已禁用 %d 个屏蔽国家订阅代理", disabled)
}
}
if deleted, err := store.DeleteWithoutExitInfo(); err == nil && deleted > 0 {
log.Printf("[main] 🧹 已清理 %d 个无出口信息的代理", deleted)
totalDeleted += int(deleted)
}
// 创建 HTTP 代理服务器:随机轮换 + 最低延迟
randomServer := proxy.New(store, cfg, "random", cfg.ProxyPort)
stableServer := proxy.New(store, cfg, "lowest-latency", cfg.StableProxyPort)
// 创建 SOCKS5 代理服务器:随机轮换 + 最低延迟
socks5RandomServer := proxy.NewSOCKS5(store, cfg, "random", cfg.SOCKS5Port)
socks5StableServer := proxy.NewSOCKS5(store, cfg, "lowest-latency", cfg.StableSOCKS5Port)
// 初始化订阅管理器
customMgr := custom.NewManager(store, validate, cfg)
// 配置变更通知 channel
configChanged := make(chan struct{}, 1)
// 启动 WebUI传递池子管理器和订阅管理器
ui := webui.New(store, cfg, poolMgr, customMgr, func() {
go smartFetchAndFill(fetch, validate, store, poolMgr)
}, configChanged)
ui.Start()
// 首次智能填充(清理后立即触发)
go func() {
if totalDeleted > 0 {
log.Printf("[main] 🚀 清理后立即启动补充填充...")
} else {
log.Println("[main] 🚀 启动初始化填充...")
}
smartFetchAndFill(fetch, validate, store, poolMgr)
}()
// 启动状态监控协程
go startStatusMonitor(poolMgr, fetch, validate, store)
// 启动健康检查器
healthChecker.StartBackground()
// 启动优化轮换器
opt.StartBackground()
// 启动订阅管理器
go customMgr.Start()
// 监听配置变更
go watchConfigChanges(configChanged, poolMgr)
// 启动 HTTP 稳定代理服务(最低延迟模式)
go func() {
if err := stableServer.Start(); err != nil {
log.Fatalf("stable http proxy server: %v", err)
}
}()
// 启动 SOCKS5 稳定代理服务(最低延迟模式)
go func() {
if err := socks5StableServer.Start(); err != nil {
log.Fatalf("stable socks5 proxy server: %v", err)
}
}()
// 启动 SOCKS5 随机代理服务
go func() {
if err := socks5RandomServer.Start(); err != nil {
log.Fatalf("random socks5 proxy server: %v", err)
}
}()
// 启动 HTTP 随机代理服务(阻塞)
if err := randomServer.Start(); err != nil {
log.Fatalf("random http proxy server: %v", err)
}
}
// smartFetchAndFill 智能抓取和填充
func smartFetchAndFill(fetch *fetcher.Fetcher, validate *validator.Validator, store *storage.Storage, poolMgr *pool.Manager) {
// 防止并发执行
if !fetchRunning.CompareAndSwap(false, true) {
log.Println("[main] 抓取已在运行,跳过")
return
}
defer fetchRunning.Store(false)
// 获取池子状态
status, err := poolMgr.GetStatus()
if err != nil {
log.Printf("[main] 获取池子状态失败: %v", err)
return
}
log.Printf("[main] 📊 池子状态: %s | HTTP=%d/%d SOCKS5=%d/%d 总计=%d/%d",
status.State, status.HTTP, status.HTTPSlots, status.SOCKS5, status.SOCKS5Slots,
status.Total, config.Get().PoolMaxSize)
// 判断是否需要抓取
needFetch, mode, preferredProtocol := poolMgr.NeedsFetch(status)
if !needFetch {
log.Println("[main] 池子健康,无需抓取")
return
}
log.Printf("[main] 🔍 智能抓取: 模式=%s 协议偏好=%s", mode, preferredProtocol)
// 智能抓取
candidates, err := fetch.FetchSmart(mode, preferredProtocol)
if err != nil {
log.Printf("[main] 抓取失败: %v", err)
return
}
// 按协议分组
var httpCandidates, socks5Candidates []storage.Proxy
for _, c := range candidates {
if c.Protocol == "http" {
httpCandidates = append(httpCandidates, c)
} else {
socks5Candidates = append(socks5Candidates, c)
}
}
log.Printf("[main] 抓取到 %d 个候选代理SOCKS5=%d HTTP=%d按协议并发验证...",
len(candidates), len(socks5Candidates), len(httpCandidates))
// 共享计数器
var addedCount atomic.Int32
var validCount atomic.Int32
var rejectedNoExit atomic.Int32
var rejectedLatency atomic.Int32
var rejectedGeo atomic.Int32
var rejectedFull atomic.Int32
// 入池处理函数(两个协程共用)
processResult := func(result validator.Result) {
if !result.Valid {
return
}
validCount.Add(1)
latencyMs := int(result.Latency.Milliseconds())
cfg := config.Get()
maxLatency := cfg.GetLatencyThreshold(status.State)
if result.ExitIP == "" || result.ExitLocation == "" {
rejectedNoExit.Add(1)
return
}
if latencyMs > maxLatency {
rejectedLatency.Add(1)
return
}
proxyToAdd := storage.Proxy{
Address: result.Proxy.Address,
Protocol: result.Proxy.Protocol,
ExitIP: result.ExitIP,
ExitLocation: result.ExitLocation,
Latency: latencyMs,
}
if added, reason := poolMgr.TryAddProxy(proxyToAdd); added {
addedCount.Add(1)
} else if reason == "slots_full" {
rejectedFull.Add(1)
} else if len(result.ExitLocation) >= 2 {
countryCode := result.ExitLocation[:2]
for _, blocked := range cfg.BlockedCountries {
if countryCode == blocked {
rejectedGeo.Add(1)
break
}
}
}
}
// 池子是否已满的检查函数
poolFilled := func() bool {
currentStatus, _ := poolMgr.GetStatus()
return !poolMgr.NeedsFetchQuick(currentStatus)
}
var wg sync.WaitGroup
// SOCKS5 协程:验证快,优先填充
if len(socks5Candidates) > 0 {
wg.Add(1)
go func() {
defer wg.Done()
count := 0
for result := range validate.ValidateStream(socks5Candidates) {
processResult(result)
count++
if count%20 == 0 && poolFilled() {
log.Println("[main] ✅ SOCKS5 验证中检测到池子已满,停止")
break
}
}
log.Printf("[main] SOCKS5 验证完成,处理 %d 个", count)
}()
}
// HTTP 协程:有额外 HTTPS 检测,较慢
if len(httpCandidates) > 0 {
wg.Add(1)
go func() {
defer wg.Done()
count := 0
for result := range validate.ValidateStream(httpCandidates) {
processResult(result)
count++
if count%20 == 0 && poolFilled() {
log.Println("[main] ✅ HTTP 验证中检测到池子已满,停止")
break
}
}
log.Printf("[main] HTTP 验证完成,处理 %d 个", count)
}()
}
wg.Wait()
// 最终状态
finalStatus, _ := poolMgr.GetStatus()
log.Printf("[main] 填充完成: 验证%d 通过%d 入池%d | 拒绝[无出口:%d 延迟:%d 地理:%d 满:%d] | 最终: %s HTTP=%d SOCKS5=%d",
len(candidates), validCount.Load(), addedCount.Load(),
rejectedNoExit.Load(), rejectedLatency.Load(), rejectedGeo.Load(), rejectedFull.Load(),
finalStatus.State, finalStatus.HTTP, finalStatus.SOCKS5)
}
// startStatusMonitor 状态监控协程
func startStatusMonitor(poolMgr *pool.Manager, fetch *fetcher.Fetcher, validate *validator.Validator, store *storage.Storage) {
ticker := time.NewTicker(30 * time.Second)
log.Println("[monitor] 📡 状态监控器已启动每30秒检查")
for range ticker.C {
status, err := poolMgr.GetStatus()
if err != nil {
continue
}
// 每分钟检查池子状态
needFetch, mode, preferredProtocol := poolMgr.NeedsFetch(status)
if needFetch {
log.Printf("[monitor] ⚠️ 检测到池子需求: 状态=%s 模式=%s 协议=%s",
status.State, mode, preferredProtocol)
// 触发智能填充
go smartFetchAndFill(fetch, validate, store, poolMgr)
}
}
}
// watchConfigChanges 监听配置变更
func watchConfigChanges(configChanged <-chan struct{}, poolMgr *pool.Manager) {
var oldSize int
var oldRatio float64
cfg := config.Get()
oldSize = cfg.PoolMaxSize
oldRatio = cfg.PoolHTTPRatio
for range configChanged {
newCfg := config.Get()
if newCfg.PoolMaxSize != oldSize || newCfg.PoolHTTPRatio != oldRatio {
log.Printf("[config] 🔧 配置变更检测: 容量 %d→%d 比例 %.2f→%.2f",
oldSize, newCfg.PoolMaxSize, oldRatio, newCfg.PoolHTTPRatio)
poolMgr.AdjustForConfigChange(oldSize, oldRatio)
oldSize = newCfg.PoolMaxSize
oldRatio = newCfg.PoolHTTPRatio
}
}
}