WEBKT

告别传统抓包,看我如何用 eBPF 在 Linux 上玩转网络流量分析?

40 0 0 0

前言:网络世界的“显微镜”——eBPF

eBPF 基础:理解内核中的“瑞士军刀”

实战:基于 eBPF 的网络流量分析

eBPF 网络流量分析的进阶技巧

总结与展望:eBPF 的无限可能

前言:网络世界的“显微镜”——eBPF

作为一名资深 Linux 玩家,我深知网络流量分析对于系统诊断、安全监控的重要性。过去,我们依赖 tcpdump、Wireshark 等工具,但它们在处理高并发、大数据量时,性能瓶颈显而易见。有没有一种更高效、更灵活的方式,让我们深入 Linux 内核,实时洞察网络流量的奥秘呢?答案是肯定的,那就是 eBPF(Extended Berkeley Packet Filter)。

eBPF,这个听起来有些神秘的技术,实际上是 Linux 内核的一项革命性创新。它允许我们在内核中安全地运行自定义代码,而无需修改内核源码或加载内核模块。这意味着我们可以编写 eBPF 程序,像“显微镜”一样观察网络数据包,进行各种分析、过滤和处理,而且性能极高!

本文将带你一步步了解如何利用 eBPF 在 Linux 系统中进行网络流量分析,包括数据包的捕获、过滤、解析,以及如何利用 eBPF 程序实现自定义的网络监控和安全策略。无论你是网络工程师、系统管理员,还是对内核技术充满好奇的开发者,都能从中受益。

eBPF 基础:理解内核中的“瑞士军刀”

要玩转 eBPF 网络流量分析,首先需要对 eBPF 的基本概念有所了解。可以将 eBPF 视为内核中的一个“沙箱”,我们可以在其中运行用户定义的程序,而无需担心破坏内核的稳定性。这些程序可以被附加到内核的各种事件(例如网络数据包的接收、系统调用的执行等),并在事件发生时被触发执行。

1. eBPF 程序类型:

eBPF 程序类型繁多,针对不同的应用场景,常见的有:

  • kprobe/kretprobe: 用于跟踪内核函数的执行,可以获取函数参数、返回值等信息。
  • uprobe/uretprobe: 用于跟踪用户空间函数的执行,类似于 kprobe,但作用于用户进程。
  • tracepoint: 附加到内核中预定义的跟踪点,可以获取特定事件的信息。
  • XDP (eXpress Data Path): 附加到网络设备驱动程序的最早阶段,可以实现高性能的数据包处理。
  • tc (Traffic Control): 附加到网络接口的流量控制层,可以实现数据包的过滤、修改和重定向。

在网络流量分析中,XDP 和 tc 是两个非常重要的程序类型。

2. eBPF Map:

eBPF 程序需要在内核中存储和共享数据,这时就需要用到 eBPF Map。Map 是一种键值对存储结构,可以在 eBPF 程序和用户空间程序之间共享数据。

常见的 Map 类型包括:

  • Hash Map: 基于哈希表的 Map,适用于快速查找。
  • Array Map: 基于数组的 Map,适用于固定大小的数据。
  • LRU Map: 最近最少使用 Map,适用于缓存场景。
  • Ring Buffer: 环形缓冲区,适用于高性能的数据传输。

3. eBPF 程序的执行流程:

  1. 编写 eBPF 程序: 使用 C 语言编写 eBPF 程序,并使用 LLVM 编译成 BPF 字节码。
  2. 加载 eBPF 程序: 使用 bpf() 系统调用将 BPF 字节码加载到内核中。
  3. 附加 eBPF 程序: 将 eBPF 程序附加到内核的特定事件(例如 XDP 或 tc)。
  4. 事件触发: 当事件发生时,内核会执行附加的 eBPF 程序。
  5. 数据共享: eBPF 程序可以使用 Map 与用户空间程序共享数据。

实战:基于 eBPF 的网络流量分析

了解了 eBPF 的基本概念,接下来我们通过几个实战案例,学习如何使用 eBPF 进行网络流量分析。

案例 1:使用 XDP 统计网络数据包数量

这个案例将演示如何使用 XDP 程序统计网络接口接收到的数据包数量。

1. 编写 eBPF 程序 (xdp_counter.c):

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf_helpers.h>
#define SEC(NAME) __attribute__((section(NAME), used))
// 定义一个 Map,用于存储数据包数量
struct bpf_map_def SEC("maps") packets_map = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(long),
.max_entries = 1,
};
SEC("xdp")
int xdp_counter(struct xdp_md *ctx) {
int key = 0;
long *value = bpf_map_lookup_elem(&packets_map, &key);
if (value) {
*value += 1;
}
return XDP_PASS; // 允许数据包通过
}
char _license[] SEC("license") = "GPL";

代码解释:

  • #include 引入必要的头文件。
  • SEC 宏用于将代码段标记为特定的 section,例如 mapsxdp
  • packets_map 定义一个 Array Map,用于存储数据包数量,key 为 0,value 为 long 类型。
  • xdp_counter 函数是 XDP 程序的主体,它会在每个接收到的数据包上执行。
  • bpf_map_lookup_elem 函数用于在 Map 中查找 key 对应的值。
  • XDP_PASS 宏表示允许数据包通过,还可以使用 XDP_DROP 丢弃数据包,XDP_TX 将数据包重定向到其他接口等。
  • _license 用于声明程序的 license,必须是 GPL 兼容的 license。

2. 编译 eBPF 程序:

clang -O2 -target bpf -c xdp_counter.c -o xdp_counter.o

3. 加载和附加 eBPF 程序:

