WEBKT

eBPF实战:如何精准监控特定进程的网络流量?

68 0 0 0

eBPF实战:如何精准监控特定进程的网络流量?

eBPF 简介:内核态的可编程利器

准备工作:搭建 eBPF 开发环境

编写 eBPF 程序:监控 TCP 连接事件

编译 eBPF 程序

编写用户态程序:加载和读取 eBPF 数据

运行 eBPF 程序

进阶应用:监控特定进程的网络流量

更进一步:分析网络延迟和丢包情况

总结:eBPF,网络监控的强大助手

eBPF实战:如何精准监控特定进程的网络流量?

作为一名系统管理员或者网络工程师,你是否曾遇到以下问题?

  • 某个进程的网络流量异常,但苦于无法精确定位原因?
  • 需要对特定进程的网络行为进行审计,以确保安全性?
  • 希望深入了解某个进程的网络连接建立、数据传输和断开过程?

传统的网络监控工具,例如tcpdump或者Wireshark,虽然功能强大,但在处理高并发、大数据量的网络流量时,往往会面临性能瓶颈。此外,它们通常只能捕获到网络数据包,而无法直接关联到具体的进程。这使得问题排查和安全审计变得异常困难。

幸运的是,eBPF(extended Berkeley Packet Filter)技术的出现,为我们提供了一种高效、灵活的网络监控解决方案。eBPF 允许我们在内核态动态地插入自定义的监控代码,从而实现对网络流量的精细化分析和控制。

本文将深入探讨如何利用 eBPF 技术,监控特定进程的网络流量,并分析其 TCP 连接建立、数据传输和断开过程,以及网络延迟和丢包情况。我们将从 eBPF 的基本概念入手,逐步介绍如何编写、编译和加载 eBPF 程序,并结合实际案例,展示 eBPF 在网络监控中的强大应用。

eBPF 简介:内核态的可编程利器

eBPF 最初是作为 Berkeley Packet Filter (BPF) 的扩展而设计的,用于网络数据包的过滤。但随着技术的不断发展,eBPF 已经超越了最初的限制,成为一种通用的内核态可编程技术。

eBPF 的核心优势:

  • 高性能: eBPF 程序运行在内核态,避免了用户态和内核态之间频繁的上下文切换,从而实现了高性能的网络数据包处理。
  • 安全性: eBPF 程序在加载到内核之前,会经过严格的验证,确保程序的安全性,防止恶意代码对系统造成损害。
  • 灵活性: 开发者可以根据自己的需求,编写自定义的 eBPF 程序,实现各种复杂的网络监控和分析功能。

eBPF 的工作原理:

  1. 编写 eBPF 程序: 使用 C 语言编写 eBPF 程序,并使用特定的编译器(例如 LLVM)将其编译成 eBPF 字节码。
  2. 加载 eBPF 程序: 使用 bpf() 系统调用将 eBPF 字节码加载到内核。
  3. 挂载 eBPF 程序: 将 eBPF 程序挂载到内核中的特定事件点(例如网络接口、函数调用等)。
  4. 事件触发: 当事件发生时,内核会执行相应的 eBPF 程序。
  5. 数据收集: eBPF 程序可以将收集到的数据存储到 eBPF Map 中。
  6. 用户态访问: 用户态程序可以通过 bpf() 系统调用访问 eBPF Map 中的数据。

准备工作:搭建 eBPF 开发环境

在开始编写 eBPF 程序之前,我们需要搭建一个合适的开发环境。以下是一些必要的工具和库:

  • Linux Kernel: 建议使用 4.14 或更高版本的 Linux 内核,以获得更好的 eBPF 支持。
  • LLVM: 用于将 C 语言代码编译成 eBPF 字节码。
  • libbpf: 用于加载和管理 eBPF 程序。
  • bcc (BPF Compiler Collection): 一个高级的 eBPF 工具包,提供了 Python 接口,简化了 eBPF 程序的开发和调试。

安装 bcc:

sudo apt-get update
sudo apt-get install -y bpfcc-tools linux-headers-$(uname -r)

验证 bcc 安装:

sudo /usr/share/bcc/tools/opensnoop

如果能够正常运行 opensnoop 工具,则说明 bcc 已经成功安装。

编写 eBPF 程序:监控 TCP 连接事件

我们将编写一个简单的 eBPF 程序,用于监控 TCP 连接的建立、关闭和数据传输事件。该程序将捕获以下信息:

  • 进程 ID (PID): 发起连接的进程 ID。
  • 源 IP 地址和端口: 连接的源 IP 地址和端口。
  • 目标 IP 地址和端口: 连接的目标 IP 地址和端口。
  • 事件类型: 连接事件的类型(例如 connect, close, send, recv)。
  • 时间戳: 事件发生的时间戳。

eBPF 程序代码 (tcp_monitor.c):

