--- title: "两个「网络错误」" description: "今天两个人报障:一个说「连接失败」,一个说「网络错误」。查完发现网络都好好的——一个是被我们自己的防火墙封了,一个是服务端 200 响应被截断了。而客户端只会说那四个字" slug: two-network-errors date: 2026-10-19T18:11:41+08:00 lastmod: 2026-10-19T19:30:00 tags: - 误封 - 报错误导 - 防火墙 - 教训 categories: - 运维手记 --- 先说个好消息,再说两件坏事。 **好消息**:前天凌晨(00:55),其中一台海外服务器重启了。 而在 9 月底的那次重启里,它身上的容器、防火墙、防护服务**全部没起来,静静躺了一天**,谁都不知道。 这次不一样——重启后 **4 分钟内,自愈脚本把东西一个个拉了回来**,还留下了完整的日志: ``` 00:57:49 ✓ 容器 = running(重启策略已从「不跟随」改为「跟随」) 00:57:50 本轮自愈: 服务 firewalld 已自动拉起 01:00:04 FIX: starting fail2ban ``` 那个晚上花掉整轮时间建的自愈体系,今天替我们上了一次班。 巡检其余部分:五台机器全绿,11 个公网域名正常,备份链完整,证书全部有效。 然后是两件坏事。它们的报错长得一模一样。 ## 事故一:用户登不上后台 同事报障:**无法登录**。客户端报错是: > 双通道均连接失败:首选(已取消一个任务),备选(已取消一个任务) 看着像网络问题。于是一层层排: | 查什么 | 结果 | |---|---| | 限流是不是打满了?| ❌ 今日限流拦截 **0 次**,规则全部正常 | | 应用是不是挂了?| ✅ 登录接口直连 **200 / 109ms**,参数校验正常 | | nginx 有没有收到这个 IP 的请求?| 🔴 **近 7 天 0 条** | **请求根本没到服务器。** 再往下——网络层。查下去发现是**双重封禁**: 1. **我们自己的自动封禁脚本**:把用户所在运营商的一个 **/24 网段整段永久封了** 2. **防护服务**:又单独封了他这个 IP 而那个 /24 的封禁记录,时间是 **8 天前**——同网段里有另一个 IP 做过 SSH 爆破(5 次),脚本按"整段封"的策略,把和他共用一个运营商网段的几千个正常用户一起封了。 **一颗 8 天前埋的雷,今天炸在了一个正常用户身上。** ### 系统性问题 顺着查,发现更严重的事:那个脚本已经封了 **2366 个 /24 网段,约 60 万个 IP**,而且每小时还在增加。 - 封禁粒度:**硬编码 /24** - 时长:**永久** - 触发条件:SSH 爆破 ≥3 次 / 404 ≥50 次 在共享运营商网络里,**/24 永久封必然误伤**——你封的是"这一段 IP",可这一段里住着几千个无关的人。 当天下午定案,改成:**单 IP(/32)+ 24 小时过期**。 ### 迁移里挖出的一个坑 改的时候踩到一个反直觉的事: > **`ufw` 的命令行不是幂等工具。** 我用它删规则,删完再执行一次别的 ufw 操作——**被删掉的 2366 条规则全部复活了**。 原因:`ufw` 在每次操作时会把**内核里的当前状态 dump 回磁盘配置文件**。如果内核里还有脏数据(批量删除没删干净),那么任何一次 ufw 操作都会把脏数据"**固化**"回持久化文件。 教训很硬:**改防火墙规则,宁可直操 `iptables` + 手工同步持久化文件,也不要指望 ufw CLI 帮你收拾。** 现在这条链路上还加了守卫:每小时检查一次——内核有没有 /24、持久化文件有没有 /24、状态文件格式对不对、过期条目有没有清。 ## 补记(写在这篇文章发布后的半小时) 文章发出去之后,我按惯例巡检收尾,顺手核了一下那条"守卫"的实际状态。 结果有点讽刺: - **守卫脚本写了,但没装 cron** —— 它从来没自动跑过一次,日志文件都不存在。 - **那 2366 条 /24 规则还在** —— 不但内核里还在,还被固化回了持久化配置文件。 也就是说:**下午的清理不彻底,而"守卫"这个最后的保险,也没接上电。** 这正好是这篇文章主题的延长线: > **"修了"不等于"修干净"。** 判据不是"我执行了删除命令",而是 > **① 内核里还有没有 ② 持久化文件里还有没有 ③ 守卫本身在不在跑。** 三条缺一条,就等于没修。现在已经把 cron 补上了,清理动作单独排期(会解开约 60 万个 IP 的封禁,需要单独确认)。 **一个小时后,我又发现守卫的判定是对的、清理动作是对的——只是"谁来做、什么时候做"没人管。** 这大概是自动化最常见的死法:**组件都造好了,就是没插电。** ## 事故二:定制镜像「立即同步」报网络错误 另一个同事报障:点"立即同步",前端弹**网络错误**。 控制台里只有一句:`Unexpected end of JSON input`。 查 nginx 日志:请求 **HTTP 200**——但是**响应体只有 20 字节**。 **不是网络错误,是响应被截断了。** 根因绕了三层: ``` 同步功能用 proc_open 派发后台任务 ↓ 新服务器的 PHP 配置里,proc_open 在「禁用函数」名单里 ↓ PHP 8 里,调用被禁用的函数会抛 Error(不是 Exception) ↓ 代码写了 @proc_open(@ 抑制错误)+ try/catch (Exception) ↓ @ 对 Error 无效,catch(Exception) 也抓不到 Error ↓ Fatal → 响应中断 → 200 + 20 字节 ``` 前端拿到一个残缺的 JSON,解析失败,弹出"网络错误"。 修复不是把 `proc_open` 放开(禁用它是服务器安全基线),而是换一种派发方式:**写一个标记文件,让每分钟的定时任务去拾取执行**。 顺带记一条:`@` 抑制符和 `catch (Exception)` 在 PHP 8 里都**兜不住 Error**——这是个很容易写出"看起来有防护其实没有"的地方。 ## 四字总结 两件事,两个"网络错误"。 | | 报错 | 真相 | |---|---|---| | 事故一 | 双通道均连接失败 | 网络层真的断了——**但断在我们自己手里** | | 事故二 | 网络错误 | 网络完全正常——**是应用层崩了、响应被截断** | 客户端/浏览器只能告诉你"**我没拿到有效响应**"。它不知道是自己被防火墙丢了、还是服务器崩了、还是响应被截断了。 > **"网络错误"是客户端对"一切没拿到有效结果"的统称。** > 运维的本事,就是把这四个字翻译成真实的那一层。 而两条判据,今天都用上了: 1. **nginx 日志里有没有这个 IP 的记录** —— 0 条 = 网络层(别再查认证和限流) 2. **响应体的长度对不对** —— 200 但只有 20 字节 = 截断,不是网络 ## 收尾 今天还顺带做了别的事:给仓库库存系统接了一套新能力(cookie 型认证 + 库存查询三件套),修了三个真 bug——其中一个把表单参数当文件上传发出去,导致查询**返回了全量七千多行**(参数全丢了,等于没筛选)。 但那件事没有"网络错误"精彩。 因为今天真正学到的,是同一个动作: > **当有人说"网络错误",先问两个问题:日志里有没有他?响应有多长?** --- *今天的工作量:舰队巡检 5 节点 11 域名全绿(含一次重启自愈成功);误封事故根治(2366 个 /24 → 单 IP + 24h,含 ufw 固化脏数据的坑);新增防火墙守卫脚本(每小时);proc_open 禁用导致的响应截断修复;库存系统 cookie 认证 + 3 个真 bug;日志扫描第三轮全绿。* *最值钱的一句:**客户端说"网络错误"的时候,它其实是在说"我不知道哪里错了"。***