TCP三次握手的本质是在不可靠的IP网络上建立起可靠的端到端连接,这个过程天然存在资源不对等。客户端发送一个SYN包,服务器就要分配传输控制块(TCB),记录序列号、窗口大小、状态机等一系列元数据。这种设计决定了握手阶段就是攻击者最青睐的发力点,因为防御方在这里的每一次资源投入,都对应着攻击者极低的成本。理解三次握手中每一个状态转换的资源消耗,就能找到防御空间的边界和极限。
SYN_SENT状态下的第一道防线客户端发出SYN包后进入SYN_SENT状态,服务器收到这个包后立即进入SYN_RCVD状态并回复SYN-ACK。关键点在于,服务器在收到SYN包的那一刻就已经分配了内存资源。现代Linux内核虽然采用了SYN Cookie机制来延迟分配资源,但在默认配置下,内核仍然会为每个SYN包创建一个轻量级的请求结构体。攻击者利用的就是这个时间窗口——大量伪造源IP的SYN包涌向服务器,每个包都迫使服务器做出响应。
在这个阶段,防御的核心思路是识别伪造源IP。真实客户端的SYN包会完成后续握手,而攻击流量往往来自反射源或者肉鸡,要么永远不会完成第三次握手,要么源IP根本不存在。基于这个特征,可以在网卡驱动层面或者XDP(eXpress Data Path)层面做第一轮过滤。XDP程序运行在内核网络栈之前,可以直接丢弃数据包而无需经过完整的内核协议栈处理。通过维护一个轻量级的IP信誉库,对历史行为异常的源IP直接丢弃SYN包,能把大量无效流量挡在协议栈之外。
具体实现上,可以用eBPF编写XDP程序,统计每个源IP在单位时间内的SYN包数量。如果某个IP在1秒内发送超过设定阈值的SYN包,且从未完成过三次握手,就将其标记为可疑源并直接丢弃后续SYN包。这种做法的优势在于,判断逻辑在数据包进入内核协议栈之前就执行完毕,CPU开销极低,单核就能处理数百万pps的SYN洪水。
SYN_RCVD状态的资源博弈服务器回复SYN-ACK后进入SYN_RCVD状态,这是整个三次握手中防御方最被动的阶段。此时服务器已经为这个半连接分配了资源,正在等待客户端的ACK确认。攻击者如果持续发送SYN包而不完成握手,服务器的半连接队列就会迅速耗尽,导致新来的正常SYN包被丢弃。
SYN Cookie技术是应对这个问题的经典方案。它的核心思想是把服务器需要保存的状态信息编码到SYN-ACK包的序列号中,然后释放本地资源。当客户端回复ACK时,服务器从确认号中解码出原始状态,重建连接。这样一来,服务器在SYN_RCVD状态完全不保存任何连接信息,半连接队列的概念被彻底消除。Linux内核从2.6版本开始就支持SYN Cookie,通过net.ipv4.tcp_syncookies参数控制。当半连接队列满时,内核自动启用SYN Cookie,不需要应用层参与。
但SYN Cookie并非没有代价。编码到序列号中的信息有限,无法携带TCP选项协商的全部内容,比如窗口缩放因子、选择性确认(SACK)、时间戳等。这意味着使用SYN Cookie建立的连接会丢失部分性能优化,在高带宽长肥网络环境下吞吐量会下降。所以SYN Cookie更适合作为应急机制,而不是默认策略。更精细的做法是动态调整半连接队列大小,结合连接建立速率监控,在队列接近满载时才触发SYN Cookie。
另一个容易被忽视的防御点是SYN-ACK重传机制。服务器发出SYN-ACK后,如果没有收到ACK,会按照指数退避算法进行重传。攻击者可以利用这一点,故意不完成握手,迫使服务器反复重传SYN-ACK,消耗出口带宽和CPU资源。调整net.ipv4.tcp_synack_retries参数,减少重传次数,可以降低这种消耗。在攻击场景下,把重传次数从默认的5次降到2次甚至1次,能显著减轻服务器负担,而正常用户由于网络延迟导致的重传失败概率并不高。
半连接队列的底层机制与调优半连接队列在Linux内核中的实现比表面看起来复杂。它实际上由两个队列组成:一个是syn_table,存储原始SYN包信息;另一个是request_sock_queue,存储经过SYN Cookie验证或者未启用SYN Cookie时的半连接。这两个队列的大小分别由net.ipv4.tcp_max_syn_backlog和net.core.netdev_max_backlog控制,同时还受到应用层listen()函数backlog参数的影响。
很多运维人员只调整tcp_max_syn_backlog,却忽略了somaxconn这个内核参数。somaxconn定义了每个套接字监听队列的最大长度,它是listen() backlog参数的上限。如果somaxconn设置得过小,即使半连接队列足够大,完成三次握手后的连接也无法进入accept队列,导致正常连接被丢弃。在高并发场景下,somaxconn至少应该设置为65535,同时应用程序的listen() backlog也要相应调大。
更深层的优化在于理解内核如何处理SYN包的到达速率。当SYN包到达速率超过内核处理能力时,网卡会将数据包暂存在ring buffer中,由net.core.netdev_max_backlog控制。如果ring buffer也满了,数据包就会被网卡直接丢弃。这意味着防御DDoS不仅仅是协议栈的事,还涉及到网卡硬件队列和内核软中断的协同。使用多队列网卡配合RSS(Receive Side Scaling),将SYN包的处理分散到多个CPU核心上,能线性提升SYN包的处理吞吐量。
利用TCP时间戳进行源验证TCP时间戳选项最初是为计算RTT和防止序列号回绕设计的,但在防御DDoS方面有意想不到的用途。服务器可以在SYN-ACK中嵌入加密的时间戳,客户端必须在ACK中原样返回。攻击者如果伪造源IP,就无法收到SYN-ACK,自然也无法返回正确的时间戳。这种机制比SYN Cookie更轻量,因为它不需要完全放弃状态保存,只需要验证客户端是否真正收到了SYN-ACK。
具体做法是,服务器在收到SYN包后,不立即分配完整的TCB,而是先计算一个带密钥的哈希值,将其作为时间戳放入SYN-ACK。当ACK到达时,服务器验证时间戳中的哈希值是否正确。如果正确,说明客户端确实收到了SYN-ACK,源IP是真实的,此时再分配完整资源。如果验证失败,直接丢弃ACK,服务器端没有任何资源损失。这种方法保留了TCP选项协商的能力,因为服务器在验证通过后才真正建立连接,此时可以正常协商窗口缩放等选项。
实现这个机制需要在网络栈中做一定的定制,标准Linux内核目前没有直接支持。但可以通过修改内核的tcp_v4_conn_request函数,在生成SYN-ACK时替换时间戳字段,在tcp_v4_do_rcv处理ACK时进行验证。对于无法修改内核的场景,也可以用eBPF程序挂载到相应的内核函数上实现类似逻辑。
反向代理与CDN的握手隔离把三次握手的终止点前移到边缘节点,是当前大规模防御体系的核心策略。反向代理或者CDN节点与客户端完成三次握手,然后通过已建立的持久连接将请求转发给源站。这样源站根本不会接触到来自攻击者的SYN包,所有握手阶段的攻击都被边缘节点承担。
边缘节点本身需要具备强大的SYN洪水防御能力。现代CDN服务商在边缘节点部署了专用的SYN Proxy模块,其工作原理是:节点收到SYN包后,代替源站回复SYN-ACK,同时不向后转发任何信息。只有当客户端回复ACK完成三次握手后,节点才将这个已验证的客户端IP和连接信息告知后端,后端再建立与节点之间的内部连接。这个过程对客户端透明,但攻击流量被完全隔离在边缘。
自建反向代理也可以实现类似效果。Nginx的stream模块配合proxy_protocol协议,能在四层做握手终止。Nginx与客户端完成三次握手后,通过proxy_protocol将客户端的真实IP传递给后端应用。后端应用只需要处理已验证的连接,不会受到SYN洪水的影响。这种架构下,反向代理服务器需要专门针对SYN洪水做内核参数优化和硬件加速,比如使用DPDK绕过内核网络栈直接处理数据包,或者使用支持硬件SYN Proxy的智能网卡。
连接建立后的持续验证三次握手完成并不意味着攻击防御的结束。攻击者完全可以用真实IP完成握手,然后在应用层发起慢速攻击或者HTTP洪水。因此,握手阶段的防御必须和应用层的防护联动。在握手完成后,立即对连接的行为特征进行分析,如果某个新建立的连接在短时间内发送的数据模式异常,就应该将其标记为可疑并主动断开。
这种联动可以通过eBPF程序实现,在tcp_set_state函数上挂载探针,当连接状态变为ESTABLISHED时,将连接信息传递给用户态的检测程序。检测程序根据预设规则判断是否需要保留这个连接。这种做法的好处是响应速度极快,连接在进入应用层处理之前就被筛选了一遍,减少了应用服务器的无效负载。
综合来看,TCP三次握手阶段的防御空间存在于协议栈的每一层:硬件层可以用智能网卡做硬件过滤,驱动层可以用XDP做早期丢弃,协议层可以用SYN Cookie和时间戳验证做无状态握手,架构层可以用反向代理做握手隔离。每一层都有可操作的具体手段,而且这些手段可以组合使用形成纵深防御。理解这些机制的原理和适用场景,才能在面对不同规模和类型的DDoS攻击时,做出最有效的防御策略选择。
