WEBKT

如何用 eBPF 监控服务器网络连接?系统管理员必看指南

50 0 0 0

为什么选择 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;
}
NetGuard eBPF网络监控系统安全

评论点评

打赏赞助
sponsor

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

分享

QRcode

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