eBPF內核探測將任意系統調用轉換成事件
前置文章:
eBPF簡介、安裝和簡單示例:
http://kerneltravel.net/blog/2020/ebpf_ljr_no1/
獲取內核網絡中的SOCKET信息:
http://kerneltravel.net/blog/2020/ebpf_ljr_no2/
程序採集系統相關參數時,通常有兩種方式。一種是程序主動去輪詢,檢查系統變化,即 poll 模型;另一種是系統主動通知程序,即 push 模型。使用 poll 模型還是 push 模型取決於具體的問題。通常情況下,如果事件頻率相對於事件處理時間來說比較低,那 push 模型比較合適;如果事件頻率很高,就採用 pull 模型。例如,通常的網絡驅動會等待網卡事件,而 dpdk 這樣的框架會主動 poll 網卡, 以獲得最高的吞吐性能和最低的延遲。理想情況下,我們需要一個通用的方式處理事件。
Linux 4.4 以上內核基於 eBPF 可以將任何內核函數調用轉換成可帶任何數據的用戶空間事件。
本文將使用 bcc工具抓取內核網絡中的數據,包括抓取 backlog 信息、 IP 地址、端口號和網絡命名空間信息等。修改上一篇文章的代碼並使用bpf_probe_read讀取到相應變量的地址,使用 perf 使得 bpf_trace_printk 帶三個以上參數,獲取IP信息,端口、backlog 信息和網絡命名空間。
上回,我們說到 bpf_trace_printk
帶的參數太多了,會出現 error: <unknown>:0:0: in function kprobe__inet_listen i32 (%struct.pt_regs*): too many args to 0x55a83e8f8320: i64 = Constant<6>
這樣的錯誤,這是 BPF 的限制。解決這個問題的辦法就是使用 perf,它支持傳遞任意大小的結構體到用戶空間。
我們對比原來的代碼進行修改,原代碼如下:
from bcc import BPF # Hello BPF Program bpf_text = """ #include <net/inet_sock.h> #include <bcc/proto.h> #include <net/sock.h> // 1. Attach kprobe to "inet_listen" int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog) { // cast types. Intermediate cast not needed, kept for readability struct sock *sk = sock->sk; struct inet_sock *inet = (struct inet_sock *)sk; // Create an populate the variable u32 netns = 0; // Read the netns inode number, like /proc does netns = sk->__sk_common.skc_net.net->ns.inum; bpf_trace_printk("Listening on %x %d with %d pending connections in container %d \\n", inet->inet_rcv_saddr, inet->inet_sport, backlog, netns); return 0; }; """ # 2. Build and Inject program b = BPF(text=bpf_text) # 3. Print debug output while True: print b.trace_readline()
運行時會出現如下錯誤:
要使用 perf,我們需要:
-
定義一個結構體
-
聲明一個事件
-
推送(push)事件
-
Python 端再定義一遍這個事件(將來這一步就不需要了)
-
消費並格式化輸出事件
爲了使內核檢測器驗證這個程序的內存訪問是合法的,我們讓內存訪問變得更加顯式,使用受信任的 bpf_probe_read
函數,可以用它讀取任何內存地址。
爲了使得程序可以正常運行, bpf_trace_printk
先使用三個參數,只獲取IP信息,端口和 backlog 信息,將程序改爲:
from bcc import BPF # BPF Program bpf_text = """ #include <net/sock.h> #include <net/inet_sock.h> #include <bcc/proto.h> // Send an event for each IPv4 listen with PID, bound address and port int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog) { // Cast types. Intermediate cast not needed, kept for readability struct sock *sk = sock->sk; struct inet_sock *inet = inet_sk(sk); // Working values. You *need* to initialize them to give them "life" on the stack and use them afterward u32 laddr = 0; u16 lport = 0; // Pull in details. As 'inet_sk' is internally a type cast, we need to use 'bpf_probe_read' // read: load into 'laddr' 'sizeof(laddr)' bytes from address 'inet->inet_rcv_saddr' bpf_probe_read(&laddr, sizeof(laddr), &(inet->inet_rcv_saddr)); bpf_probe_read(&lport, sizeof(lport), &(inet->inet_sport)); // Push event bpf_trace_printk("Listening on %x %d with %d pending connections\\n", ntohl(laddr), ntohs(lport), backlog); return 0; }; """ # Build and Inject BPF b = BPF(text=bpf_text) # Print debug output while True: print b.trace_readline()
運行程序,在另一終端使用nc小工具建立單連接,結果如下:
可以看到,我們已經成功地使用 bpf_probe_read
讀取到了相應變量的地址,獲取了IP信息,端口和 backlog 信息。接下來我們使用perf,修改程序,使得 bpf_trace_printk
帶的參數超過三個。
在bcc程序的c程序中加入以下代碼:
struct listen_evt_t { u64 laddr; u64 lport; u64 netns; u64 backlog; }; BPF_PERF_OUTPUT(listen_evt);
在 kprobe__inet_listen
函數中使用以下代碼代替 bpf_trace_printk
:
struct listen_evt_t evt = { .laddr = ntohl(laddr), .lport = ntohs(lport), .netns = netns, .backlog = backlog, }; listen_evt.perf_submit(ctx, &evt, sizeof(evt));
在python中加入以下代碼:
# We need ctypes to parse the event structure import ctypes # Declare data format class ListenEvt(ctypes.Structure): _fields_ = [ ("laddr", ctypes.c_ulonglong), ("lport", ctypes.c_ulonglong), ("netns", ctypes.c_ulonglong), ("backlog", ctypes.c_ulonglong), ] # Declare event printer def print_event(cpu, data, size): event = ctypes.cast(data, ctypes.POINTER(ListenEvt)).contents print("Listening on %x %d with %d about %d" % ( event.laddr, event.lport, event.backlog, event.netns, ))
使用以下代碼代替python中的循環輸出:
b["listen_evt"].open_perf_buffer(print_event) while True: b.kprobe_poll()
最終修改後的代碼如下:
from bcc import BPF # We need ctypes to parse the event structure import ctypes # Declare data format class ListenEvt(ctypes.Structure): _fields_ = [ ("laddr", ctypes.c_ulonglong), ("lport", ctypes.c_ulonglong), ("netns", ctypes.c_ulonglong), ("backlog", ctypes.c_ulonglong), ] # Declare event printer def print_event(cpu, data, size): event = ctypes.cast(data, ctypes.POINTER(ListenEvt)).contents print("Listening on %x %d with %d about process %d" % ( event.laddr, event.lport, event.backlog, event.netns, )) # BPF Program bpf_text = """ #include <net/sock.h> #include <net/inet_sock.h> #include <bcc/proto.h> struct listen_evt_t { u64 laddr; u64 lport; u64 netns; u64 backlog; }; BPF_PERF_OUTPUT(listen_evt); // Send an event for each IPv4 listen with PID, bound address and port int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog) { // Cast types. Intermediate cast not needed, kept for readability struct sock *sk = sock->sk; struct inet_sock *inet = inet_sk(sk); // Working values. You *need* to initialize them to give them "life" on the stack and use them afterward u32 laddr = 0; u16 lport = 0; // Create an populate the variable u32 netns = 0; // Read the netns inode number, like /proc does netns = sk->__sk_common.skc_net.net->ns.inum; // Pull in details. As 'inet_sk' is internally a type cast, we need to use 'bpf_probe_read' // read: load into 'laddr' 'sizeof(laddr)' bytes from address 'inet->inet_rcv_saddr' bpf_probe_read(&laddr, sizeof(laddr), &(inet->inet_rcv_saddr)); bpf_probe_read(&lport, sizeof(lport), &(inet->inet_sport)); // Push event struct listen_evt_t evt = { .laddr = ntohl(laddr), .lport = ntohs(lport), .netns = netns, .backlog = backlog, }; listen_evt.perf_submit(ctx, &evt, sizeof(evt)); //bpf_trace_printk("Listening on %x %d with %d pending connections\\n", ntohl(laddr), ntohs(lport), backlog); return 0; }; """ # Build and Inject BPF b = BPF(text=bpf_text) # Print debug output b["listen_evt"].open_perf_buffer(print_event) while True: b.kprobe_poll()
運行程序,在另一終端使用nc小工具建立單連接:
程序結果如下:
可以看到,我們已經成功地使用 bpf_probe_read
讀取到了相應變量的地址,使用perf使得 bpf_trace_printk
帶了四個參數,獲取了IP信息,端口、backlog 信息和網絡命名空間。使用了 eBPF 將內核的函數調用轉換成事件觸發的方式。
參考鏈接:
https://blog.yadutaf.fr/2016/03/30/turn-any-syscall-into-event-introducing-ebpf-kernel-probes/