WEBKT

为啥要用eBPF抓包?协议分析、性能监控,内核级的“透视眼”!

96 0 0 0

为啥要用eBPF抓包?协议分析、性能监控,内核级的“透视眼”!

什么是 eBPF?

eBPF 在网络抓包和协议分析方面的优势

如何使用 eBPF 进行网络抓包和协议分析?

eBPF 的局限性

总结

为啥要用eBPF抓包?协议分析、性能监控,内核级的“透视眼”!

作为一名网络工程师,你是不是经常遇到这些头疼的问题?

  • 线上服务动不动就卡顿,用户疯狂投诉,但你登上服务器,用 tcpdump 抓包,发现流量巨大,却看不出具体是哪个请求导致的?
  • 想分析某个应用的 HTTP 请求,但它使用了 TLS 加密,Wireshark 只能看到加密后的数据,根本没法定位问题?
  • 服务器 CPU 占用率飙升,怀疑是某个恶意连接导致的,但用 netstat 查了半天,也没发现异常?

传统的网络分析工具,比如 tcpdump、Wireshark,虽然功能强大,但在面对高并发、加密流量等复杂场景时,往往显得力不从心。它们工作在用户空间,需要将内核数据拷贝到用户空间进行分析,这会带来额外的性能开销。在高负载环境下,抓包本身就可能影响服务的稳定性。

这时候,eBPF(Extended Berkeley Packet Filter)就该闪亮登场了!

什么是 eBPF?

简单来说,eBPF 就像一个内核级的“可编程探针”,允许你在内核中安全地运行自定义代码,而无需修改内核源码或加载内核模块。你可以用它来监控网络流量、跟踪系统调用、分析应用性能等等。

eBPF 在网络抓包和协议分析方面的优势

  1. 内核级性能:eBPF 程序直接运行在内核中,避免了用户空间和内核空间的数据拷贝,大大提高了抓包效率,降低了性能开销。这意味着你可以在高负载环境下进行更精确的流量分析,而不用担心影响服务的稳定性。

  2. 灵活的可编程性:你可以使用 C 语言编写 eBPF 程序,然后将其编译成字节码,加载到内核中运行。这意味着你可以根据自己的需求,定制抓包逻辑,提取特定协议的字段,进行更深入的分析。

  3. 安全可靠:eBPF 程序在加载到内核之前,会经过严格的验证,确保其不会崩溃内核或访问非法内存。这保证了 eBPF 的安全性和可靠性。

  4. 强大的生态系统:现在已经有很多基于 eBPF 的开源工具,比如 bccbpftrace 等,它们提供了丰富的示例和库,可以帮助你快速上手 eBPF 开发。

如何使用 eBPF 进行网络抓包和协议分析?

接下来,我们通过几个具体的例子,来看看如何使用 eBPF 进行网络抓包和协议分析。

1. 抓取指定端口的 TCP 数据包

这个例子展示了如何使用 eBPF 抓取指定端口(比如 80 端口)的 TCP 数据包,并将数据包的长度和源 IP 地址打印出来。

#include <uapi/linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#define BPF_PROG_NAME(x) __attribute__((section("socket"), alias(#x)))
BPF_PROG_NAME(my_socket_filter) =
int my_socket_filter(struct __sk_buff *skb) {
// 获取以太网头部
struct ethhdr *eth = bpf_hdr_pointer(skb->data);
// 检查协议类型是否为 IPv4
if (eth->h_proto == htons(ETH_P_IP)) {
// 获取 IP 头部
struct iphdr *ip = bpf_hdr_pointer(skb->data + sizeof(struct ethhdr));
// 检查协议类型是否为 TCP
if (ip->protocol == IPPROTO_TCP) {
// 获取 TCP 头部
struct tcphdr *tcp = bpf_hdr_pointer(skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr));
// 检查目标端口是否为 80
if (tcp->dest == htons(80)) {
// 打印数据包长度和源 IP 地址
bpf_printk("Packet length: %d, Source IP: %x\n", skb->len, ip->saddr);
return 1; // 允许数据包通过
}
}
}
return 0; // 丢弃数据包
}
char _license[] SEC("license") = "GPL";

