端口正常、进程也在,为什么 sing-box / REALITY 还是全设备 EOF

端口正常、进程也在,为什么 sing-box / REALITY 还是全设备 EOF
Go TLS 服务 EOF 排查固定 8192 字节缓冲区与超长握手记录前言最近在一个受控测试环境里遇到了一次很有迷惑性的 TLS 故障。服务已经稳定运行了很长时间期间没有改配置也没有升级程序。某天开始多个测试客户端几乎同时无法建立连接客户端统一表现为 EOF服务端则记录 TLS handshake invalid。奇怪的是服务器网络正常进程没有退出端口也还在监听。配置语法检查能通过客户端和服务端参数也完全一致。排查到最后才发现问题不在端口、证书有效期或配置文件大小而是某个 Go TLS 派生实现使用了固定 8192 字节缓冲区。上游测试端点返回的一条 TLS Certificate 记录长度变成了 8273 字节超过实现限制握手因此被中止。本文只记录受控环境中的 TLS 兼容性分析和通用排障方法不涉及面向公众的网络服务部署与使用。问题现象故障出现后最直观的现象有三个多个测试客户端同时失败客户端错误以 EOF 为主服务端进程和监听端口看起来都正常。客户端错误可以简化为TLS connection failed: EOF服务端日志则类似inbound connection from client-address TLS handshake: processed invalid connection这类问题很容易让人先怀疑网络或认证参数。毕竟 EOF 只说明连接被对端提前关闭本身并不会告诉你握手失败在哪一步。从基础项开始排除1. 系统时间和网络TLS 故障首先要排除系统时间错误。时间偏差可能造成证书校验、握手时间窗口等一系列异常。date-Istimedatectluptimeip-briefaddressiproutecat/etc/resolv.conf再验证 DNS 和服务器出站网络getent ahostsv4 example.comcurl-4-I--max-time10https://example.com本次检查中系统时间处于同步状态DNS、默认路由和 HTTPS 出站都正常。2. 进程、服务状态和监听端口systemctl statusservice-name--no-pager-lps-ef|grepprocess-namess-lntp检查结果显示服务进程仍在运行进程没有在故障时间附近重启目标 TCP 端口正常监听没有其他程序占用该端口。这里需要注意systemctl status显示 running只能证明进程还活着不能证明应用层协议一定工作正常。3. 配置语法和文件变更时间如果程序提供配置校验命令应先执行校验再考虑重启service-binarycheck-c/path/to/config.json同时查看配置与二进制的修改时间stat/path/to/config.jsonstat/path/to/service-binary sha256sum /path/to/config.json本次配置语法检查通过配置文件和二进制都没有在故障时间附近被修改。更值得注意的是配置文件本身只有约 790 字节远小于 8192 字节。这条信息后来帮助排除了“配置文件总体积超限”的误判。4. 防火墙和系统资源iptables-L-n-v--line-numbers iptables-tnat-L-n-v--line-numbers nft list rulesetdf-hTfree-hss-s主机过滤策略没有阻止目标连接磁盘、内存和文件描述符也没有耗尽。由于应用日志中已经出现了客户端来源地址可以确认 TCP 连接实际到达了服务进程。排障重点应该从“端口通不通”转移到“应用收到连接后做了什么”。日志时间线比单条错误更有用只看最后一行processed invalid connection很容易直接判断为认证失败。我把日志向前展开后发现故障发生前存在正常的应用层连接记录而故障后只剩入站连接与 TLS handshake invalid。这说明基础 TCP 链路仍然存在故障集中在 TLS 握手阶段问题不是整个服务器网络突然中断。可以按时间查看服务日志journalctl-uservice-name\--sinceYYYY-MM-DD HH:MM:SS\--no-pager-oshort-iso也可以倒序寻找最后一次成功记录journalctl-uservice-name\--reverse--no-pager-oshort-iso用回环测试切开公网因素排查网络服务时回环测试非常好用。我在服务器上启动了一个最小测试客户端让它直接连接同机回环地址上的正式服务。这样可以绕开云安全组公网路由本地运营商中间网络设备。结果仍然是 EOF。随后又启动了一个全新的临时服务实例监听另一个回环端口复用相同的 TLS 参数。新实例同样失败。这两个结果排除了公网路径和旧进程运行状态问题范围被压缩到TLS 握手实现测试上游返回的握手内容相关依赖的边界条件。TRACE 日志给出的关键证据正式服务不适合长期打开 TRACE我只在临时实例中提高日志级别。下面是经过裁剪和脱敏后的关键输出client version: valid client time: current client identity check: passed connection accepted by authentication stage: true TLS record type: Certificate calculated record length: 8273 handshake complete: false TLS handshake: processed invalid connection这几行日志说明客户端的基础认证信息已经通过失败发生在后续解析上游 TLS 响应时。其中最值得关注的是calculated record length: 8273定位到固定 8192 字节边界当时使用的 TLS 派生库版本为MetaCubeX/utls v1.8.3。检查对应版本源码后可以把相关逻辑简化为下面的等价伪代码consthandshakeBufferSize8192buffer:make([]byte,handshakeBufferSize)ifrecordLengthhandshakeBufferSize{abortHandshake()}而 TRACE 中计算出的 Certificate 记录长度是8273边界关系非常直接8273 8192因此服务虽然成功接收了 TCP 连接也通过了前面的认证阶段但在解析上游 TLS Certificate 记录时触发长度判断握手被提前终止客户端最终只看到 EOF。这个地方挺坑的因为服务端最终日志比较笼统。如果没有 TRACE 和源码对照很容易一直在网络、证书有效期或认证参数上打转。为什么服务长期稳定后会突然出现本地程序和配置没有变化不代表远端 TLS 握手内容永远不变。可能影响 TLS Certificate 记录大小的因素包括上游证书续期证书链调整CDN 节点调度变化证书类型或中间证书变化服务端 TLS 配置调整。本次能够确定的是故障时上游返回的相关记录长度为 8273超过了当前实现的 8192 限制。至于上游具体进行了哪项调整仅凭本地日志无法严谨下结论所以这里不继续猜测。修复策略方案一升级到已经修复边界问题的依赖版本这是最理想的方向但升级前需要确认新版本是否确实修改了该限制上层程序是否已经引入对应版本配置格式是否兼容是否存在其他行为变化。不能只看到“有新版本”就认为问题必然修复。排查时我发现后续一个稳定版本仍然保留 8192 字节限制因此没有直接把升级当作修复。方案二更换受控测试中的上游 TLS 端点本次采用的是这个办法。先在临时实例中换用另一个合规测试端点分别完成同机回环测试最小客户端测试完整 TLS 握手验证应用层请求验证。确认新端点的握手记录没有触发限制后再更新正式测试环境。这里的重点不是“随便换一个地址”而是先做临时验证确认TLS 版本满足应用要求主机名校验正确握手记录大小兼容应用层交互符合预期。方案三自行修改缓冲区大小理论上可以调整源码中的固定长度但不建议只改一个常量就直接用于正式环境。固定缓冲区可能与以下逻辑共同存在长度字段类型内存分配策略后续切片和复制操作最大记录长度判断异常输入防护。如果确实需要修改应补充单元测试和边界测试至少覆盖8191 8192 8193 8273 16384 超过协议允许范围的异常值并确认内存占用和异常输入处理没有退化。正式修改前的操作顺序无论最终选择升级还是调整上游端点都建议遵循下面的顺序。1. 备份cp-a\/path/to/config.json\/path/to/config.json.bak-$(date%Y%m%d-%H%M%S)2. 修改后先校验service-binarycheck-c/path/to/config.json3. 重启服务systemctl restartservice-namesystemctl statusservice-name--no-pager-l4. 确认端口ss-lntp5. 实时观察日志journalctl-fuservice-name6. 运行应用自己的集成测试不要只看进程是否 running。至少完成TLS 握手测试一次真实应用请求服务端成功日志检查客户端错误检查重复多次连接排除偶发成功。验证结果调整受控测试的上游 TLS 端点后配置检查通过服务状态正常端口正常监听回环测试成功完整 TLS 握手完成应用层请求恢复服务日志不再出现此前的握手中止。整个验证链与故障时使用的是同一套方法避免了“改完看起来没报错就认为已经修好”的情况。这次排查留下的经验1. 多个客户端同时失败优先检查共同链路如果多个客户端同时出现相同错误单个客户端故障的概率会明显下降。服务端、统一配置、共享依赖和上游变化更值得优先检查。2. 端口监听不代表协议正常ss看到 LISTEN、systemctl看到 active只能说明进程和 TCP 监听存在。TLS 是否成功、应用是否能够处理请求需要通过日志和真实请求验证。3. EOF 是结果不是原因EOF 只表示连接被提前关闭。真正原因可能出现在TCP 建连后TLS ClientHello 解析认证阶段证书记录解析Finished 阶段应用层读取。必须结合服务端日志确定具体阶段。4. 回环测试很适合排除网络干扰当问题可以在127.0.0.1上复现时公网、防火墙和中间链路就不再是主要方向。这一步通常能节省大量时间。5. 固定大小缓冲区值得重点关注处理外部输入时固定缓冲区是一类典型边界风险。即使输入过去一直小于限制也可能因为远端内容变化在某一天突然触发。日志中如果出现异常记录长度、截断、unexpected EOF 等信息可以顺着长度计算和缓冲区分配继续检查。总结这次故障的表面现象是服务运行正常 端口正常 客户端 EOF真正的失败链路则是TCP 连接成功 - 前置校验通过 - 收到上游 TLS Certificate 记录 - 记录长度 8273 - 超过固定缓冲区 8192 - 握手中止 - 客户端看到 EOF最有价值的不是把 8192 改成多少而是定位过程用日志确认连接已经到达应用用回环测试排除公网用新实例排除旧进程状态用 TRACE 确认失败阶段用源码解释 8273 与 8192 的关系用临时环境验证修复再修改正式配置。遇到“什么都正常但就是 EOF”的问题时不妨把日志级别提高一档看看握手到底停在哪一步。很多时候答案藏在一条长度判断里。参考资料RFC 8446The Transport Layer Security (TLS) Protocol Version 1.3https://www.rfc-editor.org/rfc/rfc8446Gocrypto/tls文档https://pkg.go.dev/crypto/tlsMetaCubeX/utls 源码仓库https://github.com/MetaCubeX/utlsMetaCubeX/utls v1.8.3https://github.com/MetaCubeX/utls/tree/v1.8.3