dialer.go 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. // Package proxyutil 提供统一的代理配置功能
  2. //
  3. // 支持的代理协议:
  4. // - HTTP/HTTPS: 通过 Transport.Proxy 设置
  5. // - SOCKS5: 通过 Transport.DialContext 设置(客户端本地解析 DNS)
  6. // - SOCKS5H: 通过 Transport.DialContext 设置(代理端远程解析 DNS,推荐)
  7. //
  8. // 注意:proxyurl.Parse() 会自动将 socks5:// 升级为 socks5h://,
  9. // 确保 DNS 也由代理端解析,防止 DNS 泄漏。
  10. package proxyutil
  11. import (
  12. "context"
  13. "fmt"
  14. "net"
  15. "net/http"
  16. "net/url"
  17. "strings"
  18. "time"
  19. "golang.org/x/net/proxy"
  20. )
  21. const (
  22. // socks5DialTimeout 限制到 SOCKS5 代理自身的 TCP 建连耗时。
  23. socks5DialTimeout = 10 * time.Second
  24. // socks5DialKeepAlive 与 Go 默认 keepalive 探测间隔保持一致。
  25. socks5DialKeepAlive = 30 * time.Second
  26. )
  27. // socks5ForwardDialer 是 SOCKS5 dialer 的底层拨号器。
  28. //
  29. // proxy.FromURL 的默认 forward dialer 是 proxy.Direct(零值 net.Dialer,无超时),
  30. // 代理地址不可达时会一直卡到内核 TCP 重传耗尽(Linux 约 130 秒)。SOCKS5 分支会
  31. // 覆盖 Transport.DialContext,因此调用方在 Transport 上设置的建连超时对这条路径
  32. // 无效,必须在这里补上。
  33. var socks5ForwardDialer = &net.Dialer{
  34. Timeout: socks5DialTimeout,
  35. KeepAlive: socks5DialKeepAlive,
  36. }
  37. // ConfigureTransportProxy 根据代理 URL 配置 Transport
  38. //
  39. // 支持的协议:
  40. // - http/https: 设置 transport.Proxy
  41. // - socks5: 设置 transport.DialContext(客户端本地解析 DNS)
  42. // - socks5h: 设置 transport.DialContext(代理端远程解析 DNS,推荐)
  43. //
  44. // 参数:
  45. // - transport: 需要配置的 http.Transport
  46. // - proxyURL: 代理地址,nil 表示直连
  47. //
  48. // 返回:
  49. // - error: 代理配置错误(协议不支持或 dialer 创建失败)
  50. func ConfigureTransportProxy(transport *http.Transport, proxyURL *url.URL) error {
  51. if proxyURL == nil {
  52. return nil
  53. }
  54. scheme := strings.ToLower(proxyURL.Scheme)
  55. switch scheme {
  56. case "http", "https":
  57. transport.Proxy = http.ProxyURL(proxyURL)
  58. return nil
  59. case "socks5", "socks5h":
  60. dialer, err := proxy.FromURL(proxyURL, socks5ForwardDialer)
  61. if err != nil {
  62. return fmt.Errorf("create socks5 dialer: %w", err)
  63. }
  64. // 优先使用支持 context 的 DialContext,以支持请求取消和超时
  65. if contextDialer, ok := dialer.(proxy.ContextDialer); ok {
  66. transport.DialContext = contextDialer.DialContext
  67. } else {
  68. // 回退路径:如果 dialer 不支持 ContextDialer,则包装为简单的 DialContext
  69. // 注意:此回退不支持请求取消和超时控制
  70. transport.DialContext = func(_ context.Context, network, addr string) (net.Conn, error) {
  71. return dialer.Dial(network, addr)
  72. }
  73. }
  74. return nil
  75. default:
  76. return fmt.Errorf("unsupported proxy scheme: %s", scheme)
  77. }
  78. }