实战:使用eBPF监控特定端口流量并捕获数据包
实战:使用eBPF监控特定端口流量并捕获数据包
1. eBPF 简介
2. 准备工作
3. 编写 eBPF 程序
4. 高效过滤数据包
5. 安全地传递数据到用户空间
6. 总结
实战:使用eBPF监控特定端口流量并捕获数据包
eBPF(extended Berkeley Packet Filter)是 Linux 内核中一个强大的工具,允许用户在内核空间安全高效地运行自定义代码,而无需修改内核源代码或加载内核模块。这使得 eBPF 成为网络监控、安全分析和性能调优的理想选择。本文将介绍如何使用 eBPF 程序来监控特定网络端口上的流量,并记录指定类型的数据包,同时探讨如何高效地过滤数据包以及如何安全地将捕获的数据传递到用户空间进行分析。
1. eBPF 简介
传统的 BPF 主要用于网络数据包的过滤和捕获,而 eBPF 扩展了 BPF 的功能,使其能够用于更多场景,例如跟踪系统调用、监控内核事件和执行安全策略。eBPF 程序在内核中运行,因此具有高性能和低延迟的优势。为了保证安全性,eBPF 程序需要经过内核的验证器的验证,确保程序的安全性和可靠性。
2. 准备工作
在开始之前,需要确保系统满足以下条件:
Linux 内核版本: 建议使用 4.14 或更高版本的内核,以获得更好的 eBPF 支持。
安装 bcc 工具: bcc(BPF Compiler Collection)是一套用于创建 eBPF 程序的工具,提供了 Python 接口和各种辅助工具,简化了 eBPF 程序的开发和部署。
- Debian/Ubuntu:
sudo apt-get update && sudo apt-get install -y bpfcc-tools linux-headers-$(uname -r)
- CentOS/Fedora:
sudo yum install bpfcc-tools kernel-devel-$(uname -r)
- Debian/Ubuntu:
安装 libpcap 开发库: 用于解析和处理捕获的数据包。
- Debian/Ubuntu:
sudo apt-get install libpcap-dev
- CentOS/Fedora:
sudo yum install libpcap-devel
- Debian/Ubuntu:
3. 编写 eBPF 程序
我们将使用 Python 和 bcc 工具来编写 eBPF 程序。以下是一个简单的示例,用于监控特定端口(例如 80 端口)上的 TCP 连接,并记录 SYN 数据包。
from bcc import BPF import socket import struct # eBPF 程序代码 program = ''' #include <uapi/linux/tcp.h> #include <net/sock.h> // 定义一个哈希表,用于存储连接信息 BPF_HASH(connections, struct sock *, u64); // 当内核接收到 TCP SYN 数据包时,执行此函数 int kprobe__tcp_v4_syn_recv_sock(struct pt_regs *ctx, struct sock *sk, struct sk_buff *skb, const struct request_sock_ops *ops, struct inet_request_sock *ireq, struct sock *child) { // 获取目标端口 u16 dport = ireq->ir_rmt_port; // 检查端口是否为 80 if (dport == 80) { u64 ts = bpf_ktime_get_ns(); connections.update(&sk, &ts); } return 0; } // 当 TCP 连接关闭时,执行此函数 int kprobe__tcp_close(struct pt_regs *ctx, struct sock *sk, long timeout) { u64 *ts = connections.lookup(&sk); if (ts) { bpf_trace_printk("Connection closed, duration = %lld ns\n", bpf_ktime_get_ns() - *ts); connections.delete(&sk); } return 0; } ''' # 加载 eBPF 程序 b = BPF(text=program) # 打印跟踪信息 b.trace_print()
代码解释:
BPF_HASH(connections, struct sock *, u64)
: 定义了一个哈希表connections
,用于存储连接信息。键是struct sock *
,即 socket 结构体的指针,值是u64
,即时间戳。kprobe__tcp_v4_syn_recv_sock
: 这是一个 kprobe,用于在内核接收到 TCP SYN 数据包时执行。tcp_v4_syn_recv_sock
是内核函数,用于处理 IPv4 的 SYN 数据包。ireq->ir_rmt_port
: 获取目标端口。if (dport == 80)
: 检查端口是否为 80。bpf_ktime_get_ns()
: 获取当前时间戳,并将其存储在connections
哈希表中。
kprobe__tcp_close
: 这是一个 kprobe,用于在 TCP 连接关闭时执行。tcp_close
是内核函数,用于处理 TCP 连接关闭事件。connections.lookup(&sk)
: 在connections
哈希表中查找 socket 结构体的指针。bpf_trace_printk
: 打印跟踪信息,包括连接持续时间。connections.delete(&sk)
: 从connections
哈希表中删除 socket 结构体的指针。
运行代码:
将代码保存为 monitor_port.py
,然后使用 sudo python monitor_port.py
运行。程序将开始监控 80 端口上的 TCP 连接,并在连接关闭时打印连接持续时间。
4. 高效过滤数据包
为了高效地过滤数据包,可以使用 eBPF 的数据包过滤功能。以下是一个示例,用于只捕获 TCP SYN 数据包。
from bcc import BPF import socket import struct # eBPF 程序代码 program = ''' #include <uapi/linux/tcp.h> #include <net/sock.h> // 定义一个哈希表,用于存储连接信息 BPF_HASH(connections, struct sock *, u64); // 当内核接收到 TCP 数据包时,执行此函数 int kprobe__tcp_rcv_state_process(struct pt_regs *ctx, struct sock *sk, struct sk_buff *skb, const struct tcp_sock *tp) { // 获取目标端口 u16 dport = sk->__sk_common.skc_dport; // 获取 TCP 标志 u8 tcpflags = tp->tcp_flags; // 检查端口是否为 80,并且是 SYN 数据包 if (dport == 80 && (tcpflags & TCPHDR_SYN) ) { u64 ts = bpf_ktime_get_ns(); connections.update(&sk, &ts); } return 0; } // 当 TCP 连接关闭时,执行此函数 int kprobe__tcp_close(struct pt_regs *ctx, struct sock *sk, long timeout) { u64 *ts = connections.lookup(&sk); if (ts) { bpf_trace_printk("Connection closed, duration = %lld ns\n", bpf_ktime_get_ns() - *ts); connections.delete(&sk); } return 0; } ''' # 加载 eBPF 程序 b = BPF(text=program) # 打印跟踪信息 b.trace_print()
代码解释:
kprobe__tcp_rcv_state_process
: 这是一个 kprobe,用于在内核接收到 TCP 数据包时执行。tcp_rcv_state_process
是内核函数,用于处理 TCP 状态。sk->__sk_common.skc_dport
: 获取目标端口。tp->tcp_flags
: 获取 TCP 标志。if (dport == 80 && (tcpflags & TCPHDR_SYN))
: 检查端口是否为 80,并且是 SYN 数据包。
5. 安全地传递数据到用户空间
为了安全地将捕获的数据传递到用户空间,可以使用 eBPF 的 perf event 功能。perf event 允许 eBPF 程序将数据发送到用户空间的 perf buffer,用户空间的程序可以从 perf buffer 中读取数据。
以下是一个示例,用于将捕获的数据包信息发送到用户空间。
from bcc import BPF import socket import struct # eBPF 程序代码 program = ''' #include <uapi/linux/tcp.h> #include <net/sock.h> // 定义一个 perf event,用于将数据发送到用户空间 BPF_PERF_OUTPUT(events); struct data_t { u32 pid; u32 uid; char comm[64]; u16 dport; u64 ts; }; // 当内核接收到 TCP SYN 数据包时,执行此函数 int kprobe__tcp_v4_syn_recv_sock(struct pt_regs *ctx, struct sock *sk, struct sk_buff *skb, const struct request_sock_ops *ops, struct inet_request_sock *ireq, struct sock *child) { // 获取目标端口 u16 dport = ireq->ir_rmt_port; // 检查端口是否为 80 if (dport == 80) { struct data_t data = {}; data.pid = bpf_get_current_pid_tgid(); data.uid = bpf_get_current_uid_gid(); bpf_get_current_comm(&data.comm, sizeof(data.comm)); data.dport = dport; data.ts = bpf_ktime_get_ns(); // 将数据发送到 perf event events.perf_submit(ctx, &data, sizeof(data)); } return 0; } ''' # 加载 eBPF 程序 b = BPF(text=program) # 定义 perf event 的回调函数 def print_event(cpu, data, size): event = b["events"].event(data) print("PID: %d, UID: %d, COMM: %s, DPORT: %d, TS: %lld" % (event.pid, event.uid, event.comm.decode('utf-8', 'replace'), event.dport, event.ts)) # 绑定 perf event 的回调函数 b["events"].open_perf_buffer(print_event) # 循环读取 perf event while True: try: b.perf_buffer_poll() except KeyboardInterrupt: exit()
代码解释:
BPF_PERF_OUTPUT(events)
: 定义一个 perf eventevents
,用于将数据发送到用户空间。struct data_t
: 定义一个结构体data_t
,用于存储数据包信息。pid
: 进程 ID。uid
: 用户 ID。comm
: 进程名。dport
: 目标端口。ts
: 时间戳。
events.perf_submit(ctx, &data, sizeof(data))
: 将数据发送到 perf event。print_event
: 定义 perf event 的回调函数,用于打印数据包信息。b["events"].open_perf_buffer(print_event)
: 绑定 perf event 的回调函数。b.perf_buffer_poll()
: 循环读取 perf event。
6. 总结
本文介绍了如何使用 eBPF 程序来监控特定网络端口上的流量,并记录指定类型的数据包。通过编写 eBPF 程序,可以高效地过滤数据包,并将捕获的数据安全地传递到用户空间进行分析。eBPF 提供了强大的功能和灵活性,可以用于各种网络监控和安全分析场景。通过学习和应用 eBPF 技术,可以更好地理解和掌握网络行为,从而提高网络安全性和性能。
希望本文能够帮助读者理解和应用 eBPF 技术,解决实际的网络监控问题。