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.
355 lines
10 KiB
Go
355 lines
10 KiB
Go
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
|
||
}
|
||
}
|
||
}
|