需要使用 ip 命令或 bpftool 工具加载和附加 eBPF 程序。这里使用 bpftool

# 加载 eBPF 程序
bpftool prog load xdp_counter.o /sys/fs/bpf/xdp_counter
# 将 eBPF 程序附加到网络接口 (例如 eth0)
bpftool net attach xdp /sys/fs/bpf/xdp_counter dev eth0

4. 读取数据包数量:

编写一个用户空间程序,从 Map 中读取数据包数量:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/bpf.h>
#define BPF_MAP_GET_NEXT_KEY 8
int main() {
int map_fd, key = 0;
long value;
// 打开 Map
map_fd = open("/sys/fs/bpf/xdp_counter_map", O_RDONLY);
if (map_fd < 0) {
perror("open map failed");
return 1;
}
// 从 Map 中读取数据
if (ioctl(map_fd, BPF_MAP_GET_NEXT_KEY, &key) < 0) {
perror("ioctl failed");
close(map_fd);
return 1;
}
if (read(map_fd, &value, sizeof(value)) < 0) {
perror("read failed");
close(map_fd);
return 1;
}
printf("Packets received: %ld\n", value);
close(map_fd);
return 0;
}

5. 运行程序:

编译并运行用户空间程序,即可看到统计的数据包数量。

案例 2:使用 tc 过滤特定端口的流量

这个案例将演示如何使用 tc 程序过滤特定端口的流量,例如只允许 80 端口的流量通过。

1. 编写 eBPF 程序 (tc_filter.c):

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf_helpers.h>
#define SEC(NAME) __attribute__((section(NAME), used))
SEC("tc")
int tc_filter(struct __sk_buff *skb) {
// 获取以太网头部
struct ethhdr *eth = bpf_hdr_pointer(skb->skb, skb->data);
if (!eth) {
return TC_ACT_OK; // 允许数据包通过
}
// 检查是否为 IP 协议
if (eth->h_proto != htons(ETH_P_IP)) {
return TC_ACT_OK; // 允许数据包通过
}
// 获取 IP 头部
struct iphdr *ip = bpf_hdr_pointer(skb->skb, skb->data + sizeof(struct ethhdr));
if (!ip) {
return TC_ACT_OK; // 允许数据包通过
}
// 检查是否为 TCP 协议
if (ip->protocol != IPPROTO_TCP) {
return TC_ACT_OK; // 允许数据包通过
}
// 获取 TCP 头部
struct tcphdr *tcp = bpf_hdr_pointer(skb->skb, skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr));
if (!tcp) {
return TC_ACT_OK; // 允许数据包通过
}
// 检查目标端口是否为 80
if (ntohs(tcp->dest) == 80) {
return TC_ACT_OK; // 允许数据包通过
} else {
return TC_ACT_SHOT; // 丢弃数据包
}
}
char _license[] SEC("license") = "GPL";

代码解释:

  • skb 是 socket buffer 的缩写,包含了网络数据包的所有信息。
  • bpf_hdr_pointer 函数用于获取指定偏移量的头部指针,需要进行空指针检查。
  • TC_ACT_OK 宏表示允许数据包通过,TC_ACT_SHOT 宏表示丢弃数据包。

2. 编译 eBPF 程序:

clang -O2 -target bpf -c tc_filter.c -o tc_filter.o

3. 加载和附加 eBPF 程序:

需要使用 tc 命令加载和附加 eBPF 程序。首先创建一个 qdisc (queueing discipline):

tc qdisc add dev eth0 clsact

然后创建一个 filter,并将 eBPF 程序附加到 filter 上:

tc filter add dev eth0 ingress bpf obj tc_filter.o section tc

4. 验证:

使用 tcpdumpWireshark 抓包,可以看到只有 80 端口的流量可以通过。

eBPF 网络流量分析的进阶技巧

掌握了 eBPF 的基本用法,我们可以进一步探索 eBPF 在网络流量分析中的高级应用。

1. 使用 BPF Tracepoint 进行内核事件跟踪:

BPF Tracepoint 允许我们在内核的特定位置插入探针,收集内核事件的信息。例如,我们可以跟踪 tcp_connect 事件,记录 TCP 连接的建立过程。

2. 使用 eBPF 进行 DDoS 防护:

eBPF 可以用于实时检测和缓解 DDoS 攻击。例如,我们可以编写 eBPF 程序,统计特定 IP 地址的连接数量,当连接数量超过阈值时,可以采取相应的措施,例如丢弃数据包或限制连接速率。

3. 使用 eBPF 进行网络性能监控:

eBPF 可以用于监控网络性能指标,例如延迟、丢包率、吞吐量等。我们可以编写 eBPF 程序,收集这些指标,并将其导出到监控系统,例如 Prometheus。

4. 使用 Cilium 进行云原生网络安全:

Cilium 是一个基于 eBPF 的云原生网络安全解决方案,它提供了强大的网络策略、可观测性和安全性功能。Cilium 可以与 Kubernetes 等容器编排系统集成,实现容器间的安全通信。

总结与展望:eBPF 的无限可能

eBPF 作为一项革命性的内核技术,为网络流量分析带来了无限可能。它不仅可以提高性能,还可以实现更灵活、更强大的功能。随着 eBPF 技术的不断发展,相信它将在网络安全、性能监控、云原生等领域发挥越来越重要的作用。

作为一名开发者,我强烈建议大家学习和掌握 eBPF 技术,拥抱这个充满机遇的新世界。

内核老司机 eBPF网络流量分析Linux内核

评论点评

打赏赞助
sponsor

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

分享

QRcode

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