小包DDoS攻击的可怕之处不在于流量有多大,而在于它用极小的代价就能耗尽服务器的计算资源。一个64字节的SYN包,经过网卡中断、内核协议栈处理、连接跟踪表创建、socket缓冲区分配这一整套流程,消耗的CPU周期远超转发一个大包。当每秒涌入数百万个小包时,系统中断请求(IRQ)会瞬间飙满单个CPU核心,导致软中断(softirq)占用率接近100%,用户态程序几乎得不到调度,服务器看起来像死机一样。解决这个问题的核心思路不是增加带宽,而是调整内核的网络参数,让内核以更高效的方式处理数据包,同时在入口处丢弃恶意流量。

调整网卡中断亲和性与多队列支持

现代服务器通常配备多核CPU,但默认情况下所有网卡中断可能都绑定到CPU 0上。这意味着无论你有多少个核心,处理网络数据包的工作全压在一个核心上。首先要做的是启用网卡的多队列功能,并将中断均匀分配到多个CPU核心。查看网卡是否支持多队列,执行:

ethtool -l eth0

如果显示Combined字段大于1,说明硬件支持。设置队列数量与CPU核心数匹配:

ethtool -L eth0 combined 8

然后使用irqbalance服务或者手动设置中断亲和性。手动设置更可控,先查看中断号:

cat /proc/interrupts | grep eth0

将每个中断的smp_affinity值设置为对应CPU核心的掩码。例如将中断号123绑定到CPU 2:

echo 4 > /proc/irq/123/smp_affinity

更现代的做法是使用RPS(Receive Packet Steering)和RFS(Receive Flow Steering),即使网卡硬件队列不够,也能在内核层面将数据包处理分散到多个CPU。启用RPS:

echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

这里的ff是十六进制掩码,表示8个CPU核心都参与处理。每个接收队列都要设置。RPS通过哈希计算将数据包交给不同的CPU处理软中断,能显著缓解单核瓶颈。RFS则进一步把数据包导向应用层所在CPU,减少缓存失效,设置全局表大小:

echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

再为每个接收队列设置:

echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
调整内核网络缓冲区大小

小包攻击的特征是包速率极高而非字节量大。默认的内核缓冲区往往偏小,一旦缓冲区溢出,内核就开始丢包,但丢包策略不当会连正常流量一起丢弃。增大缓冲区能让系统在短时间内吸收更多突发流量,给上层检测机制争取时间。关键参数在/proc/sys/net/core/下:

net.core.rmem_default = 212992
net.core.rmem_max = 16777216
net.core.wmem_default = 212992
net.core.wmem_max = 16777216

rmem是接收缓冲区,wmem是发送缓冲区。default是默认值,max是允许通过setsockopt设置的最大值。对于高PPS场景,rmem_max建议设到16MB甚至更大。但光改这里不够,协议层的缓冲区也要同步调整:

net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.udp_mem = 4096 87380 16777216

tcp_rmem和tcp_wmem的三个值分别是最小值、默认值和最大值。udp_mem是UDP协议的内存限制,三个值分别代表内存页数的最小值、压力阈值和最大值。小包攻击经常利用UDP协议,因为UDP无连接、发送简单,调整udp_mem能限制UDP占用的内存总量。另外net.core.netdev_max_backlog这个参数极其关键,它定义了网卡驱动将数据包交给内核协议栈之前,内核在软中断上下文中能排队的最大数据包数量:

net.core.netdev_max_backlog = 5000

默认值通常是1000,在高PPS场景下远远不够,建议设到5000甚至10000。如果看到/proc/net/softnet_stat第二列有非零值,说明队列满过,数据包被丢弃了,这时候必须增大这个值。

缩短TIME_WAIT状态与优化连接跟踪

小包攻击很多时候采用短连接形式,每个连接只发几个包就断开,导致服务器上积累大量TIME_WAIT状态的连接。TIME_WAIT状态默认持续60秒,连接数一多,连接跟踪表(conntrack)就被占满。首先要启用TIME_WAIT状态的快速回收和重用:

net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0

注意tcp_tw_recycle在较新内核中已被移除,因为它在NAT环境下会造成问题。tcp_tw_reuse设为1允许将TIME_WAIT状态的socket用于新的连接。更直接的方式是缩短TIME_WAIT的超时时间,但内核没有直接暴露这个参数,可以通过调整tcp_fin_timeout来缩短FIN_WAIT_2状态的时间:

net.ipv4.tcp_fin_timeout = 30

默认60秒,改为30秒能更快释放半连接资源。对于连接跟踪表,要增大其最大容量和哈希表大小:

net.netfilter.nf_conntrack_max = 2097152
net.netfilter.nf_conntrack_buckets = 524288

nf_conntrack_max控制连接跟踪的最大条目数,nf_conntrack_buckets是哈希桶数量,通常设为max的四分之一。还要缩短已建立连接的超时时间:

net.netfilter.nf_conntrack_tcp_timeout_established = 3600

默认432000秒(5天),对于Web服务器来说太长了,改为3600秒(1小时)更合理。对于UDP,超时时间也要缩短:

net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 60

如果服务器不需要连接跟踪功能(比如纯静态内容服务器或使用无状态协议的内部服务),可以考虑直接禁用连接跟踪模块,但这需要谨慎评估业务影响。

