CC攻击之所以让人头疼,不在于带宽被占满,而在于它用极低的成本耗尽服务器的连接资源。一个简单的HTTP请求,如果后端需要查询数据库或进行复杂运算,攻击者只要在短时间内发起几十个并发请求,就能让服务器的CPU或者Apache/Nginx的工作进程全部卡死。在应用层防火墙或者负载均衡器缺失的情况下,直接利用Linux内核自带的netfilter/iptables模块进行速率限制,是成本最低、响应最快的止损手段。我们要做的不是单纯地封IP,而是通过限制单IP在固定时间窗口内的新连接发起速率,让正常用户的请求进来,把恶意的高频请求拦在TCP握手阶段。

理解iptables的速率限制模块

在Ubuntu系统中,iptables有一个极其强大的扩展模块叫“hashlimit”。很多人习惯用“limit”模块,但“limit”是基于全局的令牌桶算法,它无法针对每一个来源IP进行独立计数。也就是说,如果你设置了每分钟限制100个连接,那么所有用户加起来共享这100个额度,攻击者一旦耗尽额度,正常用户也进不来了。而“hashlimit”则是通过哈希表为每一个IP地址(或IP+端口组合)创建一个独立的桶,真正做到了单IP粒度的流控。这是防CC的关键所在。

在开始配置之前,务必确认系统已经加载了hashlimit模块。绝大多数现代Ubuntu发行版的内核都已经默认支持,如果不确定,可以执行以下命令验证:

iptables -m hashlimit --help

如果返回了帮助信息,说明模块可用。如果提示找不到模块,需要检查内核配置或安装相关的模块包,但这种情况在Ubuntu 20.04及之后的版本中极为罕见。

精准配置单IP连接数限制

防CC的核心逻辑是限制SYN包速率,也就是限制新连接的发起速度。我们不需要去限制已建立连接(ESTABLISHED)的传输速度,因为攻击通常通过不断发起新连接来消耗资源。以下是一套经过生产环境验证的iptables规则,直接针对TCP协议的80端口和443端口进行单IP每分钟连接数限制。

首先,在终端中输入以下命令,创建一个新的链来专门处理CC防护逻辑,这样方便管理,不会污染默认的INPUT链:

iptables -N CC_DEFENSE

接着,将访问Web服务的流量导入到这个自定义链中。假设你的服务运行在80和443端口:

iptables -A INPUT -p tcp --dport 80 -j CC_DEFENSE
iptables -A INPUT -p tcp --dport 443 -j CC_DEFENSE

现在是最关键的一步,在CC_DEFENSE链中使用hashlimit模块进行速率限制。这里的参数设置需要非常谨慎,阈值太高防不住攻击,阈值太低会误伤正常用户。对于一个常规的动态网站,单IP每分钟发起30到60个新连接已经属于非常活跃的人类行为。如果是API接口,这个数值可能需要根据业务模型调整。以下示例将限制设置为每分钟允许产生20个新连接,如果超过这个速率,就直接丢弃数据包:

iptables -A CC_DEFENSE -m hashlimit \
    --hashlimit-name cc_attack_limit \
    --hashlimit-above 20/minute \
    --hashlimit-burst 5 \
    --hashlimit-mode srcip \
    --hashlimit-htable-expire 60000 \
    -j DROP

我们来逐一拆解这些参数,因为不理解参数就上线规则是一件很危险的事。--hashlimit-name是给这条哈希规则起的名字,它会在/proc/net/ipt_hashlimit/目录下生成一个对应的文件,用于记录哈希表的状态。--hashlimit-above 20/minute表示当速率超过每分钟20个时触发动作。注意这里用的是“above”,意思是超过阈值才执行后面的-j DROP,如果是“upto”则是低于阈值时执行。--hashlimit-burst 5是突发缓冲值,允许在窗口开始时瞬间涌入5个包而不触发限制,这能很好地兼容浏览器同时加载多个资源的情况。--hashlimit-mode srcip指定以源IP为哈希键值,这是实现单IP限制的核心。--hashlimit-htable-expire 60000表示哈希表中的记录在60秒无活动后过期,单位是毫秒,这样可以防止内存中堆积太多无用的IP记录。

