前置文章:

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/

相關文章