当应用通过代理工作并且出了问题时,工程师首先打开的是客户端日志。里面写着类似 connection reset by peer、read timeout 或者干脆 EOF 这样的内容。问题在于,这些字符串几乎说明不了原因。是谁重置了连接:代理、目标服务器,还是你和代理之间的运营商?请求到底有没有到达代理?TLS 握手有没有来得及完成?HTTP 客户端库并不知道这些,所以你也不知道。

本文我们要下探一层,深入到数据包。讲解如何在客户端机器上用 tcpdump 抓包,通过 HTTP 代理和 SOCKS5 时这些包里究竟能看到什么,为什么在 HTTPS 隧道里你只会看到 CONNECT 和加密的 TLS 记录,如何在 Wireshark 中读取抓包结果,以及如何通过 RST、FIN 标志、重传和零窗口来判断连接到底是在哪一侧断开的。我们还会单独讲通过 SSLKEYLOGFILE 解密自己的流量,以及如何正确准备抓包文件以便向代理服务(例如 Proxeon)的客服求助。

重要提醒:这不是一篇关于 mitmproxy 的文章。那是关于在应用层用伪造证书拦截和替换 HTTPS。这里我们在数据包层面工作,不替换任何东西,也不破解别人的流量。我们的任务纯粹是诊断性的:搞清楚客户端—代理—目标服务器这条链路在哪里断了。

引言:当客户端日志已经不够用时

想象一个典型场景。一个 Python 脚本通过移动代理运行,每小时处理几千个请求,其中大约 2% 会以 Connection aborted, RemoteDisconnected 错误结束。开发者加了重试,错误没有消失。他给代理客服写邮件,那边要求提供请求示例和时间。客服查看自己的日志后回复:我们这边一切正常,到目标服务器的连接是建立过的。谁对谁错?

没有抓包,这场争论永无止境。有了抓包,五分钟就能结束:可以看到代理对 CONNECT 回复了 200,客户端发送了 ClientHello,180 毫秒后从代理那边飞来了 RST。这意味着要么代理没能和目标服务器协商成功,要么服务器自己关闭了连接。接下来可以看时间戳并进一步确认。关键在于,讨论从猜测的领域进入到了事实的领域。

HTTP 客户端日志工作在应用层。它们看到的是结果,而不是过程。流量抓包展示的是过程:每个数据包都带有精确的时间戳、方向、标志和大小。正因为如此,在认真运营代理基础设施时,会抓包和读懂抓包被视为基本技能,而不是什么偏门技巧。

你将从本文获得什么

  • 理解通过代理工作时,客户端网络接口上实际经过哪些数据,其中哪些能看到明文。
  • 即用型 tcpdump 命令,带过滤器、轮转和大小限制。
  • 一整套可以直接复制的 Wireshark 显示过滤器。
  • 区分是在自己这侧、代理侧还是目标服务器侧断开的方法论。
  • 安全地解密自己 TLS 流量用于调试的方法。
  • 向客服求助时准备抓包文件的清单。

基础知识:代理前、隧道内和隧道后分别能看到什么

从示意图开始。通过代理工作时,路径上有三个区段,而在客户端机器上你物理上只能观察到第一个。

三个观察区

  1. 区域 A:客户端—代理。 这是唯一一条经过你网络接口的 TCP 连接。你机器上的 tcpdump 能看到它。这里可以看到:与代理 IP 地址的 TCP 握手、代理的服务协议(HTTP CONNECT 或 SOCKS5),再往后要么是明文 HTTP,要么是加密的 TLS 记录。
  2. 区域 B:隧道内部。 代理回复 200 Connection established 之后,你和目标服务器之间的所有字节都只是被原样转发。如果目标服务器走 HTTPS,这些字节就是 TLS 记录。你能看到它们的结构(记录类型、长度、带 SNI 的 ClientHello、ServerHello、警报),但看不到内容。
  3. 区域 C:代理—目标服务器。 这是代理以自己的名义、从自己的外部地址建立的一条独立 TCP 连接。在你的客户端机器上它根本不存在。只有代理服务器本身才能看到它,而那是服务商的基础设施。你关于区域 C 所知道的一切都是间接得知的:通过 CONNECT 的响应码、通过延迟、通过代理关闭隧道的方式。

这是全文的关键思想。客户端抓包不会直接显示目标服务器。但它会显示代理的行为,而代理作为一个好的中继,会把目标服务器的行为转换成可解释的形式。我们的任务就是学会阅读这种翻译。

HTTP 代理与明文 HTTP

最透明的情况。如果目标网站走不加密的 http,客户端会向代理发送带绝对 URI 的请求:

GET http://example.com/api/items HTTP/1.1
Host: example.com
Proxy-Authorization: Basic dXNlcjpwYXNz
User-Agent: my-client/1.0

抓包里一切都能看到:方法、路径、头部、正文,以及代理转发的带目标服务器正文的响应。注意 Proxy-Authorization 头部。这是你的 base64 编码凭据,以明文形式躺在抓包文件里。记住这个事实,等我们谈到把抓包交给第三方时它会派上用场。

HTTP 代理与通过 CONNECT 的 HTTPS

有趣的部分从这里开始。对于 HTTPS,客户端先请求代理建立到主机和端口的 TCP 隧道:

CONNECT api.example.com:443 HTTP/1.1
Host: api.example.com:443
Proxy-Authorization: Basic dXNlcjpwYXNz
Proxy-Connection: Keep-Alive

代理回复:

HTTP/1.1 200 Connection established

从这一刻起,代理就不再解析 HTTP 了。它只是把字节从一个套接字复制到另一个。客户端开始直接与目标服务器进行 TLS 握手,所有 HTTP 流量(GET、POST、头部、正文)都落在 TLS 内部。这就是为什么在 HTTPS 隧道抓包里只能看到 CONNECT:这是客户端以明文对代理说的最后一句话。之后就是 TLS 记录,代理读不了,你在抓包里也读不了。

此时隧道内部仍然可见的内容:

  • ClientHello:TLS 版本、密码套件、扩展,以及通常的 SNI(明文形式的主机名)。
  • ServerHello:选定的版本和密码套件。
  • 在 TLS 1.2 中——服务器证书是明文的。在 TLS 1.3 中证书已经加密。
  • TLS Alert:在 TLS 1.2 中能看到告警类型,在 TLS 1.3 中握手之后的告警是加密的,但类型为 Alert、长度为 2 字节的记录这一事实本身还能识别出来。
  • Application Data 记录的大小和时间戳。据此可以估计在连接断开前到达了多少数据。

