译者序
本文翻译自 2017 年的一篇英文博客 Tracing a packet’s journey using Linux tracepoints, perf and eBPF ,并添加了章节号以方便阅读。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
一段时间以来,我一直在寻找 Linux 上的底层网络调试(debug)工具。
Linux 允许在主机上用虚拟网卡(virtual interface)和网络命名空间(network namespace)构建复杂的网络。但出现故障时,排障(troubleshooting)相当痛苦。如果是 3 层路由问题,mtr
1 破局
1.1 逃离迷宫:上帝视角
逃离迷宫的一种方式是在迷宫内不断左右尝试,寻找通往出口的道路。 如果是在玩迷宫游戏(置身迷宫内),那确实只能如此;但如果不是在玩游戏, 那还有另一种逃离方式:转换视角,高空俯视。
用 Linux 术语来说,就是转换到内核视角(the kernel point of view)。在这种视 角下,网络命名空间不再是容器(“containers”),而只是一些标签(labels)。内核、 数据包、网卡等此时都是“肉眼可见”的对象(objects)。
原文注:上面的 “containers” 我加了引号,因为从技术上说,网络命名空间是 构成 Linux 容器的核心部件之一。
1.2 网络跟踪:渴求利器
所以我想要的是这样一个工具,它可以直接告诉我 “嗨,我看到你的包了:它从属于这个 网络命名空间的这个网卡上发出,然后依次经过这些函数”。
本质上,我想要的是一个 2 层的 mtr
。这样的工具存在吗?不存在我们就造一个!
本文结束时,我们将拥有一个简单、易于使用的底层网络包跟踪器(packet tracker )。如果 ping 本机上的一个 Docker 容器,它会显示类似如下信息:
# ping -4 172.17.0.2
[ 4026531957] docker0 request #17146.001 172.17.0.1 -> 172.17.0.2
[ 4026531957] vetha373ab6 request #17146.001 172.17.0.1 -> 172.17.0.2
[ 4026532258] eth0 request #17146.001 172.17.0.1 -> 172.17.0.2
[ 4026532258] eth0 reply #17146.001 172.17.0.2 -> 172.17.0.1
[ 4026531957] vetha373ab6 reply #17146.001 172.17.0.2 -> 172.17.0.1
[ 4026531957] docker0 reply #17146.001 172.17.0.2 -> 172.17.0.1
1.3 巨人肩膀:perf/eBPF
在本文中,我将聚焦两个跟踪工具:perf
和 eBPF
。perf
是 Linux 上的最重要的性能分析工具之一。它和内核出自同一个源码树(source tree),但编译需要针对指定的内核版本。perf
可以跟踪内核,也可以跟踪用户程序, 还可用于采样或者设置跟踪点,可以把它想象成开销更低但功能更强大的 strace。 本文只会使用非常简单的 perf
命令。想了解更多,强烈建议访问 Brendan Gregg的博客。eBPF
是 Linux 内核新近加入的,其中 e 是 extended
eBPF 程序可用于 QoS 网络分类器(network classifier)、XDP(eXpress Data Plane) 很底层的网络功能和过滤功能组件、跟踪代理(tracing agent),以及其他很多方面。 任何在 /proc/kallsyms 导出的符号(内核函数)和 tracepoint, 都可以插入 eBPF tracing 代码。
本文将主要关注 attach 到 tracepoints 的跟踪代理(tracing agents attached to tracepoints)。想看在内核函数埋点进行跟踪的例子,或者入门级介绍,建议阅读我之前的 eBPF 文章英文 ,中文翻译。
2 Perf
本文只会使用 perf 做非常简单的内核跟踪。
2.1 安装 perf
我的环境基于 Ubuntu 17.04 (Zesty):
$ sudo apt install linux-tools-generic
$ perf # test perf
2.2 测试环境
我们将使用 4 个 IP,其中 2 个为外部可路由网段(192.168
):
- localhost,IP
127.0.0.1
- 一个干净的容器,IP
172.17.0.2
- 我的手机,通过 USB 连接,IP
192.168.42.129
- 我的手机,通过 WiFi 连接,IP
192.168.43.1
2.3 初体验:跟踪 ping 包
perf trace
是 perf
子命令,能够跟踪 packet 路径,默认输出类似于 strace
(头 信息少很多)。跟踪 ping 向 172.17.0.2
容器的包,这里我们只关心 net
$ sudo perf trace --no-syscalls --event 'net:*' ping 172.17.0.2 -c1 > /dev/null
0.000 net:net_dev_queue:dev=docker0 skbaddr=0xffff96d481988700 len=98)
0.008 net:net_dev_start_xmit:dev=docker0 queue_mapping=0 skbaddr=0xffff96d481988700 vlan_tagged=0 vlan_proto=0x0000 vlan_tci=0x0000 protocol=0x0800 ip_summed=0 len=98 data_len=0 network_offset=14 transport_offset_valid=1 transport_offset=34 tx_flags=0 gso_size=0 gso_segs=0 gso_type=0)
0.014 net:net_dev_queue:dev=veth79215ff skbaddr=0xffff96d481988700 len=98)
0.016 net:net_dev_start_xmit:dev=veth79215ff queue_mapping=0 skbaddr=0xffff96d481988700 vlan_tagged=0 vlan_proto=0x0000 vlan_tci=0x0000 protocol=0x0800 ip_summed=0 len=98 data_len=0 network_offset=14 transport_offset_valid=1 transport_offset=34 tx_flags=0 gso_size=0 gso_segs=0 gso_type=0)
0.020 net:netif_rx:dev=eth0 skbaddr=0xffff96d481988700 len=84)
0.022 net:net_dev_xmit:dev=veth79215ff skbaddr=0xffff96d481988700 len=98 rc=0)
0.024 net:net_dev_xmit:dev=docker0 skbaddr=0xffff96d481988700 len=98 rc=0)
0.027 net:netif_receive_skb:dev=eth0 skbaddr=0xffff96d481988700 len=84)
0.044 net:net_dev_queue:dev=eth0 skbaddr=0xffff96d481988b00 len=98)
0.046 net:net_dev_start_xmit:dev=eth0 queue_mapping=0 skbaddr=0xffff96d481988b00 vlan_tagged=0 vlan_proto=0x0000 vlan_tci=0x0000 protocol=0x0800 ip_summed=0 len=98 data_len=0 network_offset=14 transport_offset_valid=1 transport_offset=34 tx_flags=0 gso_size=0 gso_segs=0 gso_type=0)
0.048 net:netif_rx:dev=veth79215ff skbaddr=0xffff96d481988b00 len=84)
0.050 net:net_dev_xmit:dev=eth0 skbaddr=0xffff96d481988b00 len=98 rc=0)
0.053 net:netif_receive_skb:dev=veth79215ff skbaddr=0xffff96d481988b00 len=84)
0.060 net:netif_receive_skb_entry:dev=docker0 napi_id=0x3 queue_mapping=0 skbaddr=0xffff96d481988b00 vlan_tagged=0 vlan_proto=0x0000 vlan_tci=0x0000 protocol=0x0800 ip_summed=2 hash=0x00000000 l4_hash=0 len=84 data_len=0 truesize=768 mac_header_valid=1 mac_header=-14 nr_frags=0 gso_size=0 gso_type=0)
0.061 net:netif_receive_skb:dev=docker0 skbaddr=0xffff96d481988b00 len=84)
只保留事件名和 skbaddr
,看起来清晰很多:
net_dev_queue dev=docker0 skbaddr=0xffff96d481988700
net_dev_start_xmit dev=docker0 skbaddr=0xffff96d481988700
net_dev_queue dev=veth79215ff skbaddr=0xffff96d481988700
net_dev_start_xmit dev=veth79215ff skbaddr=0xffff96d481988700
netif_rx dev=eth0 skbaddr=0xffff96d481988700
net_dev_xmit dev=veth79215ff skbaddr=0xffff96d481988700
net_dev_xmit dev=docker0 skbaddr=0xffff96d481988700
netif_receive_skb dev=eth0 skbaddr=0xffff96d481988700
net_dev_queue dev=eth0 skbaddr=0xffff96d481988b00
net_dev_start_xmit dev=eth0 skbaddr=0xffff96d481988b00
netif_rx dev=veth79215ff skbaddr=0xffff96d481988b00
net_dev_xmit dev=eth0 skbaddr=0xffff96d481988b00
netif_receive_skb dev=veth79215ff skbaddr=0xffff96d481988b00
netif_receive_skb_entry dev=docker0 skbaddr=0xffff96d481988b00
netif_receive_skb dev=docker0 skbaddr=0xffff96d481988b00
这里面有很多信息。
首先注意,skbaddr
在中间变了(0xffff96d481988700 -> 0xffff96d481988b00
) 。变的这里,就是生成了 ICMP echo reply 包,并作为应答包发送的地方。接下来的 时间,这个包的 skbaddr
其次,我们可以清楚地看到 packet 在内核的传输路径:
docker0
- veth pair 的宿主机端(
veth79215ff
- )
- veth pair 的容器端(容器里的
eth0
- )
- 接下来是相反的返回路径
至此,虽然我们还没有看到网络命名空间,但已经得到了一个不错的全局视图。
2.4 进阶:选择跟踪点
上面的信息有些杂,还有很多重复。我们可以选择几个最合适的跟踪点,使得输出看起来 更清爽。要查看所有可用的网络跟踪点,执行 perf list
:
$ sudo perf list 'net:*'
这个命令会列出 tracepoint
列表,格式 net:netif_rx
。冒号前面是事件类型 ,后面是事件名字。这里我选择 4 个:net_dev_queue
netif_receive_skb_entry
netif_rx
napi_gro_receive_entry
效果:
$ sudo perf trace --no-syscalls \
--event 'net:net_dev_queue' \
--event 'net:netif_receive_skb_entry' \
--event 'net:netif_rx' \
--event 'net:napi_gro_receive_entry' \
ping 172.17.0.2 -c1 > /dev/null
0.000 net:net_dev_queue:dev=docker0 skbaddr=0xffff8e847720a900 len=98)
0.010 net:net_dev_queue:dev=veth7781d5c skbaddr=0xffff8e847720a900 len=98)
0.014 net:netif_rx:dev=eth0 skbaddr=0xffff8e847720a900 len=84)
0.034 net:net_dev_queue:dev=eth0 skbaddr=0xffff8e849cb8cd00 len=98)
0.036 net:netif_rx:dev=veth7781d5c skbaddr=0xffff8e849cb8cd00 len=84)
0.045 net:netif_receive_skb_entry:dev=docker0 napi_id=0x1 queue_mapping=0
漂亮!
3 eBPF
前面介绍的内容已经可以满足大部分 tracing 场景的需求了。如果你只是想学习如何在 Linux 上跟踪一个 packet 的传输路径,那到此已经足够了。但如果想跟更进一步,学习如 何写一个自定义的过滤器,跟踪网络命名空间、源 IP、目的 IP 等信息,请继续往下读。
3.1 eBPF 和 kprobes
从 Linux 内核 4.7 开始,eBPF 程序可以 attach 到内核跟踪点(kernel tracepoints) 。在此之前,要完成类似的工作,只能用 kprobes 之类的工具 attach 到导出的内核函 数(exported kernel sysbols)。后者虽然可以完成工作,但存在很多不足:
- 内核的内部(internal)API 不稳定
- 出于性能考虑,大部分网络相关的内层函数(inner functions)都是内联或者静态的( inlined or static),两者都不可探测
- 找出调用某个函数的所有地方是相当乏味的,有时所需的字段数据不全具备
这篇博客的早期版本使用了 kprobes,但结果并不是太好。 现在,诚实地说,通过内核 tracepoints 访问数据比通过 kprobe 要更加乏味。我尽量保 持本文简洁,如果你想了解本文稍老的版本,可以访问这里英文 ,中文翻译。
3.2 安装
我不是一个徒手汇编迷(fans of handwritten assembly),因此接下来将使用 bcc
。bcc
注意:eBPF 需要 Linux Kernel 4.7+。
Ubuntu 17.04 安装 (GitHub) bcc
:
# Install dependencies
$ sudo apt install bison build-essential cmake flex git libedit-dev python zlib1g-dev libelf-dev libllvm4.0 llvm-dev libclang-dev luajit luajit-5.1-dev
# Grab the sources
$ git clone https://github.com/iovisor/bcc.git
$ mkdir bcc/build
$ cd bcc/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr
$ make
$ sudo make install
3.3 自定义跟踪器:Hello World
接下来我们从一个简单的 hello world 例子展示如何在底层打点。我们还是用上一篇 文章里选择的四个点:
net_dev_queue
netif_receive_skb_entry
netif_rx
napi_gro_receive_entry
每当网络包经过这些点,我们的处理逻辑就会触发。为保持简单,我们的处理逻辑只是将程 序的 comm
#include <bcc/proto.h>
#include <linux/sched.h>
// Event structure
struct route_evt_t {
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(route_evt);
static inline int do_trace(void* ctx, struct sk_buff* skb)
{
// Built event for userland
struct route_evt_t evt = {};
bpf_get_current_comm(evt.comm, TASK_COMM_LEN);
// Send event to userland
route_evt.perf_submit(ctx, &evt, sizeof(evt));
return 0;
}
/**
* Attach to Kernel Tracepoints
*/
TRACEPOINT_PROBE(net, netif_rx) {
return do_trace(args, (struct sk_buff*)args->skbaddr);
}
TRACEPOINT_PROBE(net, net_dev_queue) {
return do_trace(args, (struct sk_buff*)args->skbaddr);
}
TRACEPOINT_PROBE(net, napi_gro_receive_entry) {
return do_trace(args, (struct sk_buff*)args->skbaddr);
}
TRACEPOINT_PROBE(net, netif_receive_skb_entry) {
return do_trace(args, (struct sk_buff*)args->skbaddr);
}
可以看到,程序 attach 到 4 个 tracepoint,并会访问 skbaddr
字段,将其传给处理 逻辑函数,这个函数现在只是将程序名字发送出来。大家可能会有疑问:args->skbaddr
是 哪里来的?答案是,每次用 TRACEPONT_PROBE
定义一个 tracepoint,bcc
就会为其自 动生成 args
不过,有另外一种简单的方式可以查看。在 Linux 上每个 tracepoint 都对应一个 /sys/kernel/debug/tracing/events
entry。例如对于 net:netif_rx
:
$ cat /sys/kernel/debug/tracing/events/net/netif_rx/format
name: netif_rx
ID: 1183
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:void * skbaddr; offset:8; size:8; signed:0;
field:unsigned int len; offset:16; size:4; signed:0;
field:__data_loc char[] name; offset:20; size:4; signed:1;
print fmt: "dev=%s skbaddr=%p len=%u", __get_str(name), REC->skbaddr, REC->len
注意最后一行 print fmt
,这正是 perf trace
在底层插入这样的探测点之后,我们再写个 Python 脚本,接收内核发出来的消息,每个 eBPF 发出的数据都打印一行:
#!/usr/bin/env python
# coding: utf-8
from socket import inet_ntop
from bcc import BPF
import ctypes as ct
bpf_text = '''<SEE CODE SNIPPET ABOVE>'''
TASK_COMM_LEN = 16 # linux/sched.h
class RouteEvt(ct.Structure):
_fields_ = [
("comm", ct.c_char * TASK_COMM_LEN),
]
def event_printer(cpu, data, size):
# Decode event
event = ct.cast(data, ct.POINTER(RouteEvt)).contents
# Print event
print "Just got a packet from %s" % (event.comm)
if __name__ == "__main__":
b = BPF(text=bpf_text)
b["route_evt"].open_perf_buffer(event_printer)
while True:
b.kprobe_poll()
现在可以测试了,注意需要 root 权限。
注意:现在的代码没有对包做任何过滤,因此即便你的机器网络流量很小,输出也很可能刷屏!
$> sudo python ./tracepkt.py
...
Just got a packet from ping6
Just got a packet from ping6
Just got a packet from ping
Just got a packet from irq/46-iwlwifi
...
上面的输出显示,我正在使用 ping 和 ping6,另外 WiFi 驱动也收到了一些包。
3.4 自定义跟踪器:改进
接下来添加一些有用的数据/过滤条件。
3.4.1 添加网卡信息
首先,可以安全地删除前面代码中的 comm
字段,它在这里没什么用处。然后,include net/inet_sock.h
头文件,这里有我们所需要的函数声明。最后给 event
结构体添加 char ifname[IFNAMSIZ]
现在可以从 device
// Get device pointer, we'll need it to get the name and network namespace
struct net_device *dev;
bpf_probe_read(&dev, sizeof(skb->dev), ((char*)skb) + offsetof(typeof(*skb), dev));
// Load interface name
bpf_probe_read(&evt.ifname, IFNAMSIZ, dev->name);
现在你可以测试一下,这样是能工作的。注意相应地修改一下 Python 部分。那么,它是怎 么工作的呢?
我们引入了 net_device
结构体来访问网卡名字字段。第一个 bpf_probe_read
从内核 的网络包中将网卡名字拷贝到 dev
,第二个将其接力复制到 evt.ifname
。不要忘了,eBPF 的目标是允许安全地编写在内核运行的脚本。这意味着,随机内存访问是绝 对不允许的。所有的内存访问都要经过验证。除非要访问的内存在协议栈,否则都需要通 过 bpf_probe_read
读取数据。这会使得代码看起来很繁琐,但非常安全。bpf_probe_read
像是 memcpy
的一个更安全的版本,它定义在内核源文件 bpf_trace.c 中:
- 它和 memcpy 类似,因此注意内存拷贝的代价
- 如果遇到错误,它会返回一个错误和一个初始化为 0 的缓冲区,而不会造成程序崩溃或停 止运行
接下来为使代码看起来更加简洁,我将使用如下宏:
#define member_read(destination, source_struct, source_member) \
do{ \
bpf_probe_read( \
destination, \
sizeof(source_struct->source_member), \
((char*)source_struct) + offsetof(typeof(*source_struct), source_member) \
); \
} while(0)
这样上面的例子就可以写成:
member_read(&dev, skb, dev);
3.4.2 添加网络命名空间 ID
采集网络命名空间信息非常有用,但是实现起来要复杂一些。原理上可以从两个地方访问:
- socket 结构体
sk
- device 结构体
dev
当我在写 solisten.py
时 ,我使用的时 socket 结构体。不幸的是,不知道为什么,网络命名空间 ID 在跨命名空间的地 方消失了。这个字段全是 0,很明显是有非法内存访问时的返回值(回忆前面介绍的 bpf_probe_read
幸好 device 结构体工作正常。想象一下,我们可以问一个 packet
它在哪个网卡
,进而 问这个网卡它在哪个网络命名空间
。
struct net* net;
// Get netns id. Equivalent to: evt.netns = dev->nd_net.net->ns.inum
possible_net_t *skc_net = &dev->nd_net;
member_read(&net, skc_net, net);
struct ns_common* ns = member_address(net, ns);
member_read(&evt.netns, ns, inum);
其中的宏定义如下:
#define member_address(source_struct, source_member) \
({ \
void* __ret; \
__ret = (void*) (((char*)source_struct) + offsetof(typeof(*source_struct), source_member)); \
__ret; \
})
这个宏还可以用于简化 member_read
,这个就留给读者作为练习了。
好了,有了以上实现,我们再运行的效果就是:
$> sudo python ./tracepkt.py
[ 4026531957] docker0
[ 4026531957] vetha373ab6
[ 4026532258] eth0
[ 4026532258] eth0
[ 4026531957] vetha373ab6
[ 4026531957] docker0
如果 ping 一个容器,你看到的就是类似上面的输出。packet 首先经过本地的 docker0 网桥, 然后经 veth pair 跨过网络命名空间,最后到达容器的 eth0 网卡。应答包沿着相反的路径回 到宿主机。
至此,功能是实现了,不过还太粗糙,继续改进。
3.4.3 只跟踪 ICMP echo request/reply 包
这次我们将读取包的 IP 信息,这里我只展示 IPv4 的例子,IPv6 的与此类似。
不过,事情也并没有那么简单。我们是在和 kernel 的网络部分打交道。一些包可能还没被打 开,这意味着,变量的很多字段是没有初始化的。我们只能从 MAC 头开始,用 offset 的方式 计算 IP 头和 ICMP 头的位置。
首先从 MAC 头地址推导 IP 头地址。这里我们不(从 skb
// Compute MAC header address
char* head;
u16 mac_header;
member_read(&head, skb, head);
member_read(&mac_header, skb, mac_header);
// Compute IP Header address
#define MAC_HEADER_SIZE 14;
char* ip_header_address = head + mac_header + MAC_HEADER_SIZE;
这假设了 IP 头从 skb->head + skb->mac_header + MAC_HEADER_SIZE
// Load IP protocol version
u8 ip_version;
bpf_probe_read(&ip_version, sizeof(u8), ip_header_address);
ip_version = ip_version >> 4 & 0xf;
// Filter IPv4 packets
if (ip_version != 4) {
return 0;
}
然后加载整个 IP 头,获取 IP 地址,以使得 Python 程序的输出看起来更有意义。另外注意,IP 包内的下一个头就是 ICMP 头。
// Load IP Header
struct iphdr iphdr;
bpf_probe_read(&iphdr, sizeof(iphdr), ip_header_address);
// Load protocol and address
u8 icmp_offset_from_ip_header = iphdr.ihl * 4;
evt.saddr[0] = iphdr.saddr;
evt.daddr[0] = iphdr.daddr;
// Filter ICMP packets
if (iphdr.protocol != IPPROTO_ICMP) {
return 0;
}
最后,加载 ICMP 头,如果是 ICMP echo request 或 reply,就读取序列号:
// Compute ICMP header address and load ICMP header
char* icmp_header_address = ip_header_address + icmp_offset_from_ip_header;
struct icmphdr icmphdr;
bpf_probe_read(&icmphdr, sizeof(icmphdr), icmp_header_address);
// Filter ICMP echo request and echo reply
if (icmphdr.type != ICMP_ECHO && icmphdr.type != ICMP_ECHOREPLY) {
return 0;
}
// Get ICMP info
evt.icmptype = icmphdr.type;
evt.icmpid = icmphdr.un.echo.id;
evt.icmpseq = icmphdr.un.echo.sequence;
// Fix endian
evt.icmpid = be16_to_cpu(evt.icmpid);
evt.icmpseq = be16_to_cpu(evt.icmpseq);
这就是全部工作了。
如果想过滤特定的 ping 进程的包,那可以认为 evt.icmpid
3.5 最终效果
再写一些比较简单的 Python 程序配合,就可以测试我们的跟踪器在多种场景下的用途。 以 root 权限启动这个程序,在不同终端发起几个 ping 进程,就会看到:
# ping -4 localhost
[ 4026531957] lo request #20212.001 127.0.0.1 -> 127.0.0.1
[ 4026531957] lo request #20212.001 127.0.0.1 -> 127.0.0.1
[ 4026531957] lo reply #20212.001 127.0.0.1 -> 127.0.0.1
[ 4026531957] lo reply #20212.001 127.0.0.1 -> 127.0.0.1
这个 ICMP 请求是进程 20212(Linux ping 的 ICMP ID)在 loopback 网卡发出的,最后的 reply 原路回到了这个 loopback。这个环回接口既是发送网卡又是接收网卡。
如果是我的 WiFi 网关会是什么样子内?
# ping -4 192.168.43.1
[ 4026531957] wlp2s0 request #20710.001 192.168.43.191 -> 192.168.43.1
[ 4026531957] wlp2s0 reply #20710.001 192.168.43.1 -> 192.168.43.191
可以看到,这种情况下走的是 WiFi 网卡,也没问题。
另外说点题外话:还记得刚开始只打印程序名的版本吗?如果在上面这种情况下执行,ICMP 请求打印的程序名会是 ping,而应答包打印的程序名会是 WiFi 驱动,因为是驱动发的应答包,至 少 Linux 上是如此。
最后还是拿我最喜欢的例子来做测试:ping 容器。之所以最喜欢并不是因为 Docker,而是 它展示了eBPF 的强大,就像给 ping 过程做了一次 X 射线检查。
# ping -4 172.17.0.2
[ 4026531957] docker0 request #17146.001 172.17.0.1 -> 172.17.0.2
[ 4026531957] vetha373ab6 request #17146.001 172.17.0.1 -> 172.17.0.2
[ 4026532258] eth0 request #17146.001 172.17.0.1 -> 172.17.0.2
[ 4026532258] eth0 reply #17146.001 172.17.0.2 -> 172.17.0.1
[ 4026531957] vetha373ab6 reply #17146.001 172.17.0.2 -> 172.17.0.1
[ 4026531957] docker0 reply #17146.001 172.17.0.2 -> 172.17.0.1
来点 ASCII 艺术,就变成:
Host netns | Container netns
+---------------------------+-----------------+
| docker0 ---> veth0e65931 ---> eth0 |
+---------------------------+-----------------+
4 结束语
在 eBPF/bcc 出现之前,要深入的排查和追踪很多网络问题,只能靠给内核打补丁。现在,我 们可以比较方便地用 eBPF/bcc 编写一些工具来完成这些事情。tracepoint 也很方便 ,提醒了我们可以在哪些地方进行探测,从而避免了去看繁杂的内核代码。即使是 kprobe 无法探测 的一些地方,例如一些内联函数和静态函数,eBPF/bcc 也可以探测。
本文的例子要添加对 IPv6 的支持也非常简单,就留给读者作为练习。
要使本文更加完善的话,需要对我们的程序做性能测试。但考虑到文章本身已经非常 长,这里就不做了。
对本文代码进行改进,然后用在跟踪路由和 iptables 判决,或是 ARP 包,也是很有意思的。 这将会把它变成一个完美的 X 射线跟踪器,对像我这样需要经常处理复杂网络问题的 人来说将非常有用。
完整的(包含 IPv6 支持)代码: https://github.com/yadutaf/tracepkt。
最后,我要感谢 @fcabestre帮我将这篇文章的草稿从 一个异常的硬盘上恢复出来,感谢 @bluxte的耐心审读, 以及技术上使得本文成为可能的 bcc 团队。