1. 先看清SYN Flood打的是哪个部位:半连接队列的攻防博弈
有一类故障现象特别容易让人懵圈:CPU没满、内存没爆、带宽也没见顶,业务却突然大面积不可用,新连接全部超时。多数人第一反应是应用挂了,重启进程没用,看日志又找不到异常。直到在服务器上执行了ss -n state syn-recv | wc -l,看到SYN_RECV状态的连接堆了几万个,才明白这不是应用问题,而是TCP层被SYN Flood攻击打穿了。
SYN Flood能存在二十多年还不过时,是因为它攻击的不是某个具体软件漏洞,而是TCP三次握手协议设计时的一个信任假设:服务端收到客户端的SYN请求后,会无条件创建一条半连接记录、分配内核资源,然后等待客户端的ACK确认。这个“先信任、后确认”的机制在正常场景下没问题,但攻击者可以伪造大量不存在的源IP,发出海量SYN包且永远不回ACK,服务器的半连接队列就会被瞬间塞满。
打个比方:正常TCP握手就像餐厅的领位台——客人来了先登记留位,等服务生确认到场。SYN Flood攻击则是雇了一帮人假装登记,登记完就消失,很快把名册占满,真正的客人来了反而没法登记。Linux内核里负责这个“名册”的队列就是半连接队列(SYN队列),对应参数是tcp_max_syn_backlog;应用层真正取走连接的队列则是全连接队列(accept队列),大小由somaxconn和应用监听时传入的backlog共同决定。
需要注意一个反直觉的事实:SYN Flood流量往往不算巨大,不像UDP Flood动辄几十Gbps打满带宽,它讲究“小而密”。每个SYN包只有几十字节,服务器却要为它付出远大于攻击包本身的资源开销——TCB分配、路由查找、协议栈处理,每一步都比收一个普通包重得多。所以哪怕只有几百Mbps的SYN Flood,也可能让服务器到处是SYN_RECV,合法用户完全连不进来。这也是为什么很多人一开始会误判:资源一切正常,但连接就是建不起来。遇到这种“看起来没事、实际已瘫”的故障,第一个怀疑对象就应该是半连接队列被耗尽。
2. 诊断先行:怎么确认是SYN Flood而不是正常高并发
被攻击不可怕,可怕的是把攻击当成高并发,或者反过来把正常业务峰值当成攻击一顿瞎调。我见过有人开着tcp_syncookies正常用了好几年,某天业务做活动流量上来,顺手把tcp_max_syn_backlog调大,结果问题没解决,反而因为队列内存占用过高引发连锁故障。所以先诊断、再动手,这个顺序一定不能反。
2.1 从连接状态里找铁证
攻击发生时,第一时间进服务器执行:
ss -n state syn-recv | wc -l如果这个数字持续高于几千甚至过万,基本可以锁定是SYN Flood。再配合源IP聚类观察——用下面的命令看SYN_RECV连接的来源分布:
ss -n state syn-recv | awk '{print $4}' | awk -F: '{print $1}' | sort | uniq -c | sort -rn | head结果无非两种形态:源IP极其分散,说明是伪造源地址的分布式攻击;集中在少数IP,则可能是单一攻击源。两种都符合攻击特征。相对更可靠的证据是协议栈统计数据:
netstat -s | grep -i "SYN" cat /proc/net/netstat | grep -i "SYN"重点关注三个计数:SYNs to LISTEN sockets dropped(半连接队列溢出丢弃)、SyncookiesSent(内核启动SYN Cookie保护)、SyncookiesFailed(Cookie校验失败)。如果SyncookiesSent呈爆发式增长,说明半连接队列早就满了,内核已经在用SYN Cookie兜底——换句话说,攻击已经打到了内核防护的临界线。
2.2 用抓包验证攻击特征
想看得更细,可以短暂抓包:
tcpdump -i eth0 -n 'tcp[tcpflags] & tcp-syn != 0' -c 5000抓下来的包通常有几个典型特征:
- 同一源IP在极短时间内出现大量SYN包,且几乎不重传——攻击者根本不在乎握手是否完成
- 大量TCP会话停留在
SYN -> SYN-ACK这一步,永远等不到对应的ACK - 大概率伴随TTL跳变、窗口值异常、IP ID连续等伪造痕迹
- 源端口和源IP的组合不断变化,一看就是随机生成的地址
抓包不能长时间开在生产环境,tcpdump在大流量下本身会消耗CPU和磁盘,务必用-c限制数量,抓完立刻停。
2.3 区分正常高并发与攻击
大促、秒杀、热点事件引发的流量高峰同样会出现大量SYN_RECV,因为客户端本来就在排队完成握手,队列繁忙是正常现象。但两者有本质区别:
| 观察维度 | 正常高并发 | SYN Flood攻击 |
|---|---|---|
| 源IP分布 | 相对集中,符合业务区域特征,可回溯 | 异常分散或伪造,不可回溯 |
| 握手完成率 | ACK陆续回来,SYN_RECV会被逐渐消化 | 几乎没有ACK回来,SYN_RECV只增不减 |
| 时间形态 | 随业务周期波动,有起有伏 | 突发且持续,或周期性脉冲 |
| 应用层指标 | QPS和连接成功率正常或可预期 | 连接成功率骤降,错误率飙升 |
| 队列统计 | SyncookiesSent偶发但不持续 | SyncookiesSent持续快速攀升 |
正常高并发时做个简单的健康验证就能分辨:如果旧连接还能正常响应,只是新建连接变慢,多半是队列容量不足,未必是攻击;如果所有新建连接全军覆没,旧连接也持续断开,那基本就是攻击或拥塞级别的事件。
2.4 估算攻击规模,决定防御策略
诊断最后一步是评估攻击大小,这会直接决定后面选什么防御手段。粗略估算方法:用sar -n DEV 1或iftop观察对外的pps和带宽,再用tcpdump抓一秒内SYN包数量乘以源IP分布,估算攻击pps。如果攻击带宽在几百Mbps以内、pps在几十万以内,本地内核调优加iptables还能勉强撑住;一旦攻击流量超过单机网络出口的带宽能力,本地再怎么调都没用,必须把流量引导到上游清洗中心处理——否则清洗流量都进不来,防御无从谈起。这个判断是所有后续方案选型的前提。
3. 内核调优:一份可落地的sysctl配置清单与取舍说明
诊断确认是SYN Flood之后,第一道防线就是内核参数。这里必须强调一个观念:调优不是把参数调大就完事,而是要理解每个参数管什么、代价是什么,否则可能在低流量时把自己调出问题。
3.1 核心参数清单
下面这份配置是我在一台4核8G、承接常规Web业务的Linux服务器上常用的sysctl.conf片段,按实际压测情况调整而来,不同机型可以在此基础上按需改动:
# 开启SYN Cookie,半连接队列满时改用Cookie方式应答 net.ipv4.tcp_syncookies = 1 # 半连接队列上限,内存足够时调到8192~16384 net.ipv4.tcp_max_syn_backlog = 8192 # 全连接队列上限,配合应用listen backlog一起调整 net.core.somaxconn = 16384 # 网卡队列积压上限,防止软中断处理不过来时过度丢包 net.core.netdev_max_backlog = 4096 # SYN-ACK重传次数,攻击场景下调到1,避免半连接长期占用资源 net.ipv4.tcp_synack_retries = 1 # 被动连接SYN重传次数,同样调小 net.ipv4.tcp_syn_retries = 2 # accept队列溢出时立即发RST而非静默丢弃 net.ipv4.tcp_abort_on_overflow = 1 # 限制TIME_WAIT总量,防止异常连接堆积拖垮内存 net.ipv4.tcp_max_tw_buckets = 65536 # 缩短FIN_WAIT_2超时,加快连接资源回收 net.ipv4.tcp_fin_timeout = 30每个参数背后的逻辑值得细说。
tcp_syncookies是第一道王牌。开启后,当半连接队列占满时,内核不再额外分配TCB,而是根据源IP、目的IP、源端口、目的端口和一个密钥计算出序列号放进SYN-ACK,等客户端的ACK回来时再校验序列号合法性。合法客户端能正确回ACK,伪造源IP的攻击者根本收不到SYN-ACK,自然也就发不出校验通过的ACK——等于用无状态的手段把攻击隔绝在资源分配之外。这背后的数学依赖是密码学哈希的不可预测性,攻击者无法通过观测SYN-ACK反推出有效Cookie。
tcp_max_syn_backlog决定半连接队列能排多长的队。调大能在攻击初期留出更大的缓冲,但每个半连接都要占内核内存,调得过大反而可能拖垮内存。我一般按“单机预估每秒新建连接数×极端时长”来估算,常规业务8192足够,秒杀类活动可以临时调到16384,不建议再往上硬堆。
tcp_synack_retries和tcp_syn_retries是攻击场景下的加速回收开关。SYN-ACK发出后永远等不来ACK时,内核默认会按指数退避重传多次,把资源白白耗在半连接上。调到1~2后,每个连接最多重传两次就能被回收,半连接队列周转速度大幅提升。这个调整在正常业务下几乎无感知,因为正常连接很快就能完成第三次握手。
tcp_abort_on_overflow在accept队列满的时候决定行为:默认0是丢弃SYN,客户端无响应后自行重试;设为1是立即回RST,让客户端马上知道连不上、尽早报错。对内部系统来说RST更利于快速失败,对面向公网的业务则需要谨慎,因为RST同样占出向带宽。多数场景下我推荐保持1,方便客户端快速感知并重试。
somaxconn和netdev_max_backlog经常被忽略。前者限制应用层accept队列,如果应用自身backlog设得小,单纯调内核没用——很多语言的服务端框架默认backlog只有128,必须同步调大;后者限制网卡驱动收包后、软中断处理前能积压多少未处理的数据包,高pps攻击下这里如果太小,SYN包会在进入协议栈之前就被丢掉,后面再怎么调优都白搭。
3.2 配置生效与验证
配置完成后执行:
sysctl -p按这个顺序验证调优效果:
- 观察
SyncookiesSent是否还在暴增——如果暴增,说明当前攻击强度已经超过队列承受能力,内核在兜底保护 - 观察SYN_RECV连接数是否从持续上升转为维持稳定或回落
- 用真实客户端做新建连接测试,确认合法连接能够正常建立
我自己的验证习惯是:本地起一个小工具,每秒发起固定数量的正常TCP连接,同时观察抓包到的ACK回包率。攻击前ACK回包率接近100%,调优后只要保持在90%以上,服务就处于基本可用状态。
3.3 SYN Cookie的取舍:开启不是万能的
这里必须泼一盆冷水:tcp_syncookies = 1并不能完全保住所有服务。使用SYN Cookie时,内核不再对每个半连接维护完整的TCB状态,所以部分TCP扩展选项无法正常协商——比如时间戳、窗口缩放、SACK。这些选项在现代高速网络中对高带宽、长RTT链路的吞吐影响很大。也就是说,大规模攻击触发SYN Cookie兜底期间,大文件传输、跨机房高RTT连接可能出现吞吐下降。
实际影响多大,取决于业务类型。对绝大多数短连接、小请求的Web业务来说基本无感;对流媒体、大规模数据回源场景,影响会明显一些。处理思路很直接:不要把SYN Cookie当作常态,而是当作紧急兜底。日常保持开启没问题,但要在监控里紧盯SyncookiesSent,一旦触发频率过高,说明队列容量不足以支撑正常峰值,该做的是扩容或上流量清洗,而不是继续硬扛。
3.4 和SYN Flood相关的其他联动参数
调优时不要只盯着握手参数,以下几个联动项也值得一起处理:
net.ipv4.tcp_max_tw_buckets:TIME_WAIT状态的socket上限。攻击导致连接反复建立失败时可能堆积TIME_WAIT,这里设到65536可以防止出现TIME-WAIT Sockets: Exceeded报错。net.ipv4.tcp_fin_timeout:主动关闭后FIN_WAIT_2的持续时间,默认60秒,缩到30秒能加快连接资源回收。net.ipv4.ip_local_port_range:如果本机作为代理主动发起大量连接,临时端口范围太小会导致端口耗尽,这在攻击排查中容易被误判成SYN Flood的同类问题。net.core.rmem_max和net.core.wmem_max:攻击期间异常连接的缓冲占用会明显上升,按需调整,不要盲目拉大。
内核调优做到位之后,单机扛住中小规模SYN Flood已经有了把握。但真正的考验在后面:如果攻击流量打满带宽,或者源IP量大到本地封禁不过来,就必须转向流量清洗方案。
4. 本地流量清洗:iptables、recent与内核SYNPROXY的组合打法
内核参数能优化协议栈的承受能力,但识别不了恶意流量。流量清洗的思路是:在流量进入协议栈之前,先用防火墙或代理机制把明显的攻击包识别出来并丢弃,减少无效资源消耗。Linux上有几种成熟做法,按推荐程度从低到高有顺序地配合使用。
4.1 iptables限速:最基本的防线
先做最基础的SYN包速率限制:
iptables -A INPUT -p tcp --syn --dport 80 -m limit --limit 200/sec --limit-burst 400 -j ACCEPT iptables -A INPUT -p tcp --syn --dport 80 -j DROP这里--limit是平均速率,--limit-burst是初始突发容量。设置上有个容易踩的坑:limit模块本质是令牌桶,burst值太小会把短时间内的合法突发连接也拦掉——比如一次抢购瞬间涌入几千个真实SYN,桶只有400的话后面全部被DROP。我会把burst调到日常峰值的2倍左右,宁可让它在攻击时漏一些到内核,也不要误伤合法请求。
4.2 用recent模块做高频IP封禁
recent模块能在内核态维护一张源IP访问记录表,超过阈值就临时DROP。这是处理中小规模攻击时最常用的方案:
iptables -A INPUT -p tcp --syn --dport 80 -m recent --name SYNFLOD --set -m limit --limit 1/sec -j ACCEPT iptables -A INPUT -p tcp --syn --dport 80 -m recent --name SYNFLOD --update --seconds 60 --hitcount 50 -j DROP逻辑很直接:每个源IP如果在60秒内触发超过50次SYN,第二条规则就把它DROP掉。--set和--update的顺序极其关键,写反了第一条规则永远先放行,后面的计数器形同虚设。另外注意,recent表默认只能记录100个IP,攻击源IP一多表就会被占满,封禁随之失效。生产环境建议用--table参数指定更大的自定义表,或配合定期清理脚本维护。
4.3 SYNPROXY:内核态的握手代理
如果上面的方案还不够,内核3.12以后提供的SYNPROXY值得认真考虑。SYNPROXY和SYN Cookie一脉相承但更进一步:它把三次握手完全接管到内核态,客户端先与SYNPROXY完成握手,确认是真实客户端之后,才把连接交给后端的协议栈和应用。攻击包在握手阶段就被大量消耗,后端协议栈根本感知不到攻击流量。
开启SYNPROXY的标准流程:
# 加载相关模块 modprobe nf_conntrack_synproxy # 在raw表跳过conntrack,避免连接跟踪表被攻击打爆 iptables -t raw -A PREROUTING -p tcp --dport 80 --syn -j CT --notrack # 将未跟踪的SYN转向SYNPROXY代理 iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate INVALID,UNTRACKED -j SYNPROXY --sack-perm --timestamp --wscale 7 --mss 1460 # 兜底丢弃未完成代理的连接 iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate INVALID,UNTRACKED -j DROP初次使用SYNPROXY时有几个细节必须注意。一是必须配合CT --notrack,否则conntrack表会在攻击下先被填满,SYNPROXY还没开始工作自己先挂了。二是--wscale、--mss等参数是为代理协商准备的,要和实际网络环境匹配,RTT偏大的链路不要开过大的窗口缩放。三是SYNPROXY对内核版本有要求,老内核需要编译补丁,生产环境务必先在测试机上完整验证一轮再上线。
4.4 本地防御的极限在哪里
不管内核参数和iptables怎么调,本地防御始终有一个不可逾越的物理极限:带宽。SYN Flood如果以几十Gbps的规模打到单机,交换机和物理链路早就被占满,规则再精妙也没用——网卡已经处理不过来了。本地防御适合的量级是:带宽远未打满、但连接建立能力被打瘫的中小规模攻击。一旦pps超过单机处理上限,或带宽被占满,正确的动作不是继续在本地加规则,而是把防护重心切换到上游清洗。这条经验很现实——有人对着被大流量打满的服务器连续折腾iptables和内核参数折腾了一下午,纯属浪费时间。
5. 流量清洗的架构级方案:单机不再是主角
攻击规模跨过单机承载线时,防御重心就要从“服务器本地调优”转移到“网络侧清洗”。这个阶段真正要考虑的问题有两个:如何让攻击流量在到达服务器之前就被过滤掉,以及清洗后的正常流量如何顺利回源。
5.1 高防IP与清洗中心:流量在入口就被处理掉
最常见的做法是接入云清洗服务,把域名的DNS解析指向清洗IP,所有流量先经过清洗中心的防火墙集群,对攻击特征进行识别和过滤,在骨干网侧拦截疑似SYN Flood的流量,再把干净流量送回源站。这类服务的选型有几个要点:
- 清洗能力:关注每秒处理pps和带宽清洗上限,而不只是看总带宽。SYN Flood本质是连接数攻击,pps指标远比带宽重要
- 回源方式:确认清洗后的流量是直接回源还是需要走特定回源线路,链路延迟是否在可接受范围
- 防护阈值:设置合理的告警和自动切换阈值——不要等业务彻底不可用才切换,也不要正常波动就频繁切换
接入清洗后并不是一劳永逸。攻击者如果拿到了源站IP,可以绕过清洗直接打源站,所以源站侧仍需保留内核调优和iptables作为第二道防线,同时遵循最小暴露原则:对公网只开放必需的端口,管理端口走内网或专用远程管理通道访问。
5.2 用负载均衡设备做SYN代理:把握手留在入口
架构中如果已经有负载均衡层,可以充分利用它的SYN代理能力。负载均衡器在客户端和真实服务器之间充当握手代理:客户端先和负载均衡器完成TCP握手,确认是真实连接后,负载均衡器再与后端服务器建立连接。攻击流量在最外层设备就被消耗掉,后端应用服务器完全接触不到畸形SYN流。
这套思路和内核SYNPROXY相似,但优势在于设备性能更强、规则更灵活、可以横向扩展。主流负载均衡方案都支持SYN Proxy模式。落地时需要特别注意:开启SYN代理后,后端服务器看到的源IP会变成负载均衡器的IP,应用如果依赖客户端真实IP做业务逻辑,必须额外处理X-Forwarded-For头或设计真实源IP传递通道,否则会出现“通过代理访问没问题、业务数据却对不上客户端”的尴尬情况。
5.3 可观测性与应急响应:防御体系里最容易被忽略的一环
无论调优还是清洗,都离不开监控。SYN Flood防御真正做得好不好,不取决于攻击来临时反应多快,而取决于平时能否在攻击发生的第一时间拥有自动化的感知和处置链路。推荐至少覆盖以下指标:
- 半连接数和全连接队列溢出次数(
ss -n state syn-recv数量) SyncookiesSent、SyncookiesFailed的速率环比变化- 网卡pps、带宽、丢包率,用
sar -n DEV 1持续采集并保留历史 - 新建连接成功率,最好能模拟真实用户从外部做探测
- 源IP汇聚统计,辅助判断攻击是集中还是分散
将这些指标接入告警后,再配合自动化脚本做分级响应:半连接数异常上升时先触发iptables临时限速;pps超过本地处理阈值时自动切换高防IP;攻击结束后自动恢复。应急过程中的所有操作留足日志,方便事后复盘攻击特征和防御效果。这个看似枯燥的闭环,实际价值往往比调几个参数重要得多——监控完备的系统和裸奔的系统面对同一次攻击,恢复时间可能差好几倍。
本地调优、流量清洗、架构改造这三层做好之后,常规SYN Flood基本就伤不到你了。最后需要再提醒一点:防御手段本身也可能被攻击者探测和绕过,比如攻击者故意伪装成高可信源IP段来规避限速规则,或者用低频慢速SYN慢慢消耗队列。所以防御方案要定期演练,不要把配置写好就去睡觉。我个人的习惯是每个月挑一个低峰窗口,模拟一次小型SYN Flood流量做测试,验证清洗链路是否通畅、规则是否生效、告警是否及时触发。这个做法成本不高,但能确保真正出事的时候,所有防御环节都在正常运作,而不是堆了一堆没有验证过的配置在服务器里吃灰。