eBPF本を読んだのでXDPとRust (Aya) に入門してPingに応答してみた

eBPF本で例として扱われていたPingへ応答するeBPFプログラムを、より低いレイヤ (XDP) でRust (Aya) で開発してみました。

## はじめに

eBPF本の和訳 ([入門 eBPF](https://www.oreilly.co.jp/books/9784814400560/)) が年末に発売されたので早速読んでみました。筆者はeBPF/XDPはおろかネットワークプログラミングやカーネル周りにもあまり精通していませんが、興味のあったネットワークパケットの高速処理の仕組みが1章まるごと割かれて説明されており、入門としてとても満足な内容でした。

:::note info
原著は[Isovalentのサイト](https://isovalent.com/books/learning-ebpf/)で (個人情報と引き換えに) 無料で手に入ります。
:::

[eBPF](https://ebpf.io/what-is-ebpf/)は、カーネルのソースコードに変更を加えたりカーネルモジュールをロードすることなく、自作のプログラムでカーネルの機能を拡張できる技術です。

eBPF本では、eBPFの利用シーンとしてネットワーク処理、オブザーバビリティ、セキュリティが主に挙げられており、ネットワーク処理の例の1つとしてPing (ICMP Echo) に応答するeBPFプログラムが紹介されていました。本記事は同じ題材について下記のアレンジを加えて入門してみます。

|  | eBPF本 | 本記事 |
|:-:|:-:|:-:|
| eBPFのプログラムタイプ  | tc  | xdp  |
| プログラミング言語  | Python (BCC) + C言語 | Rust (Aya) |

アレンジする理由はつまるところ筆者の興味ですが、詳細を説明していきます。

### Express Data Path (XDP)

プログラムタイプをXDPにしてみます。Express Data Path (XDP) とは、OSのネットワーキングスタックのほとんどをバイパスして高速にパケット処理ができるデータの経路のことです。

下図はLinuxカーネルのネットワーキングスタックの模式図です ([Wikipedia](https://en.wikipedia.org/wiki/File:Netfilter-packet-flow.svg)より)。図の最下部の「XDP_TX」の経路をみると、XDPのみで処理を行うとフロー上の大部分の処理をすっとばせることがわかり、とてもロマンを感じます。（今回のPingの例では、実際にパフォーマンスの差は感じられないだろうとも思います）

![xdp](https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg)

eBPF本の例では、Pingに応答するeBPFプログラムをtc (traffic control) のレイヤで動作させていました。下の図は[eBPF Summit 2023のeBayの講演](https://www.youtube.com/watch?v=nuXN5qSvDCw)のスライドの1つですが、XDPはtcより下のレイヤに位置することがわかります。

![Performance Analysis of XDP-native, XDP-generic, and TC eBPF hooks - Vinay Kulkarni 00-01-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121954/fe5003ff-7202-2305-abac-88f02690bf64.png)

eBayの講演や[eBPFのプログラム種別についてのCiliumのドキュメント](https://docs.cilium.io/en/stable/bpf/progtypes/#tc-traffic-control)によると、tcのプログラムではパケットが`sk_buff`というメタデータ付きの構造体として利用可能になります。XDPのプログラムでは`sk_buff`のメタデータが利用できない代わりに、`sk_buff`のバッファの確保やメタデータのパース処理を回避できるようです。

### Rust (Aya)

プログラミング言語はRustを使ってみます。eBPF本でRustでもeBPFプログラムが書けることが紹介されていた一方で、コード例も筆者のRustの経験もなく、なんとなく挑戦したかったためです。

RustでeBPFプログラムを開発するためのライブラリとして、[libbpf-rs](https://github.com/libbpf/libbpf-rs), [Redbpf](https://github.com/foniod/redbpf), [Aya](https://aya-rs.dev/), [Rust-bcc](https://github.com/rust-bpf/rust-bcc)が紹介されていましたが、eBPF本の書き味が高評価に見えたAyaを使ってみます。（名前が日本語っぽい響きですがどうなんだろうか）

[チュートリアル](https://aya-rs.dev/book/)がとても丁寧な一方でドキュメントの網羅性はあまりなく、本記事で行った開発もところどころソースコードを読みながら理解していく必要がありました。


## 「Pingを返す」とは
さっそくeBPF本のサンプルコード ([chapter8/network.bpf.c](https://github.com/lizrice/learning-ebpf/blob/main/chapter8/network.bpf.c)の`tc_pingpong()`) を読み解いて、やるべきことを整理してみます。下記はサンプルコードの抜粋に筆者が読み取った処理を日本語でコメントしたものです。


```c:network.bpf.c
int tc_pingpong(struct __sk_buff *skb) {
  bpf_trace_printk("[tc] ingress got packet");

  void *data = (void *)(long)skb->data;
  void *data_end = (void *)(long)skb->data_end;

  if (!is_icmp_ping_request(data, data_end)) {
    bpf_trace_printk("[tc] ingress not a ping request");
    return TC_ACT_OK;
  }
  // ^^^ (処理1) ICMPのEcho request以外の場合は、eBPFではなにもせず通過させる

  struct iphdr *iph = data + sizeof(struct ethhdr);
  struct icmphdr *icmp = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
  bpf_trace_printk("[tc] ICMP request for %x type %x\n", iph->daddr,
                   icmp->type);
  // ^^^ (処理2) IPやICMPのヘッダをパースして、内容をログ出力する

  swap_mac_addresses(skb);
  swap_ip_addresses(skb);
  // ^^^ (処理3) IPとMACのsource/destinationアドレスを入れ替える (=返信する)

  update_icmp_type(skb, 8, 0);
  // ^^^ (処理4) ICMPヘッダのtypeフィールドをEchoからEcho replyに書き換える
  
  // Redirecting the modified skb on the same interface to be transmitted
  // again
  bpf_clone_redirect(skb, skb->ifindex, 0);
  // ^^^ (処理5) Echo requestを受信したインターフェースからreplyを送信する

  // We modified the packet and redirected a clone of it, so drop this one
  return TC_ACT_SHOT;
}
```

読み取った処理を再掲します。

- (処理1) ICMPのEcho request以外の場合は、eBPFではなにもせず通過させる
- (処理2) IPやICMPのヘッダをパースして、内容をログ出力する
- (処理3) IPとMACのsource/destinationアドレスを入れ替える
- (処理4) ICMPヘッダのtypeフィールドをEchoからEcho replyに書き換える
- (処理5) Echo requestを受信したインターフェースからreplyを送信する

このプログラムはtc用のプログラムなのでパケットが`struct __sk_buff`の形をしていてそのままxdpに書き直すことは難しそうです。上記で分解した5つの処理を、自作のXDPのプログラムへ順番に実装していくことにします。

## RustとAyaの環境構築
### マシンとOS
Amazon Linux 2023のEC2インスタンス (t3.medium) で開発を行いました。

### Rustupのインストール
[The Book](https://doc.rust-lang.org/book/ch01-01-installation.html#installing-rustup-on-linux-or-macos)のとおりにやりました。

```sh
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
```

### Ayaの依存ライブラリのインストール
ところどころライブラリが足りず、gccとopenssl-develをdnfでインストールしてから[The Aya Book](https://aya-rs.dev/book/start/development/)の下記の手順を実施することで環境構築できました。

```sh
$ rustup install stable
$ rustup toolchain install nightly --component rust-src
$ cargo install bpf-linker
$ cargo install cargo-generate
```

## eBPFプログラムを開発する
### プロジェクトのひな形の作成
まずはAyaの新規プロジェクトを作ります。プログラムの種別はXDPを選択します。

```sh
$ cargo generate --name hello-aya -d program_type=xdp https://github.com/aya-rs/aya-template
```

下記のcrateを含んだCargoのworkspaceが生成されました。(GitHubのコミットは[こちら](https://github.com/mtannaan/hello-aya/tree/61f6646de4d9e85ba7dddb35e2d1d9c3532d9fa4))

- hello-aya: ユーザスペースのプログラム
- hello-aya-ebpf: eBPFのプログラム
- hello-aya-common: 両者に共通のライブラリ
- xtask: ビルドスクリプト

eBPFのプログラム (hello-aya-ebpf/src/main.rs) は下記のようになっており、パケットを受信するごとに`received a packet`とロギングすることがわかります。

```rust:hello-aya-ebpf/src/main.rs
#![no_std]
#![no_main]

use aya_bpf::{bindings::xdp_action, macros::xdp, programs::XdpContext};
use aya_log_ebpf::info;

#[xdp]
pub fn hello_aya(ctx: XdpContext) -> u32 {
    match try_hello_aya(ctx) {
        Ok(ret) => ret,
        Err(_) => xdp_action::XDP_ABORTED,
    }
}

fn try_hello_aya(ctx: XdpContext) -> Result<u32, u32> {
    info!(&ctx, "received a packet");
    Ok(xdp_action::XDP_PASS)
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}
```

ユーザスペースのプログラム (hello-aya/src/main.rs) は下記のようになっており、コンパイル済みのeBPFプログラムを読み込んでXDPプログラムとしてネットワークインターフェースにアタッチし、Ctrl-Cが押されるまで実行するものです。

```rust:hello-aya/src/main.rs
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let opt = Opt::parse();

    // (中略)

    #[cfg(debug_assertions)]
    let mut bpf = Bpf::load(include_bytes_aligned!("../../target/bpfel-unknown-none/debug/hello-aya"))?;
    #[cfg(not(debug_assertions))]
    let mut bpf = Bpf::load(include_bytes_aligned!("../../target/bpfel-unknown-none/release/hello-aya"))?;
    if let Err(e) = BpfLogger::init(&mut bpf) {
        // This can happen if you remove all log statements from your eBPF program.
        warn!("failed to initialize eBPF logger: {}", e);
    }
    let program: &mut Xdp = bpf.program_mut("hello_aya").unwrap().try_into()?;
    program.load()?;
    program.attach(&opt.iface, XdpFlags::default())
        .context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?;

    info!("Waiting for Ctrl-C...");
    signal::ctrl_c().await?;
    info!("Exiting...");

    Ok(())
}
```

さっそく実行してみます。

```sh
$ RUST_LOG=info cargo xtask run -- --iface ens5
Error: failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE
```

エラーで落ちました。エラーメッセージはSKB mode を指定することを提案していますが、前述の[eBayの講演](https://www.youtube.com/watch?v=nuXN5qSvDCw)によるとSKB mode (XDPのgeneric mode) は`sk_buff`を一度パースしてからXDPのデータ形式にさらに変換するためオーバーヘッドが比較的大きいそうです。したがってSKB modeには変更せず、エラーの原因を探ります。

カーネルから下記のエラーログが出ており、MTUが大きすぎることがわかりました。

```:journalctl -k
kernel: ena 0000:00:05.0 ens5: Failed to set xdp program, the current MTU (9001) is larger than the maximum allowed MTU (3498) while xdp is on
```

素直にMTUを小さくします。
```sh
$ sudo ip link set ens5 mtu 3498
```

再度実行すると今度は下記のエラーが出ました。

```:journalctl -k
kernel: ena 0000:00:05.0 ens5: Failed to set xdp program, the Rx/Tx channel count should be at most half of the maximum allowed channel count. The current queue count (2), the maximal queue count (2)
```

素直にチャネル数を最大値の半分に設定します。
```sh
$ ethtool -l ens5 
Channel parameters for ens5:
Pre-set maximums:
RX:             n/a
TX:             n/a
Other:          n/a
Combined:       2
Current hardware settings:
RX:             n/a
TX:             n/a
Other:          n/a
Combined:       2

$ sudo ethtool -L ens5 combined 1
```

再度プログラムを実行すると、大量の`received a packet`のログが確認でき、動作できていることがわかります。

```
$ RUST_LOG=info cargo xtask run -- --iface ens5
(略)
[INFO  hello_aya] Waiting for Ctrl-C...
[INFO  hello_aya] received a packet
[INFO  hello_aya] received a packet
(略)
```

:::note
先人の下記のサイトにXDPをEC2で実行するときの注意点がいい感じにまとまっています。エラーに対応する上で大変参考になりました。
https://zenn.dev/suicide_student/articles/2755385740fb2b
:::


### Pingをパースしてみる
The Aya Bookに[パケットをパースする例](https://aya-rs.dev/book/start/parsing-packets/)がありました。これに倣ってPingのパケット (ICMP Echo) を判別する処理を盛り込んでいきます。

> - (処理1) ICMPのEcho request以外の場合は、eBPFではなにもせず通過させる

パケットのヘッダのデータ構造は、[network_types](https://crates.io/crates/network-types)クレートに定義されている構造体を使ってパースします。

The Aya Bookで紹介されている下記の`ptr_at`関数を使うと、バッファオーバーラン防止のチェックをしつつ任意の構造体へのポインタを得ることができます。（チェックをしないとBPF検証器を通りません）
```rust
#[inline(always)]
fn ptr_at<T>(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> {
    let start = ctx.data();
    let end = ctx.data_end();
    let len = mem::size_of::<T>();

    if start + offset + len > end { return Err(()); }
    Ok((start + offset) as *const T)
}
```

Ethernet、IPv4、ICMPのヘッダをそれぞれパースし、ICMP Echo以外の場合でearly returnしていったところ、eBPFプログラムは下記のようになりました。(GitHubのコミットは[こちら](https://github.com/mtannaan/hello-aya/commit/ea8eb7cd7f930aa05e0a08b38c0aedd2b5e7d922))

```rust
fn try_hello_aya(ctx: XdpContext) -> Result<u32, ()> {
    let mut cursor = 0usize;

    // eth -> ip
    let ethhdr: *const EthHdr = ptr_at(&ctx, cursor)?; 
    cursor += EthHdr::LEN;
    if unsafe {(*ethhdr).ether_type} != EtherType::Ipv4 {
        return Ok(xdp_action::XDP_PASS)
    }

    // ip -> icmp
    let iphdr: *const Ipv4Hdr = ptr_at(&ctx, cursor)?;
    cursor += Ipv4Hdr::LEN;
    if unsafe {(*iphdr).proto} != IpProto::Icmp {
        return Ok(xdp_action::XDP_PASS)
    }

    // icmp -> echo request
    let icmphdr: *const IcmpHdr = ptr_at(&ctx, cursor)?;
    cursor += IcmpHdr::LEN;
    if unsafe {(*icmphdr).type_} != ICMP_TYPE_ECHO_REQUEST {
        return Ok(xdp_action::XDP_PASS)
    }

    info!(&ctx, "received an icmp echo request");
    Ok(xdp_action::XDP_PASS)
}
```

このプログラムを実行して、別インスタンスからPingを送ると、下記のようにPingの受信ログが出ました。
```
$ RUST_LOG=info cargo xtask run -- --iface ens5
(略)
[INFO  hello_aya] Waiting for Ctrl-C...
[INFO  hello_aya] received an icmp echo request
[INFO  hello_aya] received an icmp echo request
[INFO  hello_aya] received an icmp echo request
(略)
```

次に、受信した情報についてもう少し詳しい内容をロギングしてみます。

> - (処理2) IPやICMPのヘッダをパースして、内容をログ出力する

マルチバイトの整数が多く出現するので、演算する前に`u32::from_be`などを用いてnative endiannessに変換する必要がありました。

```rust
// info!(&ctx, "received an icmp echo request");
// の部分を下記に変更

    let icmp_echo_req = unsafe { (*icmphdr).un.echo };

    // log packet contents
    let orig_ip_src_addr = unsafe { (*iphdr).src_addr };
    let orig_ip_src_addr_native_endian = u32::from_be(orig_ip_src_addr);
    info!(
        &ctx,
        "received an icmp echo request src={}.{}.{}.{} ihl={} len={} ttl={} id={} seq={}",
        (orig_ip_src_addr_native_endian >> 24) & 0xff,
        (orig_ip_src_addr_native_endian >> 16) & 0xff,
        (orig_ip_src_addr_native_endian >> 8) & 0xff,
        (orig_ip_src_addr_native_endian >> 0) & 0xff,
        unsafe { (*iphdr).ihl() },
        u16::from_be(unsafe { (*iphdr).tot_len }),
        unsafe { (*iphdr).ttl },
        u16::from_be(icmp_echo_req.id),
        u16::from_be(icmp_echo_req.sequence),
    );
```

プログラムを実行した状態で別インスタンスからPingを送ると、下記のようにPingの受信ログが出ました。
```
$ RUST_LOG=info cargo xtask run -- --iface ens5
(略)
[INFO  hello_aya] Waiting for Ctrl-C...
[INFO  hello_aya] received an icmp echo request src=172.31.38.12 ihl=5 len=38 ttl=127 id=32 seq=1
[INFO  hello_aya] received an icmp echo request src=172.31.38.12 ihl=5 len=38 ttl=127 id=32 seq=2
(略)
```

### Pingに応答する
ついにPingに応答するときが来ました。GitHubのコミットは[こちら](https://github.com/mtannaan/hello-aya/commit/5318f0e3806d3b55eca69da15675dcf83bcdc7b7)です。

> - (処理3) IPとMACのsource/destinationアドレスを入れ替える

```rust
    // update ip
    unsafe {
        let orig_src_addr = (*iphdr).src_addr;
        (*iphdr).src_addr = (*iphdr).dst_addr;
        (*iphdr).dst_addr = orig_src_addr;
    }

    // update eth
    unsafe {
        let orig_src_addr = (*ethhdr).src_addr;
        (*ethhdr).src_addr = (*ethhdr).dst_addr;
        (*ethhdr).dst_addr = orig_src_addr;
    }
```

> - (処理4) ICMPヘッダのtypeフィールドをEchoからEcho replyに書き換える

```rust
    // update icmp
    unsafe {
        (*icmphdr).type_ = ICMP_TYPE_ECHO_REPLY; // == 0
    }
```

> - (処理5) Echo requestを受信したインターフェースからreplyを送信する
```diff_rust
-     Ok(xdp_action::XDP_PASS)
+     info!(&ctx, "sending an echo reply");
+     Ok(xdp_action::XDP_TX)
```

パケットの内容を更新するために、パケットへのポインタ (`ethhdr`, `iphdr`, `icmphdr`) はmutableにする必要がありました。

この状態でプログラムを実行して別のEC2からPingを送ってみます。`sending an echo reply`のログが出ていたのでBPFプログラムは応答を返しているつもりのようですが、Pingの送信元のインスタンスではタイムアウトになってしまいました。

```txt:送信元のping -Oの出力
64 bytes from 172.31.35.246: icmp_seq=3 ttl=127 time=0.320 ms
64 bytes from 172.31.35.246: icmp_seq=4 ttl=127 time=0.352 ms
<--- ここでBPFプログラムを起動
no answer yet for icmp_seq=5
no answer yet for icmp_seq=6
no answer yet for icmp_seq=7
no answer yet for icmp_seq=8
no answer yet for icmp_seq=9
<--- ここでBPFプログラムを終了
64 bytes from 172.31.35.246: icmp_seq=10 ttl=127 time=0.433 ms
64 bytes from 172.31.35.246: icmp_seq=11 ttl=127 time=0.365 ms
```

Ping送信元のインスタンスでpcapをとってみたところ、図の通りICMPのチェックサムが誤っていることがわかりました。確かに現状のプログラムでは、ICMPのtypeフィールドを変更しているのにチェックサムを更新できていませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121954/ca6d5e09-cba2-29b3-bb66-b14df7fae801.png)

### チェックサムを更新する

ICMPのチェックサムを更新する処理を盛り込みます。IPチェックサムの計算を行うBPFのヘルパ関数 `bpf_csum_diff` のバインディングをAyaが定義してくれていたので、これを活用してチェックサムの計算を行います。

[bpf-helpersのmanページ](https://man7.org/linux/man-pages/man7/bpf-helpers.7.html)によると、この関数はパケットにデータを追加する場合・除去する場合・diffをとる場合の3通りの使い方ができるようです。

> s64 bpf_csum_diff(__be32 *from, u32 from_size, __be32 *to, u32 to_size, __wsum seed)
> (略)
> This is flexible enough to be used in several ways:
> - With from_size == 0, to_size > 0 and seed set to checksum, it can be used when pushing new data.
> - With from_size > 0, to_size == 0 and seed set to checksum, it can be used when removing data from a packet.
> - With from_size > 0, to_size > 0 and seed set to 0, it can be used to compute a diff. Note that from_size and to_size do not need to be equal.

今回のEcho Request -> Echo Replyへのパケット上書きは、ICMPのヘッダ部分を下記のようにゼロクリアすることに相当します。

```
octet   0       1       2       3       
field   | Type  | Code  |   Checksum    |
request | 0x08  | 0x00  |   0x0000      |
reply   | 0x00  | 0x00  |   0x0000      |
```

このことを踏まえ、`bpf_csum_diff`の`from`引数に上書き前のICMPヘッダを指定し、`to_size`引数に0を指定することでデータを除去した場合のチェックサムを再計算します。(GitHubのコミットは[こちら](https://github.com/mtannaan/hello-aya/commit/aec9c66347e4e4185616a15afe9c531e8513eef5))

ICMPのチェックサムはICMPパケットの16ビットごとの[1の補数和](https://www.wdic.org/w/SCI/1%E3%81%AE%E8%A3%9C%E6%95%B0#1xE3x81xAExE8xA3x9CxE6x95xB0xE5x92x8C)を計算した後、その結果をビット反転することにより求められます。`bpf_csum_diff`はビット反転前のチェックサムを扱うため、`seed`には上書き前のチェックサムをビット反転したものを渡しています。

```rust
    // update icmp
    unsafe {
        let orig_csum = (*icmphdr).checksum;
        (*icmphdr).checksum = 0;

        // calculate checksum after clearing the current icmp type field
        let new_csum = fold_checksum(
            bpf_csum_diff(
                icmphdr as *mut u32, 4, 
                0 as *mut u32, 0,
                !orig_csum as u32
            )
        );

        (*icmphdr).type_ = ICMP_TYPE_ECHO_REPLY; // == 0
        (*icmphdr).checksum = new_csum;
    }
```

`bpf_csum_diff`は16ビットごとの足し込みのみを行ってくれるので、ICMPヘッダに設定できる状態にする (桁溢れを折り返してビット反転) 部分は自前で実装しました。

```rust
#[inline(always)]
fn fold_checksum(sum: i64) -> u16 {
    let mut csum = sum;
    csum = (csum & 0xffff) + (csum >> 16);
    csum = (csum & 0xffff) + (csum >> 16);
    csum = (csum & 0xffff) + (csum >> 16);
    csum = (csum & 0xffff) + (csum >> 16);
    return !csum as u16;
}
```

この状態でプログラムを実行したところ、Pingに応答が返ってきました！

```txt:送信元のping -Oの出力
PING 172.31.35.246 (172.31.35.246) 56(84) bytes of data.
64 bytes from 172.31.35.246: icmp_seq=1 ttl=127 time=0.257 ms
64 bytes from 172.31.35.246: icmp_seq=2 ttl=127 time=0.335 ms
...
```

Pingの応答がeBPFから返ってきているのかどうかがわかりにくくちょっと感動が少ないので、次の章ではpingコマンドの出力にあらわれるTTL値もeBPFで変えてみます。

### IPのTTL値を変えてみる
開発に使っていたインスタンスのTTL値のデフォルトは127だったので、適当に100を引いて27を設定します。IPヘッダを更新したので、チェックサムも計算しなおします。

```rust
        (*iphdr).ttl = 27;

        (*iphdr).check = 0;
        (*iphdr).check = fold_checksum(
            bpf_csum_diff(
                    0 as *mut u32, 0,
                    iphdr as *mut u32, Ipv4Hdr::LEN as u32,
                    0u32
                )
        );
```

この状態でプログラムを実行してPingを送ってみます。たしかにTTLが変わっていました！
```
64 bytes from 172.31.35.246: icmp_seq=1 ttl=127 time=0.343 ms
64 bytes from 172.31.35.246: icmp_seq=2 ttl=127 time=0.327 ms
64 bytes from 172.31.35.246: icmp_seq=3 ttl=127 time=0.314 ms
64 bytes from 172.31.35.246: icmp_seq=4 ttl=127 time=0.288 ms
<-- ここでBPFプログラムを起動
64 bytes from 172.31.35.246: icmp_seq=5 ttl=27 time=0.332 ms
64 bytes from 172.31.35.246: icmp_seq=6 ttl=27 time=0.372 ms
64 bytes from 172.31.35.246: icmp_seq=7 ttl=27 time=0.395 ms
<--- ここでBPFプログラムを終了
64 bytes from 172.31.35.246: icmp_seq=8 ttl=127 time=0.446 ms
64 bytes from 172.31.35.246: icmp_seq=9 ttl=127 time=0.315 ms
```

## まとめ
### 学んだこと

- XDPでパケットを読み込む (Ayaの`XdpContext`, eBPFの`xdp_md`)
- Rustの構造体へパケットをパースし、パケットを編集する (`XdpContext::data()`)
- XDPでパケットを送り返す (`xdp_action::XDP_TX`)
- BPFのヘルパ関数をAyaから呼び出す (`aya_bpf::helpers::bpf_csum_diff`)

### 微妙だったこと
- Rustを初めて使ってみたが、結局はunsafeなコードばかり書いていた
  - BPFプログラムのコードはunsafeが多くなる、とAyaのドキュメントも言っていた。ユーザスペースのコードの複雑度が上がってくるとRustの恩恵が強くなってきそう
- ケースによっては正しそうなコードでもBPF検証器に弾かれてしまった
  - 当初はICMPのチェックサムをICMPのデータ部含めて丸ごと計算しようとしたが、パケットの始点・終点のチェックをきちんと行っているつもりでも「始点がパケット外」の検証器エラーが出た。検証器ログに出力されるコードもコンパイラの最適化が適用されたあとのもののようで、Rustのコードをどう変更するとエラーの解消につながるかの見極めにかなり経験が必要に思えた

### やってみたいこと
- eBPF本にロードバランサを作る例があったので、これもアレンジして試してみたい


### おわりに

eBPF本のおかげで、eBPFやカーネルプログラミング未経験でもXDPでPingに応答することができました。

それではみなさんも良い1年 & eBPFライフを！