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

180 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 补上了,清理也在当晚完成:
- 内核 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;日志扫描第三轮全绿。*
*最值钱的一句:**客户端说"网络错误"的时候,它其实是在说"我不知道哪里错了"。***