こんにちは.学生生活も後少しとなってしまいました.悲しいです. 今回はxdpcapというツールについてです.xdpcapの使用に関する資料が日本語では非常に少なかったので使い方を紹介します.

# cloudflare/xdpcap

cloudflare/xdpcap (opens new window)はeXpress Data Path(XDP)を利用したtcpdumpのように使えるツールです. xdpcapはtcpdumpで使用するのと同様のフィルタリングルールを使用してパケットをキャプチャしたり,pcapファイルにダンプすることができます.

xdpで編集したパケットはtcpdumpでは見えなくなるのでちゃんとパケットの組み立てができているかわかりません. このツールの嬉しさはxdpで編集後のパケットをtcpdumpと同じようにキャプチャできることです.

# tcpdumpとの違い

tcpdumpは内部でパケットをフィルタリングする際にcBPFを利用しますが,xdpcapはxdpを使用します. xdpcapでキャプチャしたパケットはxdpプログラムによって変更が加えられた後のパケットです. また,xdp action codeも同時にキャプチャしてくれます. xdp action codeには以下のようなものがあります.

  • XDP_PASS
  • XDP_DROP
  • XDP_ABORTED
  • XDP_TX
  • XDP_REDIRECT

# 手法

xdpcapではフィルタリングするためにxdpcapが使えるebpfマップをフックしてあげる必要があります. このフックはeBPFのtail callを使用して実現されています. tail callはbpfプログラムから別のbpfプログラムを呼び出すための機能です. 詳しくはcilium document: tail calls (opens new window)を参照してください. tail callを行うためにはBPF_MAP_TYPE_PROG_ARRAYというタイプのbpf mapが必要です.さらにbpf_tail_call()というヘルパー関数が用意されている必要があります. xdpcapはこのtail callを利用してユーザーが定義したxdpプログラムがリターンする際のコンテキストを引き継いでxdpcapのbpfプログラムによりキャプチャされます. 詳しくはxdpcap: XDP Packet Capture (opens new window)を参照してください.

# 使い方

今回作成したプログラムはterassyi/xdpcap-with-cilium (opens new window)にあります.

# 環境

本プログラムの動作環境は以下です.

  • Linux ip-172-31-20-186 5.11.0-1019-aws #20~20.04.1-Ubuntu SMP Tue Sep 21 10:40:39 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
  • go 1.16
  • clang version 10.0.0-4ubuntu1

# 前準備

まずxdpcapをインストールします. xdpcapを使用するためにはbpffsがマウントされている必要があります. マウントは以下のコマンドで行えます.

$ sudo mount -t bpf none /sys/fs/bpf
1

# 実行

  1. makeコマンドでビルドします.
    $ make
    
    1
  2. ビルドされた実行ファイルを以下のように実行します.
    $ sudo ./xdpcap-with-cilium -iface <interface>
    
    1
  3. 別ターミナルで以下のコマンドを実行することでパケットのキャプチャとファイルへの保存を行うことができます.
    $ sudo xdpcap /sys/fs/bpf/xdpcap <pcap file> "filter rules"
    
    1
    sudo xdpcap /sys/fs/bpf/xdpcap - "filter rules" | sudo tcpdump -r -
    
    1

# 実装

# XDP Program

bpf配下にxdp用のプログラムがあります. ディレクトリの構成は以下です.

bpf
├── header
│   ├── bpf.h
│   └── bpf_helpers.h
├── hook.h
└── prog.c
1
2
3
4
5
6

bpf/header配下にあるbpf.h, bpf_helpers.hdropbox/goebpf (opens new window)から借用しています.必要なものが定義されているならどれでも構いません. bpf/hook.hにxdpcapのための関数が定義されています. 内容はフック用のPROG_ARRAYのマップの定義とtail_callをラップしたxdpcap_exit()関数のみなのでprog.cにコピペしてもかまいません. prog.cでも特に何もしていません.ただXDP_PASSでパケットをパスしますがxdpcap_exit()を呼び出すことでxdpcapのbpfプログラムをtail callします.

#include "hook.h"
#include "bpf_helpers.h"
BPF_MAP_DEF(xdpcap_hook) = {
	.map_type = BPF_MAP_TYPE_PROG_ARRAY,
	.key_size = sizeof(int),
	.value_size = sizeof(int),
	.max_entries = 5,
};
BPF_MAP_ADD(xdpcap_hook);
SEC("xdp")
int prog(struct xdp_md *ctx) {
	return xdpcap_exit(ctx, &xdpcap_hook, XDP_PASS);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Go program

今回はxdpのフロントとしてcilium/ebpf (opens new window)を使用しています.

ciliumにはbpf2goというパッケージがあってbpf2go経由でbpfプログラムをビルドすることでbpfプログラムをGo側でロードするためのインターフェースとなるGoのコードを自動生成してくれます. 自動生成されたコードのなかにコンパイルしたbpfのバイトコードをバイトスライスとして保存しているためGoのコードをビルドするとあとはシングルバイナリで動作します. main.goに以下の行を追加することでgo generateによりコードを自動生成できます.

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang XdpcapProg ./bpf/prog.c -- -I./bpf/header
1

続いてbpfプログラムのロードとアタッチ,bpfマップのピンです. Collectという型を定義してbpfプログラムとマップをGo側で保持するための構造体を作ります.

type Collect struct {
	XdpProg *ebpf.Program `ebpf:"prog"`
	XdpcapHook *ebpf.Map `ebpf:"xdpcap_hook"`
}
1
2
3
4

そしてbpf2goにより自動生成される関数を使用してbpfプログラムをロードします.

spec, err := LoadXdpcapProg()
if err != nil {
	panic(err)
}
if err := spec.LoadAndAssign(collect, nil); err != nil {
	panic(err)
}
if err := netlink.LinkSetXdpFd(link, collect.XdpProg.FD()); err != nil {
	panic(err)
}
1
2
3
4
5
6
7
8
9
10

その後,bpfマップのbpffsへのピンを行います.

tmpDir := "/sys/fs/bpf/xdpcap"
if err := collect.XdpcapHook.Pin(tmpDir); err != nil {
	panic(err)
}
1
2
3
4

実装は以上となります.ほぼ何もしないので実装は簡単です.

# まとめ

今回はxdpcapというxdpを利用したパケットキャプチャツールの使い方を紹介しました. xdpを使うときは編集したパケットをのぞくのが難しいので使えると便利です. xdp関連の話題は日本語の資料がないので参考になれば幸いです.

# 参考資料

# cloudflare/xdpcap

# cilium/ebpf