我们在 XDP 上编写了针对 DDoS 攻击的保护措施。 核部分

eXpress 数据路径 (XDP) 技术允许在数据包进入内核网络堆栈之前对 Linux 接口上的流量进行任意处理。 XDP 的应用 - 防御 DDoS 攻击 (CloudFlare)、复杂过滤器、统计数据收集 (Netflix)。 XDP 程序由 eBPF 虚拟机执行,因此根据过滤器的类型,其代码和可用的内核函数都受到限制。

这篇文章旨在弥补XDP上众多资料的不足。 首先,他们提供现成的代码,可以立即绕过 XDP 的功能:准备用于验证或太简单而不会导致问题。 当您稍后尝试从头开始编写自己的代码时,不知道如何处理典型错误。 其次,它没有涵盖在没有虚拟机和硬件的情况下本地测试 XDP 的方法,尽管它们有自己的陷阱。 本文面向熟悉网络和 Linux、对 XDP 和 eBPF 感兴趣的程序员。

在这一部分中,我们将详细了解 XDP 过滤器是如何组装以及如何测试它,然后我们将在数据包处理级别编写众所周知的 SYN cookies 机制的简单版本。 直到我们形成“白名单”
验证客户端,保留计数器并管理过滤器 - 足够的日志。

我们将用 C 语言编写 - 这并不时尚,但很实用。 所有代码都可以通过末尾的链接在 GitHub 上获取,并根据文章中描述的步骤分为提交。

免责声明。 在本文中,我们将开发一个用于抵御 DDoS 攻击的小型解决方案,因为这对于 XDP 和我的领域来说是一项现实任务。 然而,主要目标是了解技术,这并不是创建现成保护的指南。 教程代码未优化并且省略了一些细微差别。

XDP 简要概述

我将仅陈述要点,以免重复文档和现有文章。

因此,过滤器代码被加载到内核中。 过滤器传递传入数据包。 因此,过滤器必须做出决定:将数据包传递给内核(XDP_PASS),丢弃数据包(XDP_DROP)或发回(XDP_TX)。 过滤器可以改变封装,尤其是对于 XDP_TX。 您还可以使程序崩溃(XDP_ABORTED)并放下包裹,但这是类似的 assert(0) - 用于调试。

eBPF(扩展伯克利数据包过滤器)虚拟机故意变得简单,以便内核可以检查代码不循环并且不会损坏其他人的内存。 累积限制和检查:

  • 禁止循环(跳回)。
  • 有一个用于数据的堆栈,但没有函数(所有 C 函数都必须内联)。
  • 禁止访问堆栈和数据包缓冲区之外的内存。
  • 代码的大小是有限的,但实际上这并不是很重要。
  • 仅允许特殊的内核函数(eBPF 助手)。

开发和安装过滤器如下所示:

  1. 源代码(例如 kernel.c) 编译为对象 (kernel.o)用于 eBPF 虚拟机架构。 截至 2019 年 10.1 月,Clang 支持编译为 eBPF,并在 GCC XNUMX 中承诺。
  2. 如果在此目标代码中存在对内核结构(例如表和计数器)的调用,则它们的 ID 为零,而不是零,也就是说,此类代码无法执行。 在加载到内核之前,这些零必须替换为通过内核调用创建的特定对象的 ID(链接代码)。 您可以使用外部实用程序来执行此操作,也可以编写一个程序来链接和加载特定的过滤器。
  3. 内核验证正在加载的程序。 它检查是否存在循环以及包和堆栈边界是否未退出。 如果验证者不能证明代码是正确的,程序就会被拒绝——必须能够取悦他。
  4. 验证成功后,内核将eBPF架构目标代码编译为系统架构机器代码(即时)。
  5. 该程序连接到接口并开始处理数据包。

由于 XDP 在内核中运行,因此调试基于跟踪日志,实际上是基于程序过滤或生成的数据包。 但是,eBPF 可以保证下载的代码对系统的安全,因此您可以在本地 Linux 上试验 XDP。

准备环境

装配

Clang 无法直接为 eBPF 架构发布目标代码,因此该过程由两个步骤组成:

  1. 将 C 代码编译为 LLVM 字节码(clang -emit-llvm).
  2. 将字节码转换为 eBPF 目标代码(llc -march=bpf -filetype=obj).