SYN Flood防护参数调优

SYN Flood是小包攻击中最常见的形式。攻击者发送大量SYN包但不完成三次握手,服务器为每个SYN分配资源后等待ACK,资源被耗尽。内核的SYN Cookie机制能有效应对,当SYN队列满时,内核不再为新的SYN请求分配资源,而是用加密算法生成一个特殊的序列号作为SYN+ACK的确认号,只有收到合法的ACK回应时才真正分配资源。启用SYN Cookie:

net.ipv4.tcp_syncookies = 1

同时调整SYN队列和Accept队列的大小。SYN队列是存放半连接(收到SYN但未完成握手)的地方,大小由tcp_max_syn_backlog控制:

net.ipv4.tcp_max_syn_backlog = 8192

Accept队列是已完成握手但尚未被应用层accept的连接队列,大小由somaxconn和应用程序的backlog参数共同决定:

net.core.somaxconn = 4096

另外tcp_synack_retries控制SYN+ACK的重传次数,默认是5次,每次重传间隔翻倍,总共耗时约180秒。攻击场景下应该减小这个值:

net.ipv4.tcp_synack_retries = 2

tcp_syn_retries控制客户端SYN包的重传次数,同样建议减小:

net.ipv4.tcp_syn_retries = 2

启用tcp_abort_on_overflow能在Accept队列满时直接发送RST包,让客户端快速重试而不是等待超时:

net.ipv4.tcp_abort_on_overflow = 1

但开启这个参数会让正常客户端在服务器繁忙时收到RST,可能影响用户体验,需要根据业务场景权衡。

利用iptables限制连接速率

内核参数调优只能提升系统的承受能力,真正的防护还需要在入口处识别并丢弃攻击流量。iptables的hashlimit和recent模块能实现基于IP的连接速率限制。使用hashlimit模块限制每个源IP的新连接速率:

iptables -A INPUT -p tcp --dport 80 -m hashlimit \
  --hashlimit-name http_limit \
  --hashlimit-above 50/sec \
  --hashlimit-burst 100 \
  --hashlimit-mode srcip \
  -j DROP

这条规则限制每个源IP对80端口的新连接速率不超过每秒50个,突发容量为100个。hashlimit基于哈希表实现,性能比recent模块好得多。对于UDP小包攻击,限制每个IP的UDP包速率:

iptables -A INPUT -p udp --dport 53 -m hashlimit \
  --hashlimit-name dns_limit \
  --hashlimit-above 100/sec \
  --hashlimit-burst 200 \
  --hashlimit-mode srcip \
  -j DROP

更精细的做法是结合ipset和iptables。ipset使用哈希存储大量IP地址,匹配效率极高。先创建ipset集合:

ipset create blacklist hash:ip timeout 3600

然后用iptables将超过阈值的IP加入黑名单:

iptables -A INPUT -p tcp --dport 80 -m recent --name http_scan --rcheck --seconds 60 --hitcount 100 -j SET --add-set blacklist src
iptables -A INPUT -m set --match-set blacklist src -j DROP

这样一旦某个IP在60秒内发起超过100次连接,就被加入黑名单并封锁3600秒。iptables规则顺序很重要,要把这些限速规则放在最前面,避免攻击流量穿透到后端应用。

调整内核软中断调度与CPU隔离

当数据包速率极高时,软中断处理会抢占CPU资源,导致用户进程得不到调度。可以通过调整软中断处理的预算来控制。net.core.netdev_budget定义了每次软中断处理循环中最多处理的数据包数量:

net.core.netdev_budget = 600

默认值300,增大能让单次软中断处理更多包,减少中断次数,但也可能增加延迟。net.core.netdev_budget_usecs定义了每次软中断处理的最长时间(微秒):

net.core.netdev_budget_usecs = 8000

这两个参数需要配合使用。如果追求低延迟,保持默认值;如果追求高吞吐,适当增大。更极端的做法是使用CPU隔离技术,将特定的CPU核心专门用于处理网络中断,其他核心留给应用。在GRUB配置中添加isolcpus参数:

isolcpus=2,3,4,5

这样CPU 2到5不会被内核调度普通进程,然后手动将网卡中断绑定到这些隔离核心上。应用进程则运行在CPU 0和1上,互不干扰。这种方式能有效避免小包攻击导致的软中断风暴影响业务进程。

监控与验证调优效果

调优完成后必须通过监控验证效果。重点关注以下指标:/proc/net/softnet_stat中每行的第二列,如果持续增长说明netdev_max_backlog不够大;/proc/interrupts观察中断在各CPU的分布是否均匀;ss -s查看TIME_WAIT状态连接数;conntrack -S查看连接跟踪表使用率。使用sar命令观察软中断CPU占用:

sar -u ALL 1

其中%soft列如果持续接近100%且集中在少数核心,说明中断分配或RPS配置还有优化空间。使用hping3模拟小包攻击测试防护效果:

hping3 -S --flood -p 80 目标IP

观察系统负载、丢包率和应用响应时间的变化。调优是一个迭代过程,需要根据实际流量特征持续调整参数。没有一套参数适合所有场景,关键在于理解每个参数背后的机制,根据监控数据做出针对性调整。