mirror of
https://github.com/isboyjc/GoProxy.git
synced 2026-07-14 08:55:06 +08:00
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.
This commit is contained in:
@@ -98,30 +98,76 @@ func (s *Server) checkAuth(r *http.Request) bool {
|
||||
return usernameMatch && passwordMatch
|
||||
}
|
||||
|
||||
// selectProxy 根据使用模式和选择策略获取代理
|
||||
func (s *Server) selectProxy(tried []string, lowestLatency bool) (*storage.Proxy, error) {
|
||||
cfg := config.Get()
|
||||
sourceFilter := sourceFilterFromMode(cfg.CustomProxyMode)
|
||||
|
||||
// 混用 + 优先模式:先尝试优先源,无可用则 fallback 全部
|
||||
if cfg.CustomProxyMode == "mixed" && (cfg.CustomPriority || cfg.CustomFreePriority) {
|
||||
preferSource := "custom"
|
||||
if cfg.CustomFreePriority {
|
||||
preferSource = "free"
|
||||
}
|
||||
var p *storage.Proxy
|
||||
var err error
|
||||
if lowestLatency {
|
||||
p, err = s.storage.GetLowestLatencyExcludeFiltered(tried, preferSource)
|
||||
} else {
|
||||
p, err = s.storage.GetRandomExcludeFiltered(tried, preferSource)
|
||||
}
|
||||
if err == nil {
|
||||
return p, nil
|
||||
}
|
||||
// fallback 到全部
|
||||
if lowestLatency {
|
||||
return s.storage.GetLowestLatencyExcludeFiltered(tried, "")
|
||||
}
|
||||
return s.storage.GetRandomExcludeFiltered(tried, "")
|
||||
}
|
||||
|
||||
if lowestLatency {
|
||||
return s.storage.GetLowestLatencyExcludeFiltered(tried, sourceFilter)
|
||||
}
|
||||
return s.storage.GetRandomExcludeFiltered(tried, sourceFilter)
|
||||
}
|
||||
|
||||
// removeOrDisableProxy 根据代理来源决定删除或禁用
|
||||
func removeOrDisableProxy(store *storage.Storage, p *storage.Proxy) {
|
||||
if p.Source == "custom" {
|
||||
store.DisableProxy(p.Address)
|
||||
} else {
|
||||
store.Delete(p.Address)
|
||||
}
|
||||
}
|
||||
|
||||
// sourceFilterFromMode 根据使用模式返回来源过滤值
|
||||
func sourceFilterFromMode(mode string) string {
|
||||
switch mode {
|
||||
case "custom_only":
|
||||
return "custom"
|
||||
case "free_only":
|
||||
return "free"
|
||||
default:
|
||||
return "" // mixed
|
||||
}
|
||||
}
|
||||
|
||||
// handleHTTP 处理普通 HTTP 请求(带自动重试)
|
||||
func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var tried []string
|
||||
for attempt := 0; attempt <= s.cfg.MaxRetry; attempt++ {
|
||||
var p *storage.Proxy
|
||||
var err error
|
||||
|
||||
// 根据模式选择代理
|
||||
if s.mode == "lowest-latency" {
|
||||
p, err = s.storage.GetLowestLatencyExclude(tried)
|
||||
} else {
|
||||
p, err = s.storage.GetRandomExclude(tried)
|
||||
}
|
||||
|
||||
p, err := s.selectProxy(tried, s.mode == "lowest-latency")
|
||||
if err != nil {
|
||||
http.Error(w, "no available proxy", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
tried = append(tried, p.Address)
|
||||
|
||||
client, err := s.buildClient(p)
|
||||
if err != nil {
|
||||
s.storage.Delete(p.Address)
|
||||
removeOrDisableProxy(s.storage, p)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -136,7 +182,8 @@ func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("[proxy] %s via %s failed, removing", r.RequestURI, p.Address)
|
||||
s.storage.Delete(p.Address)
|
||||
s.storage.RecordProxyUse(p.Address, false)
|
||||
removeOrDisableProxy(s.storage, p)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -149,6 +196,7 @@ func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
s.storage.RecordProxyUse(p.Address, true)
|
||||
if resp.StatusCode == 429 {
|
||||
log.Printf("[proxy] ⚠️ 429 %s via %s (protocol=%s)", r.RequestURI, p.Address, p.Protocol)
|
||||
} else {
|
||||
@@ -164,30 +212,24 @@ func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) handleTunnel(w http.ResponseWriter, r *http.Request) {
|
||||
var tried []string
|
||||
for attempt := 0; attempt <= s.cfg.MaxRetry; attempt++ {
|
||||
var p *storage.Proxy
|
||||
var err error
|
||||
|
||||
// 根据模式选择代理
|
||||
if s.mode == "lowest-latency" {
|
||||
p, err = s.storage.GetLowestLatencyExclude(tried)
|
||||
} else {
|
||||
p, err = s.storage.GetRandomExclude(tried)
|
||||
}
|
||||
|
||||
p, err := s.selectProxy(tried, s.mode == "lowest-latency")
|
||||
if err != nil {
|
||||
http.Error(w, "no available proxy", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
tried = append(tried, p.Address)
|
||||
|
||||
conn, err := s.dialViaProxy(p, r.Host)
|
||||
if err != nil {
|
||||
log.Printf("[tunnel] dial %s via %s failed, removing", r.Host, p.Address)
|
||||
s.storage.Delete(p.Address)
|
||||
s.storage.RecordProxyUse(p.Address, false)
|
||||
removeOrDisableProxy(s.storage, p)
|
||||
continue
|
||||
}
|
||||
|
||||
s.storage.RecordProxyUse(p.Address, true)
|
||||
|
||||
// 告知客户端隧道建立
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
|
||||
@@ -80,16 +80,7 @@ func (s *SOCKS5Server) handleConnection(clientConn net.Conn) {
|
||||
maxRetries := s.cfg.MaxRetry + 2 // 增加重试次数以应对质量差的代理
|
||||
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
var p *storage.Proxy
|
||||
var err error
|
||||
|
||||
// SOCKS5 服务只使用 SOCKS5 上游代理
|
||||
if s.mode == "lowest-latency" {
|
||||
p, err = s.storage.GetLowestLatencyByProtocolExclude("socks5", tried)
|
||||
} else {
|
||||
p, err = s.storage.GetRandomByProtocolExclude("socks5", tried)
|
||||
}
|
||||
|
||||
p, err := s.selectSOCKS5Proxy(tried)
|
||||
if err != nil {
|
||||
log.Printf("[socks5] no available socks5 upstream proxy: %v", err)
|
||||
s.sendSOCKS5Reply(clientConn, 0x01) // General failure
|
||||
@@ -102,7 +93,8 @@ func (s *SOCKS5Server) handleConnection(clientConn net.Conn) {
|
||||
upstreamConn, err := s.dialViaProxy(p, target)
|
||||
if err != nil {
|
||||
log.Printf("[socks5] dial %s via %s (%s) failed: %v, removing", target, p.Address, p.Protocol, err)
|
||||
s.storage.Delete(p.Address)
|
||||
s.storage.RecordProxyUse(p.Address, false)
|
||||
removeOrDisableProxy(s.storage, p)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -112,6 +104,7 @@ func (s *SOCKS5Server) handleConnection(clientConn net.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
s.storage.RecordProxyUse(p.Address, true)
|
||||
log.Printf("[socks5] %s via %s established", target, p.Address)
|
||||
|
||||
// 双向转发数据
|
||||
@@ -128,6 +121,40 @@ func (s *SOCKS5Server) handleConnection(clientConn net.Conn) {
|
||||
log.Printf("[socks5] all proxies failed for %s", target)
|
||||
}
|
||||
|
||||
// selectSOCKS5Proxy 根据使用模式选择 SOCKS5 上游代理
|
||||
func (s *SOCKS5Server) selectSOCKS5Proxy(tried []string) (*storage.Proxy, error) {
|
||||
cfg := config.Get()
|
||||
sourceFilter := sourceFilterFromMode(cfg.CustomProxyMode)
|
||||
|
||||
// 混用 + 优先模式
|
||||
if cfg.CustomProxyMode == "mixed" && (cfg.CustomPriority || cfg.CustomFreePriority) {
|
||||
preferSource := "custom"
|
||||
if cfg.CustomFreePriority {
|
||||
preferSource = "free"
|
||||
}
|
||||
var p *storage.Proxy
|
||||
var err error
|
||||
if s.mode == "lowest-latency" {
|
||||
p, err = s.storage.GetLowestLatencyByProtocolExcludeFiltered("socks5", tried, preferSource)
|
||||
} else {
|
||||
p, err = s.storage.GetRandomByProtocolExcludeFiltered("socks5", tried, preferSource)
|
||||
}
|
||||
if err == nil {
|
||||
return p, nil
|
||||
}
|
||||
// fallback
|
||||
if s.mode == "lowest-latency" {
|
||||
return s.storage.GetLowestLatencyByProtocolExcludeFiltered("socks5", tried, "")
|
||||
}
|
||||
return s.storage.GetRandomByProtocolExcludeFiltered("socks5", tried, "")
|
||||
}
|
||||
|
||||
if s.mode == "lowest-latency" {
|
||||
return s.storage.GetLowestLatencyByProtocolExcludeFiltered("socks5", tried, sourceFilter)
|
||||
}
|
||||
return s.storage.GetRandomByProtocolExcludeFiltered("socks5", tried, sourceFilter)
|
||||
}
|
||||
|
||||
// socks5Handshake 处理 SOCKS5 握手
|
||||
func (s *SOCKS5Server) socks5Handshake(conn net.Conn) error {
|
||||
buf := make([]byte, 257)
|
||||
|
||||
Reference in New Issue
Block a user