XDP学习笔记
XDP概述
我们在没有引入XDP之前,内核中网络数据包传输路径是:
NIC网卡=->driver驱动=->TC流控=->netfilter=->IP/TCP协议栈=->socket
启动XDP程序之后,内核中网络包传输路径是:
NIC网卡=- > driver 驱动=-> XDP=-> TC流控 =-> netfilter =-> IP/TCP协议栈 =-> socket
XDP全称为eXpress Data Path,是Linux内核网络栈的最底层。只存在于RX路径上,允许在网络设备驱动内部网络堆栈中数据来源最早的地方进行数据包处理,此时操作系统并未给数据包分配struct sk_buff结构体也并未执行任何解析数据包的操作,即数据包并未进入协议栈之前进行处理,所以并不会取代 TCP/IP网络协议栈,根据程序重定向操作,选择是否绕过内核网络栈,实现高性能包处理,降低开销。
XDP典型的使用场景:DDoS防御、防火墙、网络统计、复杂网络下采样、基于XDP_TX的负载均衡。
XDP架构介绍
xdp 提供了一种处理网络报文的高性能方案,之所以性能高,是因为 xdp 对报文的处理在报文进入 IP TCP 协议栈之前,避免了漫长而繁琐的协议栈处理过程, 也就是 xdp 在收到包时最早能处理包的地方进行处理。即网卡收到网络数据包并将其交付到IP/TCP协议栈之前插入XDP程序。
XDP程序组成部分如下:
- XDP driver hook:XDP 程序的主入口,在网卡收到包未交付到IP/TCP协议栈之前执行。
- eBPF virtual machine:执行 XDP 程序的字节码。
- BPF maps:内核中的 key/value 存储,系统间通信通道。
- eBPF verifier:加载程序时对其执行静态验证,确保不会导致内核崩溃。
执行流如下:
1、提取网络数据包包头中的信息,例如Port、IP等,执行到XDP程序时,系统传递一个上下文对象ctx
,类型是struct xdp_md
,下文有介绍。
2、读取或更新元信息,解析包数据,XDP 程序读取 ctx
中的包元数据段。ctx
对象允许程序访问与包数据相邻的一块特殊内存区域, 在数据包包穿越整个系统的过程中,可以将自定义的数据塞在这里。除此之外,XDP 程序还可以通过 BPF map 定义和访问自己的持久数据 ,以及通过各种 helper 函数访问内核。
- BPF 程序与系统的其他部分之间通信通过BPF map;
- Helpers 使 BPF 程序能利用到某些已有的内核功能(例如路由表), 而无需穿越整个内核网络栈。
3.对网络数据包进行处理,分为5种情况:
XDP_DROP
,丢弃包,常用于防攻击,将攻击报文丢弃XDP_TX
,通过当前网卡将包发送出去XDP_REDIRECT
,重定向三个方向,① 将报文从另一个网卡发送出去;② 将报文重定向到另一个 cpu 进行处理;③ 将报文重定向到 xdp sock,用户态可以通过 xdp sock 接收这个报文进行处理。XDP_PASS
,不对报文做特殊处理,XDP代码透明,无任何影响,数据包交付到内核网络栈,此时将包放在发送队列之前还有一个能执行 BPF 程序的地方:TC BPF hook。XDP_ABORT
,丢弃数据包,数据包出错,最终也会被丢掉,与 XDP_DROP 不同的是会做异常统计
Basic-加载第一个BPF程序
设置依赖项
我们需要安装clang
、llvm
工具以及所需依赖项。
sudo apt-get update//获取最新版本
sudo apt-get install clang llvm libclang-dev libelf-dev
编写一个简单的XDP程序
/*
test learn
*/
#include <linux/bpf.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_prog_simple(struct xdp_md *ctx)
{
bpf_printk("pass");
return XDP_PASS;//网络数据包通过,交付给网络协议栈
}
char _license[] SEC("license") = "GPL";
编写的XDP程序中对网络传输的所有报文,不做任何的判断和处理,直接返回码XDP_PASS
,此时的XDP程序可以看作是透明的,允许数据包通过并交付给网络协议栈,程序中传入参数struct xdp_md
中存储的是报文的相关信息,我们在bpf.h文件中找到该结构体。
struct xdp_md {
__u32 data;//数据包数据起始位置,相对以太网帧开始的偏移
__u32 data_end;//数据包数据结束位置,相对以太网帧开始的偏移
__u32 data_meta;//数据包相关的额外信息
/* Below access go through struct xdp_rxq_info */
__u32 ingress_ifindex; //接收该数据包的网络接口索引
__u32 rx_queue_index; //接收该数据包的接收队列索引
__u32 egress_ifindex; //数据包预定发送出去的网络接口的索引
};
获取和解析数据包常用data
、data_end
,而data_meta指针初始为空,指向空闲内存地址供XDP程序与其他层交换数据包元数据时使用,而运行程序的时候访问接收数据包的网络接口和接收队列索引时,BPF代码会在内核内部重写,访问实际持有这些值的内核结构。
XDP程序使用的数据结构是xdp_buff
,并不依赖内核其他层,是在网络驱动层处理数据包;而我们熟知的sk_buff
主要用于物理层到应用层的数据传输,所以XDP可以更快的获取和处理数据包。
xdp_buff位置~/include/net/xdp.h
struct xdp_buff {
void *data;
void *data_end;
void *data_meta;
void *data_hard_start; // 数据前的头部空间的起始处
struct xdp_rxq_info *rxq; // 接收队列信息结构的指针
struct xdp_txq_info *txq; // 传输队列信息结构的指针(不总是使用),提供数据包可能被转发或重定向到的传输队列信息。
u32 frame_sz; // 帧的总大小,用于计算data_hard_end和保留的尾部空间
u32 flags; // 表示XDP处理的特定条件或动作的标志,定义在xdp_buff_flags中,如是否需要重新计算校验和、数据包是否被修改等
};
编写Makefile
XDP_TARGETS := xdp_pass_kern
USER_TARGETS := xdp_pass_user
LLC ?= llc
CLANG ?= clang
CC := gcc
COMMON_DIR = ../common
# Extend with another COMMON_OBJS
COMMON_OBJS += $(COMMON_DIR)/common_user_bpf_xdp.o
include $(COMMON_DIR)/common.mk
由于我们这次只编写内核态代码,所以在这里并不分析用户态代码编写,Makefile中我们构建了XDP目标程序xdp_pass_kern
以及用户态程序xdp_pass_user
,以及所涉及的编译器等信息。
编译过程
LLVM+Clang编译器将C代码转换成BPF字节码,并将其存储在xdp_pass_kern.o二进制文件中。我们可以利用llvm-objdump工具使用原始c代码的汇编器查看原始BPF指令。
llvm-objdump -S xdp_pass_kern.o
当我们运行命令时,终端输出xdp_pass_kern.o文件中每个函数的汇编代码,-S选项将汇编代码与源代码行对应起来。可以看出该文件是一个符合eBPF程序的ELF标准格式,输出的汇编代码 0: b7 00 00 00 02 00 00 00 r0 = 2,这是一个BPF指令,具体是BPF_ALU | BPF_K | BPF_MOV
操作,意味着将一个立即数2赋值给r0寄存器。其中:
b7:使用立即数进行的移动操作
00:目标寄存器是r0
02 00 00 00 :小端序进行存储,立即数为2
而1: 95 00 00 00 00 00 00 00 exit是一个BPF指令,具体是 BPF_JMP | BPF_EXIT
,表示程序结束,其中95表示这是一个退出操作,意味着结束程序返回之前设置在r0寄存器中的值。
我们很直观的看出在这个简单的xdp程序中,返回结果是常量2,我们在linux内核源码中bpf.h文件中查看到xdp_action结构体中枚举的XDP程序返回值,当处理完一个数据包后,XDP程序会返回一个动作输出操作,前四个动作本质是int并不需要参数,当数据包重定向到别的网咯接口时需要指定NIC网络设备名作为该数据包转发的目的地。
enum xdp_action {
XDP_ABORTED = 0,//因为某种错误或异常情况中止了数据包的处理,丢弃
XDP_DROP,//正常的流量过滤操作,丢弃
XDP_PASS,//数据包没有被 XDP 层拦截,将数据包传递置网络协议栈
XDP_TX,//数据包被重新传输出同一个网络接口
XDP_REDIRECT,//数据包重定向到别的网络接口
};
加载XDP程序
我们知道BPF 字节码存储在 ELF 文件中,为了将其加载到内核中,用户空间需要一个 ELF 加载器来读取文件,并以正确的格式将其传递到内核中。加载方式很多,我们选择基于libbpf的BPF加载功能,iproute2 ip工具加载eBPF程序到lo上。
sudo ip link set dev lo xdpgeneric obj xdp_pass_kern.o sec xdp
其中将加载XDP程序的模式设置为xdpgeneric,该模式不需要特定硬件支持,可以在大多数类型的网络接口上运行,但是性能不如xdpdrv
。
其余两种模式分别是xdpdrv
:需要网络驱动的直接支持。直接在数据包到达网络设备时进行操作,减少额外处理延迟核开销并提供最高的性能。
xdpgeneric
:适合于那些需要在不支持 xdpdrv
的设备上运行 XDP 程序的场景,或者当设备驱动不支持直接挂载 XDP 程序时。加载的XDP程序的BPF对象文件名是xdp_pass_kern.o
,并指定了从BPF对象文件中加载哪一节的程序,就是我们内核态定义的SEC(xdp)
宏。
没有加载XDP程序时查看网络接口配置信息
加载XDP程序时查看信息
其中:每个接口都有一个唯一的编号用于标识,此时记为1;本地环回接口lo;LOOPBACK环回接口并且该接口处于启用状态,此时链路层已经建立;网络包数据包最大为65536字节并且此时并不存在队列,所以数据包直接可以进行发送并需要排队;链路层中MAC地址00:00:00:00:00:00
,广播地址brd 00:00:00:00:00:00
;加载XDP程序之后多余信息为prog/xdp id 40 tag 3b185187f1855c4c jited
。
分析一下,prog/xdp指明这是一个XDP程序,并指明了程序的id和tag,通过jited可以看到当前程序已经通过编译。
使用bpftool工具验证当前xdp程序是否已经加载成功,可以看到网络接口lo已经加载了一个XDP程序,XDP程序ID是52。
sudo bpftool prog
可以看到已经安装的xdp程序。
测试结果
我们在终端ping 1.1.1.1发送icmp报文进行测试
加载XDP程序
卸载XDP程序
sudo ip link set dev lo xdpgeneric off
验证一下,明显发现,加载XDP程序之后,丢包率为0%,是因为我们在XDP程序中让所有网络数据包都通过了,全部向上交付给网络协议栈,卸载XDP程序之后,丢包率为14.2857%,网络包在传输过程中存在丢包。