當前位置:
首頁 > 新聞 > 使用 Linux tracepoints,perf以及eBPF跟蹤網路數據包的流程

使用 Linux tracepoints,perf以及eBPF跟蹤網路數據包的流程

我尋找一個低級Linux網路調試工具已經有一段時間了, Linux允許使用虛擬介面和網路命名空間的組合在主機上直接運行複雜網路。當出現問題時,排除故障相當耗時。如果這是L3路由器的問題,可以使用mtr命令進行路由分析。但是,如果這是一個較低級別的問題,我通常會手動檢查每個介面、橋接、網路命名空間以及防火牆,並啟動幾個tcpdump,以便了解發生了什麼。這個過程是如此複雜,以至於我想要找到一個可以直接發現數據包的工具。

基本上,我需要的是L2的mtr。在本文最後,我將有一個簡單易 用的低級數據包跟蹤,如果你ping一個本地的Docker容器,它會顯示如下:

在這篇文章中,我將重點介紹2個跟蹤工具——perf和eBPF。

Perf 是用來進行軟體性能分析的工具,通過它,應用程序可以利用 PMU,tracepoint 和內核中的特殊計數器來進行性能統計。它不但可以分析指定應用程序的性能問題 (per thread),也可以用來分析內核的性能問題,當然也可以同時分析應用代碼和內核,從而全面理解應用程序中的性能瓶頸。最初的時候,它叫做 Performance counter。

eBPF是Linux內核最近的一個補充,顧名思義,這是一個名為「Berkeley Packet Filter」的BPF位元組碼的擴展版本,用於過濾BSD的數據包。eBPF一共有3大類指令:跳轉、運算、載入與存儲。除此之外,它還可以用於在活動內核中安全運行獨立於平台的代碼,例如,存儲器訪問在程序可以運行之前被驗證,並且必須能夠證明該程序將在有限的時間內結束。如果內核不能證明它,即使它是安全的並也將被拒絕。eBPF不但是程序,還可以訪問外部的數據,重要的是這個外部的數據可以在用戶空間管理。

這樣的程序可以用作QOS的網路分類器,作為eXpress數據平面(XDP)的一部分的非常低級別的網路和過濾,用於跟蹤代理。跟蹤探測器可以附加到/ proc / kallsyms或任何跟蹤點中導出其符號的任何函數。不過,在這篇文章中,我將只分析對跟蹤點的跟蹤代理。

實驗所需的配置

在本文所進行的試驗中,我需要使用一些工具來與perf和eBPF共同協調合作,比如bcc。bcc是一個強大而靈活的工具,是一個開源的 Linux 動態跟蹤工具,允許你將內核探測器作為受限制的C語言編寫,並使用Python將其設置在用戶界面中。

我將在本文重現Ubuntu 17.04(Zesty)的安裝說明,注意,將eBPF附加到跟蹤點至少需要Linux內核的版本要高於 4.7。

安裝perf的示意圖:

如果你看到一條錯誤消息,可能意味著你的內核最近已更新,但你尚未重新啟動。

安裝bcc的示意圖:

找到好的跟蹤點,也稱為用perf手動跟蹤數據包,目標是追蹤數據包所採用的路徑。根據交叉界面的不同,交叉的跟蹤點也可能不同。

為了找到合適的跟蹤點,我在perf trace下使用了2個內部和2個外部目標的ping:

1.本地主機IP為127.0.0.12.一個Docker容器,IP 172.17.0.23.我的手機通過USB網路共享IP 192.168.42.1294.我的手機通過WiFi與IP 192.168.43.1

perf trace是perf的子命令,它默認產生類似於strace的輸出。我可以很容易地調整它來隱藏系統調用,例如,將ping到具有IP 172.17.0.2的Docker容器,如下所示:

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,這看起來更可讀。

最後,在eth0上看到數據包之後,我會以相反的順序命中跟蹤點。

通過在4個目標場景中重複類似的過程,我可以選擇最適合的跟蹤點來跟蹤數據包。我選了4個跟蹤點:

net_dev_queuenetif_receive_skb_entrynetif_rxnapi_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 skbaddr=0xffff8e849cb8cd00 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)

如果你想進一步瀏覽可用的網路跟蹤點列表,你可以使用用戶列表:

sudo perf list net:*

這應該會返回一個跟蹤點名稱列表,如net:netif_rx。冒號之前的部分( : )是事件類別( net )。

用eBPF/bcc編寫自定義跟蹤器

如果你正在閱讀這篇文章,那麼讀到這裡,你就知道了如何在Linux虛擬機上追蹤數據包。

從Linux Kernel 4.7開始,eBPF程序可以附加到內核跟蹤點。在此之前,構建跟蹤器的唯一替代方法是將探測器附加到導出的內核符號。雖然這可以起作用,但它有一些缺點:

