iptables 黑名单迁移到 ipset:CentOS 6 实战

背景

博客服务器(阿里云 ECS,CentOS 6.3)运行着一个旧的防攻击脚本,通过分析 Apache access_log 识别恶意请求,逐条 iptables -A 封禁 IP。随着黑名单增长,iptables 逐条匹配的性能问题日益突出。

听说 iptables 黑名单到 1 万条性能就明显不行,而 ipset 支撑到 10 万条都没问题。决定迁移到 ipset 方案。

iptables vs ipset

iptables ipset
匹配方式 逐条遍历规则 哈希查找 O(1)
1万条IP 10000条规则 1条规则 + 1个集合
性能 规则多时线性下降 几乎无影响
动态更新 操作整个链 直接操作集合
存储 内核链表 内核哈希表

一句话:ipset 是 iptables 的 IP 集合加速器,规则多时用 ipset。

环境

  • 系统:CentOS 6.3 (Final)
  • 内核:2.6.32
  • iptables:v1.4.7
  • ipset:v6.11(已预装)
  • Apache:httpd,access_log 在 /var/log/httpd/access_log
  • cron:每天凌晨 3 点自动重启

踩坑记录(6个坑,全部填平)

坑一:iptables –matchset 语法不兼容

CentOS 6 的 iptables v1.4.7 不支持 --matchset,需要用旧语法 --set

# CentOS 7+ (新语法)
iptables -A INPUT -m set --matchset blacklist src -j DROP

# CentOS 6 (旧语法)
iptables -A INPUT -m set --set blacklist src -j DROP

坑二:ipset save 不支持 -f 参数

ipset v6.11 不支持 -f 参数指定输出文件,需要手动重定向:

# 错误(v6.11 不支持)
ipset save blacklist -f /etc/sysconfig/ipset

# 正确
ipset save blacklist > /etc/sysconfig/ipset

坑三:ipset save 默认输出到屏幕

ipset v6.11 的 save 命令默认输出到 stdout,不会自动写文件。脚本中必须用重定向 > 写入文件,否则重启后集合丢失。

坑四:旧脚本未做 iptables 持久化

原来的防攻击脚本每次 iptables -A 后没有执行 iptables-save,服务器每天凌晨 3 点自动重启,所有封禁规则全部丢失。迁移到 ipset 后修复了这个问题。

坑五:两个脚本同时跑

部署新脚本时忘了删除旧脚本的 cron 任务,导致两个脚本同时运行。旧脚本用 iptables,新脚本用 ipset,互相干扰。清理后正常。

坑六:cron 环境无 PATH 导致持久化文件被清空

这是最隐蔽的一个坑。cron 运行环境极简,PATH 变量不包含 /usr/sbin/sbin,导致 ipsetiptables-save 命令找不到。而 shell 重定向 > 会先清空目标文件再执行命令,命令找不到输出为空,文件就被清空了。

表现:手动运行脚本一切正常(交互 shell 有完整 PATH),但 cron 每次执行后 /etc/sysconfig/ipset/etc/sysconfig/iptables 都变成 0 字节。

修复:在脚本开头显式设置 PATH:

#!/bin/bash
PATH=/usr/sbin:/sbin:/usr/bin:/bin:/usr/local/sbin
export PATH

坑七:iptables -C 不支持 -m set 导致规则重复添加

脚本用 iptables -C 检测规则是否已存在(幂等添加),但 CentOS 6 的 iptables v1.4.7 的 -C 命令不支持 -m set --set 参数,报错 option -C requires an argument,检测总是失败,每轮 cron 都会重复添加一条规则。

表现:iptables INPUT 链中出现 5 条相同的 blacklist DROP 规则。

修复:改用 iptables -S + grep 检测:

# 旧方式(CentOS 6 不支持)
if ! iptables -C INPUT -m set --set "$SET_NAME" src -j DROP 2>/dev/null; then

# 新方式(兼容 CentOS 6)
if ! iptables -S INPUT 2>/dev/null | grep -q "match-set $SET_NAME"; then

最终方案

防攻击脚本(block-bots-ipset.sh)

核心逻辑:

  1. 脚本开头设置 PATH(解决 cron 环境问题)
  2. 首次运行创建 ipset 集合(hash:ip, maxelem 100000)+ iptables 引用规则
  3. 增量读取 access_log(基于文件偏移量,只读新增部分)
  4. 用 awk 匹配 11 类攻击特征
  5. 新攻击 IP 通过 ipset add 加入集合
  6. 有新增则 ipset save > /etc/sysconfig/ipset + iptables-save > /etc/sysconfig/iptables 持久化
  7. iptables 引用规则用 iptables -S | grep 幂等检测,避免重复添加

11 条攻击检测规则

