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

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

为什么选择eBPF?

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

高性能: eBPF程序运行在内核态,直接在内核中进行数据过滤和分析,避免了用户态和内核态之间频繁切换的开销,性能极高。

灵活性: eBPF允许你自定义追踪逻辑,可以根据你的具体需求编写eBPF程序,实现各种复杂的网络监控和分析功能。

安全性: eBPF程序在运行前会经过内核的验证,确保程序的安全性,防止恶意代码的注入。

容器友好: eBPF可以轻松地访问容器的网络命名空间,获取容器内的网络信息,实现容器级别的网络监控。

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

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

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

追踪指定容器内的所有TCP连接。

统计每个进程的网络流量(发送和接收)。

输出结果到控制台。

准备工作

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

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

#include

#include

// 定义数据结构,用于存储进程的网络流量

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"

代码解析

引入依赖: 引入bcc、argparse、time、os等必要的库。

定义命令行参数: 使用argparse定义一个命令行参数-p或--pid,用于指定要追踪的容器PID。

定义eBPF程序: 使用多行字符串定义eBPF程序。程序主要包含以下几个部分:

数据结构定义: 定义flow_key和flow_value结构体,用于存储网络连接信息和流量统计数据。

BPF map定义: 定义flow_stats BPF map,用于存储网络流量数据,key为flow_key,value为flow_value。

内核探针函数: 定义kprobe__tcp_sendmsg和kprobe__tcp_recvmsg两个内核探针函数,分别用于追踪TCP发送和接收数据。这两个函数会在tcp_sendmsg和tcp_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程序

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

docker inspect -f '{{.State.Pid}}'

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

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

sudo python container_net_top.py -p

替换为实际的容器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来解决问题。快去尝试一下吧,相信你会爱上这个强大的工具!

Copyright © 2088 14年世界杯决赛_世界杯预选赛中国队出线形势 - pengxiaojing.com All Rights Reserved.
友情链接