SOCKS5

SOCKS5 的工作方式不同,但思路一样。客户端发送带认证方法的问候(字节 05 02 00 02),代理选择方法,然后是按用户名和密码的认证(子协议带字节 01、长度、用户名、长度、密码),然后是带地址类型 03(域名)和端口的 CONNECT 命令。代理成功时回复 05 00,或者回复错误码:01 一般错误,03 网络不可达,04 主机不可达,05 连接被拒绝,06 TTL 过期。这些错误码就是对代理在区域 C 中看到情况的直接翻译。Wireshark 能解码 SOCKS,只要通过 Decode As 指定端口。

永远看不到的内容

从客户端机器你看不到:代理用来访问目标服务器的外部 IP(对于移动代理来说那是运营商地址)、代理—服务器的 TCP 连接、代理的 DNS 查询。如果 Proxeon 客服说他们在目标服务器侧看到了错误,他们看的是区域 C,对你不可见。你的任务是把区域 A 的抓包带过去,以便按时间把两幅画面核对起来。

深入探讨:为什么代理无法展示更多,以及如何读取间接迹象

这里值得搞清楚为什么画面是这样,以及由此对诊断意味着什么。

CONNECT 隧道作为字节管道

按规范,在回复 200 之后,HTTP 代理必须在不作解释的情况下双向转发字节,直到任一侧关闭。它不知道里面是 TLS。它不知道你在发送什么 HTTP 请求。它只能看到两个事件:字节流和套接字关闭。当目标服务器关闭与代理的连接(发送 FIN 或 RST)时,代理必须关闭与你的连接。它具体怎么做取决于实现:有些代理把 FIN 原样翻译成 FIN、把 RST 翻译成 RST,有些则全部变成 FIN,还有些在从远端套接字读取出错时向客户端发送 RST。

实际后果:来自代理 IP 地址的 RST 并不意味着代理有错。它意味着隧道从代理侧关闭了,原因可能在它之后的任何地方。要弄清原因,就看上下文:RST 之前发生了什么、过了多长时间、响应有没有到达。

发送到代理的 SYN 与发送到目标服务器的 SYN 的区别

这是最常见的混淆来源。通过代理抓包时,你永远不会看到发往目标服务器 443 端口的 SYN。所有 SYN 都是发往代理 IP 和端口的。如果发往代理的 SYN 没有收到 SYN-ACK,并以 1、2、4、8 秒的间隔重复,问题就在你和代理之间:网络、防火墙、地址或端口错误、代理没在运行。目标服务器完全无关,就你的抓包而言你甚至都没尝试去碰它。

而如果与代理的 TCP 已经建立、CONNECT 已发送,响应在 20-30 秒后以 504 Gateway Timeout 或 502 Bad Gateway 回来,那就是代理没能建立区域 C。这里时间本身说明了一切:你的 CONNECT 包瞬间就发出去了,响应很久没来,意味着代理在它自己的尝试中等待超时。

TLS 1.3、ECH 以及到 2026 年什么会变化

格局正在慢慢收紧。TLS 1.3 占主导,其中服务器证书被加密,所以没有密钥的话已经无法从抓包里检查服务器返回了什么证书。ECH(Encrypted Client Hello)扩展正逐渐被浏览器和大型 CDN 部署,其中 SNI 也变成加密的。对于通过代理进行诊断来说这不太关键,因为主机名反正能在 CONNECT 行里看到,但习惯用的按 SNI 过滤的过滤器会越来越少命中。

另一个趋势是基于 QUIC 的 HTTP/3。经典 HTTP 代理用 CONNECT 只能隧道 TCP。如果你在抓包里突然看到发往目标服务器地址 443 端口的 UDP 流量、绕开了代理,这就是泄露的迹象:应用试图直接使用 QUIC 而不是走代理。正确配置的客户端要么在通过代理工作时禁用 QUIC,要么通过单独机制代理 UDP。快速检查的过滤器:udp.port == 443。如果通过代理的抓包里显示了这样的包,客户端配置就需要改。

在哪里设置抓取点

通常答案只有一个——在客户端机器上。但有些细节。如果客户端跑在 Docker 容器里,宿主机上 docker0 接口或桥接接口上的 tcpdump 会显示 NAT 之前的流量,而外部接口上则是 NAT 之后、源地址不同的流量。更简单的做法是进入容器的网络命名空间:nsenter -t PID -n tcpdump ... 或者用 --net=container:名称 启动带 tcpdump 的容器。在云上的虚拟机里注意 offloading:抓包里的包可能看起来比 MTU 大,因为网卡合并了分段。这是正常的,不影响对标志的分析。

tcpdump 实战:过滤器、写入文件、轮转

tcpdump 几乎在任何 Linux 机器和 macOS 上都有。在 Windows 上类似角色由带 Npcap 驱动的 Wireshark 或命令行 dumpcap 承担。我们来看看覆盖 95% 代理诊断任务的命令。

抓取到代理流量的基础操作

假设代理地址是 203.0.113.10,端口 8080。把我们和代理之间往来的所有流量写入文件:

sudo tcpdump -i any -nn -s 0 -w proxy.pcap 'host 203.0.113.10 and port 8080'

参数解析:

  • -i any——监听所有接口。当你不知道流量从哪个接口出去时很方便。如果知道,最好指定具体接口:-i eth0 或 -i wlan0。在 macOS 上不支持 -i any,请指定 en0。
  • -nn——不把 IP 解析成名称、不把端口解析成服务名。解析会减慢抓包并向网络添加额外的 DNS 查询。
  • -s 0——抓取完整数据包。在现代版本中这是默认值,但显式指定也无妨。
  • -w proxy.pcap——以 pcap 格式写入文件,而不是输出到屏幕。只有这样才能之后用 Wireshark 打开抓包。
  • 单引号里的过滤器——这是 BPF 抓包过滤器。它在内核里就滤掉了多余内容,所以文件里只会有需要的东西。

按主机和端口的过滤器

多个代理或端口池:

sudo tcpdump -i eth0 -nn -w proxy.pcap 'host 203.0.113.10 and portrange 10000-10100'

按域名指定代理(tcpdump 在启动时解析一次,对轮换地址不一定合适):

