http://arthurchiao.art/blog/trace-packet-with-tracepoint-perf-ebpf-zh/
Linux 允许在主机上用虚拟网卡(virtual interface)和网络命名空间(network namespace)构建复杂的网络。但出现故障时,排障(troubleshooting)相当痛苦。如果是 3 层路由问题,mtr
可以排上用场;但如果是更底层的问题,通常只能手动检查每个网 卡/网桥/网络命名空间/iptables 规则,用 tcpdump 抓一些包,以确定到底是什么状况。 如果不了解故障之前的网络配置,那排障时的感觉就像在走迷宫。
虚拟网卡:
虚拟网卡(Virtual Network Interface Card)是一种软件实现的网络接口,用于在虚拟化环境中模拟物理网络接口卡的功能。它在虚拟机和宿主机之间起到了桥接和连接的作用。
以下是虚拟网卡的一些主要作用:
-
网络连接:虚拟网卡提供了虚拟机与宿主机或其他虚拟机之间进行网络通信的接口。它使虚拟机能够通过网络与其他设备(如物理服务器、网络设备或其他虚拟机)进行通信。
-
网络隔离:每个虚拟机都可以有自己独立的虚拟网卡,使得虚拟机之间的网络流量可以相互隔离。这种隔离可以提供更高的安全性和防止不同虚拟机之间的干扰。
-
虚拟网络拓扑:通过虚拟网卡,可以创建复杂的虚拟网络拓扑,包括虚拟交换机、路由器和防火墙等网络设备的模拟。这允许虚拟机之间进行内部通信,并提供更灵活的网络配置选项。
-
网络性能优化:虚拟网卡可以针对虚拟化环境进行性能优化。它可以利用物理网卡的硬件加速功能,如SR-IOV(Single Root I/O Virtualization)或者使用虚拟化技术自身的优化机制,以提供更高的网络性能和吞吐量。
-
网络管理和监控:虚拟网卡可以与虚拟化管理工具集成,提供对虚拟机网络的管理和监控功能。管理员可以配置虚拟网卡的参数、监视网络流量和进行故障排除等操作。
虚拟交换机:
网络命名空间(Network Namespace)是Linux内核提供的一种机制,用于在同一主机上创建独立的网络环境。每个网络命名空间都有自己独立的网络栈、网络接口、路由表和防火墙规则,它们之间相互隔离,就像是在同一主机上运行了多个独立的网络堆栈。
网络堆栈:常见的四层(?)
网桥通过学习和维护转发表来实现将一个网段的数据转发到另一个网段。当一个数据帧到达网桥时,网桥会进行以下步骤来决定如何转发该数据帧:
-
学习源MAC地址:网桥会检查数据帧中的源MAC地址,并将其与输入接口(即数据帧到达网桥的接口)相关联。它会将该源MAC地址和对应的输入接口记录到转发表中。
-
查找目标MAC地址:网桥会检查数据帧中的目标MAC地址。
-
查找目标MAC地址在转发表中的条目:网桥会查找转发表,看是否存在与目标MAC地址匹配的记录。
a. 如果转发表中存在与目标MAC地址匹配的记录,网桥会获取与目标MAC地址对应的输出接口,然后将数据帧转发到该输出接口,以便达到目标网段。
b. 如果转发表中不存在与目标MAC地址匹配的记录,网桥会将数据帧广播到所有其他接口,以便学习到目标MAC地址的位置,并将其添加到转发表中。
-
更新转发表:当网桥接收到新的数据帧时,它会根据源MAC地址进行学习,并更新转发表中的条目。这样,网桥可以不断更新和维护转发表,以适应网络中设备的变化。
在Linux环境下,可以使用以下工具和命令来创建和管理网络命名空间:
-
ip命令:ip命令是一个功能强大的网络工具,可以用于创建和管理网络命名空间。以下是一些常用的ip命令选项:
- 创建网络命名空间:
ip netns add <namespace_name>
- 删除网络命名空间:
ip netns delete <namespace_name>
- 在网络命名空间中执行命令:
ip netns exec <namespace_name> <command>
- 创建网络命名空间:
-
ip命令配合虚拟网络设备:通过创建虚拟网络设备并将其分配给网络命名空间,可以实现网络隔离。以下是一些相关命令选项:
- 创建虚拟网络设备:
ip link add <device_name> type veth peer name <peer_device_name>
- 将设备分配给网络命名空间:
ip link set <device_name> netns <namespace_name>
- 配置网络设备的IP地址、路由等:
ip addr add <ip_address>/<subnet_mask> dev <device_name>
、ip route add <destination_network> via <gateway> dev <device_name>
- 创建虚拟网络设备:
-
nsenter命令:nsenter命令可以进入指定的网络命名空间,并在其中执行命令。以下是一个示例命令:
- 进入网络命名空间:
nsenter --net=<namespace_name> <command>
- 进入网络命名空间:
-
使用网络命名空间的应用程序:一些应用程序(如Docker、Kubernetes等)提供了内置的网络命名空间管理功能,可以通过它们来创建、管理和使用网络命名空间。
注意:创建和管理网络命名空间通常需要root权限或具有相应特权的用户权限。
这些工具和命令提供了创建、删除和管理网络命名空间的功能。可以使用它们来配置网络命名空间的网络接口、IP地址、路由规则和防火墙设置,以满足特定的需求和场景。
如果没有使用Docker或其他容器运行时工具,你可以手动使用Linux提供的命名空间和控制组等特性来实现容器的隔离访问。这需要更深入的了解和配置,而且相对复杂。下面是一个简化的示例,演示如何使用Linux命令行工具实现容器的隔离访问:
-
创建容器根文件系统:首先,创建一个目录作为容器的根文件系统,并将所需的文件和目录复制到其中,以构建容器的运行环境。例如,你可以使用
debootstrap
工具创建一个基本的Ubuntu根文件系统:sudo debootstrap bionic /path/to/rootfs
-
配置命名空间:使用
unshare
命令创建各种命名空间,实现进程、网络和文件系统的隔离。例如,创建PID命名空间和网络命名空间:sudo unshare --pid --fork --mount-proc=/path/to/rootfs/proc sudo unshare --net --fork --mount-proc=/path/to/rootfs/proc
-
挂载文件系统:在容器内部,使用
awk(感觉有点问题,gpt回答的)mount
命令挂载所需的文件系统。例如,挂载主机的根文件系统和其他必要的文件系统:sudo mount --bind / /path/to/rootfs sudo mount -t proc proc /path/to/rootfs/proc sudo mount -t sysfs sysfs /path/to/rootfs/sys
-
设置资源限制:使用
cgroup
工具设置容器的资源限制和分配。例如,限制容器的CPU使用和内存限制:sudo cgcreate -g cpu:/mycontainer sudo cgset -r cpu.cfs_quota_us=50000 /mycontainer sudo cgcreate -g memory:/mycontainer sudo cgset -r memory.limit_in_bytes=536870912 /mycontainer
-
执行容器进程:在容器的隔离环境中,执行容器的主进程。例如,使用
chroot
命令切换到容器的根文件系统,并启动容器中的应用程序:sudo chroot /path/to/rootfs /bin/bash
#破局
用 Linux 术语来说,就是转换到内核视角(the kernel point of view)。在这种视 角下,网络命名空间不再是容器(“containers”),而只是一些标签(labels)。内核、 数据包、网卡等此时都是“肉眼可见”的对象(objects)。
所以我想要的是这样一个工具,它可以直接告诉我 “嗨,我看到你的包了:它从属于这个 网络命名空间的这个网卡上发出,然后依次经过这些函数”。
#巨人肩膀
perf & eBPF
任何在 /proc/kallsyms 导出的符号(内核函数)和 tracepoint, 都可以插入 eBPF tracing 代码。
插入:
eBPF 内核探测:如何将任意系统调用转换成事件(2016)
http://arthurchiao.art/blog/ebpf-turn-syscall-to-event-zh/
基于 eBPF
可以将任何内核函数调用转换成可带任何 数据的用户空间事件。bcc
作为一个更上层的工具使这个过程更加方便。内核探测 代码用 C 写,数据处理代码用 Python。
当希望一个程序能对系统变化做出反应时,通常有 2 种可能的方式:
- 一种是程序主动去轮询,检查系统变化;
- 另一种,如果系统支持事件通知的话,让它主动通知程序
使用 push 还是 pull 取决于具体的问题。通常的经验是,
- 如果事件频率相对于事件处理时间来说比较低,那 push 模型比较合适;
- 如果事件频率很高,就采用 pull 模型,否则系统变得不稳定。
2 内核跟踪和 eBPF 简史
直到最近,唯一的通用方式是给内核打补丁,或者使用 SystemTap。SystemTap 是一个 tracing 系统,简单来说,它提供了一种领域特定语言(DSL),代码编译成内核模块, 然后热加载到运行中的内核。但出于安全考虑,一些生产系统禁止动态模块加载, 例如我研究 eBPF 时所用的系统就不允许。
另一种方式是给内核打补丁来触发事件,可能会基于 Netlink。这种方式不太方便,内 核 hacking 有副作用,例如新引入的特性也许有毒,而且会增加维护成本
将任何可跟踪的内核函数安全地转换成事件, 很可能将成为现实。在计算机科学的表述中,“安全地”经常是指通过“某种类型的虚拟机” 来执行代码,这里也不例外。事实上,Linux 内部的这个“虚拟机”已经存 在几年了,从 1997 年的 2.1.75
版本有了,称作伯克利包过滤器(Berkeley Packet Filter),缩写 BPF。从名字就可以看出,它最开始是为 BSD 防火墙开发的。它只有两 个寄存器,只允许前向跳转,这意味着无法用它实现循环(如果非要说行也可以:如果 你知道最大的循环次数,那可以手动做循环展开)。这样设计是为了保证程序会在有限步骤 内结束,而不会让操作系统卡住。
我们的目标是:每当有程序监听 TCP socket,就得到一个事件通知。当在 AF_INET + SOCK_STREAM
类型 socket 上调用系统调用 listen()
时,底层负责处理的内核函数就 是 inet_listen()
。我们从用 kprobe
在它的入口做 hook,打印一个 “Hello, World” 开始。
from bcc import BPF
# Hello BPF Program
bpf_text = """
#include <net/inet_sock.h>
#include <bcc/proto.h>
// 1. Attach kprobe to "inet_listen"
int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog)
{
bpf_trace_printk("Hello World!\\n");
return 0;
};
"""
# 2. Build and Inject program
b = BPF(text=bpf_text)
# 3. Print debug output
while True:
print b.trace_readline()
- 依据命名规则,将探测点 attach 到
inet_listen
函数。例如按照这种规则,如果my_probe
被调用,它 将会通过b.attach_kprobe("inet_listen", "my_probe")
显式 attach - 使用 LLM eBPF 编译,将生成的字节码用
bpf()
系统调用注入(inject)内核,并自动根据命名规则 attach 到 probe 点 - 从内核管道读取原始格式的输出
标签:perf,eBPF,虚拟机,网络,网卡,网桥,内核,命名,数据包 From: https://www.cnblogs.com/ycjstudy/p/18103154