直接说问题:你的Web服务器正遭受恶意爬虫或CC攻击,对方死磕某一个URL,比如登录口或API接口,导致服务器负载飙升。你不想装复杂的WAF,也不想改Nginx配置,CentOS 7/8自带的firewalld其实就能干这活。利用firewalld的富规则(rich rules),可以直接在内核层面基于连接频率对访问特定URL的IP进行封禁,效率比应用层工具高得多,而且不占用Web服务资源。
为什么不用fail2ban而用firewalld很多人第一反应是用fail2ban分析日志去封IP,这方案成熟,但有两个痛点:一是日志量巨大时,fail2ban解析本身消耗CPU;二是反应有延迟,攻击已经打了一波才封。firewalld的富规则直接调用内核netfilter模块,通过limit和recent匹配,能在包到达服务之前就丢掉,几乎零延迟。对于单一URL的暴力请求,这是最轻量的防御手段。
理解firewalld富规则的匹配逻辑富规则本质是firewalld对iptables复杂规则的封装。一条富规则可以同时匹配源地址、目标端口、协议、连接状态,还能附加限制条件。我们要用到的核心是limit和连接跟踪。limit用来限制新建连接的速率,一旦超过阈值就触发reject或drop动作。但要注意,富规则只能匹配到端口和IP层信息,它不解析HTTP头,所以无法直接识别URL路径。这里需要结合连接标记或借助firewalld的自定义服务配合,不过我们可以换个思路:既然攻击者盯着某个URL,那这个URL必然对应某个端口上的服务,我们可以通过限制对该端口的新建连接频率来间接实现目的。如果同一台机器上有多个站点需要区别对待,则需要更精细化的手段。
方案一:基于端口的新建连接频率限制假设你的Web服务跑在443端口,攻击者疯狂请求https://你的域名/api/login这个URL。所有请求都会建立到443端口的TCP连接。我们可以限制单个IP对443端口的新建连接速率,超过阈值就封禁一段时间。命令如下:
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="443" protocol="tcp" limit value="10/m" accept' firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="443" protocol="tcp" drop'
第一条规则的意思是,对访问443端口的TCP连接,每分钟允许新建10个,超过的进入下一条规则。第二条规则将不符合限速条件的连接直接丢弃。注意顺序很重要,firewalld按添加顺序匹配。这个方案简单粗暴,但有个问题:正常用户如果短时间内刷新页面多次,也可能触发封禁。所以阈值需要根据实际业务调整,比如正常页面加载可能同时建立4-6个连接,10/m的阈值对浏览器访问来说偏小,可以适当放宽到30/m。
方案二:利用recent模块实现动态黑名单更精准的做法是用富规则结合recent模块,对触发频率限制的IP进行动态封禁。firewalld的富规则可以直接调用iptables的recent模块。思路是:创建一个监控列表,记录访问443端口的IP,如果某IP在60秒内新建连接超过10次,就把它扔进黑名单封锁600秒。命令如下:
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="0.0.0.0/0" port port="443" protocol="tcp" accept'
但富规则对recent模块的支持有限,直接写可能不生效。更稳妥的办法是通过firewalld的直接规则(direct rules)来实现,它允许你写原始的iptables规则。虽然标题讲的是富规则,但实际生产环境中,处理这种需求往往需要direct规则配合。我们先创建一个自定义链来处理:
# 编辑/etc/firewalld/direct.xml或使用firewall-cmd --direct firewall-cmd --permanent --direct --add-chain ipv4 filter URL_LIMIT firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport 443 -m state --state NEW -j URL_LIMIT firewall-cmd --permanent --direct --add-rule ipv4 filter URL_LIMIT 0 -m recent --name weblock --rcheck --seconds 600 -j DROP firewall-cmd --permanent --direct --add-rule ipv4 filter URL_LIMIT 0 -m recent --name weblock --remove -j ACCEPT firewall-cmd --permanent --direct --add-rule ipv4 filter URL_LIMIT 0 -m recent --name weblock --set -j DROP firewall-cmd --permanent --direct --add-rule ipv4 filter URL_LIMIT 0 -m recent --name http_rate --set -j ACCEPT firewall-cmd --permanent --direct --add-rule ipv4 filter URL_LIMIT 0 -m recent --name http_rate --rcheck --seconds 60 --hitcount 10 -j SET --add-set weblock src --exist
这套规则逻辑严密:新到443端口的连接先进入URL_LIMIT链。第一步检查IP是否在黑名单weblock中,如果在且未超600秒,直接DROP。第二步,如果IP在黑名单但已超时,移除黑名单记录并放行。第三步,如果IP的http_rate列表里60秒内命中超过10次,则将其加入weblock黑名单。第四步,正常请求记录到http_rate列表并放行。这套组合拳精准实现了对单一端口高频访问IP的自动封禁。
如何真正区分URL?结合连接标记与Nginx日志触发前面说过,firewalld工作在OSI模型的3-4层,无法直接解析URL。如果一台服务器上多个网站共用443端口,而你只想限制/api/login这个URL,单纯靠firewalld确实做不到。但我们可以用混合方案:让Nginx在匹配到特定URL的请求时,通过连接标记给这个包打上标签,然后firewalld根据标记来限速。
具体操作:先在Nginx配置中,对需要限速的location添加一个自定义响应头或直接返回特定状态码,然后利用Nginx的请求标记功能配合TC或iptables的CONNMARK。不过这个方案复杂度陡增。更实战的做法是:在Nginx的location块中,使用limit_req模块限制该URL的请求速率,当请求被拒绝时,Nginx返回503,同时触发一个脚本将客户端IP通过firewall-cmd加入黑名单。这样应用层识别URL,网络层执行封堵,各司其职。
实战:Nginx limit_req + firewalld联动脚本先配置Nginx对特定URL限速:
http {
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location /api/login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://backend;
}
}
}
这里限制/api/login每分钟最多5次请求,突发允许3个。超过的请求直接返回503。接下来写一个监控脚本,实时分析Nginx访问日志,把频繁触发503的IP自动封进firewalld:
#!/bin/bash
tail -Fn0 /var/log/nginx/access.log | while read line; do
status=$(echo $line | awk '{print $9}')
if [ "$status" = "503" ]; then
ip=$(echo $line | awk '{print $1}')
# 检查该IP在最近5分钟内出现503的次数
count=$(grep "$ip" /var/log/nginx/access.log | grep "503" | wc -l)
if [ $count -gt 3 ]; then
firewall-cmd --add-rich-rule="rule family='ipv4' source address='$ip' drop" --timeout=600
echo "$(date) Blocked $ip due to excessive 503 on /api/login" >> /var/log/block.log
fi
fi
done
这个脚本作为systemd服务常驻后台,一旦某IP在短时间内产生多次503,就通过firewalld封禁10分钟。--timeout参数让规则到期自动失效,避免永久封禁误伤。
富规则与direct规则的选择建议回到firewalld本身,如果你只是简单限制某个端口的连接频率,富规则完全够用,语法简洁,管理方便。比如只开放22端口给特定IP并限速:
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port port="22" protocol="tcp" limit value="5/m" accept' firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="22" protocol="tcp" drop'
但当需要调用iptables扩展模块如recent、hashlimit、connlimit时,富规则就力不从心了,这时必须用direct规则。direct规则直接操作iptables,灵活性最高,但需要你对iptables有深入理解,否则容易写出有漏洞的规则。我的建议是:能用富规则解决的别用direct,维护成本低很多。富规则搞不定的,再上direct,并且务必在测试环境验证。
验证规则是否生效规则添加后,重载firewalld使其生效:
firewall-cmd --reload
查看当前所有富规则:
firewall-cmd --list-rich-rules
对于direct规则,查看方式不同:
firewall-cmd --direct --get-all-rules
测试时,可以用ab或wrk工具模拟并发请求,观察日志和连接状态。如果规则生效,超过阈值的连接会被直接拒绝,客户端会看到连接超时或被重置,而不是503错误。这正是网络层封禁的特征——连握手都可能完不成。
性能考量与内核参数调优使用recent模块时,内核需要维护一张IP记录表,默认最多记录100个IP,可以通过内核参数调整:
echo 1000 > /proc/sys/net/ipv4/neigh/default/gc_thresh3 sysctl -w net.netfilter.nf_conntrack_max=655360
另外,recent模块的IP表存储在/proc/net/xt_recent/目录下,可以cat查看当前被封的IP列表。如果攻击规模很大,建议将封禁动作从DROP改为REJECT,DROP会消耗更多资源等待超时,REJECT直接返回ICMP不可达,释放连接更快。
常见误区与排错指南误区一:以为富规则能直接匹配URL字符串。再次强调,firewalld不看HTTP内容,它只处理IP、端口、协议、MAC地址等。误区二:规则顺序错误导致限速失效。firewalld的富规则按添加顺序匹配,一旦某条规则ACCEPT了,后续规则就不再检查。务必把限速和DROP规则放在ACCEPT规则之前。误区三:--timeout参数只对临时规则有效,--permanent规则是永久生效的,不能带--timeout。如果想添加临时封禁,用不带--permanent的firewall-cmd命令。
排错时,先确认firewalld运行状态:systemctl status firewalld。然后检查规则是否真的被加载:iptables -L -n | grep 你的端口。如果规则没出现,可能是语法错误导致被忽略,查看/var/log/firewalld日志。另外,如果你同时使用docker,docker会绕过firewalld直接操作iptables,可能导致你的规则被覆盖,需要调整docker的iptables策略或使用docker的用户自定义链。
总结:分层防御才是正道用firewalld限制异常请求频率,本质是在网络层建立第一道防线。它无法替代应用层WAF,但能极大消耗攻击者的资源和耐心。对于中小型站点,这套方案成本极低,效果立竿见影。配合Nginx的limit_req和日志监控脚本,你就能构建一个从网络层到应用层的立体防御体系。记住,安全没有银弹,多层过滤、逐级拦截才是稳健的做法。
