在http.Transport中使用自定义DialTLS函数时如何同时配置HTTP代理
原因分析
Go标准库http.Transport配置HTTP代理后访问HTTPS站点时,默认执行流程为:
- 普通TCP拨号连接代理服务器
- 发送
CONNECT请求申请打通到目标站点的隧道 - 隧道打通后,直接在该连接上执行默认TLS握手,该流程不会触发你预先配置的
DialTLS回调,这就是自定义逻辑不生效的核心原因。
可行实现方案
你需要自己处理代理的CONNECT隧道建立逻辑,然后在隧道上运行你自定义的uTLS握手逻辑,再注册到http.Transport的DialTLSContext字段,完整实现代码如下:
首先是自定义DialTLSContext的实现:
package main import ( "bufio" "context" "errors" "fmt" "net" "net/http" "net/url" "strings" "time" utls "github.com/refraction-networking/utls" ) // 你预先定义的uTLS配置和spec var ( config *utls.Config spec *utls.ClientHelloSpec ) func customDialTLSContext(ctx context.Context, network, addr string, proxyURL *url.URL) (net.Conn, error) { // 第一步:连接代理服务器 proxyConn, err := net.DialTimeout(network, proxyURL.Host, 5*time.Second) if err != nil { return nil, fmt.Errorf("connect proxy failed: %w", err) } // 第二步:发送CONNECT请求建立隧道 connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\nProxy-Connection: keep-alive\r\n\r\n", addr, addr) _, err = proxyConn.Write([]byte(connectReq)) if err != nil { proxyConn.Close() return nil, fmt.Errorf("send connect request failed: %w", err) } // 第三步:读取CONNECT响应,确认隧道建立成功 resp, err := http.ReadResponse(bufio.NewReader(proxyConn), nil) if err != nil { proxyConn.Close() return nil, fmt.Errorf("read connect response failed: %w", err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { proxyConn.Close() return nil, fmt.Errorf("proxy connect failed, status code: %d", resp.StatusCode) } // 第四步:在代理隧道上执行自定义uTLS握手 serverName := strings.Split(addr, ":")[0] config.ServerName = serverName uTLSConn := utls.UClient(proxyConn, config, utls.HelloCustom) if err := uTLSConn.ApplyPreset(spec); err != nil { proxyConn.Close() return nil, fmt.Errorf("apply uTLS preset failed: %w", err) } if err := uTLSConn.HandshakeContext(ctx); err != nil { proxyConn.Close() return nil, fmt.Errorf("uTLS handshake failed: %w", err) } return uTLSConn, nil }
然后是初始化HTTP客户端的代码:
func newHTTPClient(proxyAddr string) (*http.Client, error) { // 先解析代理地址,注意你原代码的顺序错误,要先解析代理再赋值给Transport proxyURL, err := url.Parse(fmt.Sprintf("http://%s", proxyAddr)) if err != nil { return nil, errors.New("invalid proxy address") } // 初始化你自己的Transport tr, err := NewTransport("771,4865-4866-4867-49196-49195-49188-49187-49162-49161-52393-49200-49199-49192-49191-49172-49171-52392-157-156-61-60-53-47-49160-49170-10,65281-0-23-13-5-18-16-11-51-45-43-10-21,29-23-24-25,0") if err != nil { return nil, err } // 注意:这里不需要再给tr.Proxy赋值,因为我们已经在自定义DialTLS里处理了代理逻辑 tr.DisableKeepAlives = true // 注册自定义DialTLSContext,捕获proxyURL参数 tr.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { return customDialTLSContext(ctx, network, addr, proxyURL) } return &http.Client{ Transport: tr, Timeout: 2 * time.Second, }, nil }
注意事项
- 该实现仅针对HTTP代理场景,如果你使用HTTPS代理,需要先和代理服务器完成TLS握手,再发送CONNECT请求
- 只有HTTPS请求会触发
DialTLSContext逻辑,HTTP请求不会走TLS握手流程,自然也不会应用自定义JA3指纹 - 若代理需要身份认证,你需要在CONNECT请求头中添加
Proxy-Authorization字段,填入对应的认证信息
内容的提问来源于stack exchange,提问作者wahman51
相关产品推荐
相关产品推荐

