WEBKT

告别传统防火墙,用eBPF自制高性能网络过滤器

113 0 0 0

前言:为什么是eBPF?

eBPF基础:快速入门

动手实践:编写eBPF防火墙

1. 编写eBPF程序

2. 编译eBPF程序

3. 加载eBPF程序

4. 更新黑名单

验证防火墙

深入探索:更高级的防火墙功能

性能优化:让eBPF飞起来

总结:eBPF的无限可能

前言:为什么是eBPF?

传统的网络安全方案,比如iptables,虽然经典但也有其局限性。它们通常运行在内核空间,规则匹配和数据包过滤的效率会受到一定影响。而eBPF(extended Berkeley Packet Filter)的出现,为网络安全带来了新的可能性。作为一名追求极致性能的开发者,我一直在探索如何利用eBPF来构建更高效、更灵活的网络安全工具。

eBPF允许你在内核中安全地运行用户自定义的代码,无需修改内核源代码或加载内核模块。这意味着你可以直接在网络数据包到达内核协议栈之前对其进行过滤和处理,极大地提高了性能和灵活性。想想看,这就像在高速公路上设置了一个智能分流器,能够根据你的规则快速地决定哪些车辆可以通行,哪些需要拦截。

在这篇文章中,我将分享如何使用eBPF来实现一个简单的网络防火墙,它可以根据IP地址、端口号等信息来过滤网络数据包。我会尽量用通俗易懂的语言,结合实际的代码示例,让你能够快速上手并掌握eBPF的核心概念。

eBPF基础:快速入门

在开始编写防火墙之前,我们需要先了解一些eBPF的基础知识。别担心,我会尽量简化概念,让你能够快速理解。

  1. eBPF程序类型eBPF程序有多种类型,用于不同的场景。例如,BPF_PROG_TYPE_SOCKET_FILTER用于过滤套接字上的数据包,BPF_PROG_TYPE_KPROBE用于在内核函数执行时运行代码。对于防火墙,我们通常使用BPF_PROG_TYPE_SOCKET_FILTERBPF_PROG_TYPE_XDP(eXpress Data Path)。

  2. eBPF助手函数eBPF程序不能直接调用内核函数,而是需要使用eBPF提供的助手函数。这些函数提供了诸如获取当前时间、访问数据包内容、修改数据包等功能。

  3. eBPF MapeBPF MapeBPF程序与用户空间程序共享数据的机制。你可以将过滤规则、统计信息等存储在Map中,并在用户空间程序中进行更新和查询。

  4. BPF验证器:为了保证内核安全,eBPF程序在加载到内核之前会经过BPF验证器的检查。验证器会检查程序是否包含非法指令、是否会访问非法内存等。

  5. LLVM/Clang:通常我们使用C语言编写eBPF程序,然后使用LLVM/Clang编译器将其编译成eBPF字节码。

动手实践:编写eBPF防火墙

现在,让我们开始编写一个简单的eBPF防火墙。这个防火墙可以根据源IP地址来过滤数据包。如果源IP地址在黑名单中,则丢弃该数据包;否则,允许通过。

1. 编写eBPF程序

首先,创建一个名为firewall.c的文件,并添加以下代码:

#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define MAX_BLACKLIST_SIZE 10
struct bpf_map_def SEC("maps") blacklist_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(unsigned int), // IP address (network byte order)
.value_size = sizeof(bool), // true = blacklisted
.max_entries = MAX_BLACKLIST_SIZE,
};
SEC("socketfilter")
int firewall(struct __sk_buff *skb) {
// Load IP header
struct iphdr *ip = bpf_hdr_pointer(skb, sizeof(struct ethhdr));
if (!ip) {
return TC_ACCEPT; // Allow non-IP packets
}
// Convert source IP to network byte order
unsigned int src_ip = bpf_ntohl(ip->saddr);
// Check if source IP is in blacklist
bool *is_blacklisted = bpf_map_lookup_elem(&blacklist_map, &src_ip);
if (is_blacklisted && *is_blacklisted) {
// Drop packet
return TC_DROP;
}
// Allow packet
return TC_ACCEPT;
}
char _license[] SEC("license") = "GPL";

