CC攻击的可怕之处不在于流量有多大,而在于它模拟的是正常请求。一个攻击请求和一个真实用户点击,在TCP层几乎看不出区别。传统防护思路是堆硬件、买高防IP、接云防护,但中小站长和独立开发者往往没有这个预算。实际上,在Nginx或Apache这一层,配合内核参数和简单的应用层逻辑,就能构建一套相当有效的CC基础防护体系,完全不需要安装任何第三方插件或模块。
理解CC攻击的流量特征CC攻击本质上是对动态页面的高频请求,目标通常是搜索接口、登录页面、API端点这类消耗服务器资源的URL。攻击流量有三个明显特征:请求频率异常高、User-Agent高度一致或使用常见爬虫标识、请求来源IP集中在少数几个段甚至单IP。识别这些特征不需要机器学习,用Nginx原生的limit_req模块配合map就能精准命中。
很多人一上来就限制全站请求频率,这是错误的。静态资源如图片、CSS、JS不应该被限制,否则正常用户浏览页面时会触发大量误封。正确的做法是只对动态页面和敏感接口做频率限制,静态资源交给expires头和CDN处理。
Nginx无插件防护的核心配置Nginx自带的ngx_http_limit_req_module和ngx_http_limit_conn_module是两个极其强大的模块,编译时默认包含。limit_req基于漏桶算法控制请求速率,limit_conn控制并发连接数。先定义共享内存区域,然后在需要防护的location中引用。
http {
# 定义请求频率限制区域,10m内存可存储约16万个IP状态
limit_req_zone $binary_remote_addr zone=dynamic:10m rate=10r/s;
# 定义并发连接限制区域
limit_conn_zone $binary_remote_addr zone=connlimit:10m;
server {
location / {
# 静态资源直接返回,不做限制
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
# 动态页面和API做频率限制
location ~* \.(php|asp|aspx|jsp)$ {
limit_req zone=dynamic burst=20 nodelay;
limit_conn connlimit 10;
# 其他fastcgi或proxy配置...
}
location /api/ {
limit_req zone=dynamic burst=5 nodelay;
limit_conn connlimit 5;
# 其他代理配置...
}
}
}
burst参数是关键。burst=20表示允许20个请求排队,nodelay表示超出rate的请求立即处理而不延迟。如果不加nodelay,合法用户在短时间内密集点击时会感到明显延迟。rate=10r/s意味着每秒最多处理10个请求,超出部分根据burst设置要么排队要么拒绝。这个数值需要根据实际业务调整,一个普通动态页面的正常用户浏览频率通常不超过每秒2-3个请求。
利用map和if实现智能封禁单纯限制频率会误伤共享IP的企业用户或学校网络。更精细的做法是结合User-Agent和请求特征进行判断。Nginx的map指令可以在不安装任何模块的情况下创建变量映射,结合if指令实现条件判断。
http {
# 定义恶意UA特征
map $http_user_agent $is_bad_ua {
default 0;
"~*curl" 1;
"~*wget" 1;
"~*python" 1;
"~*scrapy" 1;
"~*Go-http-client" 1;
"~*fasthttp" 1;
"" 1; # 空UA直接标记
}
# 定义需要重点防护的URL
map $uri $is_sensitive {
default 0;
"~*search" 1;
"~*login" 1;
"~*register" 1;
"~*admin" 1;
"~*wp-login" 1;
"~*xmlrpc" 1;
}
server {
# 恶意UA直接返回444(Nginx专用状态码,直接关闭连接)
if ($is_bad_ua = 1) {
return 444;
}
location / {
# 敏感URL使用更严格的频率限制
if ($is_sensitive = 1) {
set $limit_rate_key "sensitive";
}
# 其他配置...
}
}
}
返回444状态码是Nginx特有的处理方式,它直接关闭TCP连接而不返回任何HTTP响应,攻击脚本通常会因为连接被重置而报错退出,比返回403更有效。空User-Agent几乎可以肯定是脚本,正常浏览器都会发送UA字符串。
内核层面的TCP参数优化应用层防护做得再好,如果内核TCP队列被打满,服务器照样无法响应。Linux内核有几个参数直接影响抗CC能力,这些参数不需要安装任何工具,用sysctl就能调整。
# /etc/sysctl.conf 或 /etc/sysctl.d/99-anti-ddos.conf # 启用SYN Cookie,防止SYN Flood耗尽半连接队列 net.ipv4.tcp_syncookies = 1 # 缩短SYN_RECV状态的超时时间,默认60秒改为10秒 net.ipv4.tcp_synack_retries = 2 net.ipv4.tcp_syn_retries = 2 # 加快TIME_WAIT状态回收 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 # 扩大半连接队列和全连接队列 net.ipv4.tcp_max_syn_backlog = 8192 net.core.somaxconn = 8192 # 启用TCP时间戳,提高连接复用效率 net.ipv4.tcp_timestamps = 1 # 减少孤儿连接占用的资源 net.ipv4.tcp_max_orphans = 65536 net.ipv4.tcp_orphan_retries = 2
SYN Cookie是最基础的防SYN Flood机制,几乎必须开启。tcp_synack_retries控制服务器发送SYN-ACK后等待客户端ACK的次数,默认5次意味着最长等待时间可能达到180秒,攻击者可以利用这点耗尽服务器资源。改为2次后,等待时间缩短到十几秒,能快速释放被占用的半连接资源。
tcp_tw_reuse需要和tcp_timestamps配合使用,它允许将处于TIME_WAIT状态的连接用于新的TCP连接。高并发场景下,TIME_WAIT状态的连接数量惊人,这个参数能显著提升连接复用率。但要注意,在NAT环境下tcp_tw_recycle已经不建议使用,这里只开启tcp_tw_reuse。
应用层请求指纹识别CC攻击工具通常不会执行JavaScript,也不会正确设置浏览器特有的请求头。利用这一点,可以在不依赖Cookie和JS挑战的情况下识别大部分脚本请求。Nginx可以通过检查请求头的完整性来判断请求是否来自真实浏览器。
server {
# 检查浏览器特有的请求头
set $suspicious 0;
# 真实浏览器都会发送Accept头
if ($http_accept = "") {
set $suspicious 1;
}
# 检查Accept-Language(大多数浏览器会发送)
if ($http_accept_language = "") {
set $suspicious 1;
}
# 检查Accept-Encoding(几乎所有浏览器都支持gzip)
if ($http_accept_encoding !~* "gzip") {
set $suspicious 1;
}
# 检查Connection头
if ($http_connection !~* "keep-alive") {
set $suspicious 1;
}
location / {
if ($suspicious = 1) {
# 可以返回444或重定向到验证页面
return 444;
}
}
}
这个方法有一定误判风险,部分老旧的移动设备或特殊客户端可能不发送某些头。建议先在日志中记录可疑请求,观察一段时间确认无误判后再启用拦截。或者将可疑请求导向一个轻量级的验证页面,而不是直接拒绝。
日志监控与动态黑名单被动防护总有极限,主动识别攻击源并动态封禁是更高级的做法。不需要安装fail2ban之类的工具,用一段简单的shell脚本配合crontab就能实现自动化封禁。
#!/bin/bash
# 分析Nginx访问日志,封禁高频IP
# 建议每分钟执行一次
LOG_FILE="/var/log/nginx/access.log"
BLOCK_LIST="/etc/nginx/blocked_ips.conf"
THRESHOLD=100 # 每分钟请求超过100次视为攻击
# 统计最近1分钟的请求次数
tail -n 10000 $LOG_FILE | awk -v d1="$(date -d '1 minute ago' '+%d/%b/%Y:%H:%M')" '
$0 ~ d1 {print $1}' | sort | uniq -c | sort -nr |
while read count ip; do
if [ $count -gt $THRESHOLD ]; then
# 检查是否已经被封禁
if ! grep -q "deny $ip;" $BLOCK_LIST 2>/dev/null; then
echo "deny $ip;" >> $BLOCK_LIST
echo "$(date) Blocked $ip - $count requests/min" >> /var/log/block_ip.log
fi
fi
done
# 重新加载Nginx(需要确认配置文件语法正确)
if [ -s $BLOCK_LIST ]; then
nginx -t && nginx -s reload
fi
在Nginx主配置文件中引用这个黑名单文件:
http {
# 在http块中引入黑名单
include /etc/nginx/blocked_ips.conf;
server {
# 被封禁IP访问时返回403
# deny指令会自动生效
}
}
这个脚本的阈值需要根据实际流量调整。一个日活1000的网站和日活10万的网站,正常请求频率完全不同。建议先分析历史日志,取99分位的请求频率作为基准,阈值设置为基准的3-5倍。另外要加白名单机制,避免封禁搜索引擎爬虫和合作伙伴的IP。
多层防护的组合策略单靠某一项技术无法应对复杂的CC攻击,需要将上述方法组合成多层防线。第一层是内核TCP参数优化,过滤掉最底层的SYN Flood和连接耗尽攻击。第二层是Nginx的limit_req和limit_conn,对动态页面做频率控制。第三层是请求头完整性检查和UA黑名单,拦截明显的脚本请求。第四层是日志分析和动态封禁,处理绕过前几层的漏网之鱼。
这种组合策略在实际测试中表现相当可靠。以一个2核4G的轻量云服务器为例,部署上述配置后,可以承受每秒约2000个CC攻击请求而不影响正常用户访问。攻击请求在第二层就被limit_req拦截返回503,正常用户因为burst设置合理,基本无感知。如果攻击者更换IP发起新一轮攻击,第四层的动态封禁会在1分钟内将其加入黑名单。
实际部署中的注意事项CDN或反向代理后面的服务器需要特别注意获取真实IP。如果直接使用$binary_remote_addr,限制的将是CDN节点的IP而不是用户真实IP。需要配置real_ip模块来获取X-Forwarded-For或X-Real-IP头中的真实地址。
http {
# 信任的CDN或代理IP段
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# limit_req_zone使用真实IP
limit_req_zone $binary_remote_addr zone=dynamic:10m rate=10r/s;
}
另外,503错误页面需要定制。默认的503页面会暴露Nginx版本信息,攻击者可以据此调整攻击策略。建议自定义一个简洁的503页面,不包含任何服务器信息。
server {
error_page 503 /503.html;
location = /503.html {
internal;
# 返回简单的文本提示
return 503 "Service temporarily unavailable. Please try again later.";
}
}
防护策略上线后要持续观察日志和监控指标。重点关注503响应的比例、正常用户的访问延迟、以及误封情况。根据实际数据不断调整rate和burst参数,找到防护效果和用户体验的最佳平衡点。这套无插件方案虽然比不上专业的高防服务,但对于日均攻击量在百万次以下的场景,已经足够应对绝大多数CC攻击。