规则 检测内容 特征示例
1 5xx 服务器错误 注入触发崩溃
2 SQL 注入 UNION, SELECT, SLEEP(), DROP, INSERT
3 XSS 攻击 %3Cscript, javascript:, onerror=
4 路径穿越 ../, %2e%2e, etc/passwd
5 空 UA 扫描器 User-Agent 为 “-“
6 敏感文件扫描 .env, .git, .sql, .bak, wp-config, xmlrpc.php
7 HEAD 探测 HEAD 方法扫描
8 恶意爬虫 MJ12bot, YisouSpider, DotBot
9 攻击工具 sqlmap, nikto, nmap, acunetix, nessus, wpscan
10 RFI/LFI php://input, php://filter, data://
11 命令注入 ;wget, ;curl, eval(, system(, exec(

持久化方案

组件 持久化方式
ipset 集合 ipset save > /etc/sysconfig/ipset + rc.local 开机 ipset restore
iptables 规则 iptables-save > /etc/sysconfig/iptables + iptables 服务开机自启
脚本定时执行 cron 每 5 分钟一次
封禁日志 /var/log/block-bots.log,超过 10MB 自动轮转

迁移过程

  1. 检查环境:确认 ipset v6.11 已安装,CentOS 6.3
  2. 发现 iptables 为空:之前封的 IP 因重启全丢,旧脚本未持久化
  3. 部署新脚本:上传 block-bots-ipset.sh,首次运行创建集合 + iptables 引用
  4. 修复语法兼容:–matchset -> –set,-f -> 重定向
  5. 导入历史黑名单:从 /var/log/block-bots.log 提取 855 个 IP 导入 ipset
  6. 配置 cron:每 5 分钟执行,删除旧脚本 cron
  7. 配置持久化:ipset save + iptables-save + rc.local
  8. 修复 cron PATH 问题:脚本开头加 PATH 和 export
  9. 修复规则重复添加:iptables -C 改为 iptables -S + grep
  10. 全面检查:12 项检查脚本全部通过

最终效果

  • 855 个攻击 IP 已封禁
  • iptables 只需 1 条规则匹配整个集合
  • 每 5 分钟自动检测新攻击并封禁
  • 每天凌晨重启后自动恢复所有规则
  • 封禁在 iptables 层生效,攻击 IP 进不了 Apache,不消耗服务器资源

经验总结

  1. CentOS 6 语法差异多:iptables 用 --set 不用 --matchset,ipset save 不支持 -f,都要手动重定向
  2. 持久化要做全套:ipset save + iptables-save + rc.local + iptables 服务开机自启,缺一不可
  3. 部署新脚本要清理旧任务:两个脚本同时跑会互相干扰
  4. ipset save 的输出要重定向:v6.11 默认输出到屏幕,不写文件,重启后集合丢失
  5. cron 环境极简,必须手动设 PATH:否则 > 重定向先清空文件,命令又找不到,文件就变空了。这个坑最隐蔽,手动测试一切正常,只有 cron 执行时才出问题
  6. iptables -C 不支持 -m set:CentOS 6 的幂等检测要改用 iptables -S | grep,否则每轮 cron 都重复添加规则
  7. 检查脚本很重要:写了 12 项检查脚本,一次性发现所有遗漏

附录:检查脚本

写了 12 项检查脚本(check-ipset-status.sh),覆盖:

  1. ipset 版本
  2. 集合状态与 IP 总数
  3. iptables 引用规则
  4. iptables 持久化文件
  5. iptables 服务开机自启
  6. ipset 持久化文件
  7. rc.local 开机恢复
  8. cron 定时任务
  9. 脚本文件与语法检查
  10. 封禁日志状态
  11. 实时拦截统计
  12. 抽样验证已封禁 IP

OpenClaw v2026.7.1-beta.2 升级与补丁维护实录

背景

线上运行的 OpenClaw v2026.6.11 遇到两个严重问题:

  • #99241 tool-result-placeholder:上下文累积到 65K tokens 后,工具输出被替换为 (see attached image) 占位符,模型无法读取工具结果。触发阈值从最初的 180K 一路恶化到 65K,严重影响使用。
  • #98416 reply-session-conflict:WebChat 连续发消息报 reply session initialization conflicted,会话不可用。

升级到 v2026.7.1-beta.2 后,#98416 官方已修复,#99241 靠 PR #98955(尾部 tool-result 保护)缓解。本文记录升级过程和补丁维护经验。

升级过程

  1. 备份:dist 目录 + openclaw.json 配置文件
  2. 升级npm install -g openclaw@2026.7.1-beta.2
  3. 重打补丁:6 个本地补丁重新打入(reply-session-source-fix 官方已修复,移除)
  4. patch-check:7/7 通过
  5. Gateway 重启,功能验证通过

备份位置:/usr/lib/node_modules/openclaw.bak-2026.6.11 + openclaw.json.bak-2026.6.11

补丁维护核心发现

1. attachment-normalize:遗漏的第二处栈溢出

6月25日打补丁时只修了第89行 data: URL 提取正则((.*)$ 对大字符串触发 V8 正则引擎栈溢出),改为 indexOf + slice

遗漏了第59行 isValidBase64 函数中的另一个正则/^[A-Za-z0-9+/]+={0,2}$/.test(value),同样使用 + 量词对大字符串匹配,同样栈溢出。

之前测试的文件都是 6MB 左右,刚好没触发 V8 正则引擎的栈深度上限。7月10日上传 14MB 文件时暴露,改为逐字符 charCodeAt 遍历修复。

教训:补丁记录必须完整。一个函数里有两处正则栈溢出风险,只修一处是不够的。每次修复后要排查同文件中是否有同类问题。

2. reply-session-conflict:官方三层修复已到位

v2026.7.1-beta.2 中官方已完成三层修复:

  • 源头修复(PR #98835):收窄 guard 对比范围到 sessionId/sessionFile 身份字段,允许并发 metadata drift
  • 重入保护isActiveStoreWriter/runActiveStoreWriter 正确处理 reentrant: true
  • 重试退避retryDelays = [1000, 3000, 5000, 8000]maxRetries = 4

不再需要本地补丁。patches.json 中保留检测规则作为升级后的验证检查。

3. workspace-file-viewer:官方已修复

v2026.7.1-beta.2 的官方代码已包含 statWorkspacePath 非 touched 文件处理逻辑,前端 sessionKind 限制已移除。不需要本地补丁。

升级后测试时发现”补丁打了但没用”,实际是浏览器 JS 缓存问题,硬刷新后正常。前端代码变更后必须 Ctrl+Shift+R 硬刷新。

4. control-ui-allowed-folders:官方未修复,仍需本地补丁

问题 #10210240 分两层:

  • 前端:正则 /^(\/Users\/[^/]+|\/home\/[^/]+)(?:\/|$)/ 只匹配 macOS 和 Linux 非 root 用户,不匹配 /root。本地补丁加 |\/root
  • 服务端getAgentScopedMediaLocalRoots 只返回当前 agent 的 workspace,不包含其他 agent。本地补丁遍历 cfg.agents.list 追加所有 agent workspace。

升级后验证:发送 docx 文件到 WebChat,点击预览正常,不再提示 “Outside allowed folders”。

openclaw-patch-check 技能清理

升级过程中发现技能存在两个问题:

  1. 两套目录~/.openclaw/skills/openclaw-patch-check/(实际运行)和 ~/.openclaw/workspace/skills/openclaw-patch-check/(多余副本)。删除了 workspace 下的副本。
  2. 补丁记录过时:patches.json 从 7 个精简到 4 个,移除 3 个官方已修复的条目,更新检测规则。

当前 patches.json(4 个)

ID Issue 类型
attachment-normalize #90098 两处正则栈溢出 本地补丁
reply-session-conflict #98416 reply session conflicted 验证官方修复
control-ui-allowed-folders-frontend #10210240 前端正则缺 /root 本地补丁
control-ui-allowed-folders-backend #10210240 服务端缺非默认 agent workspace 本地补丁

架构限制:大文件上传

WebChat 通过 WebSocket 上传文件,MAX_PAYLOAD_BYTES = 25 * 1024 * 1024 硬编码限制。14MB 文件 base64 编码后约 19MB,可以上传。但超过 25MB 的文件 base64 后超过限制,无法上传。

已提交 Feature Request #103560,建议增加 HTTP multipart upload endpoint,不受 WS payload 限制。

补丁状态总览

补丁 状态 说明
attachment-normalize 本地补丁 两处正则栈溢出,PR #92223 未合并
reply-session-conflict 官方已修复 v2026.7.1-beta.2 三层修复到位
workspace-file-viewer 官方已修复 代码已含 statWorkspacePath 逻辑
control-ui-allowed-folders 本地补丁 官方未修复 #10210240
tool-result-placeholder 已回滚 PR #99756 打后更差,等官方根治

经验总结

  1. 补丁记录要完整:attachment-normalize 有两处正则栈溢出,只记一处导致遗漏。每处修改都要记录原始代码、修复代码、行号、原因。
  2. 检测规则要通用:从匹配特定文件名 hash 改为 glob get-reply-*.js,从匹配特定数组值改为检测关键字 retryDelays + maxRetries
  3. 区分”补丁生效”和”官方已修复”:patches.json 中保留 reply-session-conflict 条目,作用从”检查本地补丁”变为”验证官方修复”。
  4. 前端变更后硬刷新:workspace-file-viewer 补丁打了但浏览器缓存旧 JS,硬刷新后正常。
  5. 维护单一数据源:发现两套技能目录后删除多余副本,只维护 ~/.openclaw/skills/ 下的版本。

遗留问题

  • #99241:beta.2 缓解但未根治,长上下文下仍可能触发 tool-result-placeholder
  • #103560:Feature Request,HTTP 文件上传通道
  • #90098:PR #92223 未合并,attachment-normalize 仍需本地补丁
  • #10210240:control-ui-allowed-folders 官方未修复