sudo tcpdump -i eth0 -nn -w proxy.pcap 'host proxy.example.net and port 8080'

在流量很大时只抓服务包(不含数据),以便观察握手和断开:

sudo tcpdump -i eth0 -nn -w flags.pcap 'host 203.0.113.10 and tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0'

只抓双向的 RST:

sudo tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-rst != 0'

抓包时排除自己的 SSH 会话,以免在远程服务器上抓包时把抓包弄乱:

sudo tcpdump -i eth0 -nn -w proxy.pcap 'host 203.0.113.10 and not port 22'

如何不抓到几十个 G:按大小和时间轮转

如果错误很少见、一小时才复现一次,抓包就需要长时间运行。不轮转的话磁盘会被塞满。按大小轮转,每个文件 100 MB,环形保留 10 个文件:

sudo tcpdump -i eth0 -nn -w proxy-%Y%m%d-%H%M%S.pcap -C 100 -W 10 'host 203.0.113.10 and port 8080'

这里 -C 指定大小(以百万字节为单位),-W 限制文件数量:第十一个文件会覆盖第一个。按时间轮转,每 10 分钟一个新文件,保留最近 24 个文件(4 小时):

sudo tcpdump -i eth0 -nn -w proxy-%Y%m%d-%H%M%S.pcap -G 600 -W 24 'host 203.0.113.10 and port 8080'

-G 键指定间隔(秒),而文件名里的时间模板是必须的,否则 tcpdump 会反复覆盖同一个文件。有用的键 -Z 用户 会在打开接口后放弃权限,而 -U 强制立即把包写入文件、不缓冲,如果你要并行读取文件或者担心异常终止时丢失尾部数据,这很重要。

截断有效载荷

另一种减小体积的方法——只抓头部。对于诊断断开,TLS 记录内容并不需要,每个包前 128 字节足矣:

sudo tcpdump -i eth0 -nn -s 128 -w headers.pcap 'host 203.0.113.10'

注意:用 -s 128 时 CONNECT 行和头部可能被截断,Wireshark 会把包标记为 truncated。要完整分析 TLS 握手这还不够,ClientHello 经常占 300-600 字节甚至更多。折中方案是 -s 600。

直接在控制台里读抓包

Wireshark 不一定在手边,但需要快速看一眼。读取文件、输出 TCP 标志和绝对序列号:

tcpdump -nn -r proxy.pcap -S -tttt 'tcp[tcpflags] & (tcp-rst|tcp-fin) != 0'

-tttt 键打印完整日期和时间,-S 显示绝对 sequence numbers,有助于与日志对照。以 ASCII 输出包内容,以便看到 CONNECT 行和代理的响应:

tcpdump -nn -r proxy.pcap -A 'port 8080' | grep -E 'CONNECT|HTTP/1'

tshark 作为命令行版 Wireshark

如果服务器上装了 Wireshark,tshark 能在控制台里使用同样的解析器。列出所有 CONNECT 及响应码:

tshark -r proxy.pcap -Y 'http.request.method == "CONNECT" or http.response.code' -T fields -e frame.time -e tcp.stream -e http.request.uri -e http.response.code

列出以 RST 结束的流,并指出是谁发的:

tshark -r proxy.pcap -Y 'tcp.flags.reset == 1' -T fields -e frame.time_relative -e tcp.stream -e ip.src -e ip.dst

Wireshark:显示过滤器、Follow TCP Stream、不解密读取 TLS

在 Wireshark 里打开了 pcap,看到成千上万行。从哪里开始?从显示过滤器开始。和抓包时的 BPF 过滤器不同,它们在已记录文件之上工作,并且理解协议结构。

通过代理工作的基础过滤器

所有与代理相关的内容:

ip.addr == 203.0.113.10 && tcp.port == 8080

所有 CONNECT 请求:

http.request.method == "CONNECT"

到特定主机的 CONNECT:

http.request.method == "CONNECT" && http.host contains "api.example.com"

非 200 的代理响应(授权错误 407、不可达 502、超时 504):

http.response.code >= 400 && tcp.port == 8080

只看 407 响应,这是凭据错误或额度用尽的迹象:

http.response.code == 407

隧道内部的 TLS 过滤器

Wireshark 能识别出在 CONNECT 回复 200 之后开始的是 TLS,并自动套用 TLS 解析器。如果没有识别(包被截断时会发生),右键点击包,选 Decode As,为该端口指定 TLS。

所有 ClientHello:

tls.handshake.type == 1

带特定 SNI 的 ClientHello:

tls.handshake.extensions_server_name contains "example.com"

ServerHello(如果 ClientHello 之后没有它——服务器侧没有开始握手):

tls.handshake.type == 2

TLS 告警:

tls.alert_message

要找到有 ClientHello 但没有 ServerHello 的流,单个过滤器比较难做到;更方便的是通过 Statistics > Conversations,按包数量排序,看看只有 5-7 个包的流。

TCP 问题过滤器

所有带 RST 的包:

tcp.flags.reset == 1

代理发往我们方向的 RST:

tcp.flags.reset == 1 && ip.src == 203.0.113.10

我们发出的 RST:

tcp.flags.reset == 1 && ip.dst == 203.0.113.10

重传:

tcp.analysis.retransmission

零窗口及其后果:

tcp.analysis.zero_window || tcp.analysis.window_full

Wireshark 自己发现的所有异常(重传、重复 ACK、丢段、零窗口):

tcp.analysis.flags && !tcp.analysis.window_update

没有响应的 SYN,也就是重复 SYN:

tcp.flags.syn == 1 && tcp.flags.ack == 0 && tcp.analysis.retransmission

流内部的大停顿,包间隔超过 5 秒:

tcp.time_delta > 5

这个过滤器需要打开 Edit > Preferences > Protocols > TCP > Calculate conversation timestamps。

Follow TCP Stream

找到了可疑的包,比如 RST。右键 > Follow > TCP Stream。Wireshark 会用文本方式展示这个连接的整个对话:我们的流量一种颜色,代理的流量另一种颜色。对于通过 CONNECT 的 HTTPS,你会看到 CONNECT 行、200 Connection established 响应,然后是不可读的 TLS 字节。这很正常。要看的是体积:我们发出了多少字节(我们的 ClientHello 和请求),到达了多少(ServerHello 和响应),以及一切在哪里结束。

