小带宽站点遭遇CC攻击,最致命的不是流量本身,而是服务器资源被空耗殆尽。攻击者通过大量看似正常的请求,瞬间占满你的带宽、CPU和数据库连接数,导致真实用户无法访问。对于出口带宽只有1Mbps到10Mbps的小站,传统的硬件防火墙和清洗服务成本太高,根本不现实。我们需要的是在有限资源下,用最轻量的策略把攻击流量挡在业务逻辑之外,让服务器只处理合法请求。

把防线前置到Web服务器层面

不要在应用代码里检测CC攻击,到了PHP或Python层面,服务器已经被拖垮了。Nginx和OpenResty是这层防御的最佳阵地。利用Nginx的ngx_http_limit_req_module和ngx_http_limit_conn_module模块,可以直接在请求进入时做频率限制。核心思路是限制单个IP的请求速率和并发连接数。比如设置每个IP每秒最多处理10个请求,超出就返回503或444状态码。444是Nginx特有的非标准状态码,直接关闭连接不返回任何响应,比503更省带宽。

http {
    limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    
    server {
        location / {
            limit_req zone=perip burst=5 nodelay;
            limit_conn addr 10;
        }
    }
}

这里的burst参数允许短时突发5个请求,nodelay表示突发请求也立即处理,避免排队延迟。limit_conn限制每个IP同时只有10个连接。这两个组合拳能挡住大量简单的CC攻击。但要注意,如果攻击者IP池很大,单IP限制效果会打折扣,需要配合其他策略。

用Nginx的map和geo模块做动态黑白名单

静态IP黑名单对CC攻击几乎没用,攻击源会不断变化。但我们可以根据请求特征动态封禁。通过分析访问日志,把短时间内请求量异常的IP自动加入黑名单。这里可以写一个简单的shell脚本,定时分析日志,把超过阈值的IP提取出来,动态更新Nginx的黑名单文件,然后reload配置。更优雅的做法是用OpenResty的lua-resty-redis模块,将IP访问计数存在Redis里,达到阈值后直接在Nginx层面返回403。这样无需reload,延迟极低。

access_by_lua_block {
    local redis = require "resty.redis"
    local red = redis:new()
    red:set_timeout(1000)
    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        return
    end
    
    local ip = ngx.var.binary_remote_addr
    local key = "cc:rate:" .. ip
    local count, err = red:incr(key)
    if count == 1 then
        red:expire(key, 60)
    end
    
    if count > 100 then
        ngx.exit(403)
    end
}

这段代码在每个请求到来时,对IP在Redis中的计数器加1,60秒内超过100次就封禁。Redis的内存操作极快,对性能影响微乎其微。而且计数可以跨worker进程共享,比Nginx自带的limit_req更灵活。对于小带宽站点,Redis本身占用的内存和CPU也很小,完全可以在同一台服务器上运行。

利用浏览器行为验证区分真人和攻击脚本

很多CC攻击工具只是简单发送HTTP请求,不会解析JavaScript,也不会执行浏览器端的计算任务。利用这一点,可以在请求到达真实服务器之前,插入一道JavaScript验证。具体做法是,当某个IP的请求频率超过阈值时,不直接返回业务内容,而是返回一个包含JavaScript挑战的页面。这个挑战可以是一个简单的数学计算,要求浏览器计算后带上结果重新请求,验证通过后才放行。这种方式对用户体验有一定影响,但能有效过滤掉绝大多数脚本攻击。

实现上可以用Nginx的error_page指令,把被限流的请求重定向到一个验证页面。验证逻辑用一段简单的JavaScript生成一个token,写入cookie。后续请求Nginx检查这个cookie,存在且有效就放行,否则继续返回验证页面。攻击脚本通常不会保存和发送cookie,自然就被拦在外面。对于真实用户,只需要经历一次验证,后续访问无感知。

用CDN的静态缓存分担带宽压力

小带宽站点最怕的是带宽被打满,导致正常用户连TCP握手都完不成。即使你的服务器处理能力足够,出口带宽一旦耗尽,服务就完全不可用。把静态资源分离到CDN上是基础操作,但更进一步,可以把动态页面的非个性化部分也做缓存。对于博客、企业站等内容型站点,页面内容变化不频繁,完全可以在Nginx上开启proxy_cache或fastcgi_cache,把动态生成的HTML页面缓存成静态文件。这样CC攻击请求打到缓存上时,Nginx直接返回静态文件,不经过后端应用,CPU和带宽消耗降到最低。

proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;

server {
    location / {
        proxy_cache my_cache;
        proxy_cache_key "$scheme$request_method$host$request_uri";
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404 1m;
        proxy_pass http://backend;
    }
}

缓存命中后,Nginx处理一个请求消耗的资源远低于动态请求。即使攻击流量很大,只要命中缓存,服务器就能扛住。但要注意,缓存key的设计要避免把用户cookie或session信息包含进去,否则每个用户都会生成独立缓存,失去缓存意义。对于需要登录的页面,可以关闭缓存,或者只缓存公共内容。

TCP层面的参数调优和连接快速回收

CC攻击会建立大量TCP连接,如果不及时回收,服务器的连接表会被占满,导致新连接无法建立。调整Linux内核参数可以加速无效连接的清理。net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle曾经是常用参数,但在新内核中tcp_tw_recycle已被移除,因为它对NAT网络不友好。现在更推荐使用tcp_tw_reuse配合tcp_fin_timeout的缩短。把tcp_fin_timeout从默认的60秒降到15秒,能更快释放处于TIME_WAIT状态的连接。