1.內核內部API不穩定。

2.出於性能原因,大多數網路內部函數是內聯或靜態的,這兩個都不能被探測。

3.找到這個函數的所有潛在的呼叫站點是非常耗時的,而且並不是所有需要的數據都可用。

讓我從一個簡單的hello world開始,每當我之前選擇的4個跟蹤點中的一個(net_dev_queue,netif_receive_skb_entry,netif_rx和napi_gro_receive_entry)被觸發,我都將構建一個事件。為了保持運行的簡單,我將發送comm程序,即基本上是程序名的16個字元串。

#include #include // Event structurestruct 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);}

將此片段附加到「net」類別的4個跟蹤點,載入skbaddr欄位並將其傳遞給僅載入程序名稱的公共部分。如果你想知道這個args-> skbaddr來自哪裡,那麼當你用TRACEPOINT_PROBE定義跟蹤點時,通過bcc生成args結構。由於它是在運行中生成的,所以沒有簡單的方法可以看到它的定義,但是有一個更好的方法,就是直接從內核查看數據源。幸運的是,每個跟蹤點都有一個/ sys / kernel / debug / tracing / events條目。例如,對於net:netif_rx,可以只輸出「cat」/ sys / kernel / debug / tracing / events / net / netif_rx / format。

name: netif_rxID: 1183format: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-8from socket import inet_ntopfrom bcc import BPFimport ctypes as ctbpf_text = TASK_COMM_LEN = 16 # linux/sched.hclass 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()

注意,在這個階段沒有過濾。

$> sudo python ./tracepkt.py...Just got a packet from ping6Just got a packet from ping6Just got a packet from pingJust got a packet from irq/46-iwlwifi...

在這種情況下,你可以看到我正在使用ping和ping6,而WiFi驅動程序也會收到一些數據包。於是,我開始添加一些有用的數據或過濾器。這將更好地展示eBPF的功能和局限性。

添加網路介面信息

首先,你可以安全地刪除「comm」欄位,載入和sched.h標頭。

其次,你可以包括net / inet_sock.h,以便有必要的聲明,並事件結構中添加char ifname [IFNAMSIZ]。

現在,我將從設備結構載入設備名稱。這是有趣的,因為這是一個在實踐中非常有用的信息,它能展示載入任何數據的技術:

// Get device pointer, we ll need it to get the name and network namespacestruct net_device *dev;bpf_probe_read(&dev, sizeof(skb->dev), ((char*)skb) + offsetof(typeof(*skb), dev));// Load interface namebpf_probe_read(&evt.ifname, IFNAMSIZ, dev->name);

在知道它的工作原理後,我就要載入介面名稱了,這就需要知道介面設備結構。我將從最後一個語句開始,因為它是最容易理解的。它使用bpf_probe_read從dev-> name讀取長度為IFNAMSIZ的數據,並將其複製到evt.ifname。

不過要注意的是,eBPF的目標是允許安全地編寫內核。這意味著禁止隨機存儲器訪問,必須驗證所有內存訪問。除非你在堆棧中訪問的內存,否則需要使用bpf_probe_read讀取訪問器。這使代碼讀寫變得相當麻煩,但也非常的安全。 bpf_probe_read是某種安全的memcpy版本,它在內核中的bpf_trace.c中定義。有趣的是如果發生錯誤,bpf_probe_read將返回一個初始化為0的緩衝區並返回一個錯誤,它不會崩潰或停止程序。

接下來,我將使用以下宏來幫助保持可讀性:

這允許我寫入:

member_read(&dev, skb, dev);

添加網路命名空間ID

命名空間標識符可以從2個位置載入:

1.套接字 sk 結構

2.設備 dev 結構

我最初使用的是socket結構,因為這是我在編寫solisten.py時使用的。不幸的是,我不知道為什麼一旦數據包跨越命名空間邊界,命名空間標識符就不再可讀了。讀出的欄位全為0,這是無效內存訪問的清晰標識。

幸運的是,方法仍然有效。通過調整不同的介面,來查找它所屬的命名空間的介面。

struct net* net;// Get netns id. Equivalent to: evt.netns = dev->nd_net.net->ns.inumpossible_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宏:

上圖就是你發送ping到Docker容器時應該看到的,數據包通過本地docker0網橋,然後移動到veth pair,跨越網路命名空間邊界。

跟蹤請求回復和返回的數據包

為了繼續從數據包中載入IP,我必須讀取IP報頭。我會在堅持用IPv4,但是同樣的邏輯也適用於IPv6。這個過程很複雜,由於我正在網路路徑中處理內核,所以某些數據包尚未打開。這意味著某些報頭偏移量仍未初始化,我必須把它們都計算所出來,從MAC頭到IP頭,最後到ICMP頭。

可以通過載入MAC頭地址輕輕地啟動,並推斷IP頭地址。我們不會載入MAC頭本身,而是假定它是14個位元組長。

// Compute MAC header addresschar* 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位,即第一個位元組的前半部分中解碼IP版本,並確保它是IPv4:

// Load IP protocol versionu8 ip_version;bpf_probe_read(&ip_version, sizeof(u8), ip_header_address);ip_version = ip_version >> 4 & 0xf;// Filter IPv4 packetsif (ip_version != 4) { return 0;}

現在載入完整的IP頭,得到IP,使Python信息更加有用,確保下一個頭是ICMP並導出ICMP頭部偏移量。

// Load IP Headerstruct iphdr iphdr;bpf_probe_read(&iphdr, sizeof(iphdr), ip_header_address);// Load protocol and addressu8 icmp_offset_from_ip_header = iphdr.ihl * 4;evt.saddr[0] = iphdr.saddr;evt.daddr[0] = iphdr.daddr;// Filter ICMP packetsif (iphdr.protocol != IPPROTO_ICMP) { return 0;}

最後,我可以載入ICMP頭本身,確保這是一個回應請求,並從中載入id和seq:

// Compute ICMP header address and load ICMP headerchar* 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 replyif (icmphdr.type != ICMP_ECHO && icmphdr.type != ICMP_ECHOREPLY) { return 0;}// Get ICMP infoevt.icmptype = icmphdr.type;evt.icmpid = icmphdr.un.echo.id;evt.icmpseq = icmphdr.un.echo.sequence;// Fix endianevt.icmpid = be16_to_cpu(evt.icmpid);evt.icmpseq = be16_to_cpu(evt.icmpseq);

如果要從特定的ping實例中過濾ICMP,可以假設evt.icmpid是ping的PID,至少使用Linux的ping。

使用一些簡單的Python來處理事件,就可以在幾種情況下進行測試。以root身份啟動程序,在另一終端啟動一些「ping」並觀察:

ICMP回應請求報文(ICMPEcho-Request)由進程20212(Linux ping上的ICMP id)發送到Loopback介面上,傳遞給同一個迴環介面,並生成返回應答,環回介面既是發射接收介面也是接收介面。

那WiFi網關呢?

在這種情況下,返回請求和返回應答會通過WiFi介面。請注意,當我只列印擁有數據包的進程的「comm」時,返回請求將屬於ping進程,而返回應答將屬於WiFi驅動程序,因為就Linux而言這是生成它的方式。

這是我最喜歡的方式,因為它能最好地展現eBPF的全部功能,並允許構建一個像ping的工具的「x-ray」,。

# 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

總結

eBPF/bcc使我能夠編寫一系列新的工具來深入排除故障,跟蹤和跟蹤以前無法訪問的問題,而無需修補內核。跟蹤點也非常方便,因為它們在有趣的地方提供了一個好的提示,消除了對繁瑣的內核代碼的仔細閱讀,並且可以被放置在代碼中,否則這些代碼將不能從kprobe訪問,比如內聯或靜態函數。要進一步利用,可以添加IPv6。

你可以在Github上看到完整的代碼(支持IPv6):https://github.com/yadutaf/tracepkt

點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 嘶吼RoarTalk 的精彩文章:

高危預警!移動設備安全面臨的5大新型威脅
GitHub 萬星推薦:黑客成長技術清單
一種劫持COM伺服器並繞過微軟反惡意軟體掃描介面的方法
傳韓國國家情報機構承認干涉2012年總統大選:組建30隻水軍操縱民意
深度:CVE-2017-8565分析和利用

TAG:嘶吼RoarTalk |

您可能感興趣

Linux系統BillGates botnet component查殺
SUSE Linux Enterprise High Performance Computing公測啟動
Skype的Snap安裝包發布,Microsoft Loves Linux
在 AppImage、Flathub 和 Snapcraft 平台上搜索 Linux 應用
Linux/Windows等Chrome獲Material Design更新
Linux之crontab 使用
Linux下使用Speedtest測試網速
乾貨詳解 Linux phantomJs install course
Android&Linux&Windows三平台通用實用程序推薦
科技圖鑑 Microsoft Loves Linux
WPS Office:Linux 上的 Microsoft Office 的免費替代品
談談對Linux的Huge Pages與Transparent Huge Pages的認識
從Linux到Windows的PowerShell遠程處理
Linux下C函數庫:glibc與newlibc
【linux】【retpoline】retpoline技術分析
Linux系統安裝Oracle,配置etc/security/limits.conf文件
Undo 發布用於 Linux 調試的 Live Recorder 5.0
通過 Docker 實現在 Linux 容器中運行 Microsoft SQL Server 資料庫
簡單了解dd、ext3grep、extundelete與linux數據恢復
如何在 Linux 上使用 snap 安裝 Spotify