这段代码定义了一个eBPF程序,它执行以下操作:

  • 定义了一个名为blacklist_mapeBPF Map,用于存储黑名单IP地址。KeyIP地址(网络字节序),Value是一个布尔值,表示该IP地址是否在黑名单中。
  • 定义了一个名为firewalleBPF程序,它接收一个指向__sk_buff结构的指针,该结构包含了网络数据包的信息。
  • firewall程序中,首先获取IP头部。如果不是IP数据包,则允许通过。
  • 然后,将源IP地址转换为网络字节序。
  • 接着,在blacklist_map中查找源IP地址。如果找到且该IP地址在黑名单中,则丢弃该数据包;否则,允许通过。

2. 编译eBPF程序

接下来,我们需要将eBPF程序编译成eBPF字节码。可以使用以下命令:

clang -target bpf -D__TARGET_ARCH_x86_64 -O2 -Wall -Werror -c firewall.c -o firewall.o

这条命令使用Clang编译器将firewall.c编译成firewall.o文件。-target bpf选项指定编译目标为eBPF-D__TARGET_ARCH_x86_64选项指定目标架构为x86_64

3. 加载eBPF程序

现在,我们需要将编译好的eBPF程序加载到内核中。可以使用libbpf库来实现。首先,安装libbpf库:

sudo apt-get install libbpf-dev

然后,创建一个名为loader.c的文件,并添加以下代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <linux/if_link.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#define IFACE "eth0" // Replace with your network interface
int main(int argc, char **argv)
{
struct bpf_object *obj;
int prog_fd, map_fd, err;
// Load BPF object from file
obj = bpf_object__open("firewall.o");
if (!obj) {
fprintf(stderr, "Failed to open BPF object: %s\n", strerror(errno));
return 1;
}
// Load BPF program
err = bpf_object__load(obj);
if (err) {
fprintf(stderr, "Failed to load BPF object: %s\n", strerror(errno));
bpf_object__close(obj);
return 1;
}
// Get program file descriptor
prog_fd = bpf_program__fd(bpf_object__find_program_by_name(obj, "firewall"));
if (prog_fd < 0) {
fprintf(stderr, "Failed to get program FD: %s\n", strerror(errno));
bpf_object__close(obj);
return 1;
}
// Get map file descriptor
map_fd = bpf_object__find_map_fd_by_name(obj, "blacklist_map");
if (map_fd < 0) {
fprintf(stderr, "Failed to get map FD: %s\n", strerror(errno));
bpf_object__close(obj);
return 1;
}
// Attach program to interface using tc (traffic control)
char tc_cmd[256];
snprintf(tc_cmd, sizeof(tc_cmd), "tc qdisc add dev %s clsact", IFACE);
system(tc_cmd);
snprintf(tc_cmd, sizeof(tc_cmd), "tc filter add dev %s ingress bpf obj firewall.o sec socketfilter flowid 1:1 protocol ip parent clsact", IFACE);
system(tc_cmd);
printf("eBPF firewall loaded and attached to interface %s\n", IFACE);
// Keep program running
while (1) {
sleep(1);
}
bpf_object__close(obj);
return 0;
}

这段代码执行以下操作:

  • 使用bpf_object__open函数打开firewall.o文件。
  • 使用bpf_object__load函数将eBPF程序加载到内核中。
  • 使用bpf_program__fd函数获取firewall程序的File Descriptor
  • 使用bpf_object__find_map_fd_by_name函数获取blacklist_mapFile Descriptor
  • 使用tc(traffic control)命令将eBPF程序附加到指定的网络接口(默认为eth0)。

注意: 请将IFACE宏替换为你的实际网络接口名称。

然后,编译loader.c文件:

gcc loader.c -o loader -lbpf

最后,以root权限运行loader程序:

sudo ./loader

如果一切顺利,你将看到以下输出:

eBPF firewall loaded and attached to interface eth0

4. 更新黑名单