net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_syncookies = 1

tcp_syncookies开启后,当SYN队列满时,服务器会发送带有cookie的SYN+ACK包,不占用队列资源,能有效防御SYN Flood攻击。tcp_max_syn_backlog增大SYN队列长度,给正常连接更多排队机会。这些参数调整不消耗额外资源,是小带宽站点必须做的基础加固。

应用层的请求过滤和最小化响应

攻击请求往往带有明显的特征,比如特定的User-Agent、固定的URL参数、缺失Referer头等。在Nginx中可以根据这些特征直接拒绝请求,连缓存都不用查。比如大量CC攻击工具使用默认的Python-urllib或Go-http-client作为User-Agent,正常浏览器不会这样。直接把这些请求在Nginx层面拦截。

if ($http_user_agent ~* "python-urllib|Go-http-client|curl|wget") {
    return 444;
}
if ($request_method !~ ^(GET|HEAD|POST)$) {
    return 444;
}

还可以检查请求的Host头是否合法,拒绝直接IP访问。很多扫描器和攻击工具会直接用IP发起请求,正常用户通过域名访问。设置一个默认的server块,把所有不匹配域名的请求直接关闭连接。

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

对于必须返回响应的请求,尽量返回最小化的内容。比如被限流时返回一个空的204 No Content,或者一个极简的HTML页面,大小控制在几百字节以内。这样即使有大量请求被处理,带宽消耗也极小。

利用Fail2ban实现自动化IP封禁

Fail2ban是一个日志分析工具,可以实时监控Nginx的访问日志,根据规则匹配出恶意IP,然后调用iptables或firewalld在系统层面封禁。与Nginx层面的封禁相比,iptables封禁发生在网络层,处理开销更低,不占用Web服务器的worker进程。配置Fail2ban监控Nginx日志中返回特定状态码的IP,比如频繁返回403或444的IP,说明这些IP已经被Nginx判定为恶意,可以直接用iptables封禁一段时间。

[Definition]
failregex = ^ -.*"(GET|POST).*" (403|444) .*$
ignoreregex =

封禁动作可以设置为iptables的DROP规则,比REJECT更省资源,因为DROP不返回任何响应,攻击方的连接会超时,消耗攻击方的资源。封禁时间根据攻击持续时间设定,一般600秒到3600秒足够。Fail2ban本身资源占用很小,适合小带宽站点长期运行。

用WAF模块做深度请求分析

如果攻击手法更复杂,比如模拟正常浏览器行为、使用大量代理IP,简单的频率限制和UA过滤就不够了。这时需要引入轻量级的WAF模块。ModSecurity是开源的WAF引擎,但比较重,对小站点不友好。可以考虑Nginx的naxsi模块,或者OpenResty生态里的lua-resty-waf。这些轻量WAF能分析请求中的参数、Header、Body,检测SQL注入、XSS等攻击特征,同时也能识别CC攻击的异常模式。比如同一个Cookie值但IP频繁变化,或者同一个Session但请求间隔异常短,这些特征都可以作为封禁依据。

部署轻量WAF的关键是只开启必要的规则,避免全量规则导致性能下降。针对CC攻击,重点开启请求频率、User-Agent异常、Referer异常等规则,关闭与SQL注入、文件上传相关的复杂规则,减少请求处理延迟。

架构上的最终兜底:静态化发布

如果站点内容更新不频繁,最极端的防护手段是把整个站点静态化。用Hugo、Jekyll、Hexo等静态站点生成器,把网站内容预先生成HTML、CSS、JS文件,直接由Nginx提供静态服务。没有数据库、没有后端脚本、没有动态请求,攻击者能打到的只有Nginx的静态文件服务能力。Nginx处理静态文件的能力极强,在1核1G的服务器上也能轻松应对每秒数千个并发请求。带宽打满的问题可以通过CDN解决,源站只对CDN回源请求响应,攻击流量被CDN节点分散吸收。

对于必须保留动态功能的站点,比如有搜索、评论等,可以把这些功能拆分成独立的子域名,用不同的服务器或容器承载。主站完全静态化,动态功能通过API调用,即使API被打垮,主站的内容访问不受影响。这种架构分离是应对CC攻击最彻底的方案,但需要一定的开发工作量。

监控和日志分析不能停

所有防御措施部署后,持续的监控才能保证策略有效。用轻量的工具如GoAccess实时分析Nginx日志,观察请求量、状态码分布、独立IP数量。当发现异常流量模式时,手动或自动调整限流阈值。把Nginx的日志级别设为warn或error,减少日志写入量,避免攻击期间磁盘IO成为瓶颈。日志文件本身也会消耗带宽和存储,定期轮转和清理。

小带宽站点的CC防御核心思想是分层过滤、逐级拦截,把恶意请求尽可能挡在资源消耗最小的层面。从iptables到Nginx限流,从JavaScript验证到静态缓存,每一层都过滤掉一部分攻击流量,最终到达后端应用的请求量就控制在服务器能承受的范围内。没有完美的单点防御,但组合使用这些轻量方案,小站点也能在有限的资源下扛住大多数CC攻击。