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