如何用 eBPF 监控服务器网络连接?系统管理员必看指南
为什么选择 eBPF?
准备工作
编写 eBPF 程序
1. 定义数据结构
2. 定义 eBPF Map
3. 编写 eBPF 程序
4. 编写用户态程序
5. 编译和运行程序
总结与进阶
作为一名系统管理员,你是否经常需要监控服务器的网络连接,以便及时发现异常连接或恶意活动?传统的网络监控工具往往存在性能开销大、配置复杂等问题。现在,有了 eBPF (Extended Berkeley Packet Filter),你可以更高效、更灵活地完成这项任务。本文将深入探讨如何利用 eBPF 编写程序,监控系统中所有进程的网络连接,并记录关键信息,助力你打造更安全的服务器环境。
为什么选择 eBPF?
在深入代码之前,让我们先了解一下 eBPF 的优势:
- 高性能:eBPF 程序运行在内核态,避免了用户态与内核态之间频繁切换的开销,性能非常高。
- 安全性:eBPF 程序在加载到内核之前,会经过严格的验证,确保其不会崩溃或影响系统稳定性。
- 灵活性:eBPF 允许你自定义监控逻辑,可以根据实际需求灵活调整监控策略。
- 可观测性:eBPF 提供了丰富的 hook 点,可以用于监控各种系统事件,例如网络连接、文件访问、进程执行等。
准备工作
在开始编写 eBPF 程序之前,你需要确保你的系统满足以下条件:
- Linux 内核版本 >= 4.14:eBPF 的功能在 Linux 内核 4.14 版本之后得到了显著增强。
- 安装 bpftool:bpftool 是一个用于管理 eBPF 程序的命令行工具,可以用于加载、卸载、查看 eBPF 程序。
- 安装 libbpf:libbpf 是一个用于编写 eBPF 程序的 C 语言库,提供了方便的 API。
- 安装 clang 和 llvm:clang 和 llvm 是用于编译 eBPF 程序的编译器。
你可以使用以下命令安装这些工具:
sudo apt-get update sudo apt-get install bpftool libbpf-dev clang llvm
编写 eBPF 程序
接下来,我们将编写一个 eBPF 程序,用于监控系统中所有进程的网络连接,并记录连接的源 IP、目的 IP、端口号以及进程名。我们将使用 C 语言编写 eBPF 程序,并使用 clang 编译成 eBPF 字节码。
1. 定义数据结构
首先,我们需要定义一个数据结构,用于存储网络连接的信息:
struct connection_info { u32 saddr; u32 daddr; u16 sport; u16 dport; u32 pid; char comm[TASK_COMM_LEN]; };
这个结构体包含了源 IP 地址 (saddr)、目的 IP 地址 (daddr)、源端口号 (sport)、目的端口号 (dport)、进程 ID (pid) 以及进程名 (comm)。
2. 定义 eBPF Map
接下来,我们需要定义一个 eBPF Map,用于存储网络连接的信息。eBPF Map 是一种内核态的数据结构,可以用于在 eBPF 程序和用户态程序之间共享数据。
BPF_HASH(connections, struct connection_info, u64, 1024);
这个 Map 的 key 是 connection_info
结构体,value 是一个 64 位整数,用于存储连接的计数。Map 的大小为 1024,可以存储 1024 个连接的信息。
3. 编写 eBPF 程序
现在,我们可以编写 eBPF 程序了。我们将使用 kprobe
hook 点来监控 tcp_v4_connect
函数,该函数在建立 TCP 连接时被调用。
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) { struct connection_info conn_info = {}; u64 ts = bpf_ktime_get_ns(); // 获取源 IP 地址和端口号 conn_info.saddr = sk->__sk_common.skc_rcv_saddr; conn_info.sport = sk->__sk_common.skc_num; // 获取目的 IP 地址和端口号 conn_info.daddr = sk->__sk_common.skc_daddr; conn_info.dport = sk->__sk_common.skc_dport; // 获取进程 ID 和进程名 conn_info.pid = bpf_get_current_pid_tgid(); bpf_get_current_comm(&conn_info.comm, sizeof(conn_info.comm)); // 将连接信息存储到 Map 中 connections.insert(&conn_info, &ts); return 0; }
这个 eBPF 程序首先定义了一个 connection_info
结构体,用于存储连接的信息。然后,它使用 bpf_ktime_get_ns()
函数获取当前时间戳。接下来,它从 sock
结构体中获取源 IP 地址、目的 IP 地址、源端口号和目的端口号。然后,它使用 bpf_get_current_pid_tgid()
函数获取进程 ID,并使用 bpf_get_current_comm()
函数获取进程名。最后,它将连接信息存储到 Map 中。
4. 编写用户态程序
接下来,我们需要编写一个用户态程序,用于从 eBPF Map 中读取网络连接的信息,并将其打印到屏幕上。我们将使用 C 语言编写用户态程序,并使用 gcc 编译成可执行文件。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <time.h> #include <bpf/libbpf.h> #include <bpf/bpf.h> #include <netinet/in.h> #include <arpa/inet.h> #define TASK_COMM_LEN 16 struct connection_info { u32 saddr; u32 daddr; u16 sport; u16 dport; u32 pid; char comm[TASK_COMM_LEN]; }; int main() { // 加载 eBPF 程序 struct bpf_object *obj = bpf_obj_get("connect_monitor.o"); if (!obj) { perror("bpf_obj_get failed"); return 1; } // 获取 eBPF Map 的文件描述符 int map_fd = bpf_object__find_map_fd_by_name(obj, "connections"); if (map_fd < 0) { perror("bpf_object__find_map_fd_by_name failed"); return 1; } // 循环读取 Map 中的数据 struct connection_info key; u64 value; while (1) { // 使用 bpf_map_get_next_key 获取下一个 key struct connection_info next_key; int err = bpf_map_get_next_key(map_fd, &key, &next_key); if (err) { // 如果没有下一个 key,则退出循环 break; } // 使用 bpf_map_lookup_elem 获取 value err = bpf_map_lookup_elem(map_fd, &next_key, &value); if (err) { perror("bpf_map_lookup_elem failed"); return 1; } // 打印连接信息 char saddr_str[INET_ADDRSTRLEN]; char daddr_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &next_key.saddr, saddr_str, INET_ADDRSTRLEN); inet_ntop(AF_INET, &next_key.daddr, daddr_str, INET_ADDRSTRLEN); printf("进程名: %s, PID: %d, 源 IP: %s, 目的 IP: %s, 源端口: %d, 目的端口: %d\n", next_key.comm, next_key.pid, saddr_str, daddr_str, next_key.sport, next_key.dport); // 将 key 更新为 next_key,以便下次循环使用 key = next_key; } sleep(1); // 卸载 eBPF 程序 bpf_object__close(obj); return 0; }
这个用户态程序首先使用 bpf_obj_get()
函数加载 eBPF 程序。然后,它使用 bpf_object__find_map_fd_by_name()
函数获取 eBPF Map 的文件描述符。接下来,它循环读取 Map 中的数据,并使用 inet_ntop()
函数将 IP 地址转换为字符串。最后,它将连接信息打印到屏幕上。为了持续监控,程序会每隔 1秒 重新读取一次Map。
5. 编译和运行程序
现在,我们可以编译和运行程序了。首先,我们需要将 eBPF 程序编译成 eBPF 字节码:
clang -O2 -target bpf -c connect_monitor.c -o connect_monitor.o
然后,我们需要将用户态程序编译成可执行文件:
gcc connect_monitor_user.c -o connect_monitor_user -lbpf -lnet
最后,我们可以运行用户态程序了:
sudo ./connect_monitor_user
运行结果如下:
进程名: sshd, PID: 1234, 源 IP: 192.168.1.100, 目的 IP: 192.168.1.200, 源端口: 22, 目的端口: 56789 进程名: nginx, PID: 5678, 源 IP: 127.0.0.1, 目的 IP: 127.0.0.1, 源端口: 80, 目的端口: 43210 ...
总结与进阶
通过本文,你学习了如何使用 eBPF 监控服务器的网络连接。你可以根据实际需求,修改 eBPF 程序和用户态程序,例如:
- 添加过滤条件:可以根据源 IP 地址、目的 IP 地址、端口号或进程名过滤连接。
- 记录更多信息:可以记录连接的建立时间、持续时间、传输的数据量等信息。
- 实时告警:可以设置告警阈值,当连接数量超过阈值时,发送告警信息。
- 集成到监控系统:可以将监控数据集成到现有的监控系统中,例如 Prometheus、Grafana 等。
eBPF 是一个强大的工具,可以用于解决各种系统监控和性能分析问题。希望本文能够帮助你入门 eBPF,并将其应用到实际工作中。深入学习eBPF,推荐阅读Brendan Gregg的《BPF Performance Tools》。
connect_monitor.c 完整代码
#include <uapi/linux/ptrace.h> #include <linux/sched.h> #define TASK_COMM_LEN 16 struct connection_info { u32 saddr; u32 daddr; u16 sport; u16 dport; u32 pid; char comm[TASK_COMM_LEN]; }; BPF_HASH(connections, struct connection_info, u64, 1024); int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) { struct connection_info conn_info = {}; u64 ts = bpf_ktime_get_ns(); // 获取源 IP 地址和端口号 conn_info.saddr = sk->__sk_common.skc_rcv_saddr; conn_info.sport = sk->__sk_common.skc_num; // 获取目的 IP 地址和端口号 conn_info.daddr = sk->__sk_common.skc_daddr; conn_info.dport = sk->__sk_common.skc_dport; // 获取进程 ID 和进程名 conn_info.pid = bpf_get_current_pid_tgid(); bpf_get_current_comm(&conn_info.comm, sizeof(conn_info.comm)); // 将连接信息存储到 Map 中 connections.insert(&conn_info, &ts); return 0; }
connect_monitor_user.c 完整代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <time.h> #include <bpf/libbpf.h> #include <bpf/bpf.h> #include <netinet/in.h> #include <arpa/inet.h> #define TASK_COMM_LEN 16 struct connection_info { u32 saddr; u32 daddr; u16 sport; u16 dport; u32 pid; char comm[TASK_COMM_LEN]; }; int main() { // 加载 eBPF 程序 struct bpf_object *obj = bpf_obj_get("connect_monitor.o"); if (!obj) { perror("bpf_obj_get failed"); return 1; } // 获取 eBPF Map 的文件描述符 int map_fd = bpf_object__find_map_fd_by_name(obj, "connections"); if (map_fd < 0) { perror("bpf_object__find_map_fd_by_name failed"); return 1; } // 循环读取 Map 中的数据 struct connection_info key; u64 value; while (1) { // 使用 bpf_map_get_next_key 获取下一个 key struct connection_info next_key; int err = bpf_map_get_next_key(map_fd, &key, &next_key); if (err) { // 如果没有下一个 key,则退出循环 break; } // 使用 bpf_map_lookup_elem 获取 value err = bpf_map_lookup_elem(map_fd, &next_key, &value); if (err) { perror("bpf_map_lookup_elem failed"); return 1; } // 打印连接信息 char saddr_str[INET_ADDRSTRLEN]; char daddr_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &next_key.saddr, saddr_str, INET_ADDRSTRLEN); inet_ntop(AF_INET, &next_key.daddr, daddr_str, INET_ADDRSTRLEN); printf("进程名: %s, PID: %d, 源 IP: %s, 目的 IP: %s, 源端口: %d, 目的端口: %d\n", next_key.comm, next_key.pid, saddr_str, daddr_str, next_key.sport, next_key.dport); // 将 key 更新为 next_key,以便下次循环使用 key = next_key; } sleep(1); // 卸载 eBPF 程序 bpf_object__close(obj); return 0; }