bigywater.tech
爬虫工程 · 10

HTTP 代理的 CONNECT 隧道:代理到底看到了什么

爬虫经常要走代理,但代理“能看到什么”常被说错。这篇写一个会记录自己所见的最小代理,让同一个客户端分别请求 HTTP 和 HTTPS 地址,再看看代理认证、环境变量和 NO_PROXY 各自怎么起作用。

CONNECT
HTTPS 地址经代理时,代理收到的第一行是 CONNECT 主机:端口,不含路径
False
代理看到的字节里是否出现 /secret/path、token、Cookie 值——全都没有
407
HTTP 地址遇到代理要求认证,requests 不抛异常,只返回 407

HTTP 地址:代理全看见

实测 · 本机执行输出
  客户端得到:200 'hello from origin'
  代理收到的第一行(注意是完整 URL,不是只有路径):GET http://plain.example.test:<端口>/secret/path?token=abc123 HTTP/1.1
  代理收到的头:[('Host', 'plain.example.test:<端口>'), ('User-Agent', 'python-requests/2.34.2'), ('Accept-Encoding', 'gzip, deflate, zstd'), ('Accept', '*/*'), ('Connection', 'keep-alive'), ('Cookie', 'sid=SESSION42'), ('X-Api-Key', 'KEY99')]
  源站收到的路径:/secret/path?token=abc123;源站收到的头:['Host', 'User-Agent', 'Accept-Encoding', 'Accept', 'Cookie', 'X-Api-Key', 'Connection']

代理收到的请求行是完整 URL,头也是原样的。所以走明文 HTTP 经过一个代理,路径、查询串里的 token、Cookie 都对代理可见。

HTTPS 地址:CONNECT 隧道

实测 · 本机执行输出
  客户端得到:200 'hello from origin'
  代理收到的第一行:CONNECT secure.example.test:<端口> HTTP/1.1
  代理收到的头:[('Host', 'secure.example.test:<端口>')]
  之后代理转发的字节:客户端→源站 1867 字节,源站→客户端 2511 字节
  客户端发来的第一个字节 = 0x16(TLS 握手记录的类型是 0x16);代理能在其中看到主机名 secure.example.test(SNI 是明文):True
  代理看到的字节里出现 '/secret/path':False
  代理看到的字节里出现 'token=abc123':False
  代理看到的字节里出现 'SESSION42':False
  代理看到的字节里出现 'KEY99':False
  源站(TLS 服务器)收到的路径:/secret/path?token=abc123

HTTPS 通过 CONNECT host:port 让代理在两端之间转发字节,握手之后的内容在代理这边是密文。实测代理能看到的是目标主机和端口、以及 ClientHello 里明文的主机名(SNI),看不到路径、查询串、Cookie。这里的 TLS 终点是源站,代理不参与加密;中间人式的代理(会重新签发证书)不在本文范围内。

代理认证

实测 · 本机执行输出
  HTTP 地址,代理 URL 里没有账号:没有抛异常,r.status_code = 407
  HTTPS 地址,代理 URL 里没有账号:ProxyError:onnect to proxy', OSError('Tunnel connection failed: 407 Proxy Authentication Required')))
  HTTP 地址,代理 URL 带 alice:pw:200;代理收到 Proxy-Authorization = Basic YWxpY2U6cHc=
  HTTPS 地址,代理 URL 带 alice:pw:200;CONNECT 请求里 Proxy-Authorization = Basic YWxpY2U6cHc=

代理返回 407 时,requests 对 HTTP 地址只是把 407 当成普通响应返回(r.status_code == 407),对 HTTPS 地址则在建立隧道失败时抛 ProxyError。账号写在代理 URL 里(http://alice:pw@host:port),requests 会发 Proxy-Authorization: Basic …,HTTPS 时它在 CONNECT 请求里。这里的 Basic 只是 Base64 编码,不是加密。

代理设置从哪来

实测 · 本机执行输出
  设了 HTTP_PROXY:状态 200;经过代理的请求 1 个;源站一共收到 1 个请求(经代理转发的也算)
  小写 http_proxy:状态 200;经过代理的请求 1 个;源站一共收到 1 个请求(经代理转发的也算)
  HTTP_PROXY + NO_PROXY=.example.test:状态 200;经过代理的请求 0 个;源站一共收到 1 个请求(经代理转发的也算)
  HTTP_PROXY + NO_PROXY=plain.example.test:状态 200;经过代理的请求 0 个;源站一共收到 1 个请求(经代理转发的也算)
  HTTP_PROXY + trust_env=False:状态 200;经过代理的请求 0 个;源站一共收到 1 个请求(经代理转发的也算)
  只在代码里传 proxies=:经过代理 1 个
  proxies= 传了代理,但目标是 127.0.0.1:经过代理 1 个(本进程 NO_PROXY=127.0.0.1,localhost)

环境变量 HTTP_PROXY(大小写都行)会被读取,NO_PROXY 能排除主机,trust_env=False 让 Session 不读环境变量。直接传 proxies= 则一定用,不看 NO_PROXY。排查“为什么请求被发到了代理 / 没发到代理”时,先看这几处。

分步演示

HTTP 代理与 CONNECT
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。代理是我用 Python 套接字写的最小转发代理(支持 HTTP 绝对 URI 请求与 CONNECT,可要求 Proxy-Authorization),记录收到的请求行、头和转发的字节;HTTPS 源站用本机自签名证书(SAN 含 secure.example.test);*.test 主机名通过替换本进程的 getaddrinfo 解析到回环;环境变量实验在子进程里做(清掉原有代理变量后再设置)
    • 没有验证:真实商业代理(住宅代理、隧道代理)的行为;SOCKS 代理;HTTPS 代理(代理本身也走 TLS);重新签发证书的中间人代理;HTTP/2 经代理;代理对 Connection、Proxy-Connection 头的处理差异(我的代理只做了最小实现);ECH(加密 ClientHello)下 SNI 是否仍可见
    京ICP备2026067502号-1京公网安备11010502063487号