第04章 HTTP 代理协议:绝对 URI、CONNECT 隧道、转发头与连接池
学习目标
- 掌握 HTTP 请求的四种 request-target 形式,理解正向代理为何用"绝对 URI"
- 搞懂 hop-by-hop 头与 end-to-end 头的区别,知道代理为什么"必须删头"
- 读懂
Via、X-Forwarded-For、Forwarded这套转发溯源头,及其信任边界 - 用
407+Proxy-Authorization完成代理认证 - 吃透
CONNECT隧道:从报文、200响应到隧道内的裸字节流
前置知识
- 第01章 代理统一模型(绝对 URI vs 相对路径已在此首次出现)
- 网络手册 · 应用层协议
原理
一、request-target 的四种形式
HTTP 请求行长这样:方法 请求目标 版本。这个"请求目标"(request-target)有四种形式(RFC 7230 §5.3),代理协议的一切从这里展开:
| 形式 | 样例 | 用在哪 |
|---|---|---|
| origin-form | GET /index.html HTTP/1.1 | 直连源站、反向代理(最常见) |
| absolute-form | GET http://example.com/index.html HTTP/1.1 | 正向代理(客户端→代理) |
| authority-form | CONNECT example.com:443 HTTP/1.1 | 只用于 CONNECT 建隧道 |
| asterisk-form | OPTIONS * HTTP/1.1 | 只用于服务器级 OPTIONS |
为什么正向代理必须用 absolute-form? 因为代理面向"任意目标"。客户端如果只发 GET /index.html,代理根本不知道该把请求转给谁。Host 头虽然也带主机名,但 absolute-form 是协议层面明确告诉代理"完整 URL 在此,请转发"。这就是 第01章 里"目标写在请求里 vs 写在配置里"的协议落点。
客户端 → 正向代理: GET http://example.com/p HTTP/1.1 (absolute-form)
正向代理 → 源站: GET /p HTTP/1.1 (代理转成 origin-form)
Host: example.com
一个合格的正向代理收到 absolute-form 后,会剥掉 scheme 和 host,转成 origin-form 再发给源站——因为源站不需要、有时也不接受 absolute-form。
二、hop-by-hop 头 vs end-to-end 头(代理为什么"删头")
这是 HTTP 代理最容易被忽视、却最关键的一条规则。HTTP 头分两类(RFC 2616 §13.5.1):
- end-to-end(端到端)头:要一路传到最终接收方。如
Host、Content-Type、Cache-Control。 - hop-by-hop(逐跳)头:只对当前这一跳连接有意义,代理必须删除,不得转发。
固定的 hop-by-hop 头有 8 个:
Connection Keep-Alive Proxy-Authenticate
Proxy-Authorization TE Trailer*
Transfer-Encoding Upgrade
(
Trailer在新规范中归类有争议,实践中按逐跳处理最安全。)
更重要的是:Connection 头里列出的字段名也都是 hop-by-hop,代理必须一并删除。例如客户端发 Connection: close, X-Foo,代理要删掉 Connection 自身、Foo 头。
为什么这事关安全? 如果代理不删 hop-by-hop 头,会引发:
- 连接走私(Request Smuggling):
Transfer-Encoding/Content-Length跨跳不一致时,攻击者可在一个请求里"偷藏"另一个请求 - 信息泄漏:
Proxy-Authorization(你的代理密码)被原样转发到源站
┌─────────┐ Connection: keep-alive, Foo ┌────────┐ (删除 Connection 及 Foo) ┌────────┐
│ Client │ ──────────────────────────────▶ │ Proxy │ ────────────────────────────▶ │ Origin │
│ │ Proxy-Authorization: ... │ │ (删除!不外泄代理凭证) │ │
└─────────┘ └────────┘ └────────┘
三、转发溯源头:Via / X-Forwarded-For / Forwarded
代理"一拆为二"后,源站默认只看到代理的 IP。要让源站知道"真实客户端是谁、经过了哪些代理",靠这几个头:
| 头 | 作用 | 样例 |
|---|---|---|
Via | 标记请求经过的每一跳代理(协议+版本+标识) | Via: 1.1 squid-prod (squid/5.7) |
X-Forwarded-For | 事实标准:记录原始客户端及沿途代理 IP(链式追加) | X-Forwarded-For: 203.0.113.7, 10.0.0.2 |
X-Forwarded-Proto | 原始请求的协议(http/https),TLS 终止后用它告诉后端 | X-Forwarded-Proto: https |
X-Forwarded-Host | 原始 Host | X-Forwarded-Host: example.com |
Forwarded | RFC 7239 的标准化版本,合并上面三者 | Forwarded: for=203.0.113.7;proto=https;by=10.0.0.2 |
XFF 的链式语义:每经过一个代理,就在 X-Forwarded-For 右侧追加自己看到的来源 IP:
Client(203.0.113.7) → 代理A → 代理B → Origin
Origin 看到:X-Forwarded-For: 203.0.113.7, <代理A的IP>
└最左是真实客户端┘ └右侧是沿途代理┘
⚠️ 信任边界:
X-Forwarded-For是客户端可伪造的普通头!只有"你自己掌控的、紧挨着的那个代理追加的那一段"可信。取真实客户端 IP 时,绝不能无脑取最左值——必须从你信任的代理往外数。这是 Web 安全里最常见的 IP 伪造点,详见 第33章 代理安全。
四、代理认证:407 与 Proxy-Authorization
代理认证和源站认证是两套独立机制,状态码和头都不同:
| 角色 | 质询状态码 | 质询头 | 应答头 |
|---|---|---|---|
| 源站认证 | 401 Unauthorized | WWW-Authenticate | Authorization |
| 代理认证 | 407 Proxy Authentication Required | Proxy-Authenticate | Proxy-Authorization |
握手流程:
Client → Proxy: GET http://example.com/ HTTP/1.1
Proxy → Client: 407 Proxy Authentication Required
Proxy-Authenticate: Basic realm="corp-proxy"
Client → Proxy: GET http://example.com/ HTTP/1.1
Proxy-Authorization: Basic dXNlcjpwYXNz ← base64(user:pass)
Proxy → Client: 200 OK ...(认证通过,开始转发)
注意 Proxy-Authorization 是 hop-by-hop 头——代理用完必须删掉,绝不能转发给源站(否则你的代理密码就泄漏给外部网站了)。
五、CONNECT:明文代理如何代理加密流量
正向代理能改写明文 HTTP,但 HTTPS 是加密的,代理无法解析。解法是 CONNECT:让代理只做盲转字节的 L4 隧道。
① Client → Proxy: CONNECT example.com:443 HTTP/1.1 (authority-form)
Host: example.com:443
Proxy-Authorization: ...(如需认证)
② Proxy 建立到 example.com:443 的 TCP 连接
③ Proxy → Client: HTTP/1.1 200 Connection Established (无响应体)
④ 此后这条 TCP 连接变成纯隧道:
Client ══ TLS ClientHello / 证书 / 加密应用数据 ══▶ Origin
代理只在两个 socket 间 bidirectional copy,看不懂任何明文
CONNECT 的几个要点:
- 响应是
2xx(习惯用200,reason 短语写啥都行),且没有 body、没有Content-Length - 隧道建立后,这条连接不再是 HTTP,代理不能再往里塞头
- 出于安全,代理通常限制 CONNECT 的目标端口(只放行 443 等),否则会沦为任意 TCP 跳板(开放代理风险,见 第33章)
- 想看明文?只能做 MITM:代理动态签发证书、终止 TLS 再重新加密——这是 第05章 和 mitmproxy(第18章)的主题
六、连接管理与 Proxy-Connection 的历史包袱
代理与客户端、代理与源站之间都希望复用 TCP 连接(keep-alive)省握手开销。但 Connection 是 hop-by-hop 头,早期代理实现有 bug:会把 Connection: keep-alive 原样透传,导致问题。于是浏览器发明了非标准的 Proxy-Connection 头来"绕过"老代理。
结论:Proxy-Connection 是历史遗留,不是标准。现代实践:
- 客户端↔代理、代理↔源站是两段独立的 keep-alive,各自管理连接池
- 代理对源站维护连接池复用后端连接(性能关键,见 第31章 性能调优)
- 看到
Proxy-Connection知道它是怎么来的即可,自己写代理别依赖它
命令 / 实验
实验一:看正向代理对头的处理(Via 与删头)
用 Squid 起一个标准正向代理(apt install squid,默认听 3128):
curl -v -x http://127.0.0.1:3128 http://httpbin.org/headers
httpbin.org/headers 会把它最终收到的头原样回显。你会观察到:
{
"headers": {
"Host": "httpbin.org",
"User-Agent": "curl/8.5.0",
"Via": "1.1 squid-prod (squid/5.7)", // ← 代理加的溯源头
"X-Forwarded-For": "203.0.113.7" // ← 代理追加的真实客户端 IP
// 注意:没有 Proxy-Connection、没有 Proxy-Authorization —— 被代理删掉了
}
}
对照 第01章实验一:客户端发出的请求行是
GET http://httpbin.org/headers(absolute-form),但httpbin收到的是 origin-form——证明代理做了"绝对 URI → 相对路径"的转换。
实验二:触发 407 代理认证
给 Squid 配上 Basic 认证后:
# 不带凭证 → 407
curl -v -x http://127.0.0.1:3128 http://example.com/
# < HTTP/1.1 407 Proxy Authentication Required
# < Proxy-Authenticate: Basic realm="corp-proxy"
# 带凭证 → 通过(curl 自动算 base64 并加 Proxy-Authorization)
curl -v -x http://alice:secret@127.0.0.1:3128 http://example.com/
# > Proxy-Authorization: Basic YWxpY2U6c2VjcmV0
# < HTTP/1.1 200 OK
实验三:观察 CONNECT 隧道的完整握手
curl -v -x http://127.0.0.1:3128 https://example.com/ 2>&1 | head -25
关键输出(已精简注释):
* Establish HTTP proxy tunnel to example.com:443
> CONNECT example.com:443 HTTP/1.1 ← authority-form 隧道请求
> Host: example.com:443
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 Connection established ← 代理:隧道已开
<
* CONNECT phase completed
* ALPN: offers h2,http/1.1
* TLS connection using TLSv1.3 ... ← 此后是 TLS,代理看不懂
> GET / HTTP/2 ← 这个 GET 是加密发给源站的,代理无感
抓包侧证:CONNECT 之后代理两端的流量是无法解析的 TLS record:
sudo tcpdump -i any -n -A 'tcp port 3128' 2>/dev/null | head -30
# 你会先看到明文 "CONNECT example.com:443" 和 "200 Connection established",
# 之后全是不可读的二进制(TLS)——这就是隧道的"盲转"本质。
排错
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
代理返回 502 Bad Gateway | 代理到源站这一跳失败(DNS 解析不了、源站拒连、源站返回非法响应) | 在代理上直接 curl 目标;查源站可达性 |
代理返回 504 Gateway Timeout | 代理等源站响应超时 | 查源站负载、代理的 read timeout 配置 |
CONNECT 被拒 403/405 | 代理的端口白名单不含目标端口,或禁用了 CONNECT | 查代理 ACL(Squid 的 Safe_ports/SSL_ports) |
| 源站拿到的客户端 IP 全是代理 IP | 未配 X-Forwarded-For,或后端没解析它 | 代理加 XFF;后端配 set_real_ip_from(Nginx)等 |
| 代理密码泄漏到源站 | 代理未删 Proxy-Authorization(实现 bug) | 抓源站侧入流量确认;换合规代理 |
| HTTPS 经代理后证书报错 | 代理在做 MITM(动态签证书) | 检查是否企业根证书;见 第05章 |
| 偶发请求"串味"/响应错配 | hop-by-hop 头未删导致请求走私 | 统一 Transfer-Encoding/Content-Length 处理;见 第33章 |
本章小结
- 正向代理用 absolute-form(绝对 URI)接收请求,再转成 origin-form 发给源站。
- 代理必须删除 hop-by-hop 头(含
Connection里点名的字段),这既是协议要求也是安全底线。 Via/X-Forwarded-For/Forwarded负责转发溯源;XFF 可伪造,取真实 IP 要从可信代理往外数。- 代理认证用
407+Proxy-Authorization,与源站的401/Authorization是两套。 CONNECT让 HTTP 代理用 L4 隧道盲转加密流量,是代理 HTTPS 的标准手段;想看明文得做 MITM。
下一章进入 第05章 HTTPS 与 TLS 代理,把"终止 / 透传 / MITM / SNI 路由 / mTLS"这套 TLS 代理的玩法讲透。