# 虛擬化-lab 2 ## lab2 目標 撰寫改變網路行為的 eBPF 程式,了解 `bpftool` 的操作方式和 eBPF 物件的生命週期 1. 開發 XDP 程式: `ping` 使用 ICMP 協定,過濾 ICMP 網路封包,在進入核心網路協定層(network stack)前將封包丟棄 2. 開發 sockops 程式:透過 eBPF 在 socket 層進行轉傳,加速封包傳送 ## 目標一:XDP ### XDP 介紹 XDP 附著點在網卡的接收方向,會在進入核心網路協定層前進行處理,可以對封包進行檢查、修改、轉傳或丟棄。 #### XDP 回傳值 - **XDP_PASS**:以正常方式處理資料包。 - **XDP_DROP**:丟棄資料包。 - **XDP_TX**:將封包傳送回其到達的介面。 - **XDP_REDIRECT**:將封包轉送到另一個介面。 - **XDP_ABORTED**:丟棄資料包並發出警告。 ### ping 封包和交握說明 - 封包說明:以 ICMP 的封包來說, XDP 得到的封包從最外層開始,包含 ETH, IP 和 ICMP 。 ![image](https://hackmd.io/_uploads/Bk1n8CYb1l.png) - 交握說明: <img src="https://hackmd.io/_uploads/BJIVyxq-yx.png" height=450 style="display: block; margin: auto;"> ### 封包處理流程 `vmlinux.h` 定義各協定標頭的結構體 `struct ethhdr`, `struct iphdr`, `struct icmphdr`。協定的類型 `ETH_P_IP`, `IPPROTO_ICMP` 和 ICMP 的種類 `ICMP_ECHOREPLY` 則定義在 `bpf_tracing_net.h`。 ETH, IP 的部份,只需要檢查標頭中協定的類型,確認內層的協定屬於要處理的類型,繼續下一層協定的處理。接著作為被動方,XDP 要判斷 ICMP 封包類型,在接收到 `ICMP_ECHOREPLY` 的時候將其丟掉。 ### 示範程式碼 參數類別是 `struct xdp_md *` ,封包的記憶體位置紀錄在 `data`, `data_end` ,範圍是 $[\text{data, data_end})$ ,不包含 `data_end` ```clike #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h> #include "bpf_tracing_net.h" SEC("xdp") int drop_ping(struct xdp_md *ctx) { void *data = (void *)(__u64)ctx->data; void *data_end = (void *)(__u64)ctx->data_end; struct ethhdr *l2 = data; // 和 data_end 比較避免越界存取 ... return XDP_PASS; } ``` ### eBPF 物件的生命週期 eBPF 的程式和 map 使用引用計數(reference counting)進行資源管理。Pinning eBPF 物件, 引用 maps,附著程式(建立 links)可以增加引用計數。 - Pinning:建立一個 pseudofile - links:函式庫對「附著程式」的抽象 ### `bpftool` 操作介紹 用戶程式除了附著程式以外沒有其他功能時,可以使用 `bpftool` 用命令的方式操作,避免用戶程式重複開發。 #### 將程式載入核心 ``` shell $ bpftool prog load hello.bpf.o /sys/fs/bpf/hello ``` 此處,` bpftool prog load ` 指令會載入已編譯的 eBPF 程式,並將它綁定(pinning)到指定的路徑(在此範例中為 ` /sys/fs/bpf/hello `)。 #### 檢查已載入的程式 ``` shell $ ls /sys/fs/bpf hello ``` ``` shell $ bpftool prog list ... 540: xdp name hello tag d35b94b4c0c10efb gpl loaded_at 2022-08-02T17:39:47+0000 uid 0 xlated 96B jited 148B memlock 4096B map_ids 165,166 btf_id 254 ``` 輸出顯示了 id 為 540 的程式,類型為 xdp,名稱為 hello,並包含 id 為 165 和 166 的關聯 map。 可以使用這些欄位來查找特定的 eBPF 程式或 map,例如: - ` $ bpftool prog show id 540 ` - ` $ bpftool map show id 165 ` #### 附著到事件 程式類型必須與所附著的事件類型匹配。例如,可以將 XDP 程式附著到特定網路接口上的 XDP 事件: ``` shell $ bpftool net attach xdp id 540 dev eth0 ``` 查看所有附著到網路的 eBPF 程式: ``` shell $ bpftool net list xdp: eth0(2) driver id 540 tc: flow_dissector: ``` #### 分離與卸載程式 分離程式: ``` shell $ bpftool net detach xdp dev eth0 ``` 刪除 pinning: ``` shell $ rm /sys/fs/bpf/hello ``` #### 相關 API - `bpf_htons()` ### 目標一:練習項目 - 撰寫 XDP 核心程式 - 使用 `bpftool` 加載和附著程式 - 測試程式運作。在執行 `ping` 的過程附著,應從正常回應變成沒有回應 ## 目標二:sockops 透過本機通訊的封包,發送和接收的時候都經過核心網路協定層的處理。 eBPF 可以縮短封包傳遞路徑,避免花費非必要的成本。 ![sockops](https://hackmd.io/_uploads/rJKl_-q-ke.png) ### eBPF 程式類型介紹 需要兩個類別 eBPF 程式,程式會分為兩個檔案實作: - sockops :在多處觸發,透過 `struct bpf_sock_ops` 參數中 `ops` 的欄位判斷觸發情境。大致可分為兩種: - **進行配置修改**:如 `BPF_SOCK_OPS_TIMEOUT_INIT` - **TCP 狀態改變時,觸發 callback**:如 `BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB` 或 `BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB` - sk_msg :儲存在 map 中的 socket 發送封包時觸發,標註 socket 指定後續該轉傳到特定 socket 或維持正常操作。 ### 處理流程 ![tcp](https://hackmd.io/_uploads/SkZKVmcZJe.jpg) ### 示範程式碼 這次使用特殊的 map ,定義在 *bpf_sockmap.h* ,鍵的欄位紀錄來源和目標的資訊就足夠 *bpf_sockmap.h* ```clike #include "vmlinux.h" #include <bpf/bpf_endian.h> #include <bpf/bpf_helpers.h> struct sock_key { __u32 sip; __u32 dip; __u32 sport; __u32 dport; __u32 family; }; struct { __uint(type, BPF_MAP_TYPE_SOCKHASH); __uint(max_entries, 65535); __type(key, struct sock_key); __type(value, int); } sock_ops_map SEC(".maps"); ``` 在 sockops 的部分 ```clike #include "bpf_sockmap.h" char LICENSE[] SEC("license") = "Dual BSD/GPL"; SEC("sockops") int bpf_sockops_handler(struct bpf_sock_ops *skops){ // skops->op 判斷是否為 BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB/BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB // skops->remote_ip4, skops->local_ip4 判斷是否為 127.0.0.1 struct sock_key key = ... bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST); return BPF_OK; } ``` sk_msg 的部分 ```clike #include "bpf_sockmap.h" char LICENSE[] SEC("license") = "Dual BSD/GPL"; SEC("sk_msg") int bpf_redir(struct sk_msg_md *msg) { struct sock_key key = ... // 查詢 map ,找到的話會直接對 socket 標註 // 找到對應元素回傳 SK_PASS,否則回傳 SK_DROP bpf_msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS); // 回傳 SK_DROP 會丟掉封包, // 即使沒找到也希望能走正常傳輸路徑 // 所以一律回傳 SK_PASS return SK_PASS; } ``` ### `bpftool` 操作介紹 這個目標我們需要加載兩個程式,然後 sockops 附著到 cgroup, sk_msg 附著到 map #### 加載並重用 map 普通的 `bpftool prog load` 會自動創建 map ,如果需要重用的話,命令格式是: ```shell $ bpftool prog help usage: bpftool prog load OBJ PATH map name NAME MAP ... MAP := { id MAP_ID | name MAP_NAME } ``` #### 附著 sockops - `CGROUP` 使用 /sys/fs/cgroup/ - `ATTACH_TYPE` 使用 sock_ops ```shell $ bpftool cgroup attach CGROUP ATTACH_TYPE PROG ``` #### 附著 sk_msg - `ATTACH_TYPE` 使用 msg_verdict ```shell $ bpftool prog attach PROG ATTACH_TYPE MAP ``` #### 分離與卸載程式 分離程式: ```shell $ bpftool prog detach PROG ATTACH_TYPE MAP ``` ```shell $ bpftool cgroup detach CGROUP ATTACH_TYPE PROG ``` 刪除相關 pinning ### 目標二:練習項目 - 撰寫 sockops、sk_msg 核心程式 - 使用 `bpftool` 加載和附著程式 - 測試程式運作。用 `tcpdump` 進行監聽,只會出現連線建立和結束的封包訊息