缓解 DDoS,本质上是丢弃数据包。本文讲述了如何在单核情况下一秒钟丢弃 1000 万个数据包,从最简单的用户态丢弃到使用非常新的技术 XDP,逐步将单核丢包性能提升到10 m/s
原文地址:https://blog.cloudflare.com/how-to-drop-10-million-packets/
原文标题:How to drop 10 million packets per second
原文作者:Marek Majkowski
原文写于:2018/7/6 GMT+8 下午9:00:00
译者:驱蚊器喵#ΦωΦ
翻译水平有限,有不通顺的语句,请见谅。
在团队内部,我们的 DDoS 缓解团队有时被称为”数据包丢弃者”。当其他的团队使用流经我们网络的流量,构建了很棒的产品来做一些智能的事情, 我们以找到丢弃这些数据包的新方法而感到快乐。
CC BY-SA 2.0 image by Brian Evans
在抵抗 DDoS 攻击中,能够快速的丢弃数据包显得非常重要。
丢弃攻击服务器的数据包,听起来很简单,可以在网络的多个层级做到。每种技术都有他的优点和局限。在这篇博文中,我们将回顾到目前我们尝试的所有技术。
验证阶段
我们将展示一些数字,来说明这些方法的相对功能。基准测试是用数字合成的,所以请对这些数字持保留态度。 我们将使用一台 Intel 服务器,配备 10Gbps 网卡。硬件细节不太重要,因为基准测试是针对操作系统,而不是硬件的瓶颈。
我们的测试步骤如下:
我们发送大量的小型UDP数据包,峰值达到14Mpps(每秒百万个数据包).
流量是针对目标服务器的单个CPU.
我们测量单个CPU的内核能够处理多少数据包.
我们没有设置用户空间应用程序的最大速度值,也没有设置数据包的最大吞吐量,相反,我们是想专门展示内核的瓶颈。
合成流量使用随机的源ip和端口,准备对 conntrack
(连接追踪)施加最大的压力。tcpdump
显示如下:
1 | $ tcpdump -ni vlan100 -c 10 -t udp and dst port 1234 |
在目标服务器上,所有的数据包都将会被转发到一个RX
(接收)队列, 因为只有一个CPU. 我们通过控制硬件流来实现:
1 | ethtool -N ext0 flow-type udp4 dst-ip 198.18.0.12 dst-port 1234 action 2 |
基准测试总是很难进行的.当准备测试的时候,我们才了解到任何活跃的流数据包都会破坏性能表现.事后来看这是很明显的,但是容易被忽略。在进行测试之前,请记住要确保没有任何 tcpdump
进程在运行。以下展示了如何来检查,并且显示有个正在活跃的进程:
1 | $ ss -A raw,packet_raw -l -p|cat |
最后,我们要在机器上关闭 Intel Turbo Boost 功能:
1 | echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo |
因为 Turbo Boost 技术是不错的,提升了至少20%的吞吐量,也极大的恶化了我们测试中的标准误差. 当开启 Turbo Boost 的时候,会对数值有 ±1.5% 的误差。当关闭 Turbo Boost 后,误差降低到可控的 0.25%.
Step 1. 使用程序丢弃数据包
让我们开始将数据包传送到应用程序,并在用户空间的代码中忽略他们。为了测试,我们要确保iptables 不会影响性能:
1 | iptables -I PREROUTING -t mangle -d 198.18.0.12 -p udp --dport 1234 -j ACCEPT |
程序代码是一个简单的循环,接收数据包然后在用户空间(userspace)马上丢弃掉:
1 | s = socket.socket(AF_INET, SOCK_DGRAM) |
使用我们准备好的代码, 运行如下:
1 | $ ./dropping-packets/recvmmsg-loop |
这样的设置会允许内核从硬件进站队列接收仅 175kpps 的数据包, 这里我们用 ethtool
计算数据并用 mmwatch
工具展示:
1 | $ mmwatch 'ethtool -S ext0|grep rx_2' |
硬件从网线接收到 14Mpps 的数据包,但是不可能将这些数据包传送到只有一个 CPU 核在进行内核处理的单RX队列中. mpstat
证明如下:
1 | $ watch 'mpstat -u -I SUM -P ALL 1 1|egrep -v Aver' |
你可以看到,应用程序代码不是瓶颈, CPU #1 在系统上花费了 27% 资源,在用户空间 2% ,而在 CPU #2 上网络的 SOFTIRQ 使用了 100% 的资源.
顺便说一下,使用 recvmmsg(2)
是重要的. 在 Spectre 漏洞爆发后的时间里, 系统调用变得珍贵且不可替代,我们使用开启了 KPTI (译者注:内核页表隔离,Meltdown的防御方案) 和 retpolines (译者注:避免 Spectre 攻击的修毕方案) 的 4.14版本内核:
1 | $ tail -n +1 /sys/devices/system/cpu/vulnerabilities/* |
Step 2. 关闭 conntrack
我们特别定制的测试,选取随机的源IP和端口,向连接追踪(conntrack)层施加压力。这可以通过查看 conntrack 的条目数据来验证, 在本次测试中已经达到峰值:
1 | $ conntrack -C |
你还能在 dmesg
中看到 conntrack 在嘶吼:
1 | [4029612.456673] nf_conntrack: nf_conntrack: table full, dropping packet |
为了加速我们的测试,我们将 conntrack 禁用:
1 | iptables -t raw -I PREROUTING -d 198.18.0.12 -p udp -m udp --dport 1234 -j NOTRACK |
-j NOTRACK
用于禁用 conntrack。
然后重新运行测试:
1 | $ ./dropping-packets/recvmmsg-loop |
这样瞬间将应用程序的接收性能提升至了333kpps. 哇!不可思议!
PS. 使用 SO_BUSY_POLL
我们可以将数字提升到470k pps,但是这不是本文的重点。
Step 3. 使用 BPF 进行丢包
深入思考一下, 为什么我们要把数据包传送到用户空间的应用程序? 我们可以使用setsockopt(SO_ATTACH_FILTER)
,将传统的 BPF 过滤器附加到 SOCK_DGRAM 套接字上,并通过在过滤器上编写代码,在内核空间中丢弃数据包,虽然这种技术并不常见。
点击查看代码, 运行代码:
1 | $ ./bpf-drop |
随着在 BPF (传统的BPF和升级版的eBPF,性能都差不多)中的丢弃数据包,我们大约处理了512kpps 数据. 所有的数据包都在 BPF 过滤器中被丢弃了,同时因为处于软件的中断模式中,这节约了唤醒用户空间程序所需要的 CPU 资源。
Step 4. 在路由之后使用 iptables 丢包
作为下一步,我们可以在 iptables 防火墙上简单的丢弃数据包,设置防火墙的 INPUT 规则链,添加以下规则:
1 | iptables -I INPUT -d 198.18.0.12 -p udp --dport 1234 -j DROP |
记着,我们已经通过-j NOTRACK
禁用了 conntrack. 在此前提下,这两条规则能让我们接收的数据包达到 608kpps.
在 iptables 计数器里的统计:
1 | $ mmwatch 'iptables -L -v -n -x | head' |
600kpps 不算差, 但是我们还能让它变得更好!
Step 5. 在 PREROUTING 使用 iptables 丢弃
一种更快的技术,是在数据包被路由之前丢弃. 添加规则如下:
1 | iptables -I PREROUTING -t raw -d 198.18.0.12 -p udp --dport 1234 -j DROP |
这样使我们接收的数据包达到了惊人的1.688mpps.
这是一个非常重要的性能飞跃, 我不能完全理解。要么我们的路由层非常复杂,要么在我们的服务器配置中有 bug。
不管怎么说,”原始” 的iptables 表肯定是更快了.
Step 6. 在 CONNTRACK 之前使用 nftables 丢弃
如今,iptables 被认为是过时的工具. 它的替代品是 nftables. 看这个视频会从技术方面解释为什么 nftables 是更优秀的.
nftables 承诺比年迈的 iptables 更快,原因很多,其中有一个传闻,retpolines(又名:对于没有间接跳跃的猜测)对iptables的影响非常严重。
因为本文并不是讨论比较 nftables 和 iptables 的速度,我们来尝试我能想到的最快的丢弃方案:
1 | nft add table netdev filter |
我们可以使用这个命令查看计数器:
1 | $ mmwatch 'nft --handle list chain netdev filter input' |
nftables “入口” 的 hook 吞量约为 1.53 mpps,比 PREROUTING 层中的 iptables 稍慢。 这令人费解 - 理论上”入口”发生在 PREROUTING 之前,所以应该更快。
在我们的测试中,nftables 比 iptables 略慢,但不是很多。 不过 nftables 仍然是更好的 :P
Step 7. 在流量控制入口的处理器中丢弃
一个有点令人惊讶的事实是,在 PREROUTING 之前甚至发生了 tc(traffic control,流量控制)入口的 hook。 tc 可以根据基本标准选择数据包,实际上是 - 丢弃 - 它们。 语法相当粗糙,因此建议使用此脚本进行设置。 我们需要一个更复杂的 tc 匹配,这里是命令:
1 | tc qdisc add dev vlan100 ingress |
我们可以这样验证:
1 | $ mmwatch 'tc -s filter show dev vlan100 ingress' |
使用 u32 匹配的 tc 入口 hook 允许我们在单个CPU上丢弃1.8mpps。 这太棒了!
但是我们还能更快…
Step 8. XDP_DROP
最后, 终极武器是 XDP - eXpress Data Path. 使用 XDP 我们可以在网络驱动环境中运行 eBPF 代码 . 最重要的是,这在”skbuff”内存分配之前,可以得到很快的运行速度。
通常 XDP 项目有两个部分:
- 加载到内核环境的 eBPF 代码
- 用户空间加载器,它将代码加载到正确的网卡上并对其进行管理
编写加载器非常困难,所以我们可以使用iproute2
的新功能,并使用这个简单的命令加载代码:
1 | ip link set dev ext0 xdp obj xdp-drop-ebpf.o |
Tadam!
加载了 eBPF 的 XDP 程序的源代码可在此处获得。该程序解析 IP 数据包并查找所需的特征:IP传输,UDP 协议,所需的目标子网和目标端口:
1 | if (h_proto == htons(ETH_P_IP)) { |
XDP 程序需要使用可以发出 BPF 字节码的现代 clang
进行编译。 然后,我们可以加载并验证正在运行的 XDP 程序:
1 | $ ip link show dev ext0 |
并使用ethtool -S
查看网卡统计中的数据:
1 | $ mmwatch 'ethtool -S ext0|egrep "rx"|egrep -v ": 0"|egrep -v "cache|csum"' |
哇撒! 使用 XDP,我们可以在单个 CPU 上每秒丢弃 1000 万个数据包。
CC BY-SA 2.0 image by Andrew Filer
总结
我们为 IPv4 和 IPv6 重复进行了实验,并准备了如下图表:
一般来说,在我们的设置中,IPv6 的性能略低。 请记住,IPv6 数据包略大,因此一些性能差异是不可避免的。
Linux 有许多可用于过滤数据包的 hook ,每种方式具有不同的性能和易用性特征。
对于 DDoS 目的,在应用程序中接收数据包并在用户空间中处理它们可能是完全合理的。调整适当的应用程序可以得到相当不错的数据。
对于具有随机/欺骗源 IP 的 DDoS 攻击,禁用 conntrack 来获得一些速度可能是值得的。 但要小心 - conntrack 防御有些攻击非常有帮助。
在其他情况下,将 Linux 防火墙集成到 DDoS 缓解管道中可能是有用的。在这种情况下,请记住将缓解措施放在 “-t raw PREROUTING” 层中,因为它比在”过滤”表中快得多。
对于要求更高的工作负载,我们始终使用 XDP。因为它很强大。以下是与上面相同的图表,但包括了 XDP 的统计数据:
如果您想复现这些数据,请查看我们记录了所有内容的说明文件。
在 Cloudflare,我们正在使用…几乎所有的这些技术。 一些用户空间的技巧与我们的应用程序集成在一起。iptables 层由 我们的 Gatebot DDoS 管道管理。最后,我们正在努力用 XDP 替换我们的专有内核卸载解决方案。
评论:
我们写了几篇有关优化 socket 吞吐量的文章: