WEBKT

告别盲人摸象:用 eBPF 透视 Linux 网络连接全貌,揪出幕后黑手

202 0 0 0

作为一名老运维,我深知服务器网络安全的重要性。每天面对海量的网络连接数据,就像大海捞针,想精准定位恶意连接,简直难如登天。传统的网络监控工具,要么性能开销太大,影响业务运行;要么只能提供粗略的信息,难以深入分析。直到我遇到了 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 的精髓。

Linux大玩家 eBPF网络监控系统安全

评论点评