[轉帖][譯] 使用 Linux tracepoint、perf 和 eBPF 跟蹤數據包 (2017)

http://arthurchiao.art/blog/trace-packet-with-tracepoint-perf-ebpf-zh/

 

譯者序

本文翻譯自 2017 年的一篇英文博客 Tracing a packet’s journey using Linux tracepoints, perf and eBPF ,並添加了章節號以方便閱讀。

由於譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。

以下是譯文。



一段時間以來,我一直在尋找 Linux 上的底層網絡調試(debug)工具。

Linux 允許在主機上用虛擬網卡(virtual interface)和網絡命名空間(network namespace)構建複雜的網絡。但出現故障時,排障(troubleshooting)相當痛苦。如果是 3 層路由問題,mtr 可以排上用場;但如果是更底層的問題,通常只能手動檢查每個網 卡/網橋/網絡命名空間/iptables 規則,用 tcpdump 抓一些包,以確定到底是什麼狀況。 如果不瞭解故障之前的網絡配置,那排障時的感覺就像在走迷宮。

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 的縮寫。從名字可以看出,它 是 BPF(Berkeley Packet Filter)字節碼過濾器的增強版,後者是 BSD family 的網絡包 過濾工具。在 Linux 上,eBPF 可以在運行中的內核(live kernel)中安全地執行任何平 臺無關(platform independent)代碼,只要這些代碼滿足一些安全前提。例如,在程序執 行之前必須驗證內存訪問合法性,而且要能證明程序會在有限時間內退出。如果內核無法驗 證這些條件,那即使 eBPF 代碼是安全的並且確定會退出,它也仍然會被拒絕。

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):

  1. localhost,IP 127.0.0.1
  2. 一個乾淨的容器,IP 172.17.0.2
  3. 我的手機,通過 USB 連接,IP 192.168.42.129
  4. 我的手機,通過 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 保持不變,說明沒有 copy。copy 非常耗時。

其次,我們可以清楚地看到 packet 在內核的傳輸路徑:

  1. docker0 網橋
  2. veth pair 的宿主機端(veth79215ff)
  3. veth pair 的容器端(容器裏的 eth0
  4. 接下來是相反的返回路徑

至此,雖然我們還沒有看到網絡命名空間,但已經得到了一個不錯的全局視圖。

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)。後者雖然可以完成工作,但存在很多不足:

  1. 內核的內部(internal)API 不穩定
  2. 出於性能考慮,大部分網絡相關的內層函數(inner functions)都是內聯或者靜態的( inlined or static),兩者都不可探測
  3. 找出調用某個函數的所有地方是相當乏味的,有時所需的字段數據不全具備

這篇博客的早期版本使用了 kprobes,但結果並不是太好。 現在,誠實地說,通過內核 tracepoints 訪問數據比通過 kprobe 要更加乏味。我儘量保 持本文簡潔,如果你想了解本文稍老的版本,可以訪問這裏英文 中文翻譯

3.2 安裝

我不是一個徒手彙編迷(fans of handwritten assembly),因此接下來將使用 bccbcc 是一個靈活強大的工具,允許用受限的 C 語法(restricted C)寫內核探測代碼,然後用 Python 在用戶態做控制。這種方式對於生產環境算是重量級,但對開發來說非常完美。

注意: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 字段(16 字節)發送出來(到用戶空間程序),這個字段裏存的是發 送相應的網絡包的程序的名字。

#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 結構體中訪問 device name 字段。這裏開始展示出 eBPF 代碼的 強大之處:我們可以訪問任何受控範圍內的字段。

// 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 中:

  1. 它和 memcpy 類似,因此注意內存拷貝的代價
  2. 如果遇到錯誤,它會返回一個錯誤和一個初始化爲 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

採集網絡命名空間信息非常有用,但是實現起來要複雜一些。原理上可以從兩個地方訪問:

  1. socket 結構體 sk
  2. 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 的相應字段)加載 MAC 頭長 度信息,而假設它就是固定的 14 字節。

// 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 處開始。 現在可以解析 IP 頭第一個字節的前 4 個 bit 了:

// 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 就是相應 ping 進程的進程號, 至少 Linux 上如此。

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 團隊。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章