客户端看到 200 Connection Established,随即开始 TLS,然后在本该出现 TLS 记录的地方报出一堆乱码。在责怪目标端的加密套件设置之前,先找找有没有没读完的 HTTP 响应。

对于 HTTP/1 CONNECT 交换,状态行只是响应的开头,后面还可能跟头部字段。结束头部段的空行标志着切换到隧道,其前提是 CONNECT 响应成功。

HTTP/1.1 200 Connection Established\r\n
Proxy-Agent: example\r\n
\r\n

这些字节可能一次读完,也可能分几次,甚至某次读取还会带上后续字节。TCP 并不保留服务端的写入边界。

把交接做成显式的解析器状态

  1. 收集并解析状态行。用数字状态码,不要依赖精确的英文原因短语。
  2. 持续读取,直到完整的头部段终止符到达。状态行成功并不代表后面的字段会消失。
  3. 对于成功的 CONNECT 响应,把终止符之后的第一个字节视为隧道数据的起点。对于其他响应,按相应的 HTTP 响应与认证处理来做。
  4. 把连接以及所有未消费的字节,通过下一协议支持的带缓冲 I/O 接口移交。

即使分隔符搜索写得不错,最后一步也很关键。带缓冲的读取器可能已经从套接字里多读了一些字节,超出解析器请求的范围。在裸套接字上启动 TLS 却丢弃那个读取器,字节就可能滞留在读取器的私有缓冲区里。

设置连接建立的截止时间和头部大小上限。终止符之前出现 EOF 意味着代理响应不完整,而不是让你一直等下去,或者硬着头皮上 TLS。

把几种边界情况推演一遍

设想一次读取在 Proxy-Agent: 之后就结束了:解析器还需要更多 HTTP 头部字节。再设想另一次读取停在最后一个 CRLF 序列中间:那仍然不是完整的头部段。现在设想完整的成功头部段之后,同一个缓冲区里紧跟着隧道字节:这些多出来的字节必须能挺过交接。

407 是另一种结果。它需要走认证处理;它不会授权把一个 TLS ClientHello 送进从未打开的隧道。

除非任务本身就是实现握手,否则优先使用有人维护的连接器。这里说的边界只适用于本文展示的 HTTP/1 交换。HTTP/2 代理把 CONNECT 承载在流上,帧格式不同。

相关文章