切换深浅色
第 7 章 HTTP 代理与 CONNECT 隧道
HTTP 代理有两种必须区分的用法:代理理解并转发一条 HTTP 请求,或者先用 HTTP 建立一条隧道,成功之后让其它协议使用该通道。二者可能出现在同一个代理端口,却有不同的消息解释和信任边界。本章先介绍足以看懂它们的 HTTP/1.1 语法,完整 HTTP 定界和持久连接规则留到第 9 章。
7.1 文本请求怎样成为确定的字节格式
HTTP/1.1 请求以请求行开始,随后是字段行,最后用空行结束头部区。下面每个 \r\n 代表两个字节 0d 0a;展示换行不额外计入报文:
text
GET / HTTP/1.1\r\n
Host: example.com\r\n
\r\n请求行依次是方法、一个空格、请求目标、一个空格、HTTP 版本,随后行结束。这里 GET 是方法,/ 是请求目标,HTTP/1.1 指定消息语法。Host 字段表达目标服务的主机信息。协议不是“把这些单词用任意空白排好”就算等价,字段名、分隔符、空格位置和行结束都有解析规则。RFC 9112 §2—3。
字段行的一般外形是名称、冒号、可选空白和字段值。字段名大小写不敏感,Host 与 host 是同一个名称;方法名则区分大小写,不能任意把 CONNECT 改成小写。字段值怎样解释由各字段定义,不能因字段名不区分大小写,就把所有值也无条件转成小写。RFC 9110 §5、§9.1。
响应第一行则包含 HTTP 版本、三位数字状态码和可选的原因短语,例如 HTTP/1.1 200 OK。决定机器语义的是状态码,不是给人看的 OK 或 Connection Established。客户端不能要求某个固定英文短语,才承认 CONNECT 成功。
空行只结束头部区,不普遍表示整条 HTTP 消息结束。普通响应可能还有内容,长度也许由字段或传输编码确定;CONNECT 的成功响应则会触发特殊的隧道切换。收到空行后的同一块缓冲区还可能包含后续内容,必须交给正确的解析阶段,而不是清空。
文本协议也有二进制边界问题。若一次读取只得到行结束的 0d,解析器要继续等待 0a;若一次取得多个字段和部分后续数据,则要记录确切消费位置。无界等待空行还会耗尽缓存,所以实现需要头部大小、字段数量和读取时间等限制。可读的文本外观不代替边界与状态规则。
本章不把 HTTP 当作任意 Unicode 文本。用于方法、字段名和控制分隔的语法受特定字节约束,目标名称与 URI 也各有自己的表示规则。直接用宽松文本处理把异常空白、嵌入换行或重复字段随意修正,可能让代理与下一跳解释不同。第 9 章将进一步解释这些差异为何影响消息定界和安全。
7.2 absolute-form:让 HTTP 请求自己带上完整目标
客户端直接向网站发 GET /a HTTP/1.1 时,TCP 连接已经通向相应站点,请求目标只需表达资源路径等信息。显式 HTTP 代理却面对许多网站;客户端与代理的 TCP 对端不能代替最终网站身份,所以普通代理请求采用完整的绝对 URI 作为请求目标。
例如访问 http://example.com/a?x=1,客户端可以向代理发送:
text
GET http://example.com/a?x=1 HTTP/1.1\r\n
Host: example.com\r\n
\r\n这种 request-target 叫 absolute-form。它让代理取得方案 http、主机 example.com、资源路径 /a 和查询 x=1。端口未写时按方案的默认端口处理;若写成 http://example.com:8080/a,则目标端口为 8080。客户端连接代理的端口仍然是另一项独立配置。
代理接收该 HTTP 消息,检查策略,选择下游连接,再生成发往源站的请求。直达源站的一跳通常采用 origin-form:
text
GET /a?x=1 HTTP/1.1\r\n
Host: example.com\r\n
\r\n这里的请求目标改变了,HTTP 资源语义没有因此改变。代理不是把从客户端收到的每个字节都原样写入目标 TCP;它终止并重新表达 HTTP 消息,按规范处理需要逐跳更新或移除的字段。这与 CONNECT 成功后不再解析内部 HTTP 的隧道不同。
absolute-form 中已经有主机,为什么还要 Host?HTTP/1.1 对 Host 有统一要求,但双重出现也带来一致性问题。RFC 9112 要求代理接收 absolute-form 时,以请求目标中的主机信息为依据,忽略接收到的 Host,并在转发时根据该请求目标生成相应 Host。不能一处按 URI 主机作权限检查,另一处又按不一致的 Host 选下游。RFC 9112 §3.2.2。
这个规则不是鼓励客户端故意发送冲突字段。客户端本就应正确生成目标与 Host;服务器和代理还需要按其角色验证输入。确定同一条请求的唯一目标,可以避免各阶段在两个字符串之间自行选择,正是协议防止歧义的一项具体要求。
在这种明文 HTTP 请求转发模式下,代理能看到完整 URI、字段和内容,并有机会按 HTTP 语义处理它们。缓存、请求路由、访问策略和响应转发都可能建立在这种理解上。是否允许修改、缓存哪些结果,受 HTTP 规则、配置和策略限制,不能把“代理看得到”自动等同于“代理可以任意改变而仍等价”。
若客户端希望自己与网站建立独立的 TLS 会话,就不能仅把 TLS 密文放在这条 GET 的普通内容位置,指望代理自动按目标流转发。它需要请求另一种服务:先建立到网站的隧道,再让浏览器与网站交换 TLS。HTTP 用 CONNECT 表达的正是这种语义切换。
7.3 CONNECT:头部结束以后,连接改变了用途
CONNECT 要求代理建立到指定端点的隧道。它的 HTTP/1.1 请求目标采用 authority-form,只包含主机与端口,不带 http://、资源路径或查询。例如:
text
CONNECT example.com:443 HTTP/1.1\r\n
Host: example.com:443\r\n
\r\n端口必须明确写出,即使来自 HTTPS URL,也不能依赖一个隐含的 443。IPv6 字面地址用方括号界定主机部分,例如 [2001:db8::1]:443,避免地址内部冒号与端口分隔符混淆。CONNECT 的请求目标是隧道端点,不是网站里的 /index.html 资源。RFC 9110 §9.3.6、RFC 9112 §3.2.3。
代理读取请求、检查身份与目标权限,准备目标连接或相应的下一跳隧道,成功后回复。例如:
text
HTTP/1.1 200 Connection Established\r\n
\r\n这条响应最关键的不是短语,而是 CONNECT 语境中的成功状态。任意 2xx 成功响应都触发相应隧道语义;它的头部区结束之后,后续字节属于隧道中的目标流。客户端与代理不再在这一 HTTP/1.1 连接上把它们当作普通 HTTP 响应内容定界。
这是一个准确的状态切换点。成功之前,客户端的解析器识别 HTTP 状态行与字段;成功且头部结束之后,解析器把剩余字节交给目标协议。若目标会先发送欢迎信息,一次读取可能同时取到成功响应的尾部和目标内容,必须保留消费范围。若客户端先与目标进行 TLS,切换之后的解释者就是网站 TLS 组件。
成功 CONNECT 响应不应包含 Content-Length 或 Transfer-Encoding;RFC 9110 还要求客户端忽略成功响应中误收到的这两个字段。不能把 Content-Length: 0 解释成“隧道为空,后面关闭”,也不能因为出现一个长度值就只转送相应数量的目标字节。隧道寿命不由普通 HTTP 响应内容长度决定。
非 2xx 响应则完全不同。407 可能要求代理认证,403 可能拒绝策略请求,502 可能表达网关侧问题;此时隧道尚未建立,后面的消息内容仍须按普通 HTTP 规则处理。若错误页面的首字节 < 被误交给网站 TLS,表面可能只剩一个 TLS 格式错误,却遮住真正的代理失败。
CONNECT 请求本身没有 HTTP 内容。客户端希望提前发送的目标字节如何处理,与所用 HTTP 版本和具体实现有关,不能通过给 CONNECT 添加普通请求内容长度,私自把任意数据定义成一个可移植的“早期握手”。本章完整案例采用等待成功响应后才发送目标数据的路径。
这个能力受到代理策略约束。隧道可以承载非 HTTP 的 TCP 协议,但服务器不必允许所有端口和目标;规范建议限制目标范围,防止通用连接能力被滥用。标准 CONNECT 也不是 UDP 关联命令。本章讨论传统 HTTP/1.1 的连接级隧道;后文 HTTP/2 的流级映射需要另外解释,不能把“整条连接用途改变”原样套到一条同时承载多个流的连接。
7.4 代理认证:407 属于哪一个接收者
代理服务可以要求客户端先证明使用资格。HTTP 用 407 Proxy Authentication Required 表达代理认证需求,并通过 Proxy-Authenticate 给出挑战。例如以下构造响应没有消息内容,故可用普通 HTTP 长度零定界:
text
HTTP/1.1 407 Proxy Authentication Required\r\n
Proxy-Authenticate: Basic realm="proxy"\r\n
Content-Length: 0\r\n
\r\n这不是网站在要求登录,也不是 CONNECT 已经建立。Basic 表示认证方案,realm 用于描述保护空间,帮助客户端区分凭据适用范围;它本身不提供服务器身份的密码学证明。服务器生成 407 时必须提供至少一项代理认证挑战。RFC 9110 §11.7。
客户端取得适用凭据后,可以在后续代理请求中添加 Proxy-Authorization。以无实际用途的公开教学账户 ann、密码 demo 为例,Basic 把 ann:demo 的字节用 Base64 表示,得到 YW5uOmRlbW8=:
text
CONNECT example.com:443 HTTP/1.1\r\n
Host: example.com:443\r\n
Proxy-Authorization: Basic YW5uOmRlbW8=\r\n
\r\n冒号分隔用户名与密码,Base64 是第 1 章讲过的可逆编码,不是加密。这里的可见字符串足以恢复教学凭据。实际 Basic 对字符集和用户名的约束也不能用“随便拼接任意文本”替代;本例使用 ASCII,避免引入未经约定的字符编码。RFC 7617 §2。
这与网站认证有清楚的字段对应:
| 认证对象 | 要求认证的典型状态 | 服务器挑战字段 | 客户端凭据字段 |
|---|---|---|---|
| 源站 | 401 | WWW-Authenticate | Authorization |
| 代理 | 407 | Proxy-Authenticate | Proxy-Authorization |
字段名相似不表示凭据可以互换。把网站 Authorization 错当代理密码,不会按标准完成代理认证;把 Proxy-Authorization 无意发给目标网站,则把代理凭据交给了不适当的接收者。通常代理凭据由要求它的下一跳代理消费;多个代理协作认证时,规范允许有相应转交安排,但那是明确定义的协作机制,不是自动向所有后端广播。
认证重试还受连接状态约束。若 407 正常定界且连接仍可用,客户端可以按规则重试;若代理已经关闭连接,则需要重建。无论是否复用连接,必须先完整处理失败响应,不能把失败内容与新请求、或后来的目标 TLS 字节混在一起。是否提前发送已知凭据,是客户端策略与保护空间管理的问题,不改变认证方案的安全性质。
网站 HTTPS 也不会保护先前的明文代理认证。若客户端到代理只有明文 HTTP,CONNECT 请求里的目标和 Basic 凭据就在这条链路上可见;稍后隧道中的网站 TLS 以网站为端点,不能倒过来覆盖代理头。若客户端先与代理建立经过验证的 TLS,再在内部发送 CONNECT,才是外层保护代理认证与控制信息;代理作为 TLS 终点仍能取得它们。
因此“需要密码”“使用 HTTPS 网站”“安全连接到代理”是三个独立事实。准确分析要说明凭据发给谁、哪条通道保护它、认证挑战由谁产生,以及客户端怎样确认自己面对的是预期代理。仅看到一个带编码值的认证字段,不能省略这些边界。
7.5 HTTPS 穿过隧道:两层安全会话可以有不同端点
HTTPS 经代理访问时,至少要分别问两件事:客户端怎样安全连接代理,以及客户端怎样安全访问网站。网站使用 HTTPS,不代表客户端到代理的控制请求已经加密;代理使用 HTTPS,也不代表它应该终止网站那一层 TLS。
先考虑客户端以明文 HTTP 连接代理。它发送 CONNECT,等待成功,然后启动与网站的 TLS。代理把网站 TLS 字节转给目标,并把目标返回字节送回来。在这种隧道模式下,TLS 的参与者是客户端和网站,代理只提供字节通道。网站证书由客户端验证,不由那条 200 Connection Established 响应替代。
text
客户端 代理 网站
|---CONNECT-------->|
|<--成功响应--------|
|====== 网站 TLS 字节经代理转送 =========|
| TLS 内部再承载网站 HTTP |代理能够看到 CONNECT 目标,管理目标连接,也能观察隧道的数量、长度和时序;只要它没有成为网站 TLS 的终点,就不能仅靠转送而得到网站 HTTP 的路径、字段与正文。握手阶段还可能暴露额外名称等信息,具体哪些受保护,要依据 TLS 版本和扩展逐项判断,第 16 章再展开,不能把“HTTP 内容加密”写成“所有元数据都不可见”。
再考虑客户端先与代理建立 TLS,然后在这条经过验证的安全连接内部发送 CONNECT。此时有外层客户端—代理 TLS,以及内层客户端—网站 TLS:
text
客户端到代理的外层 TLS
├─ CONNECT 请求及代理认证
├─ CONNECT 成功响应
└─ 隧道字节
└─ 客户端到网站的内层 TLS
└─ 网站 HTTP 请求与响应外层 TLS 的对端名称与证书属于代理服务,内层属于网站。二者可以使用不同域名、证书、密钥和连接生命周期。代理终止外层后能够读取 CONNECT 及代理凭据,但内层网站 TLS 仍保持其独立保护。外层一旦关闭,内层传输通道会受到影响;这不意味着两层握手或密钥本来就是同一份状态。
| 观察位置 | 可能取得的信息,按本文隧道模型 |
|---|---|
| 未受保护 CONNECT 的链路 | 请求目标、可见代理认证及隧道元数据 |
| 外层 TLS 链路上的被动观察者 | 代理网络端点、连接量与时序等外层可见信息 |
| 代理进程,已终止外层保护 | CONNECT 目标、代理身份材料、隧道字节及其元数据 |
| 网站 TLS 端点 | 本网站会话的 HTTP 明文 |
表格的“被动观察者”指读取链路、但不主动修改通信的一方;它与已经控制客户端或代理进程的一方能力不同。外层保护不向其终点隐藏内部控制信息,内层保护也不消除代理建立目标连接所需的寻址信息。
某些企业网关会让客户端信任一套额外证书体系,随后在网关终止面向网站的 TLS,再建立另一条到网站的 TLS。那是不同的安全端点架构,不属于本文“保持客户端—网站 TLS”的纯隧道模型。若把两种架构都简写成“走 HTTPS 代理”,就无法准确讨论谁看得到内容。
CONNECT 本身也不强制目标协议为 TLS。它可以建立一个允许的 TCP 端点通道,内部究竟使用明文、TLS 或其它应用协议,由双方实际交互决定。端口 443、成功状态和隧道服务,都不能单独证明目标的密码学身份已经验证。这个边界在稍后的 VLESS + TLS、REALITY 与 Vision 组合中仍然成立。
7.6 完整 CONNECT 报文:切换发生在第几个字节
下面把控制消息的精确范围算清楚。客户端访问 example.com:443,没有代理认证字段,也没有其它可选字段。HTTP/1.1 请求为:
text
CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n这行是转义展示,共 59 个真实字节,不含排版换行:
| 请求流范围 | 长度 | 内容 |
|---|---|---|
[0,7) | 7 | CONNECT |
[7,8) | 1 | 空格 20 |
[8,23) | 15 | example.com:443 |
[23,24) | 1 | 空格 20 |
[24,32) | 8 | HTTP/1.1 |
[32,34) | 2 | 0d 0a |
[34,55) | 21 | Host: example.com:443 |
[55,57) | 2 | 0d 0a |
[57,59) | 2 | 结束头部区的 0d 0a |
请求目标中的冒号与端口都是 ASCII::443 编为 3a 34 34 33。这与 SOCKS5 用 01 bb 表示同一个端口不同。端口含义相同,编码方式与宽度不同,不能把一种协议的两个二进制字节直接塞入另一种协议的请求行。
代理成功响应为:
text
HTTP/1.1 200 Connection Established\r\n\r\n| 响应流范围 | 长度 | 内容 |
|---|---|---|
[0,8) | 8 | HTTP/1.1 |
[8,9) | 1 | 空格 |
[9,12) | 3 | 200 |
[12,13) | 1 | 空格 |
[13,35) | 22 | Connection Established |
[35,37) | 2 | 第一行结束 |
[37,39) | 2 | 空头部区结束 |
因此客户端要完整消费 39 字节才完成本响应。偏移 39 开始是隧道流,不是一个按 Content-Length 定界的普通 HTTP 内容。若响应增加合法字段,这个偏移会相应改变,所以实现不能把 39 写成所有 CONNECT 的常数;它必须解析至实际头部区结束。
假设第一次只读到响应前 36 字节,最后一个字节是 0d。此时连状态行的 0d 0a 都没收齐,尽管状态码已出现,也不能宣布切换完成。第二次收到剩余三字节后才能结束控制解析。对会主动发送欢迎信息的目标,第二次读取还可能附带目标字节;剩余范围应直接交给目标解释器。本例的 TLS 客户端会在成功之后发送 ClientHello,不能凭空把尚未发送请求的目标响应插进此处。
相反,以下失败仍然是普通 HTTP 消息:
text
HTTP/1.1 502 Bad Gateway\r\n
Content-Length: 3\r\n
\r\n
BAD三字节 BAD 是代理错误响应内容,不能送给目标 TLS,也不能把它当成目标网站传来的业务结果。客户端若准备重用连接,必须先处理完这个普通响应的定界;是否还能复用取决于连接和响应规则,不由“它曾是 CONNECT 请求”自动决定。
控制阶段的字节开销也可以明确比较。在本文无认证、无额外字段的构造里,CONNECT 双向控制共 59+39=98 字节。第 6 章域名 SOCKS5 的对应例子为 33 字节,但两例的认证和字段选择不同,数字只说明这两种构造的应用层编码量,不能单凭它们给现实软件的吞吐或首包时间排名。TCP 建连、安全握手、DNS 和缓存状态都尚未计入。
如果网站接下来发送大量内容,固定控制头会被摊薄;如果每次都新建一条极短连接,它就会重复出现。进一步分析应该询问连接是否复用、每次请求等待什么,以及是否能够减少中间处理,而不是因文本头多几十字节就推导一个无条件的性能结论。
7.7 CONNECT 与 SOCKS5:服务相近,控制语言不同
对一个只需要可靠 TCP 字节流的客户端,SOCKS5 CONNECT 与 HTTP CONNECT 都可以表达“替我连接这个端点”。在成功之后,它们都能让目标 TLS 继续由应用端点处理。但认证、寻址、能力集合和状态切换并不相同。
| 维度 | SOCKS5 CONNECT | 本章 HTTP/1.1 CONNECT |
|---|---|---|
| 目标表达 | 地址类型+原始地址或长度前缀域名+二进制端口 | authority-form,主机与十进制端口文本 |
| 初始控制 | 方法列表与方法选择 | 可直接发送 CONNECT 请求 |
| 身份机制 | 选定方法的子协议 | HTTP 认证框架,如407与代理凭据字段 |
| 结果表达 | REP 及绑定端点 | HTTP 状态和字段 |
| 数据切换点 | 完整的成功响应及方法要求处理完毕 | 2xx 成功响应头部区结束 |
| 同协议其它转发命令 | BIND、UDP ASSOCIATE | 传统 CONNECT 本身是 TCP 隧道语义 |
| 基础保密 | 依认证方法或外部保护 | 依认证方案与外部保护 |
| 目的地策略 | 服务器检查命令、地址等条件 | 代理检查隧道目标及端口等条件 |
字段更短不代表等待更少。无认证 SOCKS5 先有方法选择的一次往返,之后才发 CONNECT;没有认证挑战的 HTTP CONNECT 可以直接表达连接请求。HTTP 若先得到407再重试,就增加了相应等待;SOCKS 若采用需要多轮交互的方法,也会增加阶段。必须按实际选中的路径列出依赖,不能由文本或二进制两类外观推断首包延迟。
DNS 责任同样由请求输入决定。两者都可以发送域名让代理选址,也可以发送数值地址。HTTP CONNECT 的域名在文本 authority 内,SOCKS5 的域名在其地址类型结构内,表达方式不同并不意味着其中一种“天然无 DNS 泄漏”。应用先前、旁路或为代理节点本身发起的解析,仍要单独追踪。
HTTP 代理还可能提供 ordinary HTTP 请求转发,这与 CONNECT 隧道不是同一模式;SOCKS5 通常不需要理解目标 HTTP。比较时若一边统计代理缓存命中后的网页响应,另一边统计一条未经缓存的原始隧道,就是在比较不同服务。是否理解业务、是否缓存和是否终止网站 TLS,应先对齐。
多级路径的选择也不必强迫各跳使用同一种协议。本地应用可以用 SOCKS5 入口,代理客户端再通过 HTTP CONNECT 到下一节点,或反过来。转换发生时,哪个节点解析控制、哪个节点继续产生请求,决定了名称可见性和身份凭据的接收者。它们的共通之处在服务抽象,而不是线上控制字节相同。
选用哪种入口通常取决于应用支持、所需数据服务、已有网关策略和保护通道。需要 UDP 业务却只提供传统 CONNECT,不会因隧道“很通用”就自动具备 UDP 关联;只需要 TCP 的客户端,也无需为了一个不用的命令承担额外复杂性。明确所需服务,比用协议历史或名称作简单等级排序更有解释力。
7.8 连接复用与隧道的归属
浏览器为了下载一页内容,会发起许多资源请求。“每条代理隧道只连接一个目标端点”并不表示“每个 HTTP 请求都要重新建立代理隧道”。隧道提供的是目标流,目标端点可以在流里维持自己的应用连接,按相应协议交换多条请求响应。
例如 CONNECT 成功后,浏览器与网站建立 TLS,并在其中使用持久 HTTP 连接。首页请求完成后,同一条网站连接可以继续承载后续合适请求。代理不需要重新接收 CONNECT,也不应该把内层每一条 GET 当作自己外层的新代理请求。能否复用、何时关闭,由内层协议及端点共同决定。
另一方面,传统 HTTP/1.1 CONNECT 一旦成功,原连接就已经作为这条隧道使用。客户端不能在隧道字节之间插入一条发给原代理的新 CONNECT,期待它自动把同一流切换到另一个任意目标。此时字节由当前隧道传向既定端点,那里的接收者可能是网站 TLS 解释器,它不会把一段新的代理请求当作外层控制。
若隧道的目标本身就是另一台 HTTP 代理,客户端当然可以在内部再发送 CONNECT;但解释者已变成下一跳代理。这是隧道嵌套,不是第一台代理在原层恢复了普通 HTTP 请求处理。嵌套成功以后,下一段业务字节继续穿过已有通道,外层目标端点保持原样。第 8 章将沿这条路径分析多级代理。
HTTP/2 和 HTTP/3 则有不同的承载单位。它们可以用独立逻辑流表达请求,使一条底层连接中同时存在多个任务;CONNECT 语义也有相应映射。这里不提前展开其帧与状态规则,只强调不能把 HTTP/1.1 的整连接切换,与后文的流级隧道混成一种复用机制。
因此资料中“支持连接复用”至少可能指三件事:目标应用在一条隧道内复用自己的连接;代理协议在一条外层连接中承载多个独立目标;实现通过连接池重复使用已有可用通道。这些机制有不同的状态、故障范围和资源成本。配置中一个类似名称的选项,必须映射到实际采用哪一种。
隧道关闭也有双向语义。RFC 9110 要求隧道中介在检测一侧关闭后,尝试转送相应的未完成数据,再关闭关联连接并丢弃剩余未送达部分;不能把它当作永久可重用的普通请求连接。具体承载对方向结束的表达,还要结合其 HTTP 版本和传输接口理解。RFC 9110 §9.3.6。
本章的实际报文依据 RFC 9112 §2—3、§6、CONNECT 服务依据 RFC 9110 §9.3.6、代理认证依据 RFC 9110 §11.7 与 RFC 7617。后续多级代理、HTTP/2 和 TLS 章节分别深入已经区分的数据路径、流级承载与密码学机制;这些前引不代表其高级实现已完成审查。