You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在http.Transport中使用自定义DialTLS函数时如何同时配置HTTP代理

原因分析

Go标准库http.Transport配置HTTP代理后访问HTTPS站点时,默认执行流程为:

  1. 普通TCP拨号连接代理服务器
  2. 发送CONNECT请求申请打通到目标站点的隧道
  3. 隧道打通后,直接在该连接上执行默认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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 06:57:03