如何用 eBPF 精准监控特定用户发起的网络请求?以 curl 命令为例
1. eBPF 简介
2. 准备工作
3. 编写 eBPF 程序
4. 编译 eBPF 程序
5. 加载和运行 eBPF 程序
6. 测试
7. 进阶:提取更详细的 HTTP 信息
8. 总结
想象一下,你是一位系统管理员,需要追踪某个特定用户在服务器上的网络行为。例如,你怀疑某个用户正在进行恶意的数据抓取,或者仅仅是为了调试某个特定用户的网络应用问题。传统的网络抓包工具(如 tcpdump)可能会产生大量的无关数据,让你淹没在信息的海洋中。这时,eBPF(extended Berkeley Packet Filter)就能派上大用场了。它允许你在内核中动态地插入自定义的监控代码,而无需修改内核源码或重启系统,极大地提高了灵活性和效率。
本文将深入探讨如何使用 eBPF 来监控特定用户发起的网络请求,特别是当该用户执行 curl
命令时产生的 HTTP 请求信息。我们将一步步地展示如何编写、编译和部署 eBPF 程序,从而实现对目标用户网络行为的精准监控。
1. eBPF 简介
eBPF 是一种革命性的内核技术,它允许用户在内核中安全地运行自定义代码。与传统的 BPF 相比,eBPF 具有更强大的功能和更广泛的应用场景,例如网络监控、安全分析、性能调优等。
eBPF 程序运行在内核的虚拟机中,可以访问内核数据结构和函数。为了保证安全性,eBPF 程序在加载到内核之前会经过严格的验证,以防止恶意代码或潜在的崩溃风险。
2. 准备工作
在开始之前,你需要确保你的系统满足以下条件:
- Linux 内核版本 >= 4.14:eBPF 的功能在不断发展,较新的内核版本通常提供更好的支持和更多的特性。
- 安装 bpftool:
bpftool
是一个用于管理 eBPF 程序的命令行工具,可以用来加载、卸载、查看 eBPF 程序和映射。 - 安装 libbpf:
libbpf
是一个 C 语言库,提供了用于编写和加载 eBPF 程序的 API。 - 安装 clang 和 llvm:
clang
和llvm
是用于编译 eBPF 程序的编译器和工具链。
在 Debian/Ubuntu 系统上,你可以使用以下命令安装所需的软件包:
sudo apt update sudo apt install -y clang llvm libbpf-dev bpftool
3. 编写 eBPF 程序
我们的目标是监控特定用户执行 curl
命令时产生的 HTTP 请求。为了实现这个目标,我们需要编写一个 eBPF 程序,它可以:
- 挂载到
tcp_connect
内核函数:当 TCP 连接建立时,tcp_connect
函数会被调用。我们可以在这个函数中获取连接信息,例如源 IP 地址、目标 IP 地址、端口号等。 - 获取当前用户的 UID:通过
bpf_get_current_uid_gid()
函数,我们可以获取当前用户的 UID(User ID)。 - 过滤特定用户的连接:将获取到的 UID 与目标用户的 UID 进行比较,只监控目标用户发起的连接。
- 提取 HTTP 请求信息:当检测到目标用户的连接时,我们可以进一步提取 HTTP 请求信息,例如请求方法、URL、Host 头部等。由于 HTTP 协议是基于 TCP 的,我们需要读取 TCP 流中的数据。
下面是一个示例 eBPF 程序的代码:
// license: GPL-2.0 #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h> #include <linux/socket.h> #include <linux/tcp.h> #define TARGET_UID 1000 // 替换为你要监控的用户的 UID // 定义一个 BPF 映射,用于存储 HTTP 请求信息 struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(key_size, sizeof(u32)); // PID __uint(value_size, 1024); // HTTP 请求数据,最大 1024 字节 __uint(max_entries, 10240); // 最大存储 10240 个请求 } http_requests SEC(".maps"); // 挂载到 tcp_connect 内核函数的 eBPF 程序 SEC("kprobe/tcp_connect") int BPF_KPROBE(tcp_connect, struct sock *sk) { u32 uid = bpf_get_current_uid_gid() & 0xFFFFFFFF; if (uid != TARGET_UID) { return 0; // 不是目标用户,直接返回 } u32 pid = bpf_get_current_pid_tgid() >> 32; // 获取 socket 地址信息 struct sock *skp = sk; struct inet_sock *inet = inet_sk(skp); u32 saddr = bpf_ntohl(inet->saddr); u32 daddr = bpf_ntohl(inet->daddr); u16 dport = bpf_ntohs(inet->dport); // 构造 HTTP 请求信息 char http_info[1024] = {0}; snprintf(http_info, sizeof(http_info), "[PID: %d] Source: %d.%d.%d.%d, Destination: %d.%d.%d.%d:%d\n", pid, (saddr >> 24) & 0xFF, (saddr >> 16) & 0xFF, (saddr >> 8) & 0xFF, saddr & 0xFF, (daddr >> 24) & 0xFF, (daddr >> 16) & 0xFF, (daddr >> 8) & 0xFF, daddr & 0xFF, dport); // 将 HTTP 请求信息存储到 BPF 映射中 bpf_map_update_elem(&http_requests, &pid, http_info, BPF_ANY); return 0; } char LICENSE[] SEC("license") = "GPL";
代码解释:
TARGET_UID
:定义要监控的目标用户的 UID。你需要将其替换为你实际要监控的用户的 UID。http_requests
:定义了一个 BPF 映射,用于存储 HTTP 请求信息。这个映射是一个哈希表,Key 是进程 PID,Value 是 HTTP 请求数据(最大 1024 字节)。tcp_connect
:这是一个挂载到tcp_connect
内核函数的 eBPF 程序。当 TCP 连接建立时,这个程序会被调用。bpf_get_current_uid_gid()
:用于获取当前用户的 UID。bpf_get_current_pid_tgid()
:用于获取当前进程的 PID。inet_sk(skp)->saddr
和inet_sk(skp)->daddr
:用于获取源 IP 地址和目标 IP 地址。inet_sk(skp)->dport
:用于获取目标端口号。bpf_map_update_elem()
:用于将 HTTP 请求信息存储到 BPF 映射中。
4. 编译 eBPF 程序
将上面的代码保存为 monitor_http.c
文件,然后使用以下命令编译 eBPF 程序:
clang -target bpf -D__TARGET_ARCH_x86_64 -O2 -Wall -Werror -c monitor_http.c -o monitor_http.o
这个命令会将 monitor_http.c
编译成 monitor_http.o
目标文件。
5. 加载和运行 eBPF 程序
接下来,我们需要编写一个用户空间的程序,用于加载和运行 eBPF 程序,并从 BPF 映射中读取 HTTP 请求信息。以下是一个示例 Python 代码:
import os import sys import time from bcc import BPF # 加载 eBPF 程序 b = BPF(src_file="monitor_http.c") # 获取 BPF 映射 http_requests = b["http_requests"] # 打印 HTTP 请求信息 def print_http_requests(): for k, v in http_requests.items(): pid = k.value http_info = v.decode('utf-8') print(f"{http_info.strip()}\n") http_requests.clear() # 循环读取 BPF 映射中的数据 while True: try: time.sleep(1) print_http_requests() except KeyboardInterrupt: exit()
代码解释:
BPF(src_file="monitor_http.c")
:加载 eBPF 程序。b["http_requests"]
:获取 BPF 映射。http_requests.items()
:遍历 BPF 映射中的所有元素。v.decode('utf-8')
:将 HTTP 请求数据从字节流解码成字符串。
将上面的代码保存为 run_monitor.py
文件,然后使用以下命令运行程序:
sudo python3 run_monitor.py
注意: 运行这个程序需要 root 权限。
6. 测试
现在,你可以使用 curl
命令来测试你的 eBPF 程序了。在另一个终端中,使用你指定的 UID 的用户执行以下命令:
curl https://www.example.com
你应该能在运行 run_monitor.py
的终端中看到类似以下的输出:
[PID: 1234] Source: 192.168.1.100, Destination: 93.184.216.34:443
这表明你的 eBPF 程序已经成功地监控到了该用户发起的 HTTP 请求。
7. 进阶:提取更详细的 HTTP 信息
上面的示例只是提取了基本的连接信息。如果你想提取更详细的 HTTP 信息,例如请求方法、URL、Host 头部等,你需要进一步解析 TCP 流中的数据。这涉及到 TCP 协议和 HTTP 协议的细节,比较复杂。你可以使用一些现有的 eBPF 工具,例如 HttpTracer
,它可以自动地解析 HTTP 请求和响应,并提供更丰富的信息。
8. 总结
本文介绍了如何使用 eBPF 来监控特定用户发起的网络请求,特别是用户执行 curl
命令时产生的 HTTP 请求信息。通过编写和部署 eBPF 程序,我们可以实现对目标用户网络行为的精准监控,从而帮助我们进行网络安全分析和故障排除。
eBPF 是一个非常强大的工具,它可以应用于各种各样的场景。希望本文能够帮助你入门 eBPF,并激发你对 eBPF 的更多探索。
参考资料:
- eBPF 官方网站:https://ebpf.io/
- bcc 工具:https://github.com/iovisor/bcc
- libbpf 仓库:https://github.com/libbpf/libbpf
- HttpTracer: https://github.com/iovisor/bcc/blob/master/tools/httptrace.py
请记住,在使用 eBPF 进行网络监控时,务必遵守相关法律法规和隐私政策,确保你的行为是合法的和道德的。