1.概述
XDP是Linux网络路径上内核集成的数据包处理器,具有安全、可编程、高性能的特点。当网卡驱动程序收到数据包时,该处理器执行BPF程序。XDP可以在数据包进入协议栈之前就进行处理,因此具有很高的性能,可用于DDoS防御、防火墙、负载均衡等领域。
XDP 通过在内核中运行 eBPF(Extended Berkeley Packet Filter)程序来实现这一点。其与dpdk的区别在于:
- 性能:XDP 和 DPDK 都旨在提高数据包处理性能,但它们的实现方式不同。XDP 在内核空间运行,而 DPDK 在用户空间运行。
- 易用性:XDP 更容易集成到现有的 Linux 环境中,因为它是内核的一部分。DPDK 需要专门的驱动程序和配置,另外无法利用网络栈,需要自行在用户空间额外实现一套网络栈。
- 灵活性:DPDK 提供了更丰富的 API 和功能,但需要更多的开发和维护工作。XDP 则更轻量级,适合简单的高性能数据包处理任务。
2. 数据结构
XDP程序使用的数据结构是xdp_buff
而不是sk_buff
,前者可以视为后者的轻量级版本,两者的区别在于:sk_buff
包含数据包的元数据,xdp_buff
创建更早而且不依赖于其他内核层,因此XDP可以更快地获取和处理数据包。
xdp_buff
数据结构定义:
// /linux/include/net/xdp.h
struct xdp_rxq_info {
struct net_device *dev;
u32 queue_index;
u32 reg_state;
struct xdp_mem_info mem;
} ____cacheline_aligned; /* perf critical, avoid false-sharing */
struct xdp_buff {
void *data;
void *data_end;
void *data_meta;
void *data_hard_start;
unsigned long handle;
struct xdp_rxq_info *rxq;
};
而sk_buff
数据结构定义如下:
// /include/linux/skbuff.h
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
union {
struct net_device *dev;
/* Some protocols might use this space to store information,
* while device pointer would be NULL.
* UDP receive path is one user.
*/
unsigned long dev_scratch;
};
};
struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */
struct list_head list;
};
union {
struct sock *sk;
int ip_defrag_offset;
};
union {
ktime_t tstamp;
u64 skb_mstamp_ns; /* earliest departure time */
};
/*
* This is the control buffer. It is free to use for every
* layer. Please put your private variables there. If you
* want to keep them across layers you have to do a skb_clone()
* first. This is owned by whoever has the skb queued ATM.
*/
char cb[48] __aligned(8);
union {
struct {
unsigned long _skb_refdst;
void (*destructor)(struct sk_buff *skb);
};
struct list_head tcp_tsorted_anchor;
};
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
unsigned long _nfct;
#endif
unsigned int len,
data_len;
__u16 mac_len,
hdr_len;
/* Following fields are _not_ copied in __copy_skb_header()
* Note that queue_mapping is here mostly to fill a hole.
*/
__u16 queue_mapping;
/* if you move cloned around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define CLONED_MASK (1 << 7)
#else
#define CLONED_MASK 1
#endif
#define CLONED_OFFSET() offsetof(struct sk_buff, __cloned_offset)
__u8 __cloned_offset[0];
__u8 cloned:1,
nohdr:1,
fclone:2,
peeked:1,
head_frag:1,
xmit_more:1,
pfmemalloc:1;
#ifdef CONFIG_SKB_EXTENSIONS
__u8 active_extensions;
#endif
/* fields enclosed in headers_start/headers_end are copied
* using a single memcpy() in __copy_skb_header()
*/
/* private: */
__u32 headers_start[0];
/* public: */
/* if you move pkt_type around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_TYPE_MAX (7 << 5)
#else
#define PKT_TYPE_MAX 7
#endif
#define PKT_TYPE_OFFSET() offsetof(struct sk_buff, __pkt_type_offset)
__u8 __pkt_type_offset[0];
__u8 pkt_type:3;
__u8 ignore_df:1;
__u8 nf_trace:1;
__u8 ip_summed:2;
__u8 ooo_okay:1;
__u8 l4_hash:1;
__u8 sw_hash:1;
__u8 wifi_acked_valid:1;
__u8 wifi_acked:1;
__u8 no_fcs:1;
/* Indicates the inner headers are valid in the skbuff. */
__u8 encapsulation:1;
__u8 encap_hdr_csum:1;
__u8 csum_valid:1;
#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_VLAN_PRESENT_BIT 7
#else
#define PKT_VLAN_PRESENT_BIT 0
#endif
#define PKT_VLAN_PRESENT_OFFSET() offsetof(struct sk_buff, __pkt_vlan_present_offset)
__u8 __pkt_vlan_present_offset[0];
__u8 vlan_present:1;
__u8 csum_complete_sw:1;
__u8 csum_level:2;
__u8 csum_not_inet:1;
__u8 dst_pending_confirm:1;
#ifdef CONFIG_IPV6_NDISC_NODETYPE
__u8 ndisc_nodetype:2;
#endif
__u8 ipvs_property:1;
__u8 inner_protocol_type:1;
__u8 remcsum_offload:1;
#ifdef CONFIG_NET_SWITCHDEV
__u8 offload_fwd_mark:1;
__u8 offload_l3_fwd_mark:1;
#endif
#ifdef CONFIG_NET_CLS_ACT
__u8 tc_skip_classify:1;
__u8 tc_at_ingress:1;
__u8 tc_redirected:1;
__u8 tc_from_ingress:1;
#endif
#ifdef CONFIG_TLS_DEVICE
__u8 decrypted:1;
#endif
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#endif
union {
__wsum csum;
struct {
__u16 csum_start;
__u16 csum_offset;
};
};
__u32 priority;
int skb_iif;
__u32 hash;
__be16 vlan_proto;
__u16 vlan_tci;
#if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS)
union {
unsigned int napi_id;
unsigned int sender_cpu;
};
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif
union {
__u32 mark;
__u32 reserved_tailroom;
};
union {
__be16 inner_protocol;
__u8 inner_ipproto;
};
__u16 inner_transport_header;
__u16 inner_network_header;
__u16 inner_mac_header;
__be16 protocol;
__u16 transport_header;
__u16 network_header;
__u16 mac_header;
/* private: */
__u32 headers_end[0];
/* public: */
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
unsigned int truesize;
refcount_t users;
#ifdef CONFIG_SKB_EXTENSIONS
/* only useable after checking ->active_extensions != 0 */
struct skb_ext *extensions;
#endif
};
3. 工作模式
XDP程序是通过bpf()系统调用控制的,bpf()系统调用使用程序类型BPF_PROG_TYPE_XDP
进行加载。
XDP支持三种操作模式,默认使用native
模式
- Native XDP:在native模式下,XDP BPF程序运行在网络驱动的早期接收路径上(RX队列),因此,使用该模式时需要网卡驱动程序支持。
- Offloaded XDP:在Offloaded模式下,XDP BFP程序直接在NIC(Network Interface Controller)中处理数据包,而不使用主机CPU,相比native模式,性能更高。
- Generic XDP:Generic模式主要提供给开发人员测试使用,对于网卡或驱动无法支持native或offloaded模式的情况,内核提供了通用的generic模式,运行在协议栈中,不需要对驱动做任何修改。生产环境中建议使用native或offloaded模式
一个简单的XDP程序如下:
/* SPDX-License-Identifier: GPL-2.0 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_prog_simple(struct xdp_md *ctx)
{
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
使用libbpf
提供的SEC
宏来定义xdp 程序段,每个程序段是一个函数,该函数需要返回一个XDP状态码,来指示xdp该如何处理当前收到的包,XDP共提供了5个状态码:
- XDP_DROP:丢弃数据包,发生在驱动程序的最早RX阶段
- XDP_PASS:将数据包传递到协议栈处理,操作可能为以下两种形式:
- 正常接收数据包,分配愿数据sk_buff结构并且将接收数据包入栈,然后将数据包引导到另一个CPU进行处理。他允许原始接口到用户空间进行处理。 这可能发生在数据包修改前或修改后。
- 通过GRO(Generic receive offload)方式接收大的数据包,并且合并相同连接的数据包。经过处理后,GRO最终将数据包传入“正常接收”流.。
- XDP_TX:转发数据包,将接收到的数据包发送回数据包到达的同一网卡。这可能在数据包修改前或修改后发生。
- XDP_REDIRECT:数据包重定向,XDP_TX,XDP_REDIRECT是将数据包送到另一块网卡或传入到BPF的cpumap中。
- XDP_ABORTED:表示eBPF程序发生错误,并导致数据包被丢弃。自己开发的程序不应该使用该返回码。
4. 安装运行
一般ubuntu等常用linux发行版对于bpf/xdp已有支持,可以直接编写源码并使用clang编译:
clang -O2 -Wall -target bpf -c xdp_filter.c -o xdp_filter.o
然后使用ip
命令挂载到网卡上,默认选择最合适的位置挂载,sec
参数可以指定程序入口点:
sudo ip link set dev ens33 xdp obj xdp_filter.o sec mysection
查看网卡挂载XDP程序:
ip link show dev ens33
卸载xdp程序:
sudo ip link set dev ens33 xdp off
需要注意的是,系统自带的网卡一般iproute2软件版本较低,在挂载包含全局变量的XDP程序时存在问题,因此我们可以安装最新的iproute2
,注意在此之前要安装libbpf(上一节的示例程序也需要安装libbpf),但是apt安装的libbpf版本过低,只有0.5,因此手动拉仓库安装最新版进行编译安装,注意先要安装clang-12
,并指定为默认clang:
sudo apt install clang-12
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-12 30 \
--slave /usr/bin/clang++ clang++ /usr/bin/clang++-12
如已安装clang,在执行上述命令后,使用下列命令来进行版本指定
sudo update-alternatives --config clang
拉取并安装libbpf
:
git clone https://github.com/libbpf/libbpf.git
cd libbpf/src
make
sudo make install
注意libbpf
的pc
包文件被安装到/usr/lib64/pkgconfig
下面,因此pkg-config
无法读到该包,在安装后续软件的时候会出现问题,因此我们把该路径加到~/.bashrc
中:
sudo echo 'export PKG_CONFIG_PATH="/usr/lib64/pkgconfig:$PKG_CONFIG_PATH"' >> ~/.bashrc
source ~/.bashrc
然后就可以使用pkg-config
来查看libbpf
版本以及库路径:
umbrella-leaf@DESKTOP-RVPDT4L:~$ pkg-config --modversion libbpf
1.5.0
umbrella-leaf@DESKTOP-RVPDT4L:~$ pkg-config --libs libbpf
-L/usr/lib64 -lbpf
下面安装最新版的iproute2:
git clone https://github.com/iproute2/iproute2.git
cd iproute2
sudo ./configure
sudo make install
之后就可以挂载包含全局变量的XDP程序。
另外,使用ip
命令只能在一个网卡上挂载单个XDP程序,如果想要挂载多个XDP程序,就需要用到xdp-dispatcher
,可以安装xdp-loader
工具,这个工具属于xdp-tools
仓库,我们拉下这个仓库并编译安装,注意编译前请勿安装bpftool工具,会因为额外编译的工具导致整个项目编译失败:
git clone https://github.com/xdp-project/xdp-tools.git
cd xdp-tools
./configure
make
sudo make install
然后就可以使用xdp-loader来在一块网卡上挂载多个XDP程序(通过xdp-dispatcher):
sudo xdp-loader load -m skb eth0 test_xdp.bpf.o
sudo xdp-loader load -m skb eth0 test_xdp_2.bpf.o
其中-m
参数指定XDP挂载模式,native
表示挂载在网卡驱动上,skb
表示挂载在内核栈中,hw
表示挂载在网卡硬件上。
查看网卡挂载情况:
sudo xdp-loader status eth0
卸载XDP程序:
# 卸载网卡上某个指定id的程序
sudo xdp-loader unload -i <id> eth0
# 卸载网卡上所有XDP程序
sudo xdp-loader unload -a eth0
编写下列的XDP程序test_xdp.bpf.c
:
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_drop(struct xdp_md *ctx) {
static int count = 0;
++count;
if (count % 2) {
return XDP_PASS;
} else {
return XDP_DROP;
}
}
char __license[] SEC("license") = "GPL";
这个程序使用一个全局计数器,使得网卡收到奇数编号包时直接放行,收到偶数编号包时则丢弃。
使用clang编译,xdp-loader挂载,注意编译时必须带-g参数
!否则无法以dispatcher形式挂载,因此生成的对象文件里没有BTF(BPF Type Format)
信息:
clang -O2 -Wall -target bpf -c -g test_xdp.bpf.c
sudo xdp-loader load -m skb eth0 test_xdp.bpf.o
挂载时如果有报错libxdp: No bpffs found at /sys/fs/bpf
,那么需要执行以下命令手动挂载bpf文件系统:
sudo mount -t bpf bpf /sys/fs/bpf/
查看挂载情况:
挂载的XDP程序id为387,而负责分发的dispatcher id为378,但通过ip来看挂载情况,只能看到dispatcher:
实际测试,可以通过ping命令来查看联通情况:
可以看到,挂载的XDP程序运行正确。
5. 进一步学习
进一步学习XDP程序如何编写与挂载,可以下载 xdp-tutorial 仓库进行学习。