HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 代理技术全栈手册

    • 代理技术全栈手册 - HiHuo
    • 原理篇

      • 第01章 代理是什么:正向 / 反向 / 透明 / 隧道的统一模型
      • 第02章 代理与网络层级:L3 / L4 / L5 / L7 在哪里截断流量
      • 第03章 一个请求穿过代理的一生:连接生命周期全景
    • 协议篇

      • 第04章 HTTP 代理协议:绝对 URI、CONNECT 隧道、转发头与连接池
      • 第05章 HTTPS 与 TLS 代理:终止 / 透传 / MITM / SNI / mTLS
      • 第06章 SOCKS 协议:SOCKS4/4a/5 与 UDP ASSOCIATE 报文级解析
      • 第07章 HTTP/2、gRPC 与 HTTP/3(QUIC) 代理的挑战
      • 第08章 代理自动配置:PAC / WPAD / 系统代理 / NO_PROXY
    • 层级与转发篇

      • 第09章 L4 代理:TCP/UDP 转发与连接级负载均衡
      • 第10章 L7 代理:协议感知与基于内容的路由
      • 第11章 透明代理:iptables REDIRECT/DNAT、TPROXY 与 eBPF 劫持
      • 第12章 数据搬运的艺术:splice / sendfile / 零拷贝 / io_uring
    • 组件横评篇

      • 第13章 Nginx / OpenResty:反向代理、upstream 与 Lua 可编程
      • 第14章 HAProxy:L4/L7、ACL、健康检查与 stick table
      • 第15章 Envoy:xDS 动态配置与 filter chain,为何是云原生数据面
      • 第16章 Traefik / Caddy:自动服务发现与自动 HTTPS
      • 第17章 Squid 与正向/缓存代理:企业出网、缓存与审计
      • 第18章 mitmproxy:抓包、改包、脚本化调试
      • 第19章 内网穿透与隧道:frp / gost / SSH 隧道 / ngrok
      • 第20章 科学上网生态的技术原理(技术中立)
    • 多语言手写篇

      • 第21章 Go:100 行手写 HTTP/CONNECT + SOCKS5 代理
      • 第22章 Rust:基于 tokio 的高性能 TCP 代理
      • 第23章 Python:asyncio 实现,适合调试与脚本
      • 第24章 C:epoll 裸写与零拷贝,及语言选型对比
    • 容器与K8s篇

      • 第25章 Docker 里的代理:HTTP_PROXY、build/pull 与 daemon 配置
      • 第26章 Sidecar 与流量劫持:Istio init-container 的 iptables 原理
      • 第27章 Ingress 与南北流量:Ingress-nginx 与 Gateway API
      • 第28章 Egress 与出网治理:出口网关、registry mirror、审计
      • 第29章 Service Mesh 数据面:Envoy Sidecar 全链路
    • 进阶篇

      • 第30章 可编程代理:Lua / Wasm / eBPF / xDS,代理的"软件定义"
      • 第31章 性能调优:并发模型、连接池、超时与重试、压测
      • 第32章 排错决策树:502 / 504 / 握手失败 / 环路 / 泄漏
      • 第33章 代理安全:开放代理、SSRF、凭证泄漏与攻击面
    • 底层机制篇

      • 第34章 代理的背压与流控:一个代理最难的部分
      • 第35章 socket 与 TCP 状态机:半关闭、超时、连接生命周期
      • 第36章 HTTP/2 帧、流控与 HPACK:h2 代理的内部机制
      • 第37章 负载均衡算法推导与韧性状态机
      • 第38章 Capstone:把玩具代理改造成生产级骨架
    • 综合实战篇

      • 第39章 企业多跳转发链:拓扑、协议矩阵与贯穿性难题
      • 第40章 端到端实战:把 6 类流量全代理通
      • 第41章 更刁钻的流量:gRPC、长轮询、WebRTC、大文件、双向流
      • 第42章 可落地完整参考实现:一套能跑的多协议转发栈
    • 附录

      • 附录 A:代理协议报文速查(HTTP / SOCKS / PAC / PROXY protocol)
      • 附录 B:组件选型决策树
      • 附录 C:抓包与命令速查

第03章 一个请求穿过代理的一生:连接生命周期全景

学习目标

  • 把前两章的静态模型变成动态影片:跟随一个请求走完代理的每一个阶段
  • 搞清一个反复要命的问题:DNS 到底由谁解析——客户端还是代理?这决定了"DNS 泄漏"
  • 用 curl -w 把请求拆成可测量的时间分段,知道每一毫秒花在哪
  • 建立"阶段 → 故障 → 状态码"的对应表,为 第32章排错 打底