Follow 窗口底部有计数器:客户端多少字节,服务器多少字节。如果 200 响应之后服务器来了 0 字节,说明目标服务器根本没回应 ClientHello。如果来了大约 100-4000 字节然后断了,握手开始了但没完成。如果来了几十 KB 然后断开,问题出在数据传输中途。

一个好习惯:按流号过滤。Follow 之后过滤器栏里会自动出现 tcp.stream eq 42。关掉 Follow 窗口,主列表里就只剩这个流及其标志和时间戳。这是分析断开最方便的视图。

不解密读取 TLS 握手

在包树里展开 ClientHello。要看什么:

  • Version 和 supported_versions——客户端是否提供 TLS 1.3。如果服务器要求 1.3 而客户端只提供 1.2,服务器会用 protocol_version 告警关闭连接,或者干脆 FIN。
  • server_name——SNI 是否与 CONNECT 里的主机一致。不一致通常出现在客户端配置错误时,会导致证书错误。
  • Cipher Suites——密码套件列表。太短或太旧的列表是 handshake_failure 告警的原因。
  • ALPN——客户端是否提供 h2。如果服务器选了 h2 而客户端内部期望 HTTP/1.1,应用可能以莫名其妙的错误崩掉。

在 ServerHello 里看选定的版本和密码套件。在 TLS 1.2 中之后会有明文 Certificate,可以检查名称和有效期。在 TLS 1.3 中 ServerHello 之后几乎所有东西都加密了,接下来能读的只有记录类型。Application Data 意味着握手成功完成、数据开始传输。紧跟 ServerHello 之后、或者代替 ServerHello 出现一个长度为 2 字节的 Alert,意味着出问题了:在 TLS 1.3 中没密钥看不到告警码,但这个事实本身已经有信息量。

在 Statistics > Conversations > TCP 里可以方便地评估整体情况:有多少流、每个方向多少字节、持续多久。持续 0.05 秒、6 个包的流——很可能就是握手刚结束就被重置的连接。

根据抓包诊断:RST、FIN、retransmission、zero window

现在是重点。如何根据区域 A 的抓包判断到底在哪里断了?我们来分析每个信号及其在代理语境下的解释。

方向矩阵

第一个问题永远是:谁发了关闭包?在抓包里这是 ip.src 字段。只有两种可能——我们的地址或代理地址。

  • 来自我们的 RST 或 FIN——是我们的应用、我们的操作系统或我们主机上的什么东西关闭了连接。代理和目标服务器没有参与。典型原因:HTTP 客户端超时、进程异常终止、文件描述符耗尽、本地防火墙或杀毒软件在起作用。
  • 来自代理的 RST 或 FIN——隧道从代理侧关闭。原因要么在代理本身(限额、策略、空闲超时),要么是从目标服务器翻译过来的,要么是代理和服务器之间的网络。要按上下文和时间来区分。
  • 既没有 RST 也没有 FIN,只有重传——包在我们和代理之间的路径上丢失。没有任何一方关闭连接,它只是死于丢包。

CONNECT 之后没有 200 响应的 RST

画面:TCP 已建立,我们发送了 CONNECT,代理在没有任何 HTTP 响应的情况下回了 RST。这种行为通常意味着代理在策略层面拒绝了,或者无法解析请求。原因:目标端口不被允许、并发连接数超限、请求格式错误。这里问题在区域 A 或代理本身。目标服务器无关。

对 CONNECT 的 502、503 或 504 响应

画面:CONNECT 出去了,N 秒后来了 5xx 的 HTTP 响应。看 N。如果响应在到代理的 RTT 量级时间内到达,代理是立刻拒绝的——可能是他那边 DNS 名解析不了,或者地址立即不可达(ICMP unreachable)。如果 N 大约是 10-30 秒,代理在等待连接目标服务器超时:服务器没有响应 SYN。两种情况都是区域 C,而你的抓包证明你做对了,代理诚实报告了无法连接。

ClientHello 之后立即 FIN 或 RST

画面:CONNECT - 200 - 我们的 ClientHello - 经过一个到代理的 RTT 加上一点时间后,来自代理的 FIN 或 RST 到了。这里有两个候选人:目标服务器在 TLS 阶段拒绝了连接(不喜欢 SNI、版本、缺少客户端证书),或者服务器的防护系统根据 ClientHello 特征重置了连接。关键迹象是时间。如果从我们的 ClientHello 到断开的时间明显大于到代理的 RTT,说明代理已经成功把 ClientHello 转发了出去并收到了反应。这不是代理决定关闭,而是目标服务器的反应被翻译过来了。

与你在 TCP 握手中测得(SYN 到 SYN-ACK 之间)的到代理 RTT 比较。如果到代理的 RTT 是 40 ms,而 ClientHello 之后的断开在 200 ms 后才来,那么 160 ms 的差值大约就是代理—服务器往返一次 RTT。画面与服务器拒绝完全一致。

ServerHello 之后或部分数据之后的断开

握手开始了,服务器回应了,数据在传输,然后中途 FIN 或 RST。如果是 FIN 且来自代理,而在此之前最后一条 Application Data 记录的尺寸合理,可能服务器只是回复完后就关闭了连接(TLS 内部的 Connection: close),而客户端误解了这一点。如果是数据流中间的 RST、之前没有速度下降,那就是服务器强制关闭,或者代理因流量或会话寿命限额中断了隧道。对于按时间轮换 IP 的移动代理,第二种非常可能:断开恰好发生在 IP 切换的那一刻。检查断开时间是否与代理设置中的轮换间隔一致。

重传及其方向

Wireshark 在它看到相同 sequence numbers 的重复传输时,会把包标记为 retransmission。看看谁在重传:

  • 我们在重传——我们的包没有被代理确认。要么它们在到代理的路径上丢失,要么确认在回程上丢失。两种情况问题都在区域 A 的网络。
  • 代理在重传——它的包没有被我们确认。我们没收到它们,或者我们的 ACK 没到达。同样是区域 A,但偏入方向。
  • SYN 重传——单独情况:代理在 TCP 层不可达,连接压根建立不起来。

重要:区域 C 的丢包你永远不会以重传的形式看到。代理自己会处理目标服务器那边。区域 C 丢包的唯一痕迹是停顿:代理不均匀地给我们传数据,有间隙,尽管抓包里没有重传。对来自代理的包使用过滤器 tcp.time_delta > 1 就能看到这种停顿。

Zero Window

