From 2b7fbd2a0d3b1ef68d50112c116cffa208c35033 Mon Sep 17 00:00:00 2001 From: WJQSERVER Date: Mon, 10 Feb 2025 18:53:16 +0800 Subject: [PATCH] update outbound --- CHANGELOG.md | 6 ++ README.md | 6 +- config/config.go | 9 ++- config/config.toml | 2 +- deploy/config.toml | 4 ++ proxy/chunkreq.go | 4 +- proxy/dial.go | 150 +++++++++++++++++++++++++++++++++------------ proxy/gitreq.go | 4 +- 8 files changed, 139 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 727c05f..e1f74d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w13a +--- +- PRE-RELEASE: 此版本是v2.2.0的预发布版本,请勿在生产环境中使用; +- ADD: 加入`Socks5`和`HTTP(S)`出站支持 +- CHANGE: 配置新增`Outbound`配置块 + 2.1.0 --- - RELEASE: v2.1.0正式版发布; diff --git a/README.md b/README.md index fa56138..bd615e4 100644 --- a/README.md +++ b/README.md @@ -116,9 +116,9 @@ rateMethod = "total" # "ip" or "total" 速率限制方式 ratePerMinute = 180 # 每分钟限制请求数量 burst = 5 # 突发请求数量 -[proxy] -enabled = false # 是否使用代理连接 -url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" 支持HTTP代理和SOCKS5代理 支持多级SOCKS5代理 +[outbound] +enabled = false # 是否使用自定义代理出站 +url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" 支持Socks5/HTTP(S)出站传输 ``` ### 黑名单配置 diff --git a/config/config.go b/config/config.go index 32b64ce..9335b14 100644 --- a/config/config.go +++ b/config/config.go @@ -13,7 +13,7 @@ type Config struct { Blacklist BlacklistConfig Whitelist WhitelistConfig RateLimit RateLimitConfig - Proxy ProxyConfig + Outbound OutboundConfig } type ServerConfig struct { @@ -63,7 +63,12 @@ type RateLimitConfig struct { Burst int `toml:"burst"` } -type ProxyConfig struct { +/* +[outbound] +enabled = false +url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" +*/ +type OutboundConfig struct { Enabled bool `toml:"enabled"` Url string `toml:"url"` } diff --git a/config/config.toml b/config/config.toml index b720432..5613f2e 100644 --- a/config/config.toml +++ b/config/config.toml @@ -37,6 +37,6 @@ rateMethod = "total" # "ip" or "total" ratePerMinute = 180 burst = 5 -[proxy] +[outbound] enabled = false url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" \ No newline at end of file diff --git a/deploy/config.toml b/deploy/config.toml index ec684ec..e030b1a 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -36,3 +36,7 @@ enabled = false rateMethod = "total" # "ip" or "total" ratePerMinute = 180 burst = 5 + +[outbound] +enabled = false +url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index dc8c555..f1fb693 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -47,7 +47,9 @@ func initChunkedHTTPClient(cfg *config.Config) { KeepAlive: 30 * time.Second, }).DialContext, } - initTransport(cfg, ctr) + if cfg.Outbound.Enabled { + initTransport(cfg, ctr) + } cclient = &http.Client{ Transport: ctr, } diff --git a/proxy/dial.go b/proxy/dial.go index 69d35ab..075603e 100644 --- a/proxy/dial.go +++ b/proxy/dial.go @@ -1,3 +1,8 @@ +/* + made&PR by @lfhy + https://github.com/WJQSERVER-STUDIO/ghproxy/pull/46 +*/ + package proxy import ( @@ -9,53 +14,122 @@ import ( "golang.org/x/net/proxy" ) -func newProxyDial(proxyUrls string) proxy.Dialer { - var proxyDialer proxy.Dialer = proxy.Direct - for _, proxyUrl := range strings.Split(proxyUrls, ",") { - urlInfo, err := url.Parse(proxyUrl) - if err != nil { - continue - } - if urlInfo.Scheme != "socks5" { - continue - } - var auth *proxy.Auth = nil - if urlInfo.User != nil { - pwd, ok := urlInfo.User.Password() - if !ok { - continue - } - auth = &proxy.Auth{ - User: urlInfo.User.Username(), - Password: pwd, - } - } - - dialer, err := proxy.SOCKS5("tcp", urlInfo.Host, auth, proxyDialer) - if err == nil { - proxyDialer = dialer - } - } - return proxyDialer -} - +// initTransport 初始化 HTTP 传输层的代理设置 func initTransport(cfg *config.Config, transport *http.Transport) { - if !cfg.Proxy.Enabled { + // 如果代理功能未启用,直接返回 + if !cfg.Outbound.Enabled { return } - if cfg.Proxy.Url == "" { + + // 如果代理 URL 未设置,使用环境变量中的代理配置 + if cfg.Outbound.Url == "" { + transport.Proxy = http.ProxyFromEnvironment + logWarning("Outbound proxy is not set, using environment variables") + return + } + + // 尝试解析代理 URL + proxyInfo, err := url.Parse(cfg.Outbound.Url) + if err != nil { + // 如果解析失败,记录错误日志并使用环境变量中的代理配置 + logError("Failed to parse outbound proxy URL %v", err) transport.Proxy = http.ProxyFromEnvironment return } - proxyInfo, err := url.Parse(cfg.Proxy.Url) - if err == nil { - if strings.HasPrefix(cfg.Proxy.Url, "http") { - transport.Proxy = http.ProxyURL(proxyInfo) + // 根据代理 URL 的 scheme(协议类型)选择代理类型 + switch strings.ToLower(proxyInfo.Scheme) { + case "http", "https": // 如果是 HTTP/HTTPS 代理 + transport.Proxy = http.ProxyURL(proxyInfo) // 设置 HTTP(S) 代理 + logInfo("Using HTTP(S) proxy: %s", proxyInfo.Redacted()) + case "socks5": // 如果是 SOCKS5 代理 + // 调用 newProxyDial 创建 SOCKS5 代理拨号器 + proxyDialer := newProxyDial(cfg.Outbound.Url) + transport.Proxy = nil // 禁用 HTTP Proxy 设置,因为 SOCKS5 不需要 HTTP Proxy + + // 尝试将 Dialer 转换为支持上下文的 ContextDialer + if contextDialer, ok := proxyDialer.(proxy.ContextDialer); ok { + transport.DialContext = contextDialer.DialContext } else { - proxyDialer := newProxyDial(cfg.Proxy.Url) + // 如果不支持 ContextDialer,则回退到传统的 Dial 方法 transport.Dial = proxyDialer.Dial - transport.DialContext = proxyDialer.(proxy.ContextDialer).DialContext + logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial") } + logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url) + default: // 如果代理协议不支持 + logError("Unsupported proxy scheme: %s", proxyInfo.Scheme) + transport.Proxy = http.ProxyFromEnvironment // 回退到环境变量代理 } } + +// newProxyDial 创建一个 SOCKS5 代理拨号器 +func newProxyDial(proxyUrls string) proxy.Dialer { + var proxyDialer proxy.Dialer = proxy.Direct // 初始为直接连接,不使用代理 + + // 支持多个代理 URL(以逗号分隔) + for _, proxyUrl := range strings.Split(proxyUrls, ",") { + proxyUrl = strings.TrimSpace(proxyUrl) // 去除首尾空格 + if proxyUrl == "" { // 跳过空的代理 URL + continue + } + + // 解析代理 URL + urlInfo, err := url.Parse(proxyUrl) + if err != nil { + // 如果 URL 解析失败,记录错误日志并跳过 + logError("Failed to parse proxy URL %q: %v", proxyUrl, err) + continue + } + + // 检查代理协议是否为 SOCKS5 + if urlInfo.Scheme != "socks5" { + logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme) + continue + } + + // 解析代理认证信息(用户名和密码) + auth := parseAuth(urlInfo) + + // 创建 SOCKS5 代理拨号器 + dialer, err := createSocksDialer(urlInfo.Host, auth, proxyDialer) + if err != nil { + // 如果创建失败,记录错误日志并跳过 + logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err) + continue + } + + // 更新代理拨号器,支持代理链 + proxyDialer = dialer + } + + return proxyDialer +} + +// parseAuth 解析代理 URL 中的认证信息(用户名和密码) +func parseAuth(urlInfo *url.URL) *proxy.Auth { + // 如果 URL 中没有用户信息,返回 nil + if urlInfo.User == nil { + return nil + } + + // 获取用户名 + username := urlInfo.User.Username() + + // 获取密码(注意:Password() 返回两个值,需要显式处理第二个值) + password, passwordSet := urlInfo.User.Password() + if !passwordSet { + password = "" // 如果密码未设置,使用空字符串 + } + + // 返回包含用户名和密码的认证信息 + return &proxy.Auth{ + User: username, + Password: password, // 允许空密码 + } +} + +// createSocksDialer 创建 SOCKS5 拨号器 +func createSocksDialer(host string, auth *proxy.Auth, previous proxy.Dialer) (proxy.Dialer, error) { + // 调用 golang.org/x/net/proxy 提供的 SOCKS5 方法创建拨号器 + return proxy.SOCKS5("tcp", host, auth, previous) +} diff --git a/proxy/gitreq.go b/proxy/gitreq.go index a03cd09..165af48 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -23,7 +23,9 @@ func initGitHTTPClient(cfg *config.Config) { MaxConnsPerHost: 30, IdleConnTimeout: 30 * time.Second, } - initTransport(cfg, gtr) + if cfg.Outbound.Enabled { + initTransport(cfg, ctr) + } gclient = &http.Client{ Transport: gtr, }