告别盲人摸象:用 eBPF 透视 Linux 网络连接全貌,揪出幕后黑手
什么是 eBPF?你的系统分析新利器
场景:揪出偷偷建立连接的“内鬼”
如何用 eBPF 监控网络连接?实战演练
1. 准备工作
2. 编写 eBPF 程序
3. 编译 eBPF 程序
4. 加载 eBPF 程序
5. 运行程序
进阶:更强大的分析能力
eBPF 的应用场景:无限可能
总结:拥抱 eBPF,提升你的运维技能
作为一名老运维,我深知服务器网络安全的重要性。每天面对海量的网络连接数据,就像大海捞针,想精准定位恶意连接,简直难如登天。传统的网络监控工具,要么性能开销太大,影响业务运行;要么只能提供粗略的信息,难以深入分析。直到我遇到了 eBPF,才发现原来 Linux 系统里还藏着这么一个强大的“透视镜”。
什么是 eBPF?你的系统分析新利器
eBPF (extended Berkeley Packet Filter) 是一种革命性的内核技术,它允许你在内核中安全地运行自定义代码,而无需修改内核源码或加载内核模块。你可以把它想象成一个“内核探针”,可以实时监测和分析系统中的各种事件,例如网络连接、系统调用、函数执行等等。而且,eBPF 代码运行在沙箱环境中,即使出现问题也不会影响内核的稳定性。
为什么 eBPF 这么牛?
- 高性能: eBPF 代码运行在内核中,避免了用户态和内核态之间频繁切换的开销,性能非常高。
- 安全: eBPF 代码经过严格的验证,确保其不会崩溃或恶意修改内核数据。
- 灵活: 你可以使用 C 语言编写 eBPF 代码,然后使用 LLVM 编译成 BPF 字节码,加载到内核中运行。
场景:揪出偷偷建立连接的“内鬼”
假设你是一名系统管理员,负责维护一个重要的服务器。最近你发现服务器的网络流量异常,怀疑有恶意程序在偷偷建立连接,窃取数据。传统的排查方法可能需要你登录服务器,使用 tcpdump
抓包,然后分析大量的网络数据包。这种方法效率低下,而且容易遗漏关键信息。
现在,有了 eBPF,你可以编写一个简单的 eBPF 程序,实时监测系统中所有进程的网络连接行为,识别出哪些进程正在建立新的连接,以及它们连接的目标 IP 地址和端口。这样,你就可以快速定位到可疑的进程,并采取相应的措施。
如何用 eBPF 监控网络连接?实战演练
下面,我将分享一个使用 eBPF 监控网络连接的示例程序,帮助你了解 eBPF 的基本原理和使用方法。
1. 准备工作
安装 bpftool: bpftool 是一个用于管理 eBPF 程序的命令行工具,你可以使用它来加载、卸载和查看 eBPF 程序的信息。在 Ubuntu 上,你可以使用以下命令安装 bpftool:
sudo apt-get update sudo apt-get install bpftool 安装 libbpf: libbpf 是一个用于开发 eBPF 程序的 C 语言库,它提供了一些方便的 API,可以简化 eBPF 程序的开发。你可以从 libbpf 的 GitHub 仓库下载源码,然后编译安装。
git clone https://github.com/libbpf/libbpf.git cd libbpf make sudo make install 安装 clang 和 llvm: clang 和 llvm 是用于编译 eBPF 程序的编译器和工具链,你需要安装它们才能将 C 语言代码编译成 BPF 字节码。在 Ubuntu 上,你可以使用以下命令安装 clang 和 llvm:
sudo apt-get install clang llvm
2. 编写 eBPF 程序
下面是一个简单的 eBPF 程序,用于监控网络连接的建立:
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_core_read.h> #include <linux/socket.h> #include <linux/tcp.h> #include <linux/ip.h> char LICENSE[] SEC("license") = "Dual BSD/GPL"; // 定义一个 BPF 映射,用于存储网络连接的信息 struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(key_size, sizeof(int)); __uint(value_size, sizeof(struct conn_info)); __uint(max_entries, 1024); } connections SEC(".maps"); // 定义一个结构体,用于存储网络连接的信息 struct conn_info { __u32 pid; __u32 saddr; __u32 daddr; __u16 sport; __u16 dport; }; // 定义一个 BPF 探针函数,用于在 TCP 连接建立时被调用 SEC("kprobe/tcp_v4_connect") int BPF_KPROBE(tcp_v4_connect, struct sock *sk) { int pid = bpf_get_current_pid_tgid() >> 32; struct conn_info conn = {}; // 获取进程 ID conn.pid = pid; // 获取源 IP 地址和端口 conn.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr); conn.sport = BPF_CORE_READ(sk, __sk_common.skc_num); // 获取目标 IP 地址和端口 conn.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr); conn.dport = BPF_CORE_READ(sk, __sk_common.skc_dport); // 将网络连接的信息存储到 BPF 映射中 bpf_map_update_elem(&connections, &pid, &conn, BPF_ANY); return 0; } // 定义一个 BPF 探针函数,用于在 TCP 连接关闭时被调用 SEC("kretprobe/tcp_v4_connect") int BPF_KRETPROBE(tcp_v4_connect_ret, int ret) { int pid = bpf_get_current_pid_tgid() >> 32; // 从 BPF 映射中删除网络连接的信息 bpf_map_delete_elem(&connections, &pid); return 0; }
代码解释:
SEC("kprobe/tcp_v4_connect")
:定义一个 BPF 探针函数,用于在tcp_v4_connect
函数被调用时执行。tcp_v4_connect
函数是 Linux 内核中用于建立 TCP 连接的函数。SEC("kretprobe/tcp_v4_connect")
:定义一个 BPF 返回探针函数,用于在tcp_v4_connect
函数返回时执行。bpf_get_current_pid_tgid()
:获取当前进程的 PID 和 TGID。BPF_CORE_READ()
:从内核数据结构中读取数据。例如,BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr)
用于读取sock
结构体中的源 IP 地址。bpf_map_update_elem()
:将数据更新到 BPF 映射中。bpf_map_delete_elem()
:从 BPF 映射中删除数据。
3. 编译 eBPF 程序
将上面的 C 语言代码保存为 connect_monitor.c
文件,然后使用以下命令编译成 BPF 字节码:
clang -target bpf -D__TARGET_ARCH_x86_64 -O2 -Wall -Werror -c connect_monitor.c -o connect_monitor.o
4. 加载 eBPF 程序
编写一个用户态程序,用于加载 eBPF 程序到内核中,并从 BPF 映射中读取数据:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <signal.h> #include <fcntl.h> #include <sys/resource.h> #include <bpf/bpf.h> #include <bpf/libbpf.h> #define MAP_PATH "/sys/fs/bpf/connections" struct conn_info { __u32 pid; __u32 saddr; __u32 daddr; __u16 sport; __u16 dport; }; static int map_fd; static void cleanup(int sig) { close(map_fd); exit(0); } int main(int argc, char **argv) { struct rlimit rlim_new = { .rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY, }; if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) { perror("setrlimit"); exit(1); } signal(SIGINT, cleanup); signal(SIGTERM, cleanup); // 加载 eBPF 程序 struct bpf_object *obj = bpf_object__open_file("connect_monitor.o", NULL); if (!obj) { perror("bpf_object__open_file"); exit(1); } int err = bpf_object__load(obj); if (err) { perror("bpf_object__load"); exit(1); } // 获取 BPF 映射的 FD map_fd = bpf_object__find_map_fd_by_name(obj, "connections"); if (map_fd < 0) { perror("bpf_object__find_map_fd_by_name"); exit(1); } printf("Monitoring network connections... "); // 循环读取 BPF 映射中的数据 while (1) { int key = 0; struct conn_info conn; __u32 next_key; // 遍历 BPF 映射 while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) { err = bpf_map_lookup_elem(map_fd, &next_key, &conn); if (err) { perror("bpf_map_lookup_elem"); exit(1); } // 打印网络连接的信息 printf("PID: %d, Source: %u.%u.%u.%u:%d, Destination: %u.%u.%u.%u:%d ", conn.pid, (conn.saddr >> 0) & 0xFF, (conn.saddr >> 8) & 0xFF, (conn.saddr >> 16) & 0xFF, (conn.saddr >> 24) & 0xFF, conn.sport, (conn.daddr >> 0) & 0xFF, (conn.daddr >> 8) & 0xFF, (conn.daddr >> 16) & 0xFF, (conn.daddr >> 24) & 0xFF, conn.dport); key = next_key; } sleep(1); } return 0; }
代码解释:
bpf_object__open_file()
:打开 eBPF 程序的目标文件。bpf_object__load()
:将 eBPF 程序加载到内核中。bpf_object__find_map_fd_by_name()
:根据 BPF 映射的名称查找其 FD。bpf_map_get_next_key()
:获取 BPF 映射中的下一个 key。bpf_map_lookup_elem()
:根据 key 从 BPF 映射中查找数据。
将上面的 C 语言代码保存为 main.c
文件,然后使用以下命令编译:
gcc main.c -o main -lbpf
5. 运行程序
首先,运行 connect_monitor.c
编译出的 connect_monitor.o
文件,再运行 main.c
编译出的 main
程序:
sudo ./main
现在,你可以看到程序正在实时打印系统中所有进程的网络连接信息。当你使用浏览器访问一个网站时,你就可以在终端中看到浏览器进程建立连接的信息。
进阶:更强大的分析能力
上面的示例程序只是一个简单的演示,你可以根据自己的需求,编写更复杂的 eBPF 程序,实现更强大的分析能力。
- 监控指定进程的网络连接: 你可以在 eBPF 程序中添加过滤条件,只监控指定进程的网络连接行为。
- 统计网络流量: 你可以使用 eBPF 程序统计每个进程的网络流量,找出占用带宽最多的进程。
- 检测恶意连接: 你可以编写 eBPF 程序,检测与恶意 IP 地址或端口建立的连接,及时发现潜在的安全威胁。
eBPF 的应用场景:无限可能
eBPF 的应用场景非常广泛,除了网络监控之外,还可以用于性能分析、安全审计、容器监控等等。例如:
- 性能分析: 你可以使用 eBPF 监测函数的执行时间,找出性能瓶颈。
- 安全审计: 你可以使用 eBPF 记录系统调用,审计用户的行为。
- 容器监控: 你可以使用 eBPF 监控容器的网络和文件系统活动。
总结:拥抱 eBPF,提升你的运维技能
eBPF 是一项非常强大的内核技术,它可以帮助你深入了解系统的运行状态,解决各种疑难问题。虽然 eBPF 的学习曲线比较陡峭,但是只要你掌握了基本原理和使用方法,就可以利用它来提升你的运维技能,成为一名更优秀的系统管理员。
希望这篇文章能够帮助你入门 eBPF,并在实际工作中应用 eBPF 技术。记住,实践是最好的老师,多动手尝试,你一定能够掌握 eBPF 的精髓。