一文讀懂eBPF/XDP

Init0ne發表於2021-05-03

XDP概述

XDP是Linux網路路徑上核心整合的資料包處理器,具有安全、可程式設計、高效能的特點。當網路卡驅動程式收到資料包時,該處理器執行BPF程式。XDP可以在資料包進入協議棧之前就進行處理,因此具有很高的效能,可用於DDoS防禦、防火牆、負載均衡等領域。

XDP資料結構

XDP程式使用的資料結構是xdp_buff,而不是sk_buffxdp_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程式發生錯誤,並導致資料包被丟棄。自己開發的程式不應該使用該返回碼
    image

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

  • 驗證連線阻斷效果
  1. 使用nc -l 8888監聽8888 TCP埠,使用nc xxxxx 8888連線傳送資料,目標主機未收到任何資料,說明TCP連線阻斷成功
  2. 使用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

相關文章