#include <uapi/linux/ptrace.h>
#include <linux/tcp.h>
#include <linux/inet.h>
struct event_t {
u32 pid;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u8 type;
u64 ts;
};
BPF_PERF_OUTPUT(events);
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
struct event_t event = {};
event.pid = bpf_get_current_pid_tgid();
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.sport = sk->__sk_common.skc_num;
event.dport = sk->__sk_common.skc_dport;
event.type = 1; // Connect
event.ts = bpf_ktime_get_ns();
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
int kprobe__tcp_close(struct pt_regs *ctx, struct sock *sk) {
struct event_t event = {};
event.pid = bpf_get_current_pid_tgid();
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.sport = sk->__sk_common.skc_num;
event.dport = sk->__sk_common.skc_dport;
event.type = 2; // Close
event.ts = bpf_ktime_get_ns();
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
struct event_t event = {};
event.pid = bpf_get_current_pid_tgid();
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.sport = sk->__sk_common.skc_num;
event.dport = sk->__sk_common.skc_dport;
event.type = 3; // Send
event.ts = bpf_ktime_get_ns();
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
int kprobe__tcp_recvmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
struct event_t event = {};
event.pid = bpf_get_current_pid_tgid();
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.sport = sk->__sk_common.skc_num;
event.dport = sk->__sk_common.skc_dport;
event.type = 4; // Recv
event.ts = bpf_ktime_get_ns();
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}

代码解释:

  • #include: 引入必要的头文件,包括 eBPF 相关的头文件和 TCP 协议相关的头文件。
  • struct event_t: 定义了一个事件结构体,用于存储捕获到的网络连接事件信息。
  • BPF_PERF_OUTPUT(events): 定义了一个 Perf 事件输出,用于将 eBPF 程序收集到的数据传递给用户态程序。
  • kprobe__tcp_v4_connect: 定义了一个 kprobe,用于在 tcp_v4_connect 函数被调用时执行。该函数负责捕获 TCP 连接建立事件。
  • kprobe__tcp_close: 定义了一个 kprobe,用于在 tcp_close 函数被调用时执行。该函数负责捕获 TCP 连接关闭事件。
  • kprobe__tcp_sendmsg: 定义了一个 kprobe,用于在 tcp_sendmsg 函数被调用时执行。该函数负责捕获 TCP 数据发送事件。
  • kprobe__tcp_recvmsg: 定义了一个 kprobe,用于在 tcp_recvmsg 函数被调用时执行。该函数负责捕获 TCP 数据接收事件。
  • bpf_get_current_pid_tgid(): 获取当前进程的 PID。
  • sk->__sk_common.skc_rcv_saddr: 获取源 IP 地址。
  • sk->__sk_common.skc_daddr: 获取目标 IP 地址。
  • sk->__sk_common.skc_num: 获取源端口。
  • sk->__sk_common.skc_dport: 获取目标端口。
  • bpf_ktime_get_ns(): 获取当前时间戳(纳秒)。
  • events.perf_submit(ctx, &event, sizeof(event)): 将事件数据提交到 Perf 事件输出。

编译 eBPF 程序

使用以下命令编译 eBPF 程序:

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

编写用户态程序:加载和读取 eBPF 数据

我们将编写一个 Python 程序,用于加载 eBPF 程序,并从 Perf 事件输出中读取数据。

用户态程序代码 (tcp_monitor.py):

#!/usr/bin/env python
from bcc import BPF
import socket
import struct
# Load the eBPF program
b = BPF(src_file="tcp_monitor.c")
# Define the callback function for handling events
def print_event(cpu, data, size):
event = b["events"].event(data)
print("%-9d %-6d %-16s %-6d %-16s %-6d %-2d" % (
event.ts, event.pid,
socket.inet_ntoa(struct.pack("<I", event.saddr)), event.sport,
socket.inet_ntoa(struct.pack("<I", event.daddr)), event.dport,
event.type
))
# Attach the callback function to the Perf event output
b["events"].open_perf_buffer(print_event)
# Print the header
print("TIME(ns) PID SADDR SPORT DADDR DPORT TYPE")
# Read data from the Perf event output
while True:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()

代码解释:

  • from bcc import BPF: 引入 bcc 库。
  • BPF(src_file="tcp_monitor.c"): 加载 eBPF 程序。
  • print_event(cpu, data, size): 定义一个回调函数,用于处理从 Perf 事件输出中读取到的数据。
  • socket.inet_ntoa(struct.pack("<I", event.saddr)): 将 IP 地址从网络字节序转换为字符串格式。
  • b["events"].open_perf_buffer(print_event): 将回调函数附加到 Perf 事件输出。
  • b.perf_buffer_poll(): 从 Perf 事件输出中读取数据。

运行 eBPF 程序

  1. 运行用户态程序:

    
    

sudo python tcp_monitor.py
```

  1. 执行网络操作:

    在另一个终端窗口中,执行一些网络操作,例如使用 curl 命令访问一个网站:

    
    

curl https://www.example.com
```

  1. 查看监控结果:

    在运行 tcp_monitor.py 的终端窗口中,你将会看到类似以下的输出:

    
    

