2014 年 8 月 19 日,mirrors 出现了连接数超过 connlimit 限制的问题。我们还以为是有人利用了 conntrack 的漏洞。22 日,查清问题原因是 fail2ban 干的,相当于一种防御措施干扰了另外一种防御措施。

Fail2ban 往 iptables filter 表插入规则的时候,会重置同样在 filter 表里的 connlimit 计数器。更深层的原因是,netfilter 用户态和内核态间的写接口只有两种操作,一种是替换整个表,一种是只替换表里的 counter。所有 iptables 操作,不管是插入、删除、替换,最后用户态的实现都是首先从内核态取出这张表(filter)里的所有规则及 counter(相当于 iptables-save),修改之后发给内核(相当于 iptables-restore),内核把这张表里的所有规则删掉,再逐一初始化新的规则。我做梦也想不到 netfilter 会使用如此低效的接口,不过实现上这确实是最简单的了。

在 netfilter 的模型下,现在的 connlimit 事实上是无法在 iptables 修改规则时保持状态的。首先,connlimit 作为一个 match 模块,既不知道其他的 match 条件(如 –tcp-flags syn,–dport 873),又不知道 action(-j),因此不可能实现一个全局的存储,否则不同的 connlimit 规则之间就可能存在冲突。其次,由于 netfilter 在修改表时会依次删除并重新插入所有规则,connlimit 在面临删除的时候,根本不知道是被“牵连”进去的,还是用户确实要删除这条规则。

今天 Zhang Cheng 提出用 iptables recent 规则也可以实现 fail2ban 的效果,修改 recent 规则的 IP 列表是通过 /proc/net/xt_recent/fail2ban-ssh,不需要动 iptables 规则。也就是只要我们不去修改 iptables 规则或者重启 fail2ban 之类服务,connlimit 就不会受到影响。我测试了 recent 规则,发现它对拒绝服务攻击不敏感(需要记忆状态的防火墙都要小心拒绝服务攻击)。

事实上 fail2ban 里本来就可以使用 iptables recent 来做封禁,只是默认使用的是 iptables-multiport 方式。只需要把 jail.conf 里的 banaction 修改成 iptables-xt_recent-echo

还发现发行版自带的 fail2ban 脚本里一处小问题,actionstop 没有删除 iptables 规则,这样 fail2ban 每重启一次,iptables 里就会多一条冗余的规则。强迫症不能忍啊,就自己改了改。正准备去发 issue 呢,却发现 上游在一个月前刚刚修复了

我们使用下列脚本来对各服务器启用新的 fail2ban 封禁方式(其中 actionstop 那行是修 fail2ban 的 bug):

ssh $HOSTNAME -t "
     sudo sed -i 's/^banaction = iptables-multiport$/banaction = iptables-xt_recent-echo/g' /etc/fail2ban/jail.conf;
     sudo sed -i '/actionstop =/a\             iptables -D INPUT -m recent --update --seconds 3600 --name fail2ban-<name> -j DROP' /etc/fail2ban/action.d/iptables-xt_recent-echo.conf;
     sudo service fail2ban restart;
     sudo iptables-save"
</name>

附修改后的 /etc/fail2ban/action.d/iptables-xt_recent-echo.conf 脚本,希望自己写封禁脚本的可以参考:

[Definition]
actionstart = iptables -I INPUT -m recent --update --seconds 3600 --name fail2ban-<name> -j DROP
actionstop = echo / > /proc/net/xt_recent/fail2ban-<name>
             iptables -D INPUT -m recent --update --seconds 3600 --name fail2ban-<name> -j DROP
actioncheck = test -e /proc/net/xt_recent/fail2ban-<name>
actionban = echo +<ip> > /proc/net/xt_recent/fail2ban-<name>
actionunban = echo -<ip> > /proc/net/xt_recent/fail2ban-<name>

[Init]
name = default
protocol = tcp
</name></ip></name></ip></name></name></name></name>

(其中 --seconds 3600 仅仅作为一种 fallback 机制,封禁的定时解除主要靠 fail2ban 内部的定时机制,到时执行 actionunban)

大赞 Zhang Cheng 的发现!