WEBKT

用eBPF监控TCP连接状态变更,排查网络问题的实用指南

77 0 0 0

网络工程师和系统管理员经常需要处理各种各样的网络问题,其中TCP连接问题是最常见的之一。连接建立失败、连接异常断开、连接超时等问题都可能导致服务不稳定甚至中断。传统的网络诊断工具,例如tcpdumpnetstat等,在面对高并发、复杂网络环境时,往往显得力不从心。它们要么产生大量的冗余信息,难以快速定位问题,要么对系统性能产生较大的影响。这时,eBPF(extended Berkeley Packet Filter)就派上了大用场。

什么是eBPF?

eBPF是一个内核级的虚拟机,允许用户在内核中安全地运行自定义的代码,而无需修改内核源代码或加载内核模块。这意味着我们可以利用eBPF来动态地追踪、监控和分析内核的行为,而不会对系统造成风险。eBPF程序运行在沙箱环境中,并且经过严格的验证,确保其安全性。

为什么选择eBPF监控TCP连接?

  • 高性能: eBPF程序直接运行在内核中,避免了用户态和内核态之间频繁的数据拷贝,大大提高了性能。
  • 灵活性: 我们可以根据自己的需求,编写自定义的eBPF程序,监控特定的TCP事件,例如连接建立、关闭、超时等。
  • 可编程性: eBPF提供了一套丰富的API,可以访问内核数据结构、调用内核函数,实现各种复杂的监控和分析功能。
  • 安全性: eBPF程序运行在沙箱环境中,并且经过严格的验证,确保其不会对系统造成损害。

如何使用eBPF监控TCP连接状态变更?

接下来,我将手把手地教你如何使用eBPF来监控TCP连接状态的变更,并记录相关事件信息,以便于排查网络问题。我会使用bcc(BPF Compiler Collection)工具集,它提供了一系列高级的Python接口,方便我们编写和部署eBPF程序。bcc底层依赖于LLVM,用于将我们编写的eBPF程序编译成内核可以执行的字节码。

1. 准备工作

首先,你需要确保你的系统上已经安装了bcc工具集。安装方法会因操作系统而异,你可以参考bcc官方文档(https://github.com/iovisor/bcc)进行安装。一般来说,在基于Debian/Ubuntu的系统上,可以使用以下命令安装:

sudo apt-get update
sudo apt-get install bpfcc-tools linux-headers-$(uname -r)

在基于CentOS/RHEL的系统上,可以使用以下命令安装:

sudo yum install bpfcc-tools kernel-devel-$(uname -r)

安装完成后,你需要确保你的内核版本支持eBPF。一般来说,Linux内核4.1以上的版本都支持eBPF。你可以使用uname -r命令查看你的内核版本。

2. 编写eBPF程序

接下来,我们需要编写eBPF程序来监控TCP连接状态的变更。我们可以通过hook内核中的TCP状态变更函数来实现。以下是一个示例的eBPF程序,它hook了tcp_v4_state_process函数,该函数负责处理IPv4 TCP连接的状态变更:

#!/usr/bin/env python
from bcc import BPF
import socket
import struct
# 定义eBPF程序
program = '''
#include <uapi/linux/tcp.h>
#include <net/sock.h>
#include <linux/socket.h>
#include <linux/inet.h>
// 定义事件结构体
struct event_t {
u32 pid;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u8 oldstate;
u8 newstate;
u64 ts;
};
// 定义BPF perf event ring buffer
BPF_PERF_OUTPUT(events);
// 内核探测函数
int kprobe__tcp_v4_state_process(struct pt_regs *ctx, struct sock *sk, int newstate)
{
// 获取socket信息
u32 saddr = sk->__sk_common.skc_rcv_saddr;
u32 daddr = sk->__sk_common.skc_daddr;
u16 sport = sk->__sk_common.skc_num;
u16 dport = sk->__sk_common.skc_dport;
u32 pid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
// 获取旧的状态
u8 oldstate = sk->sk_state;
// 创建事件
struct event_t event = {
.pid = pid,
.saddr = saddr,
.daddr = daddr,
.sport = sport,
.dport = dport,
.oldstate = oldstate,
.newstate = newstate,
.ts = ts,
};
// 发送事件到用户空间
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
'''
# 加载eBPF程序
bpf = BPF(text=program)
# 定义回调函数,处理perf event
def print_event(cpu, data, size):
event = bpf['events'].event(data)
print("%-18s %-6d %-16s %-16s %-4d %-4d %-10s -> %-10s" % (
socket.inet_ntoa(struct.pack('I', event.saddr)),
event.pid,
socket.inet_ntoa(struct.pack('I', event.daddr)),
socket.inet_ntoa(struct.pack('I', event.saddr)),
event.sport,
event.dport,
tcp_state_str(event.oldstate),
tcp_state_str(event.newstate)
))
# TCP状态转换函数
def tcp_state_str(state):
states = {
1: "ESTABLISHED",
2: "SYN_SENT",
3: "SYN_RECV",
4: "FIN_WAIT1",
5: "FIN_WAIT2",
6: "TIME_WAIT",
7: "CLOSE",
8: "CLOSE_WAIT",
9: "LAST_ACK",
10: "LISTEN",
11: "CLOSING",
12: "NEW_SYN_RECV",
13: "MAX_STATES"
}
return states.get(state, "UNKNOWN")
# 打印表头
print("%-18s %-6s %-16s %-16s %-4s %-4s %-10s -> %-10s" % (
"SRC ADDR", "PID", "DEST ADDR", "SRC ADDR", "SPORT", "DPORT", "OLD STATE", "NEW STATE"
))
# 绑定perf event回调函数
bpf['events'].open_perf_buffer(print_event)
# 循环读取perf event
while True:
try:
bpf.perf_buffer_poll()
except KeyboardInterrupt:
exit()

代码解释:

  • eBPF程序: 使用C语言编写,嵌入在Python代码中。它定义了一个event_t结构体,用于存储TCP连接事件的信息,例如源IP地址、目标IP地址、源端口、目标端口、旧状态、新状态等。kprobe__tcp_v4_state_process函数是一个内核探测函数,它会在tcp_v4_state_process函数被调用时执行。在该函数中,我们获取socket信息,创建event_t事件,并将其发送到用户空间。BPF_PERF_OUTPUT(events)定义了一个perf event ring buffer,用于将事件从内核空间传递到用户空间。
  • 用户空间程序: 使用Python编写。它首先加载eBPF程序,然后定义了一个print_event回调函数,用于处理perf event。在该函数中,我们将事件信息格式化输出到终端。bpf['events'].open_perf_buffer(print_event)print_event回调函数绑定到events perf event ring buffer。bpf.perf_buffer_poll()循环读取perf event,并调用print_event函数处理事件。

3. 运行eBPF程序

将上面的代码保存为tcp_state.py,然后使用以下命令运行:

sudo python tcp_state.py

运行后,你将会看到类似下面的输出:

SRC ADDR PID DEST ADDR SRC ADDR SPORT SPORT OLD STATE -> NEW STATE
192.168.1.100 1234 192.168.1.200 192.168.1.100 4567 80 ESTABLISHED -> FIN_WAIT1
192.168.1.100 1234 192.168.1.200 192.168.1.100 4567 80 FIN_WAIT1 -> FIN_WAIT2
192.168.1.200 5678 192.168.1.100 192.168.1.200 80 4567 CLOSE_WAIT -> LAST_ACK

每一行输出代表一个TCP连接状态变更事件。你可以看到源IP地址、PID、目标IP地址、源端口、目标端口、旧状态、新状态等信息。通过分析这些信息,你可以快速定位网络问题。

4. 改进和扩展

上面的示例只是一个简单的演示,你可以根据自己的需求进行改进和扩展。例如,你可以:

  • 添加过滤条件: 可以根据源IP地址、目标IP地址、源端口、目标端口等条件过滤事件,只监控你关心的连接。
  • 记录更多信息: 可以记录TCP序列号、确认号、窗口大小等信息,用于更深入的分析。
  • 使用不同的输出方式: 可以将事件信息输出到文件、数据库、或者其他监控系统。
  • 监控更多的TCP事件: 除了状态变更,还可以监控连接建立、关闭、超时等事件。
  • 添加对IPv6的支持: 修改eBPF程序,同时hook tcp_v6_state_process函数,以支持IPv6连接的监控。

例如,要添加过滤条件,只监控特定端口的连接,你可以在eBPF程序中添加如下代码:

// 添加端口过滤条件
if (sport != 80 && dport != 80) {
return 0;
}

要记录TCP序列号和确认号,你需要修改event_t结构体,并从skb(socket buffer)中获取相关信息。这需要更深入的内核知识,但也是完全可行的。

5. 总结

eBPF是一个强大的网络监控和分析工具,它可以帮助我们快速定位和解决各种网络问题。通过本文的介绍,你应该已经掌握了使用eBPF监控TCP连接状态变更的基本方法。希望你能将eBPF应用到你的实际工作中,提高你的网络问题排查效率。

一些额外的思考:

  • 安全风险: 虽然eBPF程序运行在沙箱环境中,并且经过严格的验证,但仍然存在一定的安全风险。例如,如果eBPF程序存在漏洞,可能会被恶意利用。因此,我们需要谨慎编写和测试eBPF程序,并定期更新bcc工具集。
  • 性能影响: eBPF程序虽然性能很高,但仍然会对系统性能产生一定的影响。我们需要根据实际情况,合理地选择监控的事件和信息,避免过度监控。
  • 学习曲线: eBPF的学习曲线比较陡峭,需要一定的内核知识和编程经验。但是,一旦掌握了eBPF,你将会发现它是一个非常有用的工具。

希望这篇文章能够帮助你更好地理解和使用eBPF。 记住,实践是最好的老师。 尝试编写自己的eBPF程序,解决实际的网络问题,你将会对eBPF有更深入的理解。

更进一步:利用 uprobe 监控用户态的连接

上面的例子侧重于内核态的 TCP 连接状态监控, 但在某些情况下, 我们也可能需要监控用户态程序发起的连接。 比如, 某个应用程序使用了自定义的 socket 函数, 绕过了内核的一些标准调用, 这时 kprobe 就可能无法正常工作。 这时, uprobe 就派上了用场。 uprobe 允许你 hook 用户态程序的函数, 从而监控用户态的行为。

例如, 我们可以使用 uprobe 监控 glibc 库中的 connect 函数, 从而了解应用程序发起了哪些连接。

以下是一个简单的示例:

#!/usr/bin/env python
from bcc import BPF
import socket
import struct
import argparse
# 定义参数解析
parser = argparse.ArgumentParser(description="Monitor connect() calls with uprobe")
parser.add_argument("-p", "--pid", type=int, help="PID to trace")
args = parser.parse_args()
# 定义eBPF程序
program = '''
#include <uapi/linux/ptrace.h>
#include <linux/socket.h>
#include <net/sock.h>
#include <linux/inet.h>
struct event_t {
u32 pid;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
int domain;
int type;
int protocol;
u64 ts;
};
BPF_PERF_OUTPUT(events);
int uprobe_connect(struct pt_regs *ctx, int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
struct sockaddr_in *sa = (struct sockaddr_in *)addr;
u32 pid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
// 获取socket信息
struct sock *sk = (struct sock *)sockfd; // 注意这里,sockfd 实际上是文件描述符,不能直接转换成 sock
// 获取地址族
short family = addr->sa_family;
// 只处理 IPv4 连接
if (family == AF_INET) {
u32 daddr = sa->sin_addr.s_addr;
u16 dport = sa->sin_port;
struct event_t event = {
.pid = pid,
.daddr = daddr,
.dport = ntohs(dport),
.domain = family,
.type = 0, // 无法直接从 connect 获取 type
.protocol = 0, // 无法直接从 connect 获取 protocol
.ts = ts,
};
events.perf_submit(ctx, &event, sizeof(event));
}
return 0;
}
'''
# 加载eBPF程序
bpf = BPF(text=program)
# 确定要 hook 的函数地址
libc = "/lib/x86_64-linux-gnu/libc.so.6" # 替换成你系统上的 libc 路径
bpf.attach_uprobe(name=libc, sym="connect", f=bpf.sym("uprobe_connect"), pid=args.pid if args.pid else -1)
# 定义回调函数,处理perf event
def print_event(cpu, data, size):
event = bpf['events'].event(data)
print("%-6d %-16s %-4d %-2d" % (
event.pid,
socket.inet_ntoa(struct.pack('I', event.daddr)),
event.dport,
event.domain,
))
# 打印表头
print("%-6s %-16s %-4s %-2s" % (
"PID", "DEST ADDR", "DPORT", "DOMAIN"
))
# 绑定perf event回调函数
bpf['events'].open_perf_buffer(print_event)
# 循环读取perf event
while True:
try:
bpf.perf_buffer_poll()
except KeyboardInterrupt:
exit()

代码解释:

  • 确定 libc 路径: 需要根据你的系统架构和 libc 版本, 修改 libc = "/lib/x86_64-linux-gnu/libc.so.6" 这一行, 确保指向正确的 libc 库文件。
  • bpf.attach_uprobe: 使用 bpf.attach_uprobeuprobe_connect 函数 hook 到 connect 函数上。 pid=args.pid if args.pid else -1 表示如果没有指定 PID, 则 hook 所有进程的 connect 函数。 如果指定了 PID, 则只 hook 该进程的 connect 函数。
  • sockfd 的处理:uprobe 中, sockfd 参数实际上是文件描述符, 而不是指向 sock 结构的指针。 因此, 无法直接获取 socket 的详细信息 (例如源 IP 和端口)。 这个例子只获取了目标 IP 和端口。

运行方式:

sudo python uprobe_connect.py -p <PID>

替换 <PID> 为你想要监控的进程的 PID。 如果省略 -p <PID>, 则会监控所有进程的 connect 调用。

局限性:

  • 信息有限: uprobe 只能获取传递给 connect 函数的参数, 无法直接访问 sock 结构, 因此获取的信息比较有限。 例如, 无法直接获取源 IP 和端口。
  • 性能开销: uprobe 的性能开销通常比 kprobe 更大, 因为它需要在用户态和内核态之间切换。

总的来说, uprobekprobe 的一个补充, 可以在某些特定情况下提供有用的信息。 选择使用 kprobe 还是 uprobe, 取决于你的具体需求和场景。

希望这些更深入的探讨能够帮助你更全面地理解 eBPF 在 TCP 连接监控方面的应用。

网络巡查员 eBPFTCP监控网络排查

评论点评

打赏赞助
sponsor

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

分享

QRcode

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