近20年以上,Linux系统的传统安全功能一直围绕iPtables,这是Linux内核中的事实上的数据包过滤机制。
但是,网络速度的提高和Linux服务器中运行的应用程序类型的转换导致了意识,即当前实施可能无法应对现代需求,尤其是在可扩展性方面。
那是XDP和EBPF进来的地方。
那么什么是xdp?
XDP是上游Linux内核的一部分,并使用户能够将数据包处理程序注入内核,在内核对数据上进行其他任何处理之前,将对每个到达数据包执行。
XDP的早期采用者之一是Cloudflare。由于其内容交付网络收到和计算的大量请求,使用iptables为其用例(例如DDos Mitigation platform或the Layer-4 Load balancer)使用iptables并不像他们想要的那样高效。 XDP似乎可以解决它们的iptables问题,因为随着服务的数量的增长,测量结果显示出不可预测的延迟和降低的性能。
编写代码
因此,关于技术的足够书呆子讲话,我们如何使用它?
要演示,我们将编写一个简单的防火墙,该防火墙会使用XDP根据其IP地址删除传入数据包。
该程序将由2个部分组成:
- 内核空间代码
- 用户空间代码
内核端是用C或Rust编写的(我们将使用C),并将其编译到EBPF字节代码格式中,该格式经过验证并在内核中进行了jit编译。
我将用GO编写的用户空间控制器编写XDP应用程序。
我应该提到的是,编写复杂的EBPF程序比今天要做的要多得多。关于EBPF和XDP有很多了解。我们几乎不会刮擦表面。
该项目的所有代码都可以在here中找到。
首先,让我们讨论事物的内核方面。
内核空间代码
如前所述,我们在这里使用的语言受到了一些限制。可以使用的可能的语言是生锈和C。
我们会使用C,因为几乎每个标准用例场景都有一个C程序供我们捕获和使用,除非我们试图做的是非常特定于我们的用户酶。 (Cloudflare的L4 DDos Mitigation platform很好地展示了这一点)
我们的内核空间程序将由一个称为firewall
的功能和一张名为blacklist
的函数。
我们已经知道什么是功能,但是什么是地图,为什么我们需要它?
地图是用于存储不同类型数据的通用数据结构,可用于在EBPF程序以及内核和用户空间之间共享数据。地图的密钥和值可以是任意大小,如创建地图时所定义的。用户还定义了使用max_entries
的最大条目数。
blacklist
将存储所有需要删除的IP地址,firewall
函数将检查每个数据包,并查看该数据包的源IP是否匹配blacklist
Map中存储的任何IP地址。
我们将使用以下XDP操作:
-
XDP_PASS
如果IP地址匹配blacklist
地图中的IP addresers。 -
XDP_DROP
如果它确实与地图中的IP地址匹配。 -
XDP_ABORTED
如果数据包的ipv4 header
或ethernet header
被畸形。
首先,让我们看一下blacklist
地图的定义:
BPF_MAP_DEF(blacklist) = {
.map_type = BPF_MAP_TYPE_LPM_TRIE,
.key_size = sizeof(__u64),
.value_size = sizeof(__u32),
.max_entries = 16,
};
BPF_MAP_ADD(blacklist);
定义很简单。
我们将首先定义地图的类型。
这是bpf_helpers.h
标头文件中的地图类型列表:
enum bpf_map_type {
BPF_MAP_TYPE_UNSPEC = 0,
BPF_MAP_TYPE_HASH,
BPF_MAP_TYPE_ARRAY,
BPF_MAP_TYPE_PROG_ARRAY,
BPF_MAP_TYPE_PERF_EVENT_ARRAY,
BPF_MAP_TYPE_PERCPU_HASH,
BPF_MAP_TYPE_PERCPU_ARRAY,
BPF_MAP_TYPE_STACK_TRACE,
BPF_MAP_TYPE_CGROUP_ARRAY,
BPF_MAP_TYPE_LRU_HASH,
BPF_MAP_TYPE_LRU_PERCPU_HASH,
BPF_MAP_TYPE_LPM_TRIE,
BPF_MAP_TYPE_ARRAY_OF_MAPS,
BPF_MAP_TYPE_HASH_OF_MAPS,
BPF_MAP_TYPE_DEVMAP,
BPF_MAP_TYPE_SOCKMAP,
BPF_MAP_TYPE_CPUMAP,
BPF_MAP_TYPE_XSKMAP,
BPF_MAP_TYPE_SOCKHASH,
BPF_MAP_TYPE_CGROUP_STORAGE,
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
BPF_MAP_TYPE_QUEUE,
BPF_MAP_TYPE_STACK,
BPF_MAP_TYPE_SK_STORAGE,
};
然后,我们将定义.key_size
,.value_size
和.max_entries
。
现在,看看firewall
函数。
在定义函数之前,我们需要定义2个结构:
-
ethhdr
以太网标头 -
iphdr
IPv4标头
// Ethernet header
struct ethhdr {
__u8 h_dest[6];
__u8 h_source[6];
__u16 h_proto;
} __attribute__((packed));
// IPv4 header
struct iphdr {
__u8 ihl : 4;
__u8 version : 4;
__u8 tos;
__u16 tot_len;
__u16 id;
__u16 frag_off;
__u8 ttl;
__u8 protocol;
__u16 check;
__u32 saddr;
__u32 daddr;
} __attribute__((packed));
我们将需要定义这些结构,因此我们分析了收到的数据包。所有这些逻辑发生在firewall
函数中。
让我们看一下功能的定义:
SEC("xdp")
int firewall(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *ether = data;
// Check if the Ethernet header is malformed
if (data + sizeof(*ether) > data_end) {
return XDP_ABORTED;
}
if (ether->h_proto != 0x08U) { // htons(ETH_P_IP) -> 0x08U
// If not IPv4 Traffic, pass the packet
return XDP_PASS;
}
data += sizeof(*ether);
struct iphdr *ip = data;
// Check if the IPv4 header is malformed
if (data + sizeof(*ip) > data_end) {
return XDP_ABORTED;
}
struct {
__u32 prefixlen;
__u32 saddr;
} key;
key.prefixlen = 32;
key.saddr = ip->saddr;
// Lookup the IP Address in the blacklsit map
// we defined earlier.
// If the source IP matches the IP in the blacklist map
// We drop the packet!!
__u64 *rule_idx = bpf_map_lookup_elem(&blacklist, &key);
if (rule_idx) {
__u32 index = *(__u32*)rule_idx; // make verifier happy
return XDP_DROP;
}
return XDP_PASS;
}
不要被恐吓!
C代码可能令人生畏,但是仔细查看代码,我们可以看到该功能要完成的工作。
我应该提到此XDP程序仅支持ipv4
数据包,因此将使用XDP_PASS
传递任何其他类型的流量(包括ipv6
)。
我们几乎完成了内核空间的一面。
我们只需要编译我们的代码EBPF字节代码。
但是在这样做之前,我们需要在C
文件开始时包含bpf_helpers.h
标头文件:
#include "bpf_helpers.h"
这是不是 BCC GitHub存储库中相同的bpf_helpers.h
标头文件。您可以在here中找到标题文件。
下载它并将其放入您的项目中,或克隆回购并编译。
我们将使用Clang将代码编译为EBPF字节代码。
安装并运行以下命令:
$ clang -I ../headers -O -target bpf -c xdp.c -o xdp.elf
-I
指向我们从前下载bpf_helpers.h
文件的目录,因此请注意。 xdp.c
是我们C源代码的文件名,输出将以ELF
格式,这就是我们的用户空间代码将加载到内核中。
我们可以使用readelf
命令来验证文件:
$ readelf -a xdp.elf
...
Symbol table '.symtab' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS xdp.c
2: 0000000000000130 0 NOTYPE LOCAL DEFAULT 3 LBB0_5
3: 0000000000000000 312 FUNC GLOBAL DEFAULT 3 firewall
4: 0000000000000000 40 OBJECT GLOBAL DEFAULT 5 blacklist
如果您有问题编译,请查看source code并检查是否缺少任何内容,或在下面发表评论。
让我们将这2个文件放入一个名为bpf
的目录中,然后转到教程的令人兴奋的部分!
用户空间代码
现在是我们计划中令人兴奋的(且毫无意义的)一部分!编写GO代码。
在我们潜水之前,让我们首先看一下世界各地可爱的开源社区为我们提供的所有EBPF/GO图书馆。
这是最受欢迎的GO EBPF库的列表:
- ebpf-go by Cilium是一个纯库,可以读取,修改和加载EBPF程序,并将其附加到Linux内核中的各种钩子上。
- gobpf by iovisor为BCC框架提供GO绑定以及从.elf文件加载和使用EBPF程序的低级例程。
- libbpfgo by aquasecurity围绕libbpf建造 - 与EBPF程序互动的标准库
- Dropbox的goebpf-使用EBPF程序 / PERF活动的一种很好而便捷的方式。< / li>
我决定使用本教程中的Dropbox的GoeBPF,这主要是因为它很简单,并且很容易对EBPF进行初学者友好的方法。
让我们通过Dropbox安装GOEBPF库:
go get github.com/dropbox/goebpf
现在我们有了这个问题,创建一个名为main.go
的文件,让我们开始编码!
注意:确保main.go
文件和C
和ELF
文件的其余部分与可能导致错误和警告的目录不在同一目录中,请将main.go
文件放在我们之前制作的bpf
目录旁边的main.go
文件。
因此,目录层次结构看起来与此相似:
.
├── bpf
│ ├── xdp.c
│ └── xdp.elf
├── go.mod
├── go.sum
├── headers
│ └── bpf_helpers.h
└── main.go
让我们首先定义两个变量,即我们将XDP程序附加到包含IP地址及其子网的切片。
package main
import (
"fmt"
"log"
"os"
"os/signal"
"github.com/dropbox/goebpf"
)
func main() {
// Specify Interface Name
interfaceName := "lo"
// IP BlockList
// Add the IPs you want to be blocked
ipList := []string{
"8.8.8.8",
}
...
在我的示例中,我将把我的XDP程序附加到系统的环回设备上,我将删除来自源IP地址8.8.8.8
的任何数据包。
现在让我们加载XDP程序:
// Load XDP Into App
bpf := goebpf.NewDefaultEbpfSystem()
// According to our directory hierarchy,
// The xdp.elf file is placed in the bpf directory
// next to the main.go file
err := bpf.LoadElf("bpf/xdp.elf")
if err != nil {
log.Fatalf("LoadELF() failed: %s", err)
}
// Load blacklist map
blacklist := bpf.GetMapByName("blacklist")
if blacklist == nil {
log.Fatalf("eBPF map 'blacklist' not found\n")
}
// Load firewall function
xdp := bpf.GetProgramByName("firewall")
if xdp == nil {
log.Fatalln("Program 'firewall' not found in Program")
}
err = xdp.Load()
if err != nil {
fmt.Printf("xdp.Attach(): %v", err)
}
// attach the program to the interface
// in this case, the lo interface
err = xdp.Attach(interfaceName)
if err != nil {
log.Fatalf("Error attaching to Interface: %s", err)
}
代码是非常自我解释的。
我们从bpf
目录加载EBPF ELF文件。然后,我们将blacklist
映射和firewall
函数分配给变量blacklist
&firewall
,最后我们将程序附加到接口上,在这种情况下,在这种情况下为loopback接口(lo
)。
我们只需要创建一个将IP地址在ipList
中的函数,并将其添加到黑名单映射中。其余的是在我们之前编写的C代码中处理的。
这是功能:
func BlockIPAddress(ipAddreses []string, blacklist goebpf.Map) error {
for index, ip := range ipAddreses {
fmt.Printf("\t%s\n", ip)
err := blacklist.Insert(goebpf.CreateLPMtrieKey(ip), index)
if err != nil {
return err
}
}
return nil
}
非常简单。
现在,我们可以在将程序连接到接口之后使用此功能:
....
err = xdp.Attach(interfaceName)
if err != nil {
log.Fatalf("Error attaching to Interface: %s", err)
}
BlockIPAddress(ipList, blacklist)
defer xdp.Detach()
我们几乎完成了。唯一缺少的是,如果我们构建和运行程序,它将立即退出,并且XDP程序将被分离(请参阅defer xdp.Detach
)。
我们不想要那个,我们希望程序运行,直到中断它为止。我们将使用频道为此。
这是最终的GO代码:
package main
import (
"fmt"
"log"
"os"
"os/signal"
"github.com/dropbox/goebpf"
)
func main() {
// Specify Interface Name
interfaceName := "lo"
// IP BlockList
// Add the IPs you want to be blocked
ipList := []string{
"8.8.8.8",
}
// Load XDP Into App
bpf := goebpf.NewDefaultEbpfSystem()
err := bpf.LoadElf("bpf/xdp.elf")
if err != nil {
log.Fatalf("LoadELF() failed: %s", err)
}
blacklist := bpf.GetMapByName("blacklist")
if blacklist == nil {
log.Fatalf("eBPF map 'blacklist' not found\n")
}
xdp := bpf.GetProgramByName("firewall")
if xdp == nil {
log.Fatalln("Program 'firewall' not found in Program")
}
err = xdp.Load()
if err != nil {
fmt.Printf("xdp.Attach(): %v", err)
}
err = xdp.Attach(interfaceName)
if err != nil {
log.Fatalf("Error attaching to Interface: %s", err)
}
BlockIPAddress(ipList, blacklist)
defer xdp.Detach()
ctrlC := make(chan os.Signal, 1)
signal.Notify(ctrlC, os.Interrupt)
log.Println("XDP Program Loaded successfuly into the Kernel.")
log.Println("Press CTRL+C to stop.")
<-ctrlC
}
// The Function That adds the IPs to the blacklist map
func BlockIPAddress(ipAddreses []string, blacklist goebpf.Map) error {
for index, ip := range ipAddreses {
err := blacklist.Insert(goebpf.CreateLPMtrieKey(ip), index)
if err != nil {
return err
}
}
return nil
}
使用sudo构建并运行GO代码:
$ go build
$ sudo ./xdp-firewall
2022/12/02 02:36:46 XDP Program Loaded successfuly into the Kernel.
2022/12/02 02:36:46 Press CTRL+C to stop.
现在在程序运行时,打开另一个终端并运行以下命令:
$ ip link list dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
prog/xdp id 173
我们现在可以看到具有173个ID的XDP程序。
如果我们回到最后一个终端并退出程序,然后再次运行命令:
$ ip link list dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
XDP程序已从lo
接口中消失。
这几乎是它!
如果您想了解有关此主题的更多信息,我将在下面添加一些链接供您查看。