凌晨一点,手机连环震动。打开运维群一看,某台线上业务服务器的告警已经刷屏:TCP连接数暴涨、CPU毛刺、业务接口响应超时。登录跳板机看了一眼,netstat -ant里SYN_RECV状态连接密密麻麻,从同一个或几个可疑IP段源源不断地涌进来——第一反应就是:SYN Flood 来了。
这应该是每个干过Linux运维的人都经历过的场景。SYN Flood 作为最经典的DDoS攻击方式之一,原理不难,但真要防御好,牵扯到内核参数、系统架构、流量调度等多个层面。网上讲 SYN Flood 原理的文章很多,讲单个sysctl参数的文章也不少,但能把“内核调优”和“流量清洗”串成一套可落地方案的并不多。这篇文章想做的,就是把这两块整合到一起,以实际运维视角,讲清楚攻击为什么能打挂服务、内核参数调哪些最有效、流量清洗怎么做,以及实战中怎么验证效果和排查问题。适合正在维护线上业务的后端工程师、SRE、安全运维,也适合准备面试时想深入理解 TCP 协议栈的开发者。
1. 先弄清楚SYN Flood到底在打什么
1.1 三次握手与半连接队列:攻击为什么能拖垮服务器
TCP 三次握手大家都不陌生:客户端发SYN,服务器回SYN+ACK,客户端再回ACK,连接建立。问题就出在第二步。当服务器收到一个SYN包时,它不会立刻建立完整连接,而是把这个连接标记为“半连接”,放入一个专门的队列,等待客户端回ACK。这个队列在内核里叫半连接队列(SYN Queue),它的大小由tcp_max_syn_backlog参数控制。只有三次握手完成后,连接才会从半连接队列移到全连接队列(Accept Queue),然后用户态的应用程序(比如 Nginx、Java进程)才能接手处理。
SYN Flood 的打法恰恰就是利用了这个机制。攻击者伪造大量源IP地址,向服务器持续发送SYN包,但从来不回应SYN+ACK。服务器收到这些假SYN后,把半连接队列一条条填满,队列满了以后,新的正常连接请求就被内核直接丢弃,表现为:用户在浏览器里拼命转圈刷新,后端服务明明还活着,可就是连不上。
这个设计缺陷在于 TCP 协议本身不校验源IP的真实性,接收方必须在有限的资源池里为每个“来路不明”的握手请求预留状态资源。想象一下,一家餐厅的等位区只有20个座位,恶意人员伪造了1000个手机号订餐,但就是不出现,真顾客来了却因为等位区满了只能干站在门外——SYN Flood 干的就是这个事。
1.2 攻击特征与识别方法
线上出了状况,第一件事是确认到底是不是 SYN Flood。我建议通过下面几个维度快速判断,而不是凭感觉。
看连接状态统计。netstat -ant | grep SYN_RECV | wc -l是快速判断的经典命令。正常情况下,服务器上的SYN_RECV连接数极少,甚至长期为0;被打的时候这个数字会持续走高,甚至达到数千、数万。更直观的方法是看整体状态分布:
netstat -ant | awk '{print $6}' | sort | uniq -c | sort -rn正常时输出里ESTABLISHED和TIME_WAIT占绝大多数;攻击时SYN_RECV会异军突起,挤进前几名。更精确的做法是用ss命令统计:
ss -ant state syn-recv | wc -l看内核日志。如果半连接队列被打满,内核会在系统日志里输出possible SYN flooding on port 80. Sending cookies.之类的信息。看到这行日志基本可以实锤了。配合dmesg -T看时间戳,还能大致推断攻击起止时间。
看流量进出比。每收到一个SYN,服务器会回一个SYN+ACK。如果SYN+ACK的发出量远大于后续收到的ACK,说明大量握手根本没有完成。用sar -n TCP 1或nstat -az能看到实时的 TCP 报文统计,TcpExtTCPAbortReqData、TcpExtTCPSyncookiesSent等计数项都能反映异常。
1.3 明确防御边界:不是所有SYN Flood都靠内核参数解决
这里要先说一个重要认知:内核调优和流量清洗不是二选一,而是配合关系。内核参数调优解决的是“在攻击流量已经到达服务器网卡时,尽量撑住不崩溃”,属于被动防御;流量清洗则是把攻击流量在到达服务器之前就拦掉、稀释掉,属于主动防御。
实际上,一条完整的 SYN Flood 防御链路应该是:上游清洗设备或云厂商高防(拦掉大部分攻击流量) → 网络层防火墙/负载均衡做限速(挡住一部分漏网流量) → 服务器内核参数调优(吸收残余压力)。就算你的业务部署在云上且开了高防,内核参数调优依然不可或缺,因为高防不是万能的,配置有切换延迟,攻击流量也可能绕过清洗节点直接打到源站。相反,如果只在服务器上做调优,攻击流量一大,服务器的出口带宽和CPU照样会被打满。
还有一个现实约束:很多时候攻击峰值并不大,比如只有一两Gbps的流量,但半连接基数很大,这种场景靠内核调优完全能扛住,没必要付费买高防。理解这个边界,才不会在方案设计上走弯路。
2. 内核参数调优:先把半连接队列稳住
2.1 核心参数逐个拆解
Linux 内核提供的 TCP 协议栈参数非常多,但真正针对 SYN Flood 防御有效的,主要就是下面这几个。我在不同发行版(CentOS 7/8、Ubuntu 20.04、Debian 11)上都验证过,行为基本一致,默认值可能略有差异。
net.ipv4.tcp_max_syn_backlog:半连接队列的最大长度,也就是内核为尚未完成握手的连接预留的槽位数量。默认值通常是 1024 或 2048,这个数值在白话场景下可以理解成“餐厅等位区的座位数”。攻击发生时,这个值越大,服务器能接纳的挂起连接就越多,留给正常连接的余量也越大。但注意,它不是越大越好——每个半连接在内核里都要占用内存和定时器资源,设置过大会消耗大量内存,反而造成性能下降。
net.ipv4.tcp_syncookies:SYN Cookie 机制的总开关。这是对抗 SYN Flood 最核心的一招。开启后(值为1),当半连接队列满时,内核不再丢弃新到的 SYN 包,而是利用 SYN+ACK 报文的序号字段,把源地址、端口、时间戳等信息通过哈希编码成一个 Cookie 值回给客户端。如果客户端是真实的,它会回一个带正确序列号的 ACK;内核验证序列号合法后,直接建立连接,不再依赖半连接队列。相当于餐厅开始发放“加密排队号”,恶意订餐的“顾客”拿不到有效号就没法进入。
net.ipv4.tcp_syn_retries:服务器发送 SYN+ACK 后,尝试重发的次数。SYN Flood 攻击时,服务器回完 SYN+ACK 等不到 ACK,就会反复重试,白白耗资源。调低这个值(比如改成 2),可以减少 SYN+ACK 的重发次数,加快无效半连接状态的回收。
net.ipv4.tcp_synack_retries:主动连接外部时 SYN 重试次数,攻击场景下一般用到它的场景不多,但保持默认或略调低均可。
net.ipv4.tcp_abort_on_overflow:当全连接队列(Accept Queue)溢出时,是否直接 RST 拒绝新连接。默认值为0,代表内核直接丢弃并让对端重试;设置为1时,内核直接发 RST 让对端快速失败。这个参数配合应用层队列长度设置,能避免客户端长时间悬挂等待。不过要注意,设为1可能会导致正常用户在突发流量下被快速断连重试,没那么友好。
net.ipv4.tcp_synack_retries与net.ipv4.tcp_syn_retries的作用方向相反但逻辑对称,前者是服务端收到 SYN 后回复 SYN+ACK 的重试次数,后者是主动发起连接时 SYN 的重试次数。攻击时一般把前者调低。
还有一个经常被忽视的参数:net.core.somaxconn。它限制了端口监听队列(Accept Queue)的上限。应用层调用listen(fd, backlog)时,backlog 会被内核截断到somaxconn以内。很多 MySQL、Redis、Java 服务默认请求的 backlog 是 128 或 512,如果somaxconn保持默认 128,应用层设置了 1024 也无效。防御 SYN Flood 时,全连接队列长度同样关键——即使半连接扛住了,全连接队列如果太短,瞬间的瞬时并发也会把队列打爆。所以调优时一定要把somaxconn提上来(比如 65535),应用层的 backlog 参数同步调大。
2.2 一套实测可用的sysctl配置
下面这套配置,是我在多个项目中实际用过的,可以算作一个基线模板。你完全可以在此基础上,结合业务情况进行微调。
# /etc/sysctl.d/99-syn-flood.conf # 半连接队列调大,给正常握手留足空间 net.ipv4.tcp_max_syn_backlog = 65536 # 开启 SYN Cookie,队列满后仍能维持服务 net.ipv4.tcp_syncookies = 1 # 降低 SYN+ACK 重试次数,加快无效资源回收 net.ipv4.tcp_synack_retries = 2 net.ipv4.tcp_syn_retries = 3 # 全连接队列上限调大,配合应用层 backlog net.core.somaxconn = 65535 # 空连接与 keepalive 优化,减少无用连接占用 net.ipv4.tcp_keepalive_time = 60 net.ipv4.tcp_keepalive_intvl = 30 net.ipv4.tcp_keepalive_probes = 5 # 端口与 TIME_WAIT 相关优化(辅助) net.ipv4.tcp_fin_timeout = 30 net.ipv4.tcp_tw_reuse = 1 net.ipv4.ip_local_port_range = 1024 65535改完执行sysctl -p /etc/sysctl.d/99-syn-flood.conf生效。验证配置是否写入,用sysctl net.ipv4.tcp_max_syn_backlog这种单参数查看方式,避免用sysctl -a | grep去拉全文,输出太冗长且很容易看花眼。
关于tcp_tw_reuse,这里多说一句:这个参数只对客户端(主动连接方)有意义,它在 NAT 环境下不会引发问题,只要你不碰tcp_tw_recycle。tcp_tw_recycle这个参数在传统运维教程里出镜率极高,但在新内核中已被移除(4.12+),而且它开启后通过 NAT 访问的客户端会被误杀,导致部分用户无法访问。看到网上老文章让你开tcp_tw_recycle的,直接忽略就好。
2.3 调优的边界和陷阱
参数不是越多越好、越大越好。我见过有人在配置里把tcp_max_syn_backlog直接拉到 262144,结果攻击还没来,机器内存先涨了一大截。每个半连接在内核里占用的内存不算大,但几十万个叠加起来,再乘以超时时间tcp_synack_retries,内存和 CPU 都会是压力。合理的做法是:先观察正常情况下高峰期半连接队列的占用峰值,再往上留 2~3 倍余量即可。普通中小型业务,4096 到 65536 这个区间基本够用。
另一个容易踩的坑是:改了tcp_max_syn_backlog,但应用层 Accept Queue 没同步调大,或者反过来,全连接队列调大了但半连接队列太小。两个队列是串联关系,一个短板就会限制整体吞吐。按前面给的配置模板,两条同步调整才有效果。
还有一点经验:临时验证没错,但上线前一定要把参数写进/etc/sysctl.d/下的独立配置文件,不要直接改/etc/sysctl.conf或/proc。用独立文件的好处是回滚容易——把文件删掉,sysctl -p重新加载默认文件下的配置即可。直接在/proc改系统参数,重启机器则丢失;直接在/etc/sysctl.conf里改,后续想排查是谁改了什么,历史记录也容易混乱。
提示:调优参数时要评估业务类型。Web 服务需要短连接快速建立,可以激进一点;数据库中间件这类长连接服务,SYN 攻击影响相对小,参数反而不要乱动,尤其不要开 syncookies 后把半连接队列拉太大,因为连接建立本身就不是瓶颈。
3. 流量清洗:在流量进门之前拦住攻击
3.1 单机层面的快速止损
内核参数是兜底,但在攻击流量刚开始的十分钟内,手边没有清洗设备、没有云高防的情况下,最直接的方法是靠防火墙限速,先止损再慢慢恢复策略。
iptables 可以用来限制单个来源IP的连接速率和并发连接数。下面是两个简单有效的规则:
# 限制单个IP的SYN包速率为每秒5个,超出部分丢弃 iptables -I INPUT -p tcp --syn -m limit --limit 5/s --limit-burst 10 -j ACCEPT iptables -A INPUT -p tcp --syn -j DROP # 限制单个IP对80端口的并发连接数不超过100条 iptables -I INPUT -p tcp --dport 80 -m connlimit --connlimit-above 100 -j REJECT --reject-with tcp-reset第一条规则用limit模块做速率限制,第二条用connlimit做并发限制。把这两条插到INPUT链靠前的位置,能在内核连接跟踪层就直接拦截掉恶意流量,消耗的资源远小于让协议栈去处理。
这种“限速+并发限制”方案的问题也很明显:如果攻击源IP分散,规则条数会堆得很多,而且 IPtables 规则多了会影响整体转发性能。所以在大型攻击面前,单机防火墙只能做临时止损,不能当主力防御。
另外提一句,很多人写防火墙规则时会只关注端口,忽略状态。完整写法应该先把已建立连接放行:iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT,避免把正常连接也误伤。
3.2 SYN Proxy与反向代理的作用
到了集群部署层面,负载均衡器和反向代理天然承担了一部分清洗职能。Nginx 和 HAProxy 都有 SYN Proxy 或类似机制,核心思路是:代理服务器替后端完成 TCP 三次握手,握手成功后,再和后端建立独立连接,转发数据。
这样做的好处显而易见:攻击流量被挡在代理层,代理帮你吞掉半连接,后端的业务服务器永远看不到伪造的 SYN 包。相当于餐厅在大门外先验一波号,恶意订餐的连餐厅大门都进不来。Nginx 从 1.11.4 开始内置了 SYN Flood 防护开关,监听 socket 时可以开启 defer_accept,配合proxy_bind等参数,能将未完成握手的连接延迟到收到数据后再建立上游连接。
负载均衡层做清洗还有一个额外收益:它能把 SYN Cookie 验证逻辑应用到大规模流量上,而不用消耗后端服务器的 CPU。实践中我推荐在这类组件的系统层同样做好内核调优,因为负载均衡器一旦被攻击流量打挂,整个集群都会受影响。负载均衡器上的参数和业务服务器类似,但数值可以更激进,因为它的职责就是接受大量连接再转发。
3.3 上游清洗与硬件防火墙
攻击流量达到一定规模、单机和负载均衡都扛不住时,就得靠上游链路了。这里的方案分几种:
- 云厂商 DDoS 高防:把业务接入高防 IP,DNS 解析指向高防 IP,所有流量先过清洗中心,清洗后再回源到你的服务器。适合有预算、不想自己折腾硬件的团队。
- IDC 清洗设备:很多自建机房的 IDC 都提供 DDoS 清洗服务,攻击峰值超过一定阈值后,机房的流量调度设备会把攻击流量牵引到清洗设备上,处理后再放行。这种方案需要提前和 IDC 确定好流量阈值和联系方式,真出事时再联系往往来不及。
- BGP 黑洞路由:把攻击目标 IP 的流量直接丢弃,相当于把业务“拔线”。这是彻底止损的招,但也意味着业务全线不可用,一般只针对极端攻击,或者作为最后保底手段。
选择哪种清洗方案,取决于业务对可用性的要求。能接受少量丢包,就用黑洞(流量直接丢弃);要求业务绝对不能中断,就要有回源能力的高防。成本差异也很大,需要结合预算做取舍。
3.4 清洗策略的联动设计
一个成熟的防御体系应该像层层设防的关卡,而不是只靠某一层。我画不出图,但可以用文字描述一下推荐的分层布防结构:
第一层是接入链路层的清洗(运营商黑洞、高防IP),负责过滤掉大部分攻击流量;第二层是四层负载均衡器(LVS、F5、ELB),开启 SYN Proxy,丢弃未完成握手的连接;第三层是应用层代理(Nginx/HAProxy),做连接队列管理和延时转发;第四层才是业务服务器的内核参数调优,兜底吸收漏网之鱼。
每一层都应该有对应的监控告警。高防有流量报表,负载均衡器有并发数监控,业务服务器看SYN_RECV数量和系统负载。层层联动,才能做到既能快速止血,又不影响正常用户。我在实际项目中还会把内核参数、iptables 规则都沉淀成配置模板,通过自动化工具(Ansible/SaltStack)批量下发,避免一台台手改耗费时间。
4. 实战验证与故障排查实录
4.1 搭建测试环境,模拟攻击流量
防御方案落地后,不能靠运气,必须自己有意识地做攻击测试。测试环境不需要太复杂,一台目标服务器、一台测试机即可。测试机上最常用的工具是hping3,它能精确控制 TCP 包的源地址、端口和发送速率,模拟出不同形态的 SYN Flood。
一个基本的攻击测试命令如下:
# 以伪造源IP 198.51.100.10,对目标192.0.2.10的80端口发起SYN Flood hping3 -S -p 80 --flood --rand-source 192.0.2.10--rand-source表示随机伪造源IP,--flood表示尽可能快发包。如果你想模拟固定的少数几个源IP,可以把--rand-source替换成-a 198.51.100.10。实际测试时,建议先从低速(比如每秒1000个包)开始,逐步提升,观察服务器在哪个临界点开始出现连接异常。这比一上来就全速打,更容易定位问题。
注意:测试一定要在隔离环境或者业务低峰期进行,且提前和相关同事打招呼。我见过有人拿生产环境做压力测试,结果把客户的真实流量也误伤了,场面极其尴尬。
4.2 攻防前后的状态对比
以一台 4C8G 的 CentOS 7 测试服务器为例,默认内核参数下,用hping3以每秒约 3000 个伪造 SYN 包发起攻击,观察到的现象是:
- 攻击前:
SYN_RECV连接数为 0,CPU 使用率 5% 以下,页面访问延迟约 30ms。 - 攻击后约 1 分钟:
SYN_RECV暴涨到 5000+,出现大量possible SYN flooding内核日志,CPU 的软中断(si)从 1% 蹿到 60%,80 端口新连接基本无法建立,页面访问直接超时。
接着把测试服务器应用第 2.2 节的内核参数,重新执行同样的攻击:
- 攻防中:
SYN_RECV依然存在,但稳定在数百到一两千之间,不再暴涨,内核日志不再报 flooding。 - 80 端口的新连接仍能正常建立,页面访问响应时间从 30ms 小幅升到 120ms 左右,但不至于中断。
- CPU 软中断虽然在 60% 左右徘徊,但硬中断和用户态 CPU 没有被打爆,服务可用。
这里的本质区别是:调优前,内核把每个半连接都切切实实保存在队列里等待超时,资源被一点一点吃光;调优后,SYN Cookie 机制让内核不再为无效半连接维护状态,攻击包虽然仍在涌入,但处理它们所需的资源被大幅压缩。简单说,调优前的服务器是在“硬抗”,调优后的服务器是在“巧卸”。
4.3 五个典型故障场景速查表
实战中,SYN Flood 攻击的表现千奇百怪,问题往往不在攻击本身,而在防御配置。下面这个速查表是我踩过坑后整理的,值得留在手边:
| 现象 | 可能原因 | 快速定位命令 | 解决方案 |
|---|---|---|---|
| SYN_RECV 数量大但不涨 | syncookies 生效,半连接队列被跳过 | ss -ant state syn-recv查看队列长度 | 加观察即可,必要时继续调大 backlog |
| 业务报错 connect timeout | 半连接队列满,握手包被丢 | netstat -s看SYNtoSTL计数 | 开启 syncookies,调大 backlog |
| 大量 connection reset | tcp_abort_on_overflow=1 触发 | nstat -az | grep Abort | 若误报正常用户,关闭该参数 |
| 应用连接数到顶 | 全连接队列板,应用层 backlog 不够 | ss -ltnp看 Recv-Q 是否满 | 调大 somaxconn 与应用 backlog |
| 访问延迟极不稳定 | keepalive 超时设置不合理 | cat /proc/sys/net/ipv4/tcp_keepalive_time | 调低 keepalive 间隔,快速回收死连接 |
| 网络层丢包但 CPU 空闲 | 带宽被占满,上游流量瓶颈 | sar -n DEV看 drop 率 | 接入高防或清洗链路,扩大带宽 |
上面表格里提到的netstat -s输出里有一堆计数器,平时没人看,但排障时非常有用。比如TcpExtTCPAbortOnOverflow增长说明全连接队列溢出次数在增多,TcpExtTCPSynRetrans增长说明 SYN+ACK 重传较多,这些都能辅助定位问题。
4.4 排障过程中的两条铁律
第一,不要在生产环境乱改参数。改内核参数要配置管理,要先在测试环境验证,要记录变更时间和人。我见过太多同行,一遇到攻击就急着把tcp_max_syn_backlog拉大、把 iptables 规则一股脑堆上去,结果攻击结束后,这些临时规则还留在线上,影响正常业务。正确做法是:每次变更前先在测试环境压测,记录基线数据,再上生产;攻击结束后,按变更记录把临时的防御策略梳理一遍,能撤的撤、能固化的固化。
第二,监控先行。没有告警就没有防御。内核参数调得再好,如果攻击发生时没人收到通知,一样是白搭。基本的监控项至少要覆盖:SYN_RECV连接数、半连接队列利用率(可通过ss -lnt观察Recv-Q)、软中断 CPU 使用率、整机负载、业务请求延迟。这些指标建议在攻击发生后的 5 分钟内就要能反映出异常。
我在实际项目中踩过的一个很坑的教训是:一直以为自己的服务器有高防,所以内核参数没调,结果某天高防对一两个大 IP 的清洗策略没触发,流量直接打到源站,服务瞬间挂掉。所以不管你有没有高防,源站服务器的内核参数都要按兜底标准去调,这是最后一道防线,必须有。
结尾
处理过几次 SYN Flood 之后,我的体感是:这类攻击在技术上并不高级,但它极其考验运维平时是否做足了准备。内核对 SYN Flood 的防御能力确实不弱,但只靠调参扛不了大流量,只靠清洗设备又存在策略切换空窗期,所以最稳妥的做法永远是“上游清洗+本机调优”两条腿走路。
最后分享一个小细节:调完sysctl后别急着走,用sysctl -a | grep tcp_max_syn_backlog和ss -lnt两条命令确认参数值和队列变化,再配合nstat -az | grep -i syn观察统计计数,基本上就能在攻击到来的第一时间判断出当前防御策略是否真正生效。防御 SYN Flood 不是一次性的项目,而是需要定期演练、定期 review 的持续性工作——把这个当成习惯,比临时抱佛脚靠谱得多。