有一类 CC 攻击很反直觉:带宽平稳、QPS 也不高,监控上一片绿色,网站却突然打不开。原因是它只建立连接、不发请求——把服务端的可用连接数占满。本文用 9 个问答讲清长连接占用型攻击的原理、排查方法和 Nginx 与内核层的防护配置。
Q:为什么带宽和 QPS 都不高,网站却打不开了?
- 因为被占用的资源既不是带宽,也不是计算能力,而是连接数本身。
- 典型表现是三组数字互相矛盾:出带宽正常甚至偏低、QPS 没有明显上涨、但网站响应超时或直接连不上。
- 这时候看服务端连接状态,通常能看到
ESTABLISHED数量远超日常水平,且每个连接的存活时间远长于正常用户。 - 这类攻击的成本极低:握手一次,连接挂住不收,攻击者几乎不消耗自己的出口带宽,却能持续吃掉对手的连接槽位。
Q:长连接是怎么被拿来攻击的?
- 长连接本身是好东西,能省掉反复握手的开销,但它的「保持」特性正好可以被反向利用。
- 四种常见用法:
- Keep-Alive 滞留:发起 HTTP 连接后不发送或极慢发送后续请求,让服务端一直等着。
- WebSocket 空连接:完成协议升级后不推送任何数据,靠心跳维持,连接数只增不减。
- SSE 与流式接口挂住:请求流式响应后不主动断开,每条连接都在等一个永远不发的事件。
- HTTP/2 单连接多路复用:少数几条 TCP 连接上开满并发流,连接数看着少,实际并发压力很大。
- 共同点是请求数很少,占用时间很长——这恰好绕过了「按 QPS 限流」这一层防护。
Q:它和半开连接攻击、和堵带宽的 CC 有什么区别?
- 三者经常被混为一谈,但排查路径和防护手段完全不同,先分清再动手。
- 半开连接攻击(SYN Flood):连接握手没完成,堆积在
SYN_RECV状态的半开队列里,打的是内核的 SYN 队列与 conntrack 表。 - 长连接占用(本篇):握手已经完成,连接处于
ESTABLISHED,占的是应用层可用的连接槽位、进程文件描述符和 worker 并发额度。 - 带宽型 CC:疯狂请求大文件或重复拉取,带宽曲线直接飙上去,打的是出口带宽与流量费。
- 一句话判据:连接数正常而带宽满 = 带宽型;带宽正常而 QPS 高 = 传统 CC;带宽和 QPS 都正常而连接数暴涨 = 长连接占用。
Q:怎么确认是长连接被占满了?
- 按「连接总量、单 IP 分布、连接存活时长、进程限制」四步查,基本能一锤定音。
# 1) 连接状态总览:ESTABLISHED 是否远超日常
ss -s
# 2) 单 IP 的并发连接数 TOP(正常用户通常只有几条)
ss -ant state established '( sport = :80 or sport = :443 )' \
| awk 'NR>1 {print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20
# 3) 看连接存活时长:存活很久又几乎不发数据的高度可疑
ss -antop state established | grep -E ':80|:443' | head -30
# 4) Nginx worker 的连接数与进程文件描述符上限
nginx -T 2>/dev/null | grep -E 'worker_connections|worker_rlimit_nofile'
ulimit -n
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'
- 如果结论是「单 IP 握着几十上百条连接、几乎不发请求、存活时间以小时计」,基本就是长连接占用型攻击。
Q:Nginx 层怎么限制长连接?
- 思路是给每条连接设定「寿命」和「活跃度」两条底线:连太久要断,闲着也要断,同时限制单 IP 的连接数量。
# 连接数与频率两个维度分别限流
limit_conn_zone $binary_remote_addr zone=connip:10m;
limit_req_zone $binary_remote_addr zone=reqip:10m rate=10r/s;
http {
keepalive_timeout 15s; # 闲置连接最多保持 15 秒,正常浏览器够用
keepalive_requests 100; # 单连接最多复用 100 次请求,用完即断
keepalive_time 1h; # 连接最长总寿命,防止被长时间霸占
server {
limit_conn connip 20; # 单 IP 最多 20 条并发连接
limit_conn_status 429;
limit_req zone=reqip burst=20 nodelay;
limit_req_status 429;
# 客户端超时参数:慢速发请求会在这里被断开
client_header_timeout 10s;
client_body_timeout 10s;
send_timeout 20s;
reset_timedout_connection on; # 超时后直接回收,不再走四次挥手
# 长连接型业务的 location 单独收紧(如 WebSocket、SSE)
location ~ ^/(ws|socket|stream|sse) {
proxy_read_timeout 60s; # 超过 60 秒没有数据就断开
proxy_send_timeout 60s;
limit_conn connip 5; # 单 IP 最多 5 条实时长连接
limit_conn_status 429;
}
}
}
- 关键参数就四个:keepalive_timeout(闲置时限)、keepalive_requests(复用上限)、limit_conn(单 IP 并发)、client_header_timeout(慢速发送时限)。
- 取值原则是贴着正常业务来定:网页站
keepalive_timeout给 10~20 秒足够;实时推送类业务可以放宽连接数,但仍必须加空闲超时。
Q:WebSocket 和 SSE 业务必须用长连接,怎么办?
- 业务需要长连接就别关它,改成带着约束用它,重点放在鉴权和空闲回收上。
- 四条规则:
- 先鉴权再升级协议:未通过身份校验的连接不允许完成协议升级,直接拒绝建立长连接。
- 应用层心跳必须有:客户端和服务端互相探活,超时未收到心跳就主动断开并回收槽位。
- 单用户连接数设上限:同一账号最多允许几条实时连接,超出部分挤掉最旧的。
- 空闲连接定期清理:服务端维护一个清扫任务,把超过阈值的闲置连接主动关闭,别等系统 fd 用光才处理。
# 示例:未携带有效令牌的连接不升级协议(应用层配合返回 401)
map $http_upgrade $upgrade_ok {
default "";
websocket "upgrade";
}
- 经验一条:所有长连接都必须有「必死条件」——要么有超时,要么有配额,什么都没有的连接就是攻击者的免费资源。
Q:系统与内核层还要注意什么?
- 应用层限制了,系统层也不能拖后腿,否则连接还没到 Nginx 就已经卡死在 fd 或连接跟踪表上。
# 单 IP 并发连接硬限制(50 条以上直接丢包,阈值按业务调整)
iptables -I INPUT -p tcp -m multiport --dports 80,443 \
-m connlimit --connlimit-above 50 --connlimit-mask 32 \
--connlimit-saddr -j DROP
# 内核参数:让系统能扛住大量普通连接,同时尽快回收
sysctl -w net.core.somaxconn=4096
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
sysctl -w net.ipv4.tcp_fin_timeout=15
sysctl -w net.ipv4.tcp_tw_reuse=1
# 连接跟踪表是否接近上限(打满会导致新连接直接被丢弃)
sysctl net.netfilter.nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_count
dmesg | grep -i 'table full' | tail -5
- 进程侧同步抬高句柄上限:Nginx 的 worker_rlimit_nofile 要大于 worker_connections 乘以 worker 进程数,否则并发连接数配得再高也用不上。
console端口与 SSH 不要跟 Web 端口用同一套封禁规则,避免处置时把自己锁在外面。
Q:这层防护有哪些容易踩的坑?
- 只监控 QPS,不看连接数:这是最大的坑。长连接型攻击在 QPS 曲线上几乎是平的,监控必须补上「活跃连接数」「单 IP 连接数」「新建连接速率」三个指标。
- limit_conn 直接把 NAT 用户误伤:学校、企业、手机运营商出口都是地址转换,成百上千人共享一个公网 IP。单 IP 阈值定得太低,会先把这些真实用户挡在门外,必要时给已知大出口加白名单。
- keepalive_timeout 调得过短:连接复用率下降,握手次数暴涨,反而增加 CPU 开销与延迟。合理区间是 10~30 秒,别为了防攻击一刀切到 1 秒。
- 返回码用错:限流统一返回 429,语义清晰,也方便日志统计与实际故障区分。
Q:还有什么补充建议?
- 把「连接数」补进常规监控:带宽、QPS、连接数三个指标一起看,任何一个单独看都会漏掉一类攻击。
- 处置顺序:先保命、再限流、后封禁:先把闲置连接批量回收、给入口限速,让网站能打开,再去分析 IP 清单决定封禁范围。
- 封禁要带过期时间:家庭宽带 IP 会被运营商回收再分配,永久封禁迟早误伤正常访客,短时封禁加自动解封才是正解。
- 规则先观察再收紧:新阈值先以宽松值跑一周,观察误报情况后再压到目标值,避免上线当天就出故障。
- 一句话总结:长连接占用型攻击吃的是「连接」而不是「带宽」和「QPS」——防护的关键是给每条连接设寿命(keepalive_timeout)和配额(limit_conn),监控必须能看见连接数,否则你会一直在一条平坦的曲线上找原因。