这个 eBPF 程序的原理很简单:

  • 它首先获取以太网头部、IP 头部和 TCP 头部。
  • 然后,它检查协议类型是否为 IPv4 和 TCP,以及目标端口是否为 80。
  • 如果所有条件都满足,它就打印数据包的长度和源 IP 地址,并允许数据包通过。否则,它就丢弃数据包。

要运行这个 eBPF 程序,你需要使用 bcc 工具链。首先,将上面的代码保存为 tcp_filter.c 文件,然后使用以下命令编译它:

cc -O2 -Wall -target bpf -c tcp_filter.c
llc -march=bpf -filetype=obj tcp_filter.o -o tcp_filter.bpf.o

接下来,你需要编写一个 Python 脚本来加载和运行 eBPF 程序。以下是一个简单的 Python 脚本:

from bcc import BPF
# 加载 eBPF 程序
b = BPF(obj="tcp_filter.bpf.o")
# 附加 eBPF 程序到 socket
socket_fd = b.load_func("my_socket_filter", BPF.SOCKET_FILTER)
BPF.attach_socket_filter(socket_fd, "lo") # 监听 lo 接口
# 打印 eBPF 程序的输出
b.trace_print()

将上面的代码保存为 tcp_filter.py 文件,然后使用以下命令运行它:

sudo python tcp_filter.py

现在,你可以使用 curl 命令向 localhost:80 发送一个 HTTP 请求,看看 eBPF 程序是否能够抓取到数据包并打印相关信息。

2. 分析 HTTP 请求的 URL

这个例子展示了如何使用 eBPF 分析 HTTP 请求的 URL,并将 URL 打印出来。这对于分析 Web 应用的性能瓶颈非常有用。

#include <uapi/linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#define BPF_PROG_NAME(x) __attribute__((section("socket"), alias(#x)))
BPF_PROG_NAME(http_url_filter) =
int http_url_filter(struct __sk_buff *skb) {
// 获取以太网头部
struct ethhdr *eth = bpf_hdr_pointer(skb->data);
// 检查协议类型是否为 IPv4
if (eth->h_proto == htons(ETH_P_IP)) {
// 获取 IP 头部
struct iphdr *ip = bpf_hdr_pointer(skb->data + sizeof(struct ethhdr));
// 检查协议类型是否为 TCP
if (ip->protocol == IPPROTO_TCP) {
// 获取 TCP 头部
struct tcphdr *tcp = bpf_hdr_pointer(skb->data + sizeof(struct ethhdr));
// 检查目标端口是否为 80
if (tcp->dest == htons(80)) {
// 计算 HTTP 头部起始位置
unsigned int http_header_start = sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct tcphdr);
// 检查 HTTP 请求是否完整
if (skb->len > http_header_start) {
// 获取 HTTP 请求数据
char *http_data = bpf_hdr_pointer(skb->data + http_header_start);
// 查找 URL
char *url_start = strchr(http_data, ' ');
if (url_start != NULL) {
url_start++; // 跳过空格
char *url_end = strchr(url_start, ' ');
if (url_end != NULL) {
// 计算 URL 长度
unsigned int url_len = url_end - url_start;
// 限制 URL 长度
if (url_len < 128) {
// 复制 URL 到缓冲区
char url[128];
bpf_probe_read_str(url, sizeof(url), url_start);
// 打印 URL
bpf_printk("URL: %s\n", url);
return 1; // 允许数据包通过
}
}
}
}
}
}
}
return 0; // 丢弃数据包
}
char _license[] SEC("license") = "GPL";

这个 eBPF 程序的原理如下:

  • 它首先获取以太网头部、IP 头部和 TCP 头部,并检查协议类型和目标端口。
  • 然后,它计算 HTTP 头部起始位置,并检查 HTTP 请求是否完整。
  • 接下来,它在 HTTP 请求数据中查找 URL,并将其复制到缓冲区中。
  • 最后,它打印 URL,并允许数据包通过。

编译和运行这个 eBPF 程序的步骤与前面的例子类似。你需要将代码保存为 http_url_filter.c 文件,然后使用 bcc 工具链编译它,并编写一个 Python 脚本来加载和运行 eBPF 程序。

3. 监控 TCP 连接的延迟

