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

作者: Jack.shang

jack.shang 程序员->项目经理->技术总监->项目总监->部门总监->事业部总经理->子公司总经理->集团产品运营支持