编写过滤器时,几个带有辅助函数和宏的文件会派上用场 来自内核测试。 它们与内核版本匹配很重要(KVER)。 将它们下载到 helpers/:

export KVER=v5.3.7
export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf
wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}"
unset KVER BASE

Arch Linux(内核 5.3.7)的 Makefile:

CLANG ?= clang
LLC ?= llc

KDIR ?= /lib/modules/$(shell uname -r)/build
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

CFLAGS = 
    -Ihelpers 
    
    -I$(KDIR)/include 
    -I$(KDIR)/include/uapi 
    -I$(KDIR)/include/generated/uapi 
    -I$(KDIR)/arch/$(ARCH)/include 
    -I$(KDIR)/arch/$(ARCH)/include/generated 
    -I$(KDIR)/arch/$(ARCH)/include/uapi 
    -I$(KDIR)/arch/$(ARCH)/include/generated/uapi 
    -D__KERNEL__ 
    
    -fno-stack-protector -O2 -g

xdp_%.o: xdp_%.c Makefile
    $(CLANG) -c -emit-llvm $(CFLAGS) $< -o - | 
    $(LLC) -march=bpf -filetype=obj -o $@

.PHONY: all clean

all: xdp_filter.o

clean:
    rm -f ./*.o

KDIR 包含内核头的路径, ARCH - 系统架构。 不同发行版的路径和工具可能略有不同。

Debian 10(内核 4.19.67)的差异示例

# другая команда
CLANG ?= clang
LLC ?= llc-7

# другой каталог
KDIR ?= /usr/src/linux-headers-$(shell uname -r)
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

# два дополнительных каталога -I
CFLAGS = 
    -Ihelpers 
    
    -I/usr/src/linux-headers-4.19.0-6-common/include 
    -I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include 
    # далее без изменений

CFLAGS 包括一个带有辅助头的目录和几个带有内核头的目录。 象征 __KERNEL__ 意味着 UAPI(用户空间 API)标头是为内核代码定义的,因为过滤器是在内核中执行的。

可以禁用堆栈保护(-fno-stack-protector)因为 eBPF 代码验证器无论如何都会检查非堆栈边界。 您应该立即启用优化,因为 eBPF 字节码的大小是有限的。

让我们从一个通过所有数据包但不执行任何操作的过滤器开始:

#include <uapi/linux/bpf.h>

#include <bpf_helpers.h>

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

团队 make 收集 xdp_filter.o。 现在哪里可以测试呢?

测试台

该支架应包括两个接口:其上将有一个过滤器,并且将从其发送数据包。 这些必须是具有自己的 IP 的完整 Linux 设备,以便检查常规应用程序如何与我们的过滤器配合使用。

像veth(虚拟以太网)这样的设备很适合我们:它们是一对直接相互“连接”的虚拟网络接口。 您可以像这样创建它们(在本节中,所有命令 ip 执行自 root):

ip link add xdp-remote type veth peer name xdp-local

这是 xdp-remote и xdp-local — 设备名称。 在 xdp-local (192.0.2.1/24) 将附加一个过滤器,其中 xdp-remote (192.0.2.2/24) 传入流量将被发送。 然而,有一个问题:这些接口位于同一台机器上,Linux 不会通过其中一个接口向另一个接口发送流量。 你可以用棘手的规则来解决它 iptables,但是必须要换包,调试的时候很不方便。 最好使用网络命名空间(网络命名空间,进一步的netns)。

网络命名空间包含一组接口、路由表和 NetFilter 规则,这些规则与其他 netns 中的类似对象隔离。 每个进程都在某个名称空间中运行,并且只有该网络名称空间的对象可供其使用。 默认情况下,系统对所有对象都有一个网络命名空间,因此您可以在 Linux 上工作而无需了解 netns。

让我们创建一个新的命名空间 xdp-test 并搬到那里 xdp-remote.

ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test

然后进程运行在 xdp-test,不会“看到” xdp-local (默认情况下它将保留在 netns 中)并且当发送数据包到 192.0.2.1 时将通过它 xdp-remote,因为这是该进程唯一可用的 192.0.2.0/24 接口。 这也适用于相反的情况。

在 netns 之间移动时,接口会关闭并丢失地址。 要在 netns 中设置接口,您需要运行 ip ... 在此命令命名空间中 ip netns exec:

ip netns exec xdp-test 
    ip address add 192.0.2.2/24 dev xdp-remote
ip netns exec xdp-test 
    ip link set xdp-remote up

正如你所看到的,这与设置没有什么不同 xdp-local 在默认命名空间中:

    ip address add 192.0.2.1/24 dev xdp-local
    ip link set xdp-local up

如果你跑 tcpdump -tnevi xdp-local,您可以看到发送的数据包 xdp-test,被传递到这个接口:

ip netns exec xdp-test   ping 192.0.2.1

运行shell很方便 xdp-test。 存储库有一个脚本可以自动使用支架进行工作,例如,您可以使用以下命令设置支架 sudo ./stand up 并删除它 sudo ./stand down.

追踪

过滤器像这样连接到设备上:

ip -force link set dev xdp-local xdp object xdp_filter.o verbose

关键 -force 如果已经链接了另一个程序,则需要链接新程序。 “没有消息就是好消息”与此命令无关,无论如何输出都是大量的。 表明 verbose 可选,但随之而来的是有关代码验证器工作的报告以及汇编器列表:

Verifier analysis:

0: (b7) r0 = 2
1: (95) exit

从界面中分离程序:

ip link set dev xdp-local xdp off

在脚本中,这些是命令 sudo ./stand attach и sudo ./stand detach.

通过绑定过滤器,您可以确保 ping 继续工作,但是该程序是否有效? 让我们添加徽标。 功能 bpf_trace_printk() 相似 printf(),但仅支持除模式之外的最多三个参数以及有限的说明符列表。 宏 bpf_printk() 简化通话。

   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("got packet: %pn", ctx);
       return XDP_PASS;
   }

输出进入内核跟踪通道,需要启用该通道:

echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk

查看消息流:

cat /sys/kernel/debug/tracing/trace_pipe

这两支球队都打了电话 sudo ./stand log.

Ping 现在应该在其中生成如下消息:

<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377

如果仔细观察验证器的输出,您会发现奇怪的计算:

0: (bf) r3 = r1
1: (18) r1 = 0xa7025203a7465
3: (7b) *(u64 *)(r10 -8) = r1
4: (18) r1 = 0x6b63617020746f67
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
8: (07) r1 += -16
9: (b7) r2 = 16
10: (85) call bpf_trace_printk#6
<...>

事实上,eBPF 程序没有数据部分,因此对格式字符串进行编码的唯一方法是 VM 命令的直接参数:

$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'

因此,调试输出会大大增加生成的代码。

发送XDP数据包

让我们更改过滤器:让它将所有传入数据包发回。 从网络的角度来看,这是不正确的,因为有必要更改标头中的地址,但现在原则上的工作很重要。

       bpf_printk("got packet: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

我们启动 tcpdump 上 xdp-remote。 它应该显示相同的传出和传入 ICMP 回显请求,并停止显示 ICMP 回显回复。 但它没有显示。 结果可以工作 XDP_TX 在计划中 xdp-local 必要配对接口 xdp-remote 一个程序也被分配了,即使它是空的,并且它被提出了。

我怎么知道的?

跟踪内核中包的路径 顺便说一句,perf events机制允许使用相同的虚拟机,即eBPF用于与eBPF进行反汇编。

你必须化恶为善,因为除此之外别无他法。

$ sudo perf trace --call-graph dwarf -e 'xdp:*'
   0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 action=TX sent=0 drops=1 err=-6
                                     veth_xdp_flush_bq ([veth])
                                     veth_xdp_flush_bq ([veth])
                                     veth_poll ([veth])
                                     <...>

代码6是什么?

$ errno 6
ENXIO 6 No such device or address

功能 veth_xdp_flush_bq() 从中获取错误代码 veth_xdp_xmit(),其中搜索 ENXIO 并找到评论。

恢复最小过滤器(XDP_PASS) 在文件中 xdp_dummy.c,将其添加到Makefile中,绑定到 xdp-remote:

ip netns exec remote 
    ip link set dev int xdp object dummy.o

现在 tcpdump 显示预期内容:

62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64

如果只显示 ARP,则需要删除过滤器(这使得 sudo ./stand detach), 让 ping,然后安装过滤器并重试。 问题是过滤器 XDP_TX 也会影响 ARP,如果堆栈
命名空间 xdp-test 如果“忘记”了 MAC 地址 192.0.2.1,他将无法解析该 IP。

制定问题

让我们继续完成既定的任务:在 XDP 上编写 SYN cookie 机制。

到目前为止,SYN Flood 仍然是一种流行的 DDoS 攻击,其本质如下。 当建立连接(TCP 握手)时,服务器接收 SYN,为将来的连接分配资源,用 SYNACK 数据包进行响应,并等待 ACK。 攻击者只需从数千个僵尸网络中的每台主机每秒从假地址发送数千个 SYN 数据包。 服务器在数据包到达时被迫立即分配资源,但在长时间超时后释放资源,导致内存或限制耗尽,不接受新连接,服务不可用。

如果不对SYN报文分配资源,而只响应SYNACK报文,那么服务器怎么能知道后面到来的ACK报文属于没有保存的SYN报文呢? 毕竟,攻击者也可以生成假 ACK。 SYN cookie的本质是编码 seqnum 连接参数为地址、端口和变化盐的哈希值。 如果 ACK 在盐变化之前成功到达,您可以再次计算哈希并与 acknum。 伪造的 acknum 攻击者不能,因为盐包含秘密,并且由于通道有限而没有时间对其进行排序。

SYN cookie 已经在 Linux 内核中实现了很长时间,如果 SYN 到达太快且大量,甚至可以自动启用。

TCP 握手教育计划

TCP 以字节流的形式提供数据传输,例如,HTTP 请求通过 TCP 传输。 流以数据包的形式逐段传输。 所有 TCP 数据包都有逻辑标志和 32 位序列号:

  • 标志的组合定义了特定包的角色。 SYN 标志意味着这是发送方在连接上的第一个数据包。 ACK 标志意味着发送方已收到最多一个字节的所有连接数据。 acknum。 一个数据包可能有多个标志,并根据它们的组合来命名,例如 SYNACK 数据包。

  • 序列号 (seqnum) 指定此数据包中发送的第一个字节在数据流中的偏移量。 例如,如果在第一个包含 X 字节数据的数据包中,该数字为 N,则在下一个包含新数据的数据包中,该数字将为 N+X。 在通话开始时,双方随机选择该号码。

  • 确认号(acknum) - 与 seqnum 相同的偏移量,但它并不确定传输字节的编号,而是确定来自接收方的第一个字节的编号,发送方没有看到该编号。

连接开始时,双方必须同意 seqnum и acknum。 客户端发送一个 SYN 数据包,其中包含 seqnum = X。 服务器用 SYNACK 数据包进行响应,并在其中写入自己的数据包 seqnum = Y 并暴露 acknum = X + 1。 客户端用 ACK 数据包响应 SYNACK,其中 seqnum = X + 1, acknum = Y + 1。 之后,实际的数据传输开始。

如果对话者未确认收到数据包,TCP 将在超时后重新发送数据包。

为什么不总是使用 SYN cookie?

首先,如果 SYNACK 或 ACK 丢失,您将不得不等待重新发送 - 连接建立速度会减慢。 其次,在 SYN 数据包中 - 并且仅在其中! - 传输的许多选项会影响连接的进一步操作。 服务器不记得传入的 SYN 数据包,因此会忽略这些选项,在接下来的数据包中,客户端将不再发送它们。 TCP 在这种情况下可以工作,但至少在初始阶段,连接质量会下降。

就软件包而言,XDP 程序应该执行以下操作:

  • 使用带有 cookie 的 SYNACK 响应 SYN;
  • 用 RST 应答 ACK(断开连接);
  • 丢弃其他数据包。

算法的伪代码以及数据包解析:

Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

(*) 需要管理系统状态的点已被标记 - 在第一阶段,您可以通过简单地实现 TCP 握手并生成 SYN cookie 作为序列号而无需它们。

现场 (**),虽然我们没有桌子,但我们会跳过数据包。

TCP握手实现

包解析和代码验证

我们需要网络头结构:以太网(uapi/linux/if_ether.h)、IPv4(uapi/linux/ip.h) 和 TCP (uapi/linux/tcp.h)。 由于与以下相关的错误,我无法连接最后一个 atomic64_t,我必须将必要的定义复制到代码中。

所有在 C 中为了可读性而区分的函数都必须在调用站点内联,因为内核中的 eBPF 验证器禁止向后跳转,即实际上禁止循环和函数调用。

#define INTERNAL static __attribute__((always_inline))

LOG() 在发布版本中禁用打印。

该程序是一个函数管道。 每个接收到一个数据包,其中相应级别的标头被突出显示,例如, process_ether() 等待被填满 ether。 根据现场分析的结果,该功能可以将数据包传输到更高的级别。 该函数的结果是 XDP 操作。 而 SYN 和 ACK 处理程序则让所有数据包通过。

struct Packet {
    struct xdp_md* ctx;

    struct ethhdr* ether;
    struct iphdr* ip;
    struct tcphdr* tcp;
};

INTERNAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp(struct Packet* packet) { ... }
INTERNAL int process_ip(struct Packet* packet) { ... }

INTERNAL int
process_ether(struct Packet* packet) {
    struct ethhdr* ether = packet->ether;

    LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

    if (ether->h_proto != bpf_ntohs(ETH_P_IP)) {
        return XDP_PASS;
    }

    // B
    struct iphdr* ip = (struct iphdr*)(ether + 1);
    if ((void*)(ip + 1) > (void*)packet->ctx->data_end) {
        return XDP_DROP; /* malformed packet */
    }

    packet->ip = ip;
    return process_ip(packet);
}

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    struct Packet packet;
    packet.ctx = ctx;

    // A
    struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data;
    if ((void*)(ether + 1) > (void*)ctx->data_end) {
        return XDP_PASS;
    }

    packet.ether = ether;
    return process_ether(&packet);
}

