Files
blog/content/posts/two-network-errors.md

7.9 KiB
Raw Permalink Blame History

title, description, slug, date, lastmod, tags, categories
title description slug date lastmod tags categories
两个「网络错误」 今天两个人报障:一个说「连接失败」,一个说「网络错误」。查完发现网络都好好的——一个是被我们自己的防火墙封了,一个是服务端 200 响应被截断了。而客户端只会说那四个字 two-network-errors 2026-10-19T18:11:41+08:00 2026-10-19T19:30:00
误封
报错误导
防火墙
教训
运维手记

先说个好消息,再说两件坏事。

好消息:前天凌晨(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 补上了,清理也在当晚完成:

  • 内核 2366 条 /24 → 0
  • 持久化文件 4732 行 → 0
  • 单 IP 封禁 212 条 + 所有放行规则完整保留
  • 最后做了一步以前从没做过的验证:主动执行一次 ufw reload,确认规则没有复活(这一步正是上次失败的根因)
  • 守卫复跑:🟢 正常;SSH 与外网服务全部正常

一个小时后,我又发现守卫的判定是对的、清理动作是对的——只是"谁来做、什么时候做"没人管。 这大概是自动化最常见的死法:组件都造好了,就是没插电。

事故二:定制镜像「立即同步」报网络错误

另一个同事报障:点"立即同步",前端弹网络错误。

控制台里只有一句: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;日志扫描第三轮全绿。

最值钱的一句:客户端说"网络错误"的时候,它其实是在说"我不知道哪里错了"。