现在,我们需要编写一个用户空间程序来更新黑名单。创建一个名为blacklist_updater.c的文件,并添加以下代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdbool.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
int main(int argc, char **argv)
{
if (argc != 3) {
fprintf(stderr, "Usage: %s <ip_address> <add|remove>\n", argv[0]);
return 1;
}
const char *ip_address_str = argv[1];
const char *action = argv[2];
// Convert IP address string to network byte order
unsigned int ip_address;
if (inet_pton(AF_INET, ip_address_str, &ip_address) != 1) {
fprintf(stderr, "Invalid IP address: %s\n", ip_address_str);
return 1;
}
// Open the BPF map
int map_fd = bpf_obj_get("/sys/fs/bpf/blacklist_map");
if (map_fd < 0) {
fprintf(stderr, "Failed to open BPF map: %s\n", strerror(errno));
return 1;
}
bool is_blacklisted = strcmp(action, "add") == 0;
// Update the BPF map
if (bpf_map_update_elem(map_fd, &ip_address, &is_blacklisted, BPF_ANY) != 0) {
fprintf(stderr, "Failed to update BPF map: %s\n", strerror(errno));
close(map_fd);
return 1;
}
printf("IP address %s %s to blacklist\n", ip_address_str, is_blacklisted ? "added" : "removed");
close(map_fd);
return 0;
}

这段代码执行以下操作:

  • 接收两个命令行参数:IP地址和操作(addremove)。
  • IP地址字符串转换为网络字节序。
  • 打开blacklist_map
  • 根据操作,将IP地址添加到黑名单或从黑名单中删除。

注意: 在较新的内核版本中,bpf_obj_get 可能不再直接可用,你需要通过挂载BPF文件系统来访问Map。 创建/sys/fs/bpf目录(如果不存在), 然后挂载:

mount -t bpf bpf /sys/fs/bpf

编译blacklist_updater.c文件:

gcc blacklist_updater.c -o blacklist_updater -lbpf

然后,以root权限运行blacklist_updater程序:

sudo ./blacklist_updater 192.168.1.100 add

这条命令将192.168.1.100添加到黑名单。你可以使用remove操作将其从黑名单中删除:

sudo ./blacklist_updater 192.168.1.100 remove

验证防火墙

为了验证防火墙是否正常工作,你可以使用ping命令从被阻止的IP地址向你的机器发送数据包。如果防火墙正常工作,你将无法ping通你的机器。

例如,如果你将192.168.1.100添加到黑名单,然后从192.168.1.100的机器上执行以下命令:

ping <你的机器IP地址>

你将看不到任何回复。

深入探索:更高级的防火墙功能

我们实现的只是一个非常简单的防火墙。你可以通过以下方式来扩展其功能:

  • 支持更多的过滤规则:可以根据端口号、协议类型等信息来过滤数据包。
  • 支持IP地址范围:可以使用CIDR表示法来表示IP地址范围。
  • 支持白名单:可以创建一个白名单,只允许来自白名单IP地址的数据包通过。
  • 统计信息:可以统计被阻止的数据包数量、来源IP地址等信息。
  • 动态规则更新:可以使用Netlink套接字来动态更新过滤规则。

性能优化:让eBPF飞起来

eBPF的性能非常出色,但仍然有一些技巧可以用来进一步优化其性能:

  • 减少内存访问:尽量减少对内存的访问,尤其是在循环中。
  • 使用bpf_tail_call:可以使用bpf_tail_call函数将执行流程转移到另一个eBPF程序,避免重复代码。
  • 使用BPF_F_NO_PREALLOC标志:在创建eBPF Map时,可以使用BPF_F_NO_PREALLOC标志来禁用预分配内存,减少内存占用。
  • 优化Map类型:选择合适的Map类型可以提高性能。例如,如果只需要存储少量数据,可以使用BPF_MAP_TYPE_ARRAY;如果需要频繁查找数据,可以使用BPF_MAP_TYPE_HASH

总结:eBPF的无限可能

eBPF为网络安全带来了革命性的变化。它不仅可以用于构建高性能的防火墙,还可以用于流量监控、入侵检测、性能分析等领域。作为一名开发者,我强烈建议你深入学习eBPF,并将其应用到你的项目中。我相信,eBPF将会成为未来网络安全领域的重要技术。

通过本文,我希望你已经掌握了使用eBPF构建简单防火墙的基本知识。现在,你可以开始尝试构建更复杂的网络安全工具,并探索eBPF的无限可能。记住,安全无小事,让我们一起用技术守护网络安全!

NetGuard侠 eBPF网络安全防火墙

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9429