一文读懂eBPF/XDP
XDP概述
XDP是Linux网络路径上内核集成的数据包处理器,具有安全、可编程、高性能的特点。当网卡驱动程序收到数据包时,该处理器执行BPF程序。XDP可以在数据包进入协议栈之前就进行处理,因此具有很高的性能,可用于DDoS防御、防火墙、负载均衡等领域。
XDP数据结构
XDP程序使用的数据结构是xdp_buff
,而不是sk_buff
,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
};
XDP与eBPF的关系
XDP程序是通过bpf()系统调用控制的,bpf()系统调用使用程序类型BPF_PROG_TYPE_XDP
进行加载。
XDP操作模式
XDP支持3种工作模式,默认使用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操作结果码
- XDP_DROP:丢弃数据包,发生在驱动程序的最早RX阶段
- XDP_PASS:将数据包传递到协议栈处理,操作可能为以下两种形式:
1、正常接收数据包,分配愿数据sk_buff结构并且将接收数据包入栈,然后将数据包引导到另一个CPU进行处理。他允许原始接口到用户空间进行处理。 这可能发生在数据包修改前或修改后。
2、通过GRO(Generic receive offload)方式接收大的数据包,并且合并相同连接的数据包。经过处理后,GRO最终将数据包传入“正常接收”流 - XDP_TX:转发数据包,将接收到的数据包发送回数据包到达的同一网卡。这可能在数据包修改前或修改后发生
- XDP_REDIRECT:数据包重定向,XDP_TX,XDP_REDIRECT是将数据包送到另一块网卡或传入到BPF的cpumap中
- XDP_ABORTED:表示eBPF程序发生错误,并导致数据包被丢弃。自己开发的程序不应该使用该返回码
XDP和iproute2加载器
iproute2工具中提供的ip命令可以充当XDP加载器的角色,将XDP程序编译成ELF文件并加载他。
- 编写XDP程序xdp_filter.c,程序功能为丢弃所有TCP连接包,程序将
xdp_md
结构指针作为输入,相当于驱动程序xdp_buff
的BPF结构。程序的入口函数为filter,编译后ELF文件的区域名为mysection。
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#define SEC(NAME) __attribute__((section(NAME), used))
SEC("mysection")
int filter(struct xdp_md *ctx) {
int ipsize = 0;
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
struct iphdr *ip;
ipsize = sizeof(*eth);
ip = data + ipsize;
ipsize += sizeof(struct iphdr);
if (data + ipsize > data_end) {
return XDP_DROP;
}
if (ip->protocol == IPPROTO_TCP) {
return XDP_DROP;
}
return XDP_PASS;
}
- 将XDP程序编译为ELF文件
clang -O2 -target bpf -c xdp_filter.c -o xdp_filter.o
- 使用ip命令加载XDP程序,将mysection部分作为程序的入口点
sudo ip link set dev ens33 xdp obj xdp_filter.o sec mysection
没有报错即完成加载,可以通过以下命令查看结果:
$ sudo ip a show ens33
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric/id:56 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:2f:a8:41 brd ff:ff:ff:ff:ff:ff
inet 192.168.136.140/24 brd 192.168.136.255 scope global dynamic noprefixroute ens33
valid_lft 1629sec preferred_lft 1629sec
inet6 fe80::d411:ff0d:f428:ce2a/64 scope link noprefixroute
valid_lft forever preferred_lft forever
其中,xdpgeneric/id:56
说明使用的驱动程序为xdpgeneric,XDP程序id为56
- 验证连接阻断效果
- 使用
nc -l 8888
监听8888 TCP端口,使用nc xxxxx 8888
连接发送数据,目标主机未收到任何数据,说明TCP连接阻断成功 - 使用
nc -kul 9999
监听UDP 9999端口,使用nc -u xxxxx 9999
连接发送数据,目标主机正常收到数据,说明UDP连接不受影响
- 卸载XDP程序
$ sudo ip link set dev ens33 xdp off
卸载后,连接8888端口,发送数据,通信正常。
XDP和BCC
编写C代码xdp_bcc.c,当TCP连接目的端口为9999时DROP:
#define KBUILD_MODNAME "program"
#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>
int filter(struct xdp_md *ctx) {
int ipsize = 0;
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
struct iphdr *ip;
ipsize = sizeof(*eth);
ip = data + ipsize;
ipsize += sizeof(struct iphdr);
if (data + ipsize > data_end) {
return XDP_DROP;
}
if (ip->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (void *)ip + sizeof(*ip);
ipsize += sizeof(struct tcphdr);
if (data + ipsize > data_end) {
return XDP_DROP;
}
if (tcp->dest == ntohs(9999)) {
bpf_trace_printk("drop tcp dest port 9999\n");
return XDP_DROP;
}
}
return XDP_PASS;
}
与使用ip命令加载XDP程序类似,这里编写python加载程序实现对XDP程序的编译和内核注入。
#!/usr/bin/python
from bcc import BPF
import time
device = "ens33"
b = BPF(src_file="xdp_bcc.c")
fn = b.load_func("filter", BPF.XDP)
b.attach_xdp(device, fn, 0)
try:
b.trace_print()
except KeyboardInterrupt:
pass
b.remove_xdp(device, 0)
验证效果,使用nc测试,无法与目标主机9999端口实现通信
$ sudo python xdp_bcc.py
<idle>-0 [003] ..s. 22870.984559: 0: drop tcp dest port 9999
<idle>-0 [003] ..s. 22871.987644: 0: drop tcp dest port 9999
<idle>-0 [003] ..s. 22872.988840: 0: drop tcp dest port 9999
<idle>-0 [003] ..s. 22873.997261: 0: drop tcp dest port 9999
<idle>-0 [003] ..s. 22875.000567: 0: drop tcp dest port 9999
<idle>-0 [003] ..s. 22876.002998: 0: drop tcp dest port 9999
<idle>-0 [003] ..s. 22878.005414: 0: drop tcp dest port 9999
<idle>-0 [003] ..s. 22882.018119: 0: drop tcp dest port 9999
参考
https://duo.com/labs/tech-notes/writing-an-xdp-network-filter-with-ebpf
https://davidlovezoe.club/wordpress/archives/937
一文读懂eBPF/XDP的更多相关文章
- 一文读懂HTTP/2及HTTP/3特性
摘要: 学习 HTTP/2 与 HTTP/3. 前言 HTTP/2 相比于 HTTP/1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作,当然兼容问题以及如何 ...
- 一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现
一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现 导读:近日,马云.马化腾.李彦宏等互联网大佬纷纷亮相2018世界人工智能大会,并登台演讲.关于人工智能的现状与未来,他们提出了各自的观点,也引 ...
- 一文读懂高性能网络编程中的I/O模型
1.前言 随着互联网的发展,面对海量用户高并发业务,传统的阻塞式的服务端架构模式已经无能为力.本文(和下篇<高性能网络编程(六):一文读懂高性能网络编程中的线程模型>)旨在为大家提供有用的 ...
- 从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路
本文原作者阮一峰,作者博客:ruanyifeng.com. 1.引言 HTTP 协议是最重要的互联网基础协议之一,它从最初的仅为浏览网页的目的进化到现在,已经是短连接通信的事实工业标准,最新版本 HT ...
- 一文读懂 深度强化学习算法 A3C (Actor-Critic Algorithm)
一文读懂 深度强化学习算法 A3C (Actor-Critic Algorithm) 2017-12-25 16:29:19 对于 A3C 算法感觉自己总是一知半解,现将其梳理一下,记录在此,也 ...
- [转帖]MerkleDAG全面解析 一文读懂什么是默克尔有向无环图
MerkleDAG全面解析 一文读懂什么是默克尔有向无环图 2018-08-16 15:58区块链/技术 MerkleDAG作为IPFS的核心数据结构,它融合了Merkle Tree和DAG的优点,今 ...
- [转帖]一文读懂 HTTP/2
一文读懂 HTTP/2 http://support.upyun.com/hc/kb/article/1048799/ 又小拍 • 发表于:2017年05月18日 15:34:45 • 更新于:201 ...
- [转帖]从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路
从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路 http://www.52im.net/thread-1709-1-2.html 本文原作者阮一峰,作者博客:r ...
- 一文读懂HDMI和VGA接口针脚定义
一文读懂HDMI和VGA接口针脚定义 摘自:http://www.elecfans.com/yuanqijian/jiekou/20180423666604.html HDMI概述 HDMI是高清 ...
随机推荐
- mysql主从复制,主主复制,级联复制,半同步复制
-------------------------------------------------------------------------------主从复制----------------- ...
- Aliyun Oss 上传文件
目录 Aliyun OSS OSS 简介 OSS 基本概念 OSS 功能概述 OSS 使用 创建存储空间Bucket 创建子目录 Java编码 测试 Aliyun OSS OSS 简介 阿里云对象存储 ...
- 2.5w字 + 36 张图爆肝操作系统面试题,太牛逼了!
欢迎各位大佬访问我的 github ,跪求 star bestJavaer 大家好,我是 cxuan,我之前汇总了一下关于操作系统的面试题,最近又重新翻阅了一下发现不是很全,现在也到了面试季了,所以我 ...
- 翻译:《实用的Python编程》03_04_Modules
目录 | 上一节 (3.3 错误检查) | 下一节 (3.5 主模块) 3.4 模块 本节介绍模块的概念以及如何使用跨多个文件的函数. 模块和导入 任何一个 Python 源文件都是一个模块. # f ...
- java内存区域的划分
前言 之前我们探讨过一个.class文件是如何被加载到jvm中的.但是jvm内又是如何划分内存的呢?这个内被加载到了那一块内存中?jvm内存划分也是面试当中必被问到的一个面试题. 什么是JVM内存区域 ...
- APICloud Avm.js跨端框架的优势
AVM(Application-View-Model)是APICloud推出的一个跨端的高性能 JavaScript框架,更趋近于原生的编程体验,它提供简洁的模型来分离应用的用户界面.业务逻辑和数据模 ...
- Linux速通05 文件处理与编辑
使用 cat 命令进行文件的纵向合并 # 例:使用 cat 命令将 baby.age.baby.weight.baby.sex 这三个文件纵向合并为 baby文件 * cat baby.age bab ...
- 通达OA 任意文件上传-2013/2015版本
参考 http://wiki.0-sec.org/0day/%E9%80%9A%E8%BE%BEoa/11.html 影响版本 2013版本 2015版本 漏洞文件 general/vmeet/wbU ...
- Protobuf在Cmake中的正确使用
Protobuf是google开发的一个序列化和反序列化的协议库,我们可以自己设计传递数据的格式,通过.proto文件定义我们的要传递的数据格式.例如,在深度学习中常用的ONNX交换模型就是使用.pr ...
- c++ 11 是如何简化你的数据库访问接口的
之前写过一篇文章专门分析了 c++ 模板编译过程中报的一个错误:<fatal error C1045: 编译器限制 : 链接规范嵌套太深 >,其中涉及到了 qtl -- 一个使用 c++ ...