TIME(ns) PID SADDR SPORT DADDR DPORT TYPE
1678886400000 1234 192.168.1.100 54321 93.184.216.34 443 1
1678886401000 1234 192.168.1.100 54321 93.184.216.34 443 3
1678886402000 1234 192.168.1.100 54321 93.184.216.34 443 4
1678886403000 1234 192.168.1.100 54321 93.184.216.34 443 2
```

* `TIME(ns)`: 事件发生的时间戳(纳秒)。
* `PID`: 发起连接的进程 ID。
* `SADDR`: 源 IP 地址。
* `SPORT`: 源端口。
* `DADDR`: 目标 IP 地址。
* `DPORT`: 目标端口。
* `TYPE`: 事件类型(1: Connect, 2: Close, 3: Send, 4: Recv)。

进阶应用:监控特定进程的网络流量

上面的例子展示了如何监控所有进程的网络流量。为了只监控特定进程的网络流量,我们需要修改 eBPF 程序,添加一个过滤条件。

修改后的 eBPF 程序代码 (tcp_monitor_pid.c):

#include <uapi/linux/ptrace.h>
#include <linux/tcp.h>
#include <linux/inet.h>
struct event_t {
u32 pid;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u8 type;
u64 ts;
};
BPF_PERF_OUTPUT(events);
// Define the PID to monitor
#define TARGET_PID 1234 // Replace with the actual PID
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
u32 pid = bpf_get_current_pid_tgid();
if (pid != TARGET_PID) {
return 0; // Ignore events from other processes
}
struct event_t event = {};
event.pid = pid;
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.sport = sk->__sk_common.skc_num;
event.dport = sk->__sk_common.skc_dport;
event.type = 1; // Connect
event.ts = bpf_ktime_get_ns();
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
int kprobe__tcp_close(struct pt_regs *ctx, struct sock *sk) {
u32 pid = bpf_get_current_pid_tgid();
if (pid != TARGET_PID) {
return 0; // Ignore events from other processes
}
struct event_t event = {};
event.pid = pid;
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.sport = sk->__sk_common.skc_num;
event.dport = sk->__sk_common.skc_dport;
event.type = 2; // Close
event.ts = bpf_ktime_get_ns();
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
u32 pid = bpf_get_current_pid_tgid();
if (pid != TARGET_PID) {
return 0; // Ignore events from other processes
}
struct event_t event = {};
event.pid = pid;
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.sport = sk->__sk_common.skc_num;
event.dport = sk->__sk_common.skc_dport;
event.type = 3; // Send
event.ts = bpf_ktime_get_ns();
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
int kprobe__tcp_recvmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
u32 pid = bpf_get_current_pid_tgid();
if (pid != TARGET_PID) {
return 0; // Ignore events from other processes
}
struct event_t event = {};
event.pid = pid;
event.saddr = sk->__sk_common.skc_rcv_saddr;
event.daddr = sk->__sk_common.skc_daddr;
event.sport = sk->__sk_common.skc_num;
event.dport = sk->__sk_common.skc_dport;
event.type = 4; // Recv
event.ts = bpf_ktime_get_ns();
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}

代码修改说明:

  • #define TARGET_PID 1234: 定义了一个宏,用于指定要监控的进程 ID。请将 1234 替换为实际的进程 ID。
  • 在每个 kprobe 函数中,添加了一个 if (pid != TARGET_PID) 的判断条件,用于过滤掉来自其他进程的事件。

编译和运行修改后的程序:

  1. 编译 eBPF 程序:

    
    

clang -O2 -target bpf -c tcp_monitor_pid.c -o tcp_monitor_pid.o
```

  1. 修改用户态程序:

    tcp_monitor.py 中的 src_file="tcp_monitor.c" 修改为 src_file="tcp_monitor_pid.c"

  2. 运行用户态程序:

    
    

sudo python tcp_monitor.py
```

现在,你只会看到指定进程的网络流量事件。

更进一步:分析网络延迟和丢包情况

除了监控 TCP 连接事件,eBPF 还可以用于分析网络延迟和丢包情况。这需要我们捕获更多的数据包信息,例如时间戳和序列号,并进行更复杂的计算。

以下是一些可以尝试的方向:

  • 使用 tracepoint 替代 kprobe tracepoint 提供了更稳定的接口,可以减少内核版本升级带来的影响。
  • 利用 eBPF Map 存储数据: 使用 eBPF Map 存储数据包信息,例如时间戳和序列号,并在后续的数据包到达时进行计算。
  • 使用 BPF_HISTOGRAM 创建直方图: 使用 BPF_HISTOGRAM 创建直方图,统计网络延迟和丢包率的分布情况。

总结:eBPF,网络监控的强大助手

eBPF 是一种强大的内核态可编程技术,为网络监控和分析提供了无限可能。通过编写自定义的 eBPF 程序,我们可以实现对网络流量的精细化控制和分析,从而解决各种复杂的网络问题。

希望本文能够帮助你入门 eBPF,并在实际工作中应用 eBPF 技术,提升网络管理和安全能力。

网络游侠 eBPF网络监控进程流量

评论点评

打赏赞助
sponsor

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

分享

QRcode

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