结合connlimit模块进行双重防护

仅仅限制速率有时还不够,有些慢速CC攻击会控制好请求频率,在速率阈值之下,但会同时保持大量的并发连接,以此耗尽服务器的连接队列。这时候需要引入另一个模块“connlimit”,它专门用来限制单个IP的并发连接总数。我们需要在CC_DEFENSE链的最前面加上并发连接数的限制:

iptables -I CC_DEFENSE 1 -m connlimit \
    --connlimit-above 15 \
    --connlimit-mask 32 \
    --connlimit-saddr \
    -j REJECT --reject-with tcp-reset

这条规则的意思是,当同一个源IP(--connlimit-mask 32表示子网掩码为32位,即精确匹配单个IP)的并发连接数超过15个时,直接拒绝连接并发送TCP重置包。这里使用REJECT而不是DROP,是为了让合法的客户端浏览器能立即知道连接被拒绝,从而停止重试,减少客户端资源的浪费。如果使用DROP,客户端会一直等待超时,反而会积压更多的半连接。15这个数值对于一般的网页浏览足够了,因为现代浏览器通常会使用2到4个并发连接来加载资源,即使打开多个标签页,单个IP的并发连接数也很少超过10个。如果你的网站是单页应用或者WebSocket长连接场景,需要根据实际情况调高这个值。

处理已建立连接的白名单逻辑

在添加了上述严格的限制后,必须确保已经建立连接的合法流量不会被误伤。iptables的规则是顺序匹配的,一旦匹配到规则就不再往下执行。因此,我们需要在INPUT链的最顶端放行所有已建立连接和回环接口的流量。这是防火墙配置的基础常识,但很多人在紧急防攻击时往往会忽略,导致SSH连接被踢断或者数据库连接异常。请务必执行以下命令:

iptables -I INPUT 1 -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -I INPUT 2 -i lo -j ACCEPT

这样,所有已经完成三次握手的数据包以及关联的ICMP包都会直接被放行,不会进入CC_DEFENSE链进行速率统计。这既提高了性能,也保证了长连接服务不受后续规则的影响。

保存规则并设置开机自启

Ubuntu系统重启后,内存中的iptables规则会丢失。为了不让半夜的服务器重启导致防护失效,必须将规则持久化。在Ubuntu 22.04及之后的版本中,通常使用netfilter-persistent工具来管理规则。首先安装这个工具:

apt update
apt install iptables-persistent -y

安装过程中可能会弹出对话框询问是否保存当前规则,选择是即可。之后,如果需要手动保存规则,可以使用以下命令:

netfilter-persistent save

这条命令会将当前的IPv4规则保存到/etc/iptables/rules.v4文件中。如果系统没有安装iptables-persistent,也可以使用传统的iptables-save命令配合rc.local来实现,但那样不够规范,不推荐使用。

调试与动态监控

规则上线后,不能凭感觉判断它是否生效。我们需要实时查看hashlimit模块的匹配情况,以判断是否有IP触发了限制。通过查看自定义链的计数器,可以清楚地看到有多少数据包被丢弃了:

watch -n 1 'iptables -L CC_DEFENSE -v -n'

这条命令每秒刷新一次CC_DEFENSE链的统计信息。重点关注pkts和bytes这两列,如果DROP规则后面的计数器在持续增长,说明确实有流量被限制了。但此时还需要进一步分析,这些被限制的IP是真实的攻击者还是被误伤的合法用户。可以通过查看内核日志来获取被封锁的IP信息。首先,我们需要在DROP规则上加一条日志记录,将其放在DROP动作之前:

iptables -I CC_DEFENSE 3 -m hashlimit \
    --hashlimit-name cc_attack_limit \
    --hashlimit-above 20/minute \
    --hashlimit-burst 5 \
    --hashlimit-mode srcip \
    -j LOG --log-prefix "CC_ATTACK_DROP: " --log-level 4

注意,这里把日志规则插在了DROP规则之前,并且匹配条件完全一样。这样,任何即将被丢弃的数据包都会先在内核日志中留下记录。然后可以通过dmesg或查看/var/log/syslog来监控被封锁的IP:

tail -f /var/log/syslog | grep "CC_ATTACK_DROP"

如果发现某个内网IP或者CDN回源IP频繁出现在日志里,那就说明阈值设置得太严格了,需要调整--hashlimit-above的数值。对于使用了CDN服务的网站,这一点尤其重要。因为所有用户的请求在到达源站时,来源IP都是CDN节点的IP。如果对CDN节点IP进行单IP速率限制,那将是一场灾难,瞬间就会把所有用户都挡在门外。在这种情况下,必须在CC_DEFENSE链的最前面,将CDN的IP段全部加入白名单并直接放行,或者在hashlimit规则中使用--hashlimit-mode srcip,dstport来细化限制维度,但最稳妥的做法还是通过获取真实用户IP的模块(如Nginx的real_ip模块)来还原IP后,再将iptables部署在更靠前的位置,或者直接在应用层使用Nginx的limit_req模块进行限制。

针对UDP洪水和其他协议的扩展

虽然本文主要讨论TCP层的CC攻击,但如果服务器运行着游戏或DNS服务,UDP洪水也是常见威胁。hashlimit同样支持对UDP协议进行速率限制,只需要将规则中的-p tcp改为-p udp即可。但UDP是无连接协议,没有SYN包的概念,所以限制的是所有进入的UDP数据包速率。对于基于UDP的DNS服务,通常限制每个IP每秒不超过10个查询请求是比较合理的:

iptables -A INPUT -p udp --dport 53 -m hashlimit \
    --hashlimit-name dns_limit \
    --hashlimit-above 10/second \
    --hashlimit-burst 5 \
    --hashlimit-mode srcip \
    -j DROP

这种基于内核的限速,处理效率极高,在应对小规模UDP反射放大攻击时,能在不消耗多少CPU资源的情况下把攻击流量丢弃,为上层服务争取宝贵的处理时间。

权衡内核防护与应用层防护的边界

iptables的hashlimit方案虽然高效,但它有一个天然的盲区:它工作在TCP/IP协议栈的第三层和第四层,完全无法识别HTTP请求的具体内容。一个TCP连接可能传输一个复杂的数据库查询请求,也可能只是一个简单的静态文件请求,但在iptables眼里,它们都只是一个SYN包。这就导致了一种可能性:攻击者可以用极低的连接速率,但每个连接都请求一个极其消耗资源的动态页面,从而绕过iptables的限制。因此,iptables防CC的最佳实践是作为第一道防线,解决洪水般的海量连接耗尽问题。对于精心构造的慢速攻击或针对特定URL的应用层攻击,必须在Nginx或Apache层面使用limit_req模块,甚至接入专业的人机识别验证服务。iptables负责把大范围的噪音过滤掉,应用层防火墙负责处理剩下的精细流量,这种分层防御体系才是成熟运维架构的体现。

在实际操作中,建议先在测试环境或者服务器的非业务高峰期,用更宽松的阈值(如每分钟100个请求)上线规则,观察一段时间,确认没有误杀正常用户后,再逐步收紧阈值。直接在生产环境高峰期贸然设置每分钟10个连接这种严苛规则,很可能会导致大量用户无法访问,造成业务中断。安全防护永远是在可用性和安全性之间寻找平衡点,iptables给了我们一把非常锋利的刀,但如何精准地切掉病灶而不伤及筋骨,考验的是对业务逻辑和网络协议的深度理解。