这个例子展示了如何使用 eBPF 监控 TCP 连接的延迟,并将延迟信息记录下来。这对于分析网络性能问题非常有用。

#include <uapi/linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/time.h>
#define BPF_PROG_NAME(x) __attribute__((section("kprobe"), alias(#x)))
// 定义一个 BPF map,用于存储 TCP 连接的起始时间
BPF_TABLE("hash", struct tcp_key_t, u64, connection_start_time, 1024);
// 定义 TCP 连接的 key
struct tcp_key_t {
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
};
// kprobe 函数,在 TCP 连接建立时记录起始时间
BPF_PROG_NAME(tcp_connect_entry) =
int tcp_connect_entry(struct sock *sk) {
// 获取 TCP 连接的 key
struct tcp_key_t key = {
.saddr = sk->__sk_common.skc_rcv_saddr,
.daddr = sk->__sk_common.skc_daddr,
.sport = sk->__sk_common.skc_num,
.dport = sk->__sk_common.skc_dport,
};
// 获取当前时间
u64 current_time = bpf_ktime_get_ns();
// 存储起始时间到 BPF map 中
connection_start_time.update(&key, &current_time);
return 0;
}
// kretprobe 函数,在 TCP 连接关闭时计算延迟
BPF_PROG_NAME(tcp_connect_exit) =
int tcp_connect_exit(struct sock *sk) {
// 获取 TCP 连接的 key
struct tcp_key_t key = {
.saddr = sk->__sk_common.skc_rcv_saddr,
.daddr = sk->__sk_common.skc_daddr,
.sport = sk->__sk_common.skc_num,
.dport = sk->__sk_common.skc_dport,
};
// 从 BPF map 中获取起始时间
u64 *start_time = connection_start_time.lookup(&key);
if (start_time != NULL) {
// 获取当前时间
u64 current_time = bpf_ktime_get_ns();
// 计算延迟
u64 latency = current_time - *start_time;
// 打印延迟信息
bpf_printk("Latency: %llu ns\n", latency);
// 从 BPF map 中删除起始时间
connection_start_time.delete(&key);
}
return 0;
}
char _license[] SEC("license") = "GPL";

这个 eBPF 程序的原理如下:

  • 它使用 kprobe 探针在 tcp_connect 函数的入口处记录 TCP 连接的起始时间,并将其存储到 BPF map 中。
  • 然后,它使用 kretprobe 探针在 tcp_connect 函数的出口处计算 TCP 连接的延迟,并打印延迟信息。
  • 最后,它从 BPF map 中删除起始时间。

编译和运行这个 eBPF 程序的步骤与前面的例子类似。你需要将代码保存为 tcp_latency.c 文件,然后使用 bcc 工具链编译它,并编写一个 Python 脚本来加载和运行 eBPF 程序。需要注意的是,你需要将 eBPF 程序附加到 tcp_connect 函数上,而不是 socket 上。

from bcc import BPF
# 加载 eBPF 程序
b = BPF(src_file="tcp_latency.c")
# 附加 eBPF 程序到 kprobe
b.attach_kprobe(event="tcp_v4_connect", fn_name="tcp_connect_entry")
b.attach_kretprobe(event="tcp_v4_connect", fn_name="tcp_connect_exit")
# 打印 eBPF 程序的输出
b.trace_print()

eBPF 的局限性

虽然 eBPF 功能强大,但也存在一些局限性:

  • 学习曲线陡峭:eBPF 开发需要掌握 C 语言、内核编程等知识,学习曲线比较陡峭。
  • 调试困难:eBPF 程序运行在内核中,调试起来比较困难。
  • 安全风险:虽然 eBPF 程序会经过严格的验证,但仍然存在一定的安全风险。

总结

eBPF 是一种强大的网络分析工具,可以帮助你解决传统工具无法解决的问题。虽然 eBPF 存在一些局限性,但随着 eBPF 生态系统的不断完善,相信它会越来越普及,成为网络工程师的必备技能。 掌握 eBPF,你就能拥有内核级的“透视眼”,轻松定位网络问题,优化应用性能!

内核探针侠 eBPF抓包协议分析性能监控

评论点评

打赏赞助
sponsor

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

分享

QRcode

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