我注意标记A和B的检查。如果注释掉A,程序将构建,但加载时会出现验证错误:

Verifier analysis:

<...>
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset is outside of the packet
processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Error fetching program/map!

关键字符串 invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0):当缓冲区开头的第 12 个字节位于数据包外部时,存在执行路径。 从列表中很难看出我们正在谈论哪一行,但有一个指令编号 (XNUMX) 和一个显示源代码行的反汇编程序:

llvm-objdump -S xdp_filter.o | less

在这种情况下,它指向该行

LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

这清楚地表明问题是 ether。 永远都是这样。

回复 SYN

此阶段的目标是生成具有固定值的正确 SYNACK 数据包 seqnum,将来会被 SYN cookie 取代。 所有的改变都发生在 process_tcp_syn() 和周围环境。

检查包裹

奇怪的是,这是最引人注目的一行,或者更确切地说,是对其的评论:

/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;

在编写第一个版本的代码时,使用的是5.1内核,对于其验证器来说,两者之间存在差异 data_end и (const void*)ctx->data_end。 在撰写本文时,5.3.1 内核没有这个问题。 也许编译器访问局部变量的方式与访问字段的方式不同。 寓意 - 对于大型嵌套,简化代码会有所帮助。

为了验证者的荣耀,进一步对长度进行例行检查; 氧 MAX_CSUM_BYTES 下方。

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* malformed packet */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* malformed packet */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