前置知识

  • 第01章 统一模型、第02章 网络层级
  • 第04章 HTTP 代理协议(CONNECT、连接池)

原理

全景时间线

一个客户端经正向 HTTP 代理访问 https://example.com/,完整生命周期是这样的(注意它其实是两条连接的接力):

 客户端侧                          代理                            源站侧
 ────────                        ──────                          ──────
  │
  │ ① 解析代理地址(读 HTTP_PROXY / PAC)
  │ ② DNS 解析"代理"的主机名(不是源站!)
  │ ③ TCP 三次握手 → 连上代理 :3128
  ├──────────────────────────────▶│
  │ ④ 发送 CONNECT example.com:443 │
  ├──────────────────────────────▶│
  │                                │ ⑤ 代理认证(401? 407?)
  │                                │ ⑥ DNS 解析"源站"example.com ← 代理来解析!
  │                                │ ⑦ 连接池查找/新建 → TCP 连源站:443
  │                                ├──────────────────────────────▶│
  │ ⑧ 200 Connection Established   │◀──── 握手完成 ─────────────────┤
  │◀──────────────────────────────┤
  │ ⑨ 与源站做 TLS 握手(端到端,代理盲转密文)
  ├═══════════════════════════════│═══════════════════════════════▶│
  │ ⑩ 发 HTTP 请求(加密)          │  盲转                          │
  ├═══════════════════════════════│═══════════════════════════════▶│
  │                                │           ⑪ 源站处理、生成响应  │
  │ ⑫ 流式收到响应(代理边收边转)  │◀══════════════════════════════┤
  │◀═══════════════════════════════│                                │
  │ ⑬ 连接保活(keep-alive)放回两端的连接池,或关闭、拆除

下面把关键阶段逐个讲透。

阶段②⑥:DNS 由谁解析——"DNS 泄漏"的根源

这是初学者最大的盲点。谁来把域名变成 IP,取决于代理类型和配置:

场景客户端解析谁代理解析谁源站域名谁解析
正向 HTTP 代理(absolute-form / CONNECT)只解析代理的地址解析源站域名代理(远端 DNS)
SOCKS5 + --socks5(本地 DNS)解析代理 + 源站不解析客户端(本地 DNS)
SOCKS5 + --socks5-hostname(远端 DNS)只解析代理解析源站代理(远端 DNS)
直连(无代理)解析源站—客户端

为什么这事关重大?

  • DNS 泄漏:用代理本是为了隐藏访问目标,但若客户端在本地解析了源站域名,你的本地 DNS(运营商)就看到了你要访问谁——代理白用了。这正是科学上网里 --socks5 vs --socks5-hostname 的关键差异(第20章)。
  • 内网解析:企业里只有代理能解析内网域名,客户端本地解析会失败——必须用远端 DNS。

记忆点:正向 HTTP 代理和 --socks5-hostname 都是"远端 DNS"(代理解析),SOCKS5 默认 --socks5 是"本地 DNS"。 想不泄漏,用远端 DNS。

阶段⑤:认证发生在连源站之前

代理认证(第04章 的 407)发生在代理真正去连源站之前——代理得先确认"你有没有资格用我",才会替你出门。认证失败,源站这一跳根本不会发生。

阶段⑦:连接池——为什么第二个请求快得多

代理到源站这一跳,不会每次都新建 TCP+TLS 连接——那太贵了(一次 TLS 握手就是 1~2 个 RTT)。代理维护连接池:

第 1 次请求 example.com:池里没有 → 新建 TCP+TLS(慢,~100ms+)→ 用完放回池
第 2 次请求 example.com:池里有空闲连接 → 直接复用(快,省掉握手)

这就是为什么压测时第一个请求慢、后续快。连接池的大小、空闲超时是代理性能的核心调参(第31章)。客户端↔代理、代理↔源站是两个独立的池。

阶段⑫:流式转发 vs 缓冲整包

代理拿到源站响应后,何时开始转给客户端?两种策略:

  • 流式(streaming):边收边转,第一个字节就转发——延迟低,内存省。适合大文件、SSE、流式响应。
  • 缓冲(buffering):等收完整个响应体再转——便于改写、压缩、做内容检查,但增加延迟和内存。Nginx 默认对响应做缓冲(proxy_buffering on)。

这个选择直接影响 TTFB(首字节时间)和大响应的内存占用,第31章 详谈。

阶段⑬:连接拆除与半关闭

转发完成后,两条连接各自决定:keep-alive 就放回池等复用,否则四次挥手关闭。隧道场景要处理半关闭(half-close):一个方向 EOF 了,另一个方向可能还在传——这正是 第21章 里"任一方向结束就关闭双端"那段代码要小心的地方,处理不好会留下一堆 CLOSE_WAIT。


命令 / 实验

实验一:把请求拆成可测量的时间分段

curl -w 能打印生命周期各阶段的累计耗时。建一个模板:

cat > curl-format.txt <<'EOF'
   DNS 解析     : %{time_namelookup}s
   TCP 连接     : %{time_connect}s
   TLS 握手     : %{time_appconnect}s
   传输前就绪   : %{time_pretransfer}s
   首字节(TTFB) : %{time_starttransfer}s
   总耗时       : %{time_total}s
EOF

直连 vs 走代理对比:

# 直连
curl -w "@curl-format.txt" -s -o /dev/null https://example.com/

# 走代理(注意 time_connect 现在是"连到代理"的耗时)
curl -w "@curl-format.txt" -s -o /dev/null -x http://127.0.0.1:3128 https://example.com/

典型输出(走代理):

   DNS 解析     : 0.004s     ← 解析"代理"地址(②)
   TCP 连接     : 0.005s     ← 连上代理(③)
   TLS 握手     : 0.210s     ← 含 CONNECT 隧道(④⑧) + 到源站的 TLS(⑨)
   传输前就绪   : 0.211s
   首字节(TTFB) : 0.355s     ← 源站处理 + 回传(⑪⑫)
   总耗时       : 0.360s

把这些数字对回时间线:time_connect 是"到代理",time_appconnect 把 CONNECT 隧道和端到端 TLS 都算进去了,time_starttransfer 减去 time_appconnect 大致是源站处理时间。学会读这几个数,排查"慢"就有了坐标。

实验二:验证"DNS 由谁解析"(抓本地 DNS 看泄漏)

# 监听本机 DNS 查询
sudo tcpdump -i any -n 'udp port 53' &

# A. SOCKS5 本地 DNS:本地会查 example.com → 泄漏!
curl --socks5 127.0.0.1:1080 https://example.com/ -s -o /dev/null
# tcpdump 里能看到对 example.com 的 A 查询

# B. SOCKS5 远端 DNS:本地不查 example.com → 不泄漏
curl --socks5-hostname 127.0.0.1:1080 https://example.com/ -s -o /dev/null
# tcpdump 里没有对 example.com 的查询(代理替你查了)

亲眼看到 A 泄漏、B 不泄漏,你就彻底懂了"远端 DNS"的价值。

实验三:人为制造 502 与 504,看故障落在哪个阶段

# 502:源站直接不可达(阶段⑦失败)——代理连不上后端
#   配一个 proxy_pass 指向一个根本没监听的端口,curl 走它 → 502 Bad Gateway

# 504:源站存在但响应极慢(阶段⑪⑫超时)
#   后端 sleep 超过代理 read timeout → 504 Gateway Timeout

记住这条因果链:502 = 代理连/读源站这一跳"坏了";504 = 这一跳"超时了"。两者都发生在"代理→源站"段,与"客户端→代理"段无关。


排错:阶段 → 故障 → 状态码

把生命周期翻过来用,就是一张排障地图:

卡在哪个阶段现象多半是
② 解析代理地址Could not resolve proxy代理主机名错、本地 DNS 挂了
③ 连代理Connection refused 连 :3128代理没起、端口错、防火墙
⑤ 代理认证407 Proxy Authentication Required没给/给错 Proxy-Authorization
⑥ 代理解析源站502,日志含 DNS 失败代理侧 DNS 配置、内网域名
⑦ 连源站502 Bad Gateway源站宕、端口错、网络不通
⑪⑫ 源站慢504 Gateway Timeout源站负载高、代理 read timeout 太短
⑬ 拆连接大量 CLOSE_WAIT、连接泄漏半关闭处理不当、连接池泄漏
CONNECT 阶段403/405代理端口白名单不含目标端口

完整的排错决策树(含抓包定位、分段二分法)见 第32章。


本章小结

  • 一个走代理的请求是两条连接的接力:客户端↔代理、代理↔源站,各有独立的握手、连接池和超时。
  • DNS 由谁解析决定是否泄漏:正向 HTTP 代理与 --socks5-hostname 是远端 DNS(不泄漏),SOCKS5 默认本地 DNS(泄漏)。
  • curl -w 的 time_namelookup/connect/appconnect/starttransfer 能把生命周期拆成可测量分段。
  • 502 = 代理→源站这一跳坏了;504 = 这一跳超时,都与客户端侧无关。

至此原理篇收官——你已经有了看穿任何代理的统一坐标系(类型 × 层级 × 生命周期)。下一篇进入协议篇,从 第05章 HTTPS 与 TLS 代理 开始,把每种代理协议的报文细节逐一拆开。

Prev
第02章 代理与网络层级:L3 / L4 / L5 / L7 在哪里截断流量