TCP 窗口大小为零意味着接收方来不及从套接字缓冲区里读走数据。如果我们宣布零窗口,说明我们的应用读响应不够快:线程忙、锁、处理慢。代理在这种情况下会等待,之后发送 Zero Window Probe,如果应用还是没开始读,几十秒后可能会关闭连接。客户端会看到断开并怪代理,而原因其实在客户端。

如果宣布零窗口的是代理,意味着它来不及把我们的数据往下传——目标服务器接收慢。这是区域 C 问题的间接迹象,在通过代理向慢服务器上传大文件时会遇到。

重复 ACK 和 SACK

来自代理的 Duplicate ACK——信号是它收到了乱序的包,我们的某个包丢了。大量重复 ACK 和随后的重传——我们这侧移动或 Wi-Fi 网络丢包的典型画面。来自我们的重复 ACK——代理的包丢了。握手时带 SACK 选项能让 TCP 恢复得更高效,但丢包的事实仍然摆在眼前。

TTL 启发式

进阶技巧。看看来自代理的正常包和 RST 包里的 IP TTL 字段。如果 RST 里的 TTL 差了几个数,说明这个 RST 不是代理本身生成的,而是路径上的中间节点:防火墙、负载均衡器或运营商的过滤系统。这不是绝对证据,但强烈暗示应该检查你和代理之间的路径,而不是怪代理或目标服务器。

解释汇总表

  • SYN 重复、没有 SYN-ACK:代理不可达或网络不通。区域 A。
  • SYN - RST:代理端口关闭或被过滤。区域 A。
  • CONNECT - 407:凭据错误或账户限额。代理。
  • CONNECT - 快速 502/504:代理无法开始连接服务器。区域 C,多半是 DNS 或路由。
  • CONNECT - 20-30 秒后的 504:服务器不响应代理的 SYN。区域 C。
  • 200 - ClientHello - 大于 RTT 之后 RST/FIN:服务器拒绝了 TLS。区域 C。
  • 200 - ClientHello - 静默 - 客户端超时后断开:服务器接受 TCP 但不响应 TLS。区域 C 或代理后的阻断过滤。
  • 数据在传 - 代理在固定时刻发 RST:代理限额或轮换。代理。
  • 重传和重复 ACK、没有 RST:我们和代理之间网络丢包。区域 A。
  • 来自我们的 Zero Window:我们的应用不读。客户端。
  • 长时间静默后来自我们的 RST:我们的超时。客户端。

通过 SSLKEYLOGFILE 解密自己的流量

有时光有标志不够,需要看到服务器在 TLS 内部到底返回了什么:响应码、头部、错误正文。这不需要 mitmproxy 和伪造证书。只要你自己的客户端把会话密钥写到文件里,Wireshark 用它们来解密就行。这只对你自己的客户端和你自己的连接有效:密钥只属于参与握手的那一方。用这种方法无法解密别人的流量或代理在区域 C 与服务器之间的流量,这是理所当然的。

在不同客户端中如何启用

基于 Chromium 和 Firefox 的浏览器会读取环境变量 SSLKEYLOGFILE 并以 NSS Key Log 格式把密钥写进去:

export SSLKEYLOGFILE=/home/user/tls-keys.log
google-chrome --proxy-server=http://203.0.113.10:8080

用 OpenSSL 编译的 curl 也读取这个变量:

export SSLKEYLOGFILE=/home/user/tls-keys.log
curl -v -x http://user:pass@203.0.113.10:8080 https://api.example.com/health

带命令行参数的 Node.js:

node --tls-keylog=/home/user/tls-keys.log app.js

Python 不会自动读这个变量,但从 3.8 版本开始 SSLContext 有 keylog_filename 属性。通过适配器给 requests 用:

import os, ssl, requests
from requests.adapters import HTTPAdapter

class KeyLogAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = ssl.create_default_context()
ctx.keylog_filename = os.environ.get("SSLKEYLOGFILE")
kwargs["ssl_context"] = ctx
return super().init_poolmanager(*args, **kwargs)

s = requests.Session()
s.mount("https://", KeyLogAdapter())
s.proxies = {"https": "http://user:pass@203.0.113.10:8080"}
r = s.get("https://api.example.com/health", timeout=30)
print(r.status_code)

Go 通过 tls.Config 里的 KeyLogWriter 字段:

f, _ := os.OpenFile(os.Getenv("SSLKEYLOGFILE"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
tlsCfg := &tls.Config{KeyLogWriter: f}
tr := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: tlsCfg,
}

在 Wireshark 中接入密钥

两种方式。第一种:Edit > Preferences > Protocols > TLS > (Pre)-Master-Secret log filename,指定密钥文件路径。Wireshark 会解密所有按 Client Random 匹配到的流。第二种方式在把文件交给同事时更可靠:把密钥直接嵌入 pcapng:

editcap --inject-secrets tls,/home/user/tls-keys.log proxy.pcap proxy-with-keys.pcapng

之后在 Wireshark 里 CONNECT 隧道内部会出现解密后的 HTTP/1.1 或 HTTP/2 请求和响应。解密后的 HTTP/2 过滤器:

http2.header.name == ":status"

对隧道内解密后的 HTTP/1.1,普通的 http.response.code 和 http.request.uri 都能用。

这对通过代理诊断有什么用

解密消除了最后的不确定性。你能看到服务器回复了 429 带 Retry-After 头部然后关闭了连接,或者返回了 200 但正文在 40% 处断了,或者请求发出去了但压根没有响应。有了这样的画面,联系代理客服就变得有针对性了:要么问题明显在服务器,要么明显在隧道。

安全规则

  • 密钥文件可以完整解密已记录的会话,包括 cookie 和 token。把它当密码保存,分析完就删除。
  • 不要把密钥文件和抓包一起发给代理客服或任何其他人。客服分析断开不需要密钥,标志和时间戳就够了。
  • 如果确实需要展示解密内容,请用目标服务的测试账户和代理的测试凭据。
  • 不要在生产环境里让 SSLKEYLOGFILE 一直开着。一个不小心进入服务配置的环境变量会年复一年地把密钥写到磁盘上。

典型画面:连接超时、响应中途断开、移动网络丢包

我们把分析过的迹象组合成可识别的场景。每个场景都按它在过滤器 tcp.stream eq N 之后的 Wireshark 包列表里呈现的方式来描述。

画面 1:连接代理超时

0.000 client -> proxy [SYN]
1.002 client -> proxy [SYN] (TCP Retransmission)
3.006 client -> proxy [SYN] (TCP Retransmission)
7.010 client -> proxy [SYN] (TCP Retransmission)
15.02 client -> proxy [SYN] (TCP Retransmission)

没有任何来自代理的包。1、2、4、8 秒的指数间隔——Linux 内核标准的重传退避。诊断:从你的位置代理不可达。检查地址、端口、防火墙、路由,以及代理地址是不是变了。如果这是带专属端口的 Proxeon 移动代理,确认你个人中心里的端口与客户端配置里的一致。

画面 2:代理连接目标服务器超时

0.000 client -> proxy [SYN]
0.041 proxy -> client [SYN, ACK]
0.041 client -> proxy [ACK]
0.042 client -> proxy CONNECT api.example.com:443 HTTP/1.1
0.083 proxy -> client [ACK]
30.10 proxy -> client HTTP/1.1 504 Gateway Timeout
30.10 proxy -> client [FIN, ACK]

到代理的 RTT 是 41 ms,代理确认收到了 CONNECT,然后沉默了 30 秒并返回 504。诊断:目标服务器不响应代理的连接尝试。可能原因——服务器挂了、端口对代理地址池关闭、代理到服务器路由上的网络问题。你的客户端和你的网络与此无关。

画面 3:服务器拒绝 TLS

0.000 client -> proxy CONNECT api.example.com:443 HTTP/1.1
0.045 proxy -> client HTTP/1.1 200 Connection established
0.046 client -> proxy TLSv1.3 Client Hello (SNI=api.example.com)
0.210 proxy -> client [FIN, ACK]
0.210 client -> proxy [FIN, ACK]

ClientHello 之后 164 ms 断开,而到代理的 RTT 是 45 ms。约 120 ms 的差值——这是代理转发 ClientHello 给服务器并收到关闭所需的时间。诊断:服务器接受了 TCP 但在 TLS 阶段关闭了连接。检查握手参数:版本、密码套件、SNI、ALPN。如果没有 ServerHello 也没有告警,说明服务器静默关闭了连接,这通常是防护系统在 ClientHello 不典型时的做法。

画面 4:响应中途断开

0.000 client -> proxy CONNECT cdn.example.com:443 HTTP/1.1
0.044 proxy -> client HTTP/1.1 200 Connection established
0.045 client -> proxy Client Hello
0.190 proxy -> client Server Hello, Change Cipher Spec, Application Data
0.195 client -> proxy Application Data (request)
0.350 proxy -> client Application Data (1460 bytes)
... ещё 340 пакетов данных
4.812 proxy -> client Application Data (1460 bytes)
4.813 proxy -> client [RST, ACK]

握手通过了,约 500 KB 数据到达了,然后是没有减速、没有重传、没有 zero window 的 RST。看绝对时间。如果它与移动代理 IP 轮换的时刻或会话时长限额到期重合,原因在代理,解决办法是让轮换间隔与你的请求时长对齐,或者长时间下载期间使用不轮换模式。如果没有重合,可能是服务器或 CDN 侧的断开。这里通过 SSLKEYLOGFILE 解密会有帮助:如果内部能看到 Content-Length 头部而正文没有完整到达,那就是服务器中断了传输。

画面 5:客户端侧移动网络丢包

0.000 client -> proxy Application Data (1460 bytes)
0.001 client -> proxy Application Data (1460 bytes)
0.002 client -> proxy Application Data (1460 bytes)
0.180 proxy -> client [ACK] (Dup ACK 1)
0.181 proxy -> client [ACK] (Dup ACK 2)
0.182 proxy -> client [ACK] (Dup ACK 3)
0.183 client -> proxy Application Data (TCP Fast Retransmission)
0.420 client -> proxy Application Data (TCP Retransmission)
0.900 client -> proxy Application Data (TCP Retransmission)

来自代理的重复 ACK,之后是我们的重传且间隔递增。既没有 RST 也没有 FIN。诊断:客户端—代理路径上丢包。如果客户端本身通过蜂窝网络或 Wi-Fi 连接,在基站切换时这是预期的。解决办法是增大客户端超时、启用 TCP keepalive、不要保持长时间空闲连接,因为运营商的 NAT 会清除空闲连接的记录,通常在 30-300 秒后,之后的下一个包就石沉大海。评估丢包规模的过滤器:

tcp.analysis.retransmission && ip.dst == 203.0.113.10

通过 Statistics > Capture File Properties 计算这类包占总数的百分比。百分之一二——对移动网络来说还能接受,超过百分之五——就去查无线电条件或设备问题。

画面 6:我们自己的超时

0.000 client -> proxy CONNECT api.example.com:443 HTTP/1.1
0.044 proxy -> client HTTP/1.1 200 Connection established
0.045 client -> proxy Client Hello
0.200 proxy -> client Server Hello ... Application Data
0.205 client -> proxy Application Data (request)
0.250 proxy -> client [ACK]
10.21 client -> proxy [FIN, ACK]
10.25 proxy -> client [FIN, ACK]

请求发出去了,代理确认了,响应没来,正好 10 秒后我们发出了 FIN。这是我们的 read timeout。客户端日志里的错误看起来像断开,但从抓包可以看出:连接是我们自己关闭的,因为服务器思考的时间比我们愿意等的时间长。解决办法要么是增大超时,要么是搞清楚服务器为什么慢,但代理这里只是诚实地陪我们一起等待。

抓包和读包时的常见错误

列出即使有经验的工程师也会经常踩的坑。

  • 在繁忙机器上不带抓包过滤器抓包。 一分钟就攒下一个 G,而在里面找需要的 200 个包很不方便。始终按代理主机过滤。
  • 按目标服务器地址过滤。 通过代理时你机器上没有这样的包。过滤器会返回空,然后人就会以为流量没走。走了,但是发往代理的。
  • 混淆抓包过滤器和显示过滤器。 tcpdump 里的 BPF 语法(host、port、tcp[tcpflags])和 Wireshark 里的语法(ip.addr、tcp.port、tcp.flags.reset)不同。把 Wireshark 过滤器放进 tcpdump 会报语法错误,反之亦然。
  • 看到来自代理的 RST 就立刻怪代理。 来自代理地址的 RST 是隧道的关闭,不是认罪。要看时间戳和 RST 之前发生了什么。
  • 忽视 RTT。 没有测得到代理的延迟,就无法区分代理的即时拒绝和服务器拒绝的翻译。
  • 用 -s 64 抓包然后试图读 CONNECT。 头部会被截断。诊断代理需要完整抓包,或者至少 -s 600。
  • 时间没同步。 如果带抓包的机器时钟慢一分钟,就无法把抓包与客服日志对照起来。启用 NTP 并在求助时指明 UTC。
  • 发送带代理凭据的抓包。 向代理的明文 HTTP 请求中的 Proxy-Authorization 头部包含用户名和密码。要么用测试数据抓包,要么发完之后改密码。
  • 把密钥文件和抓包一起发出去。 这会泄露所有会话内容。客服分析断开不需要密钥。
  • 长时间抓包不轮转。 磁盘会在最不该的时候被塞满,tcpdump 会和需要的包一起崩溃。
  • 不检查 QUIC 泄露。 直达 443 的 UDP——部分流量绕开代理的迹象,按 TCP 隧道诊断什么也看不出来。
  • 忘记分段卸载。 虚拟机上抓包里 60 KB 的包并不意味着 MTU 错误。这是内核把还没切分的段交给了 tcpdump。

工具和资源

实践本文方法的最小工具集。

抓包

  • tcpdump——Linux 和 macOS 上的标准工具。几乎到处都有,需要 root 或 CAP_NET_RAW 权限。
  • dumpcap——Wireshark 自带的命令行抓包器,支持相同的轮转参数(-b filesize、-b files)和 pcapng 格式。
  • 带 Npcap 的 Wireshark——用于 Windows。可以直接从 GUI 抓包,抓包过滤器语法也是同样的 BPF。
  • tshark——带 Wireshark 解析器的命令行分析工具,适合没有图形界面的服务器和自动化。

分析

  • Wireshark——主打工具。Follow TCP Stream、Expert Info、Statistics > Conversations、用于可视化停顿和重传尖峰的 IO Graph。
  • editcap——切割和过滤 pcap,把 TLS 密钥注入 pcapng。
  • mergecap——把轮转文件拼成一个以便分析。
  • capinfos——对文件的快速汇总:时长、包数量、大小。

支持调试的客户端

  • 带 -v 和 --trace-time 参数的 curl——在应用层复现抓包画面,并支持 SSLKEYLOGFILE。
  • openssl s_client -proxy host:port——允许手动执行通过代理的 CONNECT 和 TLS 握手,不用 HTTP 客户端也能看到服务器的响应。

有用的单行命令

拼接轮转文件:

mergecap -w all.pcapng proxy-*.pcap

从抓包里只切出一个流用于发给客服:

tshark -r all.pcapng -Y 'tcp.stream eq 42' -w stream42.pcapng

带 RST 流的快速统计:

tshark -r all.pcapng -q -z conv,tcp | head -40

通过 openssl 手动检查 CONNECT:

openssl s_client -proxy 203.0.113.10:8080 -connect api.example.com:443 -servername api.example.com -brief

案例和结果

案例 1:2% 的断开,结果错在客户端

一个价格采集服务通过一批移动代理运行,每天约 4 万个请求。大约 2.3% 的请求以 RemoteDisconnected 失败。团队确信是代理的问题。按每天 100 MB 轮转抓包,过滤 tcp.flags.reset == 1 看 ip.src。91% 的情况下 RST 是客户端自己发的。分析流后发现:RST 之前是发出请求后整整 5 秒的沉默,然后是客户端的 RST。代码里 read timeout 设的是 5 秒,而目标服务器在高峰时段需要 6-8 秒才响应。把超时增加到 15 秒后错误降到 0.3%。剩下的 0.3% 是真正的来自代理的 FIN,在 ClientHello 之后 160-200 ms 出现:服务器周期性地拒绝来自部分地址池的连接。这些数据交给了客服,他们用自己的区域 C 日志确认了画面。

案例 2:大文件下载正好每 10 分钟断开一次

客户端通过移动代理下载 300-800 MB 的压缩包,抱怨中途断开。抓包显示来自代理的 RST 彼此间隔正好 600 秒、精确到秒,与下载开始时间无关。原因原来是配置了每 10 分钟按计划轮换 IP:换地址时代理会关闭活动隧道。解决办法是切到按需轮换的端口,在下载之间触发地址切换。断开完全消失,诊断花了两个小时而不是几周来回邮件。

案例 3:服务器挂了,却看起来像代理的错

早上所有对某个 API 的请求都开始返回连接错误。客户端日志:Connection aborted。第一反应——代理挂了。一分钟的抓包显示:与代理的 TCP 在 38 ms 内建立,CONNECT 出去了,30 秒后来了 504 和 FIN。同时通过同一代理访问另一个主机的请求 300 ms 就通过了。诊断:目标服务器不接受来自代理的连接。40 分钟后目标服务的状态页确认了他们的故障。抓包省下了一个上午,也避免了一个给代理客服的误报工单。

案例 4:Wi-Fi 丢包看起来像代理问题

开发者在笔记本上通过 Proxeon 移动代理测试集成,遇到偶发超时。抓包显示客户端重传频率 6%、来自代理的重复 ACK,同时没有任何来自代理的 RST。把笔记本用网线连接后重传降到零,超时消失。问题出在办公室过载的接入点。

向客服求助时的抓包清单

如果你打算把抓包发给代理服务客服,下面是能节省双方时间的操作顺序。

准备

  1. 通过 NTP 同步机器时钟。用 timedatectl 或 date -u 检查。
  2. 如果可能,诊断期间申请单独的代理测试凭据,或者计划在发出抓包后更换密码。
  3. 记录客户端版本、HTTP 库、操作系统、上网方式(有线、Wi-Fi、蜂窝网络)。
  4. 记下代理地址和端口、目标主机、预期行为和实际行为。

抓包

  1. 用按代理主机和端口过滤、带轮转、完整包大小的 tcpdump 启动:
sudo tcpdump -i eth0 -nn -s 0 -U -w proxy-%Y%m%d-%H%M%S.pcap -G 300 -W 48 'host 203.0.113.10 and port 8080'
  1. 复现问题。如果很少复现,就让抓包运行足够的时间;48 个 5 分钟的文件覆盖 4 小时。
  2. 同时用 curl -v 和 --trace-time 做一次对照请求,保存输出。它会给出与包精确对应的时间锚点。
  3. 记录客户端日志里每次错误出现的确切时间(UTC)。
  4. 用 Ctrl+C 停止 tcpdump。确认文件不是空的:capinfos proxy-*.pcap。

处理

  1. 拼接文件:mergecap -w all.pcapng proxy-*.pcap。
  2. 用过滤器 tcp.flags.reset == 1 或按日志时间找到问题流。
  3. 只切出需要的流:tshark -r all.pcapng -Y 'tcp.stream in {12 47 93}' -w report.pcapng。客服不需要几个小时的正常流量。
  4. 检查切出的抓包里没有多余的东西:别人的连接、其他服务的流量。
  5. TLS 密钥文件不要附上。

求助文本

  • 问题出现时间,UTC,精确到秒。
  • 代理地址和端口、目标主机。
  • 简要解释:你在抓包里看到什么(例如,CONNECT 得到 200,然后 ClientHello 之后 170 ms 来自代理的 FIN,到代理的 RTT 是 45 ms)。
  • 附件的帧号或流号。
  • 带时间标记的 curl -v 输出。
  • 你已经检查并排除的内容:通过同一代理访问另一个主机、通过另一条连接访问同一主机。

这样的求助客服一次就能处理完。他们只需把你的时间标记与区域 C 的日志对照,确认或推翻你的判断。

常见问题

为什么抓包里没有目标服务器的 IP,我不是在访问它吗?

因为你访问它不直接,而是通过代理。你的客户端建立与代理地址的 TCP 连接,请求它连到目标主机。代理—服务器的连接只存在于代理侧。抓包里目标主机名在 CONNECT 行和 ClientHello 里的 SNI 中可见,但你的机器上不会有也绝不可能有发往它 IP 的包。

能不能从抓包看出代理用哪个外部 IP 访问服务器?

不能。这个信息属于区域 C。唯一的办法是通过代理请求一个返回客户端地址的服务,或者在服务商个人中心看那一刻哪个地址是活跃的。

代理发了 RST。说明问题在代理?

不一定。来自代理地址的 RST 意味着隧道从代理侧关闭,原因可能在目标服务器或代理之后的网络。看时间:如果 RST 在你最后一个包之后、经过明显超过到代理 RTT 的时间才来,多半是代理翻译了服务器的反应。如果 RST 在固定时刻或精确间隔后到达,很可能是代理自己的策略,比如轮换或限额。

如何根据抓包测量到代理的 RTT?

任何连接开始时你的 SYN 与代理的 SYN-ACK 之间的时间差。在 Wireshark 里可以为流打开 Statistics > TCP Stream Graphs > Round Trip Time,或者把 tcp.analysis.ack_rtt 字段用作列。

Wireshark 不显示隧道内的 TLS,只有 TCP 数据。怎么办?

右键点击 200 Connection established 响应之后的包,选 Decode As,在 Current 列里为代理的 TCP 端口选 TLS。同时确认包没有被截断(抓包时 -s 0),并且如果代理端口非标准,HTTP 解析器配置了该端口:Edit > Preferences > Protocols > HTTP > TCP ports。

这个方法与 mitmproxy 有何不同?

mitmproxy 工作在应用层:它用伪造证书终结 TLS,读取甚至可以修改 HTTP 请求。为此客户端必须信任它的根证书。tcpdump 和 Wireshark 工作在数据包层,不替换任何东西:你看到的是真实的包、真实的标志和时间,包括 mitmproxy 会掩盖掉的 TCP 错误,因为它自己会处理这些错误。对诊断断开来说,数据包层更诚实。要看请求内容,mitmproxy 更方便,但如果愿意,自己的流量内容也可以在 Wireshark 里通过 SSLKEYLOGFILE 看到,而不需要替换证书。

能不能在 Wireshark 里解密代理与服务器之间的流量?

不能。你既没有这条连接的包,也没有它的密钥。SSLKEYLOGFILE 只给出你客户端会话的密钥,而在 CONNECT 隧道内部这正是你与服务器的会话,所以可以解密。但在 CONNECT 的情况下,代理和服务器之间并没有单独的 TLS:代理只是转发你的字节。

怎么知道客户端绕过了代理?

按整个接口而不按代理过滤抓包,看发往 80 和 443 端口、地址不是代理的出站连接,以及 UDP 443(QUIC)和发往外部解析器、带目标主机名的 DNS 查询。Wireshark 过滤器:(tcp.port == 443 || udp.port == 443) && !(ip.addr == 203.0.113.10)。任何命中都是配置泄露。

给客服的抓包应该多大?

越小越好。一个切出的问题流占 5 到 500 KB。大于 50 MB 的文件客服分析起来更久,而其中一半会是正常流量。通过 tshark 或 Wireshark 的 File > Export Specified Packets 切出需要的流。

如果虚拟机上 tcpdump 显示比 MTU 大的包怎么办?

这是 generic segmentation offload:内核把大段交给 tcpdump,之后网卡才切分它们。这不影响对标志、时间和断开的分析。如果碍事,抓包期间用 ethtool -K eth0 gso off tso off gro off 命令关闭卸载,但要记住这会降低网络性能。

结语

通过代理时的流量抓包不是什么窥探内容的事,也不是什么魔法。它是对线上实际发生情况的诚实记录:精确到毫秒、精确到每一个标志。客户端日志说连接断了。抓包说这是谁干的、具体什么时候、之前发生了什么。

总结方法。在客户端机器上你只能看到到代理的连接:TCP 握手、CONNECT 或 SOCKS 对话,以及隧道内加密的 TLS 记录。目标服务器不直接可见,但它的行为会被代理通过对 CONNECT 的响应码、时间戳和关闭隧道的方式翻译出来。来自你地址的 RST 或 FIN——是你的问题或你的超时。没有关闭的重传和重复 ACK——到代理路径上的丢包。来自代理的关闭、经过远超到它的 RTT 的时间——翻译过来的服务器反应。在固定时刻关闭——代理策略。来自你的 Zero Window——你的应用不读。

今天就可以做的实际步骤:把本文中带轮转的 tcpdump 命令和 Wireshark 过滤器加入书签;检查跑你客户端的机器时钟是否同步;确认你的客户端没有绕过代理泄露 QUIC;创建用于诊断的测试凭据,以免在抓包里暴露生产凭据。下次当日志说 connection reset by peer 时,别再猜了。抓个包,打开流,看方向和时机。五分钟后你就会知道该找谁:自己、代理客服,还是目标服务器的所有者。