套餐传播

填写 seqnum и acknum,设置 ACK(SYN 已设置):

const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;

交换 TCP 端口、IP 和 MAC 地址。 标准库无法从 XDP 程序中获得,因此 memcpy() — 隐藏 Clang intrinsik 的宏。

const u16 temp_port = tcp->source;
tcp->source = tcp->dest;
tcp->dest = temp_port;

const u32 temp_ip = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = temp_ip;

struct ethhdr temp_ether = *ether;
memcpy(ether->h_dest, temp_ether.h_source, ETH_ALEN);
memcpy(ether->h_source, temp_ether.h_dest, ETH_ALEN);

校验和重新计算

IPv4和TCP校验和要求将报头中的所有16位字相加,并且报头的大小写在其中,即在编译时是未知的。 这是一个问题,因为验证者不会跳过正常循环直到边界变量。 但标头的大小是有限的:每个标头最多 64 字节。 您可以创建一个具有固定迭代次数的循环,该循环可以提前结束。

我注意到有 RFC 1624 如果仅更改数据包的固定字,如何部分重新计算校验和。 然而,该方法并不通用,并且实施起来会更难以维护。

校验和计算函数:

#define MAX_CSUM_WORDS 32
#define MAX_CSUM_BYTES (MAX_CSUM_WORDS * 2)

INTERNAL u32
sum16(const void* data, u32 size, const void* data_end) {
    u32 s = 0;
#pragma unroll
    for (u32 i = 0; i < MAX_CSUM_WORDS; i++) {
        if (2*i >= size) {
            return s; /* normal exit */
        }
        if (data + 2*i + 1 + 1 > data_end) {
            return 0; /* should be unreachable */
        }
        s += ((const u16*)data)[i];
    }
    return s;
}

虽然 size 由调用代码检查,第二个退出条件是必要的,以便验证者可以证明循环的结束。

对于 32 位字,实现了一个更简单的版本:

INTERNAL u32
sum16_32(u32 v) {
    return (v >> 16) + (v & 0xffff);
}

实际上重新计算校验和并将数据包发回:

ip->check = 0;
ip->check = carry(sum16(ip, ip_len, data_end));

u32 tcp_csum = 0;
tcp_csum += sum16_32(ip->saddr);
tcp_csum += sum16_32(ip->daddr);
tcp_csum += 0x0600;
tcp_csum += tcp_len << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

功能 carry() 根据 RFC 32,从 16 位字的 791 位和中得出校验和。

TCP握手检查

过滤器正确建立与 netcat,跳过最后的 ACK,Linux 使用 RST 数据包对其进行响应,因为网络堆栈没有收到 SYN - 它被转换为 SYNACK 并发回 - 从操作系统的角度来看,到达的数据包不是与开放连接相关。

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

检查成熟的应用程序并观察非常重要 tcpdump 上 xdp-remote 因为,例如, hping3 不响应错误的校验和。

从 XDP 的角度来看,检查本身是微不足道的。 计算算法很原始,可能容易受到老练的攻击者的攻击。 例如,Linux 内核使用加密的 SipHash,但其 XDP 的实现显然超出了本文的范围。

出现与外部交互相关的新 TODO:

  • XDP程序无法存储 cookie_seed (盐的秘密部分)在全局变量中,您需要一个内核存储,其值将从可靠的生成器定期更新。

  • 如果 ACK 数据包中的 SYN cookie 匹配,则不需要打印消息,而是记住已验证客户端的 IP,以便进一步跳过其中的数据包。

由合法客户验证:

$ sudoip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

日志记录了检查的通过(flags=0x2 是同步, flags=0x10 是 ACK):

Ether(proto=0x800)
  IP(src=0x20e6e11a dst=0x20e6e11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x2)
Ether(proto=0x800)
  IP(src=0xfe2cb11a dst=0xfe2cb11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x10)
      cookie matches for client 20200c0

只要没有检查的 IP 列表,就不会针对 SYN 洪水本身提供保护,但以下是对此命令发起的 ACK 洪水的反应:

sudo ip netns exec xdp-test   hping3 --flood -A -s 1111 -p 2222 192.0.2.1

日志条目:

Ether(proto=0x800)
  IP(src=0x15bd11a dst=0x15bd11e proto=6)
    TCP(sport=3236 dport=2222 flags=0x10)
      cookie mismatch

结论

有时,eBPF(尤其是 XDP)更多地被视为高级管理员工具,而不是开发平台。 事实上,XDP 是一种干扰内核数据包处理的工具,而不是像 DPDK 和其他内核旁路选项那样替代内核堆栈。 另一方面,XDP 允许您实现相当复杂的逻辑,而且很容易更新,无需暂停流量处理。 验证器不会产生大问题,就我个人而言,我不会拒绝部分用户空间代码。

在第二部分中,如果主题有趣,我们将完成已验证客户端的表并断开连接,实现计数器并编写用户空间实用程序来管理过滤器。

参考文献:

来源: habr.com

添加评论