WEBKT

告别抓包!用eBPF硬核追踪容器网络流量,揪出偷跑流量的进程

82 0 0 0

为什么选择eBPF?

实战:使用eBPF追踪容器网络流量

进阶:更强大的功能

注意事项

总结

作为一名整天和容器打交道的开发者,你是不是经常遇到这样的问题?容器里的应用网络连接异常,疯狂占用带宽,但你却像无头苍蝇一样,不知道是哪个进程在作祟?传统的抓包工具?太慢了!而且在容器环境下,各种网络命名空间、Veth Pair,绕来绕去早就晕头转向了。别慌,今天我就教你一招,用eBPF来硬核追踪容器内的网络流量,精准定位问题进程,让那些偷跑流量的家伙无处遁形!

为什么选择eBPF?

在深入之前,我们先来聊聊为什么选择eBPF,而不是其他工具。原因很简单,eBPF就是为这种场景而生的!

  • 高性能: eBPF程序运行在内核态,直接在内核中进行数据过滤和分析,避免了用户态和内核态之间频繁切换的开销,性能极高。
  • 灵活性: eBPF允许你自定义追踪逻辑,可以根据你的具体需求编写eBPF程序,实现各种复杂的网络监控和分析功能。
  • 安全性: eBPF程序在运行前会经过内核的验证,确保程序的安全性,防止恶意代码的注入。
  • 容器友好: eBPF可以轻松地访问容器的网络命名空间,获取容器内的网络信息,实现容器级别的网络监控。

简单来说,eBPF就像一个内核级的“瑞士军刀”,可以让你在不影响系统性能的情况下,深入到内核中进行各种骚操作。对于容器网络监控来说,简直是神器!

实战:使用eBPF追踪容器网络流量

接下来,我们就通过一个实战案例,来演示如何使用eBPF追踪容器内的网络流量。我们的目标是:

  1. 追踪指定容器内的所有TCP连接。
  2. 统计每个进程的网络流量(发送和接收)。
  3. 输出结果到控制台。

准备工作

在开始之前,你需要确保你的系统满足以下条件:

  • Linux内核版本 >= 4.14(推荐5.x以上,功能更完善)。
  • 安装了bcc工具包(BPF Compiler Collection),这是一个用于编写和运行eBPF程序的工具集。
  • 安装了docker,并且已经启动了你想要监控的容器。

如果没有安装bcc,你可以参考官方文档进行安装:https://github.com/iovisor/bcc

编写eBPF程序

接下来,我们需要编写一个eBPF程序来实现我们的目标。创建一个名为container_net_top.py的文件,并将以下代码复制到文件中:

#!/usr/bin/env python
from bcc import BPF
import argparse
import time
import os
# 定义命令行参数
parser = argparse.ArgumentParser(
description="Trace network traffic in a container and print top processes."
)
parser.add_argument(
"-p", "--pid", type=int, help="Container PID to trace", required=True
)
args = parser.parse_args()
container_pid = args.pid
# 定义eBPF程序
program = """
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <bcc/proto.h>
// 定义数据结构,用于存储进程的网络流量
struct flow_key {
u32 pid;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u8 protocol;
};
struct flow_value {
u64 rx_bytes;
u64 tx_bytes;
};
// 定义BPF map,用于存储网络流量数据
BPF_HASH(flow_stats, struct flow_key, struct flow_value);
// 内核探针函数,用于追踪TCP发送数据
int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 只追踪指定容器内的进程
if (pid != CONTAINER_PID) {
return 0;
}
// 获取网络连接信息
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;
u8 protocol = IPPROTO_TCP;
// 构造flow_key
struct flow_key key = {.pid = pid, .saddr = saddr, .daddr = daddr, .sport = sport, .dport = bpf_ntohs(dport), .protocol = protocol};
// 更新网络流量统计
struct flow_value *value = flow_stats.lookup(&key);
if (value) {
value->tx_bytes += size;
} else {
struct flow_value initial_value = {.rx_bytes = 0, .tx_bytes = size};
flow_stats.insert(&key, &initial_value);
}
return 0;
}
// 内核探针函数,用于追踪TCP接收数据
int kprobe__tcp_recvmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 只追踪指定容器内的进程
if (pid != CONTAINER_PID) {
return 0;
}
// 获取网络连接信息
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;
u8 protocol = IPPROTO_TCP;
// 构造flow_key
struct flow_key key = {.pid = pid, .saddr = saddr, .daddr = daddr, .sport = sport, .dport = bpf_ntohs(dport), .protocol = protocol};
// 更新网络流量统计
struct flow_value *value = flow_stats.lookup(&key);
if (value) {
value->rx_bytes += size;
} else {
struct flow_value initial_value = {.rx_bytes = size, .tx_bytes = 0};
flow_stats.insert(&key, &initial_value);
}
return 0;
}
""
# 替换CONTAINER_PID宏
program = program.replace("CONTAINER_PID", str(container_pid))
# 加载eBPF程序
bpf = BPF(text=program)
# 打印表头
print("Tracing container PID %d... Ctrl-C to end" % container_pid)
print("%-6s %-16s %-16s %-6s %-6s %-10s %-10s" % ("PID", "SOURCE", "DESTINATION", "SPORT", "DPORT", "RX_BYTES", "TX_BYTES"))
# 循环打印网络流量统计
while True:
try:
time.sleep(1)
flow_stats = {}
for key, value in bpf["flow_stats"].items():
flow_stats[(key.pid, key.saddr, key.daddr, key.sport, key.dport, key.protocol)] = (value.rx_bytes, value.tx_bytes)
# 清空BPF map
bpf["flow_stats"].clear()
for key, value in sorted(flow_stats.items(), key=lambda x: sum(x[1]), reverse=True):
pid, saddr, daddr, sport, dport, protocol = key
rx_bytes, tx_bytes = value
print("%-6d %-16s %-16s %-6d %-6d %-10d %-10d" % (
pid,
inet_ntoa(saddr),
inet_ntoa(daddr),
sport,
dport,
rx_bytes,
tx_bytes
))
except KeyboardInterrupt:
exit()
# Helper function to convert IP address from integer to string
from socket import inet_ntoa
import struct
def inet_ntoa(addr):
try:
return inet_ntoa(struct.pack(">I", addr))
except OverflowError:
return "N/A"

代码解析

  • 引入依赖: 引入bccargparsetimeos等必要的库。
  • 定义命令行参数: 使用argparse定义一个命令行参数-p--pid,用于指定要追踪的容器PID。
  • 定义eBPF程序: 使用多行字符串定义eBPF程序。程序主要包含以下几个部分:
    • 数据结构定义: 定义flow_keyflow_value结构体,用于存储网络连接信息和流量统计数据。
    • BPF map定义: 定义flow_stats BPF map,用于存储网络流量数据,key为flow_key,value为flow_value
    • 内核探针函数: 定义kprobe__tcp_sendmsgkprobe__tcp_recvmsg两个内核探针函数,分别用于追踪TCP发送和接收数据。这两个函数会在tcp_sendmsgtcp_recvmsg函数被调用时执行。
    • 流量统计: 在探针函数中,首先获取当前进程的PID,并判断是否为指定容器内的进程。如果是,则获取网络连接信息,构造flow_key,然后更新flow_stats map中的流量统计数据。
  • 替换宏: 使用replace函数将eBPF程序中的CONTAINER_PID宏替换为实际的容器PID。
  • 加载eBPF程序: 使用BPF(text=program)加载eBPF程序。
  • 打印表头: 打印表头,显示各个字段的含义。
  • 循环打印网络流量统计: 循环执行以下操作:
    • 休眠1秒。
    • flow_stats map中读取数据。
    • 清空flow_stats map。
    • 对数据进行排序,按照流量总和从大到小排序。
    • 打印网络流量统计数据。
  • 异常处理: 使用try...except块捕获KeyboardInterrupt异常,当用户按下Ctrl-C时退出程序。

运行eBPF程序

  1. 获取容器PID: 使用docker inspect命令获取要追踪的容器的PID。

    docker inspect -f '{{.State.Pid}}' <container_name_or_id>
    

    <container_name_or_id>替换为你要监控的容器的名称或ID。

  2. 运行eBPF程序: 使用以下命令运行container_net_top.py脚本,并将上一步获取的容器PID作为参数传递给脚本。

    sudo python container_net_top.py -p <container_pid>
    

    <container_pid>替换为实际的容器PID。

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

    Tracing container PID 1234... Ctrl-C to end
    PID SOURCE DESTINATION SPORT DPORT RX_BYTES TX_BYTES
    1234 172.17.0.2 10.10.10.1 54321 80 1234567 8765432
    1234 172.17.0.2 8.8.8.8 54322 53 12345 67890
    ...

    每一行代表一个网络连接的流量统计数据,包括PID、源IP地址、目标IP地址、源端口、目标端口、接收字节数和发送字节数。

    你可以根据这些数据,快速定位占用带宽最多的进程和网络连接,从而排查网络问题。

进阶:更强大的功能

上面的例子只是一个简单的演示,eBPF的强大之处在于它的灵活性。你可以根据自己的需求,编写更复杂的eBPF程序来实现各种高级功能。

  • 按协议过滤: 可以根据协议类型(TCP、UDP、ICMP等)过滤网络流量。
  • 按端口过滤: 可以根据端口号过滤网络流量。
  • 统计连接数: 可以统计每个进程的连接数。
  • 追踪HTTP请求: 可以追踪HTTP请求的URL、状态码、响应时间等信息。
  • 生成火焰图: 可以生成火焰图,可视化网络流量的调用栈,帮助你更深入地分析网络性能问题。

这些功能都可以通过编写eBPF程序来实现。你可以参考bcc工具包中的其他示例程序,学习如何使用eBPF来实现这些功能。

注意事项

  • 性能影响: 虽然eBPF性能很高,但是过度使用eBPF程序仍然会对系统性能产生一定的影响。因此,在编写eBPF程序时,要尽量减少程序的复杂度,避免不必要的计算和数据拷贝。
  • 内核版本兼容性: 不同的内核版本可能支持不同的eBPF功能。在编写eBPF程序时,要考虑到内核版本兼容性问题,尽量使用通用的eBPF特性。
  • 安全问题: 虽然eBPF程序在运行前会经过内核的验证,但是仍然存在一定的安全风险。在编写eBPF程序时,要仔细检查程序的逻辑,避免潜在的安全漏洞。

总结

eBPF是一个非常强大的网络监控和分析工具,可以让你深入到内核中,实时追踪容器内的网络流量。通过使用eBPF,你可以快速定位网络问题,优化网络性能,保障容器的安全。如果你是一名容器开发者或运维人员,那么学习eBPF绝对会让你受益匪浅!

希望这篇文章能够帮助你入门eBPF,并在实际工作中应用eBPF来解决问题。快去尝试一下吧,相信你会爱上这个强大的工具!

容器巡洋舰 eBPF容器网络网络监控

评论点评

打赏赞助
sponsor

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

分享

QRcode

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