---
# System prepended metadata

title: 虛擬化-lab 2
tags: [清大虛擬化]

---

# 虛擬化-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` 進行監聽，只會出現連線建立和結束的封包訊息