Slide 1

Slide 1 text

Perlで始めるeBPF: 自作Loaderの作り方 2024/10/05 Takeru Hayasaka YAPC::Hakodate 2024

Slide 2

Slide 2 text

本発表の狙い なお、本資料で扱う技術には、国立研究開発法人情報通信研究機構(NICT)の助成事業(社会実装・海 外展開志向型戦略的プログラム)(JPJ012368G51001)により得られた成果が含まれています。 1.eBPFの基礎を説明することで仕組みとユースケースを押さえる 2.Perlで実装したeBPF Loaderを通じ、仕組みを深掘りする

Slide 3

Slide 3 text

Agenda ● eBPFの基礎 ○ 概要と歴史 ○ 動作の仕組みとアーキテクチャ ● PerlでeBPF Loaderを自作する ○ 解説 ○ デモ ■ オブザーバービリティの例: kprobe ■ パケット処理の例: XDP ● まとめと今後の展望

Slide 4

Slide 4 text

この発表で話すこと・話さないこと - 🙆eBPFに主軸をおいて話します - 簡単な仕組み - ユースケース - Perlでローダーを作るって何するんですか?みたいな話 - 🙅BTFに関しては話しません - 🙅kprobeの仕組みなどについても話しません - 🙅Netlinkについての仕組みについても話しません - 🙅XDPとかのパケット処理の仕組みも(あまり)話しません

Slide 5

Slide 5 text

自己紹介 ● 早坂 彪流 (Hayasaka Takeru|@takemioIO) ● さくらインターネット に所属 現在は BBSakura Networksへ出向中 ○ 前職はゲーム会社でゲーム機のファームを書いていた ○ モバイルコアの研究開発・運用に従事 ■ eBPF を使った開発を業務でやってます(パケット処理) ● 一言: 発表当日の10/5は自分の26歳の誕生日です。 裏番組でやってるU25支援にギリギリ応募できなかった... ○ 社会人4年目はもう若者ではないのかもしれない ...orz ● YAPC初参加です ○ 学生支援が通ってたけどコロナ時代で無くなってました ...(YAPC kyoto…) ● なお、昨日初めてCPANにuploadするぐらいにはPerl初心者 ○ CPANアカウントは TAKEMIO でやってます

Slide 6

Slide 6 text

PR: eBPF Japan Meetup 第二回やります - 正式名:Cloud Native Community Japan - eBPF Japan Meetup #2 - eBPF ユーザー会みたいなものを CNCFでやってたりしてます - 次回第二回が12/6に さくらインターネット東京支社で 開催予定です(会場スポンサーをしてます - eBPFに興味が出てきました! みたいな人がいたらぜひご参加ください cf. https://ebpf.connpass.com/

Slide 7

Slide 7 text

eBPF の基礎

Slide 8

Slide 8 text

概要と歴史

Slide 9

Slide 9 text

eBPF(extended Berkeley Packet Filter)とは? - Linuxカーネルに対する拡張を楽に書いて、動的にロードさせる仕組み - つまりカーネル空間で動作する拡張プログラムを用意できる仕組み - 言語仕様としてはRISC型仮想マシンとして表現される - 主に以下の用途で利用される - セキュリティ: seccomp, LSM… - オブザーバビリティ: kprobe, uprobe, retprobe, tracepoint, fentry… - ネットワーク: XDP, TC, TCP-BPF, cgroup_skb… - その他: CPUスケジューラー … cf. https://ebpf.io/what-is-ebpf/ 誤解を恐れずにいうと, eBPFを使えば ● 手軽にkernelの拡張ができる ● Kernel内部で実行された関数の結果を ほぼ何でも取れる ● 100GbEを超える速度のパケット処理 が手軽にできるようになる と言ったことが出来る嬉しさがある

Slide 10

Slide 10 text

プロダクトユースケース - セキュリティ - Isovalent, Cisco/ Tetragon(Podセキュリティイベント監視、ContainerRuntimeポリシー...) - Sysdig/ Falco(Teragonと同様にセキュリティ監視) - オブザーバビリティ - Isovalent, Cisco/ Hubble(Metrics, Service map, flow log…) - ネットワーク - Isovalent, Cisco / Cilium(KubernetesのCNIやセキュリティ) - Meta / Katran(L4Loadbrancer) - Cloudflare / Gatebot(DDoS Mitigation), Unimog(L4Loadbrancer) - LINEヤフー / L4LB, SRv6 - MIXI / StaticNAT, PayloadCutterなど - さくらインターネット&BBSakura Networks /パケット交換機(PGW-U) - 弊社でも使ってます:)

Slide 11

Slide 11 text

- Linuxにはカーネルモジュールと呼ばれる機能がある - これを使えばカーネル空間で動く独自の拡張を書くことができる - eBPFで実装できる機能は基本的はカーネルモジュールで作れる機能と同じ - しかし以下の点が異なる - 1. 安全性: Verifier による検証でプログラマのミスを未然に防ぐ - メモリアクセス違反でカーネルがハングすることを防いだり、 メモリリークや無限ループに対する事前検証をVerifierが行ってくれる - 2. 後方互換性: カーネルモジュールが後方互換性を保証しないのに対し、 eBPFはAPI Interface経由で動作するため後方互換性が保証される - 更に仮想マシンなのでCPUアーキテクチャに依存しない、ポータビリティもある - なので頑張ってLinuxのアップストリームに入れる必要もない - カーネルメンテナと議論する必要もなくなり、開発アジリティも改善する KernelModule(従来の仕組み)と何が違う?

Slide 12

Slide 12 text

つまりKernelModule(従来の仕組み)と比べると - 別にeBPFで技術的にできることが増えた訳では無い󰢄 - eBPFを使って実装すると、カーネル開発へのアクセスが良くなる󰢐 - システム上でセキュリティ的にも担保され、後方互換も担保されてる - 開発者もユーザーも(ある程度)保証されたプロダクトが手に入る - アップストリームに入れなくていいので早く機能が手に入る󰢐

Slide 13

Slide 13 text

歴史:最初期 - 1992: 「The BSD Packet Filter: A New Architecture for User-level Packet Capture」のパケットキャプチャを効率化するアイディアが始まり - classic BPF (cBPF)と呼ばれ、LinuxにおいてLinuxSocketFilter(LSF)という 名前になって入れられている。libpcap(tcpdump)での利用が知られている。 $ sudo tcpdump -d "ip proto \tcp" (000) ldh [12] ; read EtherType (001) jeq #0x800 jt 2 jf 5 ; if (is_ipv4){goto 2}else{goto 5} (002) ldb [23] ; read IPv4Proto (003) jeq #0x6 jt 4 jf 5 ; if(is_tcp){goto 4}else{goto 5} (004) ret #262144 ; パケットキャプチャするようにして exit (005) ret #0 ; 何もせず exit tcpdumpで利用される、cBPFアセンブリの例 IPv4で尚且つTCPのパケットをフィルタできるバイトコードを示している

Slide 14

Slide 14 text

- 2013: 「[PATCH net-next] extended BPF」をAlexei StarovoitovがLKML に投稿してBPFの拡張を提案する - この段階でパケット処理やSeccompなど汎用的な仕組みを目指していたことがMLから読める - Alexei自身は、IOVisorの元となってるPLUMgridの開発者で、 後にXDPの初期パッチを投稿していたりしてる - cf. PLUMgridによるLinuxへの開発の様子(主にeBPFとNetwork関連に力を入れてる) - cf. 最初期のXDPのパッチ(Jul 2016) - 2024現在、約32年の時を経て、BPFは拡張されまくった結果 パケットフィルタに留まらない、いろいろ便利な機能として育った 歴史:約20年後、eBPFが生まれる

Slide 15

Slide 15 text

eBPFの仕組みとアーキテクチャ

Slide 16

Slide 16 text

eBPFの仕組みとアーキテクチャ cf. https://ebpf.io/what-is-ebpf/

Slide 17

Slide 17 text

eBPFの仕組みとアーキテクチャ cf. https://ebpf.io/what-is-ebpf/ cf.https://speakerdeck.com/yutarohayakawa/ebpfnokoremadetokorekara この仕組みの章の発表スタイルはこちらのスライドを参考にしてます。 大変良い資料なのでおすすめです。 (絵を描くのが大変なので同様にwhat-is-ebpfの画像を引用して 説明したらほぼ同じような感じになってしまった...)

Slide 18

Slide 18 text

eBPFの仕組みとアーキテクチャ cf. https://ebpf.io/what-is-ebpf/ つまりELFファイル

Slide 19

Slide 19 text

- eBPFのプログラムは右のような スタイルのC言語で書くことになる - clangのバックエンドにeBPFバイト コードを吐き出す仕組みがあり、 このプログラムを食わせることで eBPFバイトコード(ELFファイル) を取得できる - 最近だとgccでコンパイルしたり、 Rustでもかけたりするらしい... eBPFプログラミング #include #include #include #include SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; cf. Get started with XDP/Task 2: Drop specific packets with XDP

Slide 20

Slide 20 text

eBPFプログラミング - 右図はIPv6パケットをDropするコード例 - 1. SECマクロをつけることで エントリポイントを指定する - Cのmainに相当するものを自分で指定する - どこにhookを設定するかによって、 SECマクロと関数の引数の中身が変更される - 2. 安全性を保つためにデータを読むたび に境界値チェックをしている #include #include #include #include SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; cf. Get started with XDP/Task 2: Drop specific packets with XDP 1 2

Slide 21

Slide 21 text

eBPFの仕組みとアーキテクチャ cf. https://ebpf.io/what-is-ebpf/ 今回はここが Perl になります!

Slide 22

Slide 22 text

eBPF Loader - eBPFバイトコード等をBPFシステムコールに渡せるようにするモノ - eBPF Mapとの読み書きの中継もするのでユーザー空間とのインターフェースみたいなモノ - 今日はここを自作します!!!!(後で詳しく解説予定) - よく知られているものはこの辺 - libbpf(C): 本家大元、これが一番実装されてる。 - cilium/ebpf(Go):pure-goで頑張っててえらい。libbpfと比べると機能が足りないが... - libbpf-rs(Rust): libbpfのrust wrapper, libbpf-sysというFFIバインディングでやってる - Aya(Rust): pure-rustで頑張っててえらい。 - BCC(C,Python,Go,lua): 初めて勉強する時はこれがわかりやすい方かもしれない。 今回はPerl

Slide 23

Slide 23 text

eBPFの仕組みとアーキテクチャ cf. https://ebpf.io/what-is-ebpf/

Slide 24

Slide 24 text

- bpf syscallでロードされたプログラムはVerifierにより二段階でチェック - 一段階目は全ての分岐をトレースした大雑把なもの - DAGをみることで無限ループの回避 - 最大命令長(100万命令)を超えてしまわないかのチェック - 不正なジャンプがないかの確認 - 内部的にはcheck_cfgで深さ優先探索して実現してる - 二段階目は制約の上でレジスタの演算ができるなどの細かい確認 - 例えばスカラー値をポインタとしてメモリに対して読み書きできたら、 任意のアドレス空間の読み書きができてしまい大変危ない。 - バッファオーバーフローしないかのチェックもここでやっている - 型や定数の追跡をすることでこれらを実現している - 具体的にはbpf_reg_typeやbpf_reg_stateに定義してある eBPF Verifier

Slide 25

Slide 25 text

eBPFの仕組みとアーキテクチャ cf. https://ebpf.io/what-is-ebpf/

Slide 26

Slide 26 text

- Verifierを通過したBPFプログラムは安全と仮定し、パフォーマンスのために ネイティブマシンコードに変換される - ここでeBPFバイトコードからArchごとのネイティブコードに変換されるため、 eBPFはアーキテクチャ非依存で展開することができる - そもそもネイティブで動いてくれないと、実行速度に難があるので・・・ - 実際にはdo_jitというカーネル内部の関数でJITコンパイルされる - バイトコードからの変換なので結構愚直で素朴な実装で書かれてる JIT Compiler case BPF_ALU | BPF_ADD | BPF_X: case (色々なcase条件を中略...): case BPF_ALU64 | BPF_XOR | BPF_X: maybe_emit_mod(&prog, dst_reg, src_reg, BPF_CLASS(insn->code) == BPF_ALU64); b2 = simple_alu_opcodes[BPF_OP(insn->code)]; EMIT2(b2, add_2reg(0xC0, dst_reg, src_reg)); break; cf. https://elixir.bootlin.com/linux/v6.11/source/arch/x86/net/bpf_jit_comp.c#L1379 dst_reg ^= src_reg 相当のコード

Slide 27

Slide 27 text

eBPFの仕組みとアーキテクチャ cf. https://ebpf.io/what-is-ebpf/

Slide 28

Slide 28 text

eBPF Attach & Hook - カーネルにロードしたら、実行したい場所にアタッチする必要がある - 何かのイベントをトリガーにeBPFのプログラムが実行される - Hookの例 - Socket: socketに対してのIOをhookしてフィルタ - kprobe, kretprobe: カーネルの関数呼び出し・返り値をトレース - TC, XDP: NICに対してのIOをhookして読み書き - アタッチ可能なタイプがbpf_attach_typeに定義されている - Linux v6.11現在で58のattach pointがある

Slide 29

Slide 29 text

eBPFの仕組みとアーキテクチャ cf. https://ebpf.io/what-is-ebpf/

Slide 30

Slide 30 text

eBPF Map - eBPFプログラムとユーザー空間から読み書きできるKVストア - 様々なMap Typeが存在している - Hash, Array - Trie(IPアドレスのプレフィックスマッチなどに使う) - Per-CPU (Hash|Array): RWでロックしないように割り込みCPU毎でデータを持ちたい時に使う - LRU, CPUMAP, QUEUE etc… - 例えば、IPアドレスのブラックリストをeBPF Mapで持っていれば、 それを参照することでカーネルレベルでパケットを落とせる。 このブラックリストはユーザー空間から更新可能である

Slide 31

Slide 31 text

eBPFの仕組みとアーキテクチャのまとめ - Cなどのコードを書いてeBPFプログラムを実装し、カーネルにロードできる - eBPFプログラムはVerifierにかけられ、最後はネイティブコードにJITされる - eBPF Mapを使ってユーザー空間とデータのやり取りができる cf. https://ebpf.io/what-is-ebpf/

Slide 32

Slide 32 text

PerlでeBPF Loaderを自作する

Slide 33

Slide 33 text

今回資料の参考としてこちらを公開中 - あくまで参考です。 - Sys::Ebpf という拙作のライブラリとして実装済み - Pure-Perlにこだわって制作しました(XSを利用してません!) - お陰でXDPにアタッチするのに使うNetlinkのコール部分とかもスクラッチ!😇 - https://github.com/takehaya/Sys-Ebpf - 本発表のコードに関して詳しくはこちらを併せて参照ください - コードの形式に関して @AnaTofuZ さんがレビューをくれたのでこの場でお礼申し上げます:) - よければStarお待ちしてます!

Slide 34

Slide 34 text

ちなみにCPANにもアップロード済み - 昨日初めてCPANにUploadしました(Perl初心者並みの感想) - Docs変更忘れてて無意味にバージョンを上げてしまった😇 - cf. https://metacpan.org/pod/Sys::Ebpf

Slide 35

Slide 35 text

今朝知った話で、ebpf界隈で密かに話題らしい - Perlがまだ人気あるの嬉しいです!みたいな反応が来た - Perlコミュニティに貢献できて嬉しい😂 https://x.com/breakawaybilly/status/1842155387593642092

Slide 36

Slide 36 text

eBPF Loaderを自作するというのはどういうことか? - Loaderはカーネルとユーザー空間のインターフェースの役割を持つ - 実装するべき項目をざっくり挙げると... - ELFバイナリをパースして、実行に必要な情報を取り出す - リロケーションでeBPF MapのFD(File Descriptor)を埋め込む - bpf(2) syscallを使ってeBPFプログラムをロードする - bpf(2) syscallを使ってeBPF Mapを読み書きする - つまりこれらをPerlで実装すればOK! - 今回触れないところ諦めたところ - BTFのパースとそれのコール... - おかげでCO-REとかも非対応... - とりあえずELFパーサーを書けという話から始まる Perl Lib Loader

Slide 37

Slide 37 text

説明用にこのプログラムをコンパイルして利用 - なんかファイルが開かれたら カウンターが増えるコード - kprobeにAttachするコードで sys_openに対してhookする #include #include #include struct bpf_map_def SEC("maps") kprobe_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeof(__u32), .value_size = sizeof(__u64), .max_entries = 1, }; SEC("kprobe/sys_open") int kprobe_sysopen() { __u32 key = 1; __u64 initval = 1, *valp; valp = bpf_map_lookup_elem(&kprobe_map, &key); if (!valp){ bpf_map_update_elem(&kprobe_map, &key, &initval, BPF_ANY); return 0; } __sync_fetch_and_add(valp, 1); return 0; } char LICENSE[] SEC("license") = "GPL";

Slide 38

Slide 38 text

そもそもELFファイルとは? - Linuxなどでの実行ファイルフォーマット - clangでコンパイルをすると出てくる - 中にはProgram Headerとその参照や Section Headerとその各種Sectionが 含まれている - BPFの場合は基本的にSectionの話 cf. https://ja.wikipedia.org/wiki/Executable_and_Linkable_Format

Slide 39

Slide 39 text

readelfして構造を見る - readelfコマンドを使うと ELFバイナリの中身が見える - 右図はセクション情報の抜粋 - 必要な情報は以下の4つ - kprobe/sys_open - .relkprobe/sys_open - license - maps Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .strtab STRTAB 0000000000000000 00000250 0000000000000093 0000000000000000 0 0 1 [ 2] .text PROGBITS 0000000000000000 00000040 0000000000000000 0000000000000000 AX 0 0 4 [ 3] kprobe/sys_open PROGBITS 0000000000000000 00000040 00000000000000b0 0000000000000000 AX 0 0 8 [ 4] .relkprobe/s[...] REL 0000000000000000 00000220 0000000000000020 0000000000000010 I 9 3 8 [ 5] license PROGBITS 0000000000000000 000000f0 0000000000000011 0000000000000000 WA 0 0 1 [ 6] maps PROGBITS 0000000000000000 00000104 0000000000000014 0000000000000000 WA 0 0 4 [ 7] .eh_frame PROGBITS 0000000000000000 00000118 0000000000000030 0000000000000000 A 0 0 8 [ 8] .rel.eh_frame REL 0000000000000000 00000240 0000000000000010 0000000000000010 I 9 7 8 [ 9] .symtab SYMTAB 0000000000000000 00000148 00000000000000d8 0000000000000018 1 5 8

Slide 40

Slide 40 text

必要な情報をざっくり解説 - kprobe/sys_open: BPFのプログラム本体 - .relkprobe/sys_open: リロケーション情報(実行時に決定するもの) - license: ライセンス情報 - char _license[] SEC("license") = "GPL"; みたいなやつのことである - printkやその他eBPFで動かすhelper関数を使うためにはGPLライセンスかどうかを 検証されたうえで使う必要があるため、明示的に指定しないとVerifierに怒られる - eBPF プログラムを実行するときに利用する - maps: eBPF Map本体の情報 Relocation section '.relkprobe/sys_open' at offset 0x220 contains 2 entries: Offset Info Type Sym. Value Sym. Name 000000000030 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map 000000000070 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map  .relkprobe/sys_openのリロケーション情報の例

Slide 41

Slide 41 text

PerlでELFファイルをパースする(ELFヘッダ) - まずはELFヘッダをパース - 今回は struct Elf64_Ehdr を 参考に実装 - ELF Fileをreadをして、 substrで文字列のように切りだし て、unpackでPerlのデータ構造に 変換することで実現 - unpack: 指定されたバイナリデータを テンプレート文字列に合わせてPerlの データ構造に変換する組み込み関数 my $byte_offset = 0; my $byte_range = 16;# ELFヘッダは16バイト # e_identをパース my ( $magic, $class, $endian, $version, $abi, $abi_version ) = unpack( 'A4C3A5C2', substr( $data, $byte_offset, $byte_offset + $byte_range ) ); $byte_offset += $byte_range; $byte_range = 32; # 細かいのは中略 ... # ELFファイルのサイズなどを取得 my ($e_type, $e_machine, $e_version, $e_entry,$e_phoff, $e_shoff, $e_flags, $e_ehsize, $e_phentsize, $e_phnum, $e_shentsize, $e_shnum, $e_shstrndx)= unpack( 'S S L Q Q Q L S S S S S S', substr( $data, $byte_offset, $byte_offset + $byte_range ) ); cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Elf/Parser.pm#L60

Slide 42

Slide 42 text

参考: Elf64_Ehdr - ELFヘッダーの 定義が書かれた 構造体をCatして みる - ELFのスペックを LinuxFoundation が出しているので それを参考にする と良い $ sudo cat /usr/include/elf.h | grep -B16 " Elf64_Ehdr;" typedef struct { unsigned char e_ident[EI_NIDENT];/* Magic number and other info */ Elf64_Half e_type; /* Object file type */ Elf64_Half e_machine; /* Architecture */ Elf64_Word e_version; /* Object file version */ Elf64_Addr e_entry; /* Entry point virtual address */ Elf64_Off e_phoff; /* Program header table file offset */ Elf64_Off e_shoff; /* Section header table file offset */ Elf64_Word e_flags; /* Processor-specific flags */ Elf64_Half e_ehsize; /* ELF header size in bytes */ Elf64_Half e_phentsize; /* Program header table entry size */ Elf64_Half e_phnum; /* Program header table entry count */ Elf64_Half e_shentsize; /* Section header table entry size */ Elf64_Half e_shnum; /* Section header table entry count */ Elf64_Half e_shstrndx; /* Section header string table index */ } Elf64_Ehdr;

Slide 43

Slide 43 text

PerlでELFファイルをパースする(sectionとsymbol) - ELFではデータの文字列部分を symbolテーブルに分けているため、 symbolテーブルと今回のデータ の実態があるsectionテーブル を紐付けつつ読み下す必要がある - e.g. sectionテーブルにはindex 1 みたいなのがあり、そのデータをsymbol テーブルから引いてみて初めて kprobe/sys_openという文字がわかる - ついでにリロケーションテーブルも パースしてる # section tableのセクション名を取得するために文字列テーブルセクションを取得 my $strtab_section_offset = $elf->{e_shoff} + $elf->{e_shstrndx} * $elf->{e_shentsize}; my $strtab_offset = unpack( 'Q', substr( $data, $strtab_section_offset + 24, 8 ) ); # セクションヘッダとシンボルテーブルをパースするための追加処理 $elf->{sections} = parse_sections( $data, $elf->{e_shoff}, $elf->{e_shnum}, $elf->{e_shentsize}, $strtab_offset ); $elf->{symbols} = parse_symbols( $data, $elf->{sections}, $elf->{e_shstrndx} ); $elf->{relocations} = parse_relocations( $data, $elf->{sections} ); return $elf; cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Elf/Parser.pm#L97

Slide 44

Slide 44 text

参考: Elf64_Shdr - sectionヘッダーの 定義が書かれた構造体 をCatしてみる - ELF Section Hdrに 関するスペックもある $ sudo cat /usr/include/elf.h | grep -B16 " Elf64_Shdr;" typedef struct { Elf64_Word sh_name; /* Section name (string tbl index) */ Elf64_Word sh_type; /* Section type */ Elf64_Xword sh_flags; /* Section flags */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Section size in bytes */ Elf64_Word sh_link; /* Link to another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign;/* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr;

Slide 45

Slide 45 text

bpf(2) syscallでmap/programを生成・ロードする - bpf(2) syscallを実行することでELFから取り出したeBPF Mapとプログラム を生成およびロードできる - eBPF Map生成はbpf(2)サブコマンドでBPF_MAP_CREATEを実行 - 既存のeBPF Mapをロードするときはbpf(2)サブコマンドBPF_OBJ_GETを実行 - eBPF Programのロードはbpf(2)サブコマンドでBPF_PROG_LOADを実行 - これらの引数は union bpf_attr で表現される - 詳しくはLinux KernelのeBPF Syscallを参照

Slide 46

Slide 46 text

ではPerlでSyscallを叩くのは? - h2ph とPerlの組み込み関数の syscall を利用する - syscallを実行するには、システムコール番号が必要 - h2phはPerlの開発環境に組み込まれたコマンド - Cで定義されたC Headerファイルから定数に変換してくれるのでLinuxに 定義されているSyscall番号を引っ張ってきて実行する pushd /usr/include/x86_64-linux-gnu/sys/ h2ph -d "$ORIG_PWD/lib/Sys/Ebpf/Syscall" -a -l syscall.h popd require 'syscall.ph'; my $s = "hi there\n"; syscall(SYS_write(), fileno(STDOUT), $s, length $s); h2phでSyscall番号を引っこ抜く図 syswrite関数をエミュレートする図 cf. https://perldoc.jp/func/syscall

Slide 47

Slide 47 text

いやまて、PerlでCPointerどうするの? - BPF_PROG_LOADの共用体を みると、プログラム列の長さと プログラムのポインタを渡すこと で実現しようとしてる - ログのバッファとかもそう - Perlでポインタみたいなのは どう取り扱えばいいのか? union bpf_attr { struct { /* Used by BPF_PROG_LOAD */ __u32 prog_type; __u32 insn_cnt; __aligned_u64 insns; /* 'const struct bpf_insn *' */ __aligned_u64 license; /* 'const char *' */ __u32 log_level; __u32 log_size; /* size of user buffer */ __aligned_u64 log_buf; /* user supplied 'char *' buffer */ __u32 kern_version; }; } __attribute__((aligned(8))); cf. https://man7.org/linux/man-pages/man2/bpf.2.html より、改変及び抜粋

Slide 48

Slide 48 text

- 右の図はBPF_PROG_LOADをPerlで 実行するコード - pack(“P”)でメモリアドレス化して バイナリ文字列に変換 - unpack(“Q”)でuint64として解釈 - 再度pack(“Q”)でバイナリに - 一度Unpackしてるのはポインタを 常にuint64であることを保証 させるため - これで無事PerlからSyscallを 叩くことができて嬉しい!🥳 - BPF_MAP_CREATEは工夫無しでやるだけな ので気になったらコードを読んでください PackしてUnpackして解決 my $attr = pack( "L L Q Q L L Q L L", $attrs->{prog_type}, $attrs->{insn_cnt}, unpack( "Q", pack( "P", $attrs->{insns} ) ), unpack( "Q", pack( "P", $attrs->{license} ) ), $attrs->{log_level}, $attrs->{log_size}, unpack( "Q", pack( "P", $attrs->{log_buf} ) ), $attrs->{kern_version}, $attrs->{prog_flags} ); my $fd = syscall( Sys::Ebpf::Syscall::SYS_bpf(), BPF_PROG_LOAD, $attr, length($attr) ); cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Loader.pm#L171

Slide 49

Slide 49 text

bpf(2) BPF_PROG_LOADは失敗した模様... Log buffer content: 0: (b7) r1 = 0 1: (63) *(u32 *)(r10 -4) = r1 last_idx 1 first_idx 0 regs=2 stack=0 before 0: (b7) r1 = 0 2: (b7) r6 = 1 3: (7b) *(u64 *)(r10 -16) = r6 4: (bf) r2 = r10 5: (07) r2 += -4 6: (18) r1 = 0x0 8: (85) call bpf_map_lookup_elem#1 R1 type=inv expected=map_ptr - syscallは叩くことができたが、 map_ptrがinvalidらしい... verifierで怒られている模様 - Mapも生成したし、正しくeBPF の ProgramをロードしたのにeBPF Verifierに怒られてる...🥺 - r1 = 0x0 … ????

Slide 50

Slide 50 text

ここでeBPFバイトコードの呼び出し規約を確認する - R1~R5は引数レジスタ - つまりR1は第一引数に対応 - R1がなぜか空っぽ? R0 汎用レジスタ(戻り値を格納) R1~R5 汎用レジスタ(引数レジスタ) R6~R9 汎用レジスタ R10 フレームポインタ(読み出し専用) cf. https://www.kernel.org/doc/html/v6.11/bpf/standardization/abi.htm l#bpf-abi-recommended-conventions-and-guidelines-v1-0

Slide 51

Slide 51 text

bpf_map_lookup_elemの第一引数は? - 第一引数がeBPF Map - 第二引数がmap key - つまりeBPF Mapが無効なので どうにかして有効なeBPF Map にしてあげる必要がある - よく考えれば、確かにMapの インスタンスを知らないと Progは接続できない - そもそも、createしたeBPF Map との紐づけがされてない... struct bpf_map_def SEC("maps") kprobe_map = { .type = BPF_MAP_TYPE_ARRAY, .key_size = sizeof(__u32), .value_size = sizeof(__u64), .max_entries = 1, }; __u32 key = 1; valp = bpf_map_lookup_elem(&kprobe_map, &key);

Slide 52

Slide 52 text

- 実はBPF_MAP_CREATE を叩くと eBPF MapのFDが手に入るのでこれで 一意に生成されたMapのIDが取れる - これを実行時に渡してあげることでeBPFMapとの紐づけができる - リロケーションというのは、プログラムロード時にシンボルや メモリアドレスが決定されるプロセスである - で、前述した.relkprobe/sys_openに返ってくる... - つまり r1=[eBPF MapのFD] になるようにFDを埋め込む必要がある リロケーションでeBPF MapのFDを埋め込む Relocation section '.relkprobe/sys_open' at offset 0x220 contains 2 entries: Offset Info Type Sym. Value Sym. Name 000000000030 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map 000000000070 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map リロケーションセクションの図

Slide 53

Slide 53 text

- 実はBPF_MAP_CREATE を叩くと eBPF MapのFDが手に入るのでこれで 一意に生成されたMapのIDが取れる - これを実行時に渡してあげることでeBPFMapとの紐づけができる - リロケーションというのはプログラム実行時に決まる領域のこと - で、前述した.relkprobe/sys_openに返ってくる... - つまり r1=[eBPF MapのFD] になるようにFDを埋め込む必要がある リロケーションでeBPF MapのFDを埋め込む Relocation section '.relkprobe/sys_open' at offset 0x220 contains 2 entries: Offset Info Type Sym. Value Sym. Name 000000000030 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map 000000000070 000600000001 R_BPF_INSN_64 0000000000000000 kprobe_map リロケーションセクションの図 ここのオフセットの値を 利用して書き換える

Slide 54

Slide 54 text

リロケーションとオフセットを照らし合わせる - offsetは0x30, 0x70 - 48byte目と112byte目 なので8で割ると5と 13行目が書き換えの 対象とわかる - 0オリジンに注意 - r1 = 0 ll とは...? $ llvm-objdump -D kprobe_file_open_counter.o kprobe_file_open_counter.o: file format elf64-bpf Disassembly of section kprobe/sys_open: 0000000000000000 : 0: b7 06 00 00 01 00 00 00 r6 = 1 1: 63 6a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r6 2: 7b 6a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r6 3: bf a2 00 00 00 00 00 00 r2 = r10 4: 07 02 00 00 fc ff ff ff r2 += -4 5: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 7: 85 00 00 00 01 00 00 00 call 1 8: 55 00 09 00 00 00 00 00 if r0 != 0 goto +9 9: bf a2 00 00 00 00 00 00 r2 = r10 10: 07 02 00 00 fc ff ff ff r2 += -4 11: bf a3 00 00 00 00 00 00 r3 = r10 12: 07 03 00 00 f0 ff ff ff r3 += -16 13: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 15: b7 04 00 00 00 00 00 00 r4 = 0 16: 85 00 00 00 02 00 00 00 call 2

Slide 55

Slide 55 text

“r1 = 0 ll”ってなに? - lddw命令: 64bitの即値をレジスタにロードしてくれる - BPFは64bitの命令長で、immが32bitしかない=>128bit使って実現 - 1命令目: 下位32bit, 2命令目: 上位32bit - ここにfdを入れる必要がある msb 32 48 52 56 lsb +------------------------+----------------+----+----+--------+ |immediate |offset |src |dst |opcode | +------------------------+----------------+----+----+--------+ BPFの命令をアスキーアートで書いた図 cf. https://github.com/iovisor/bpf-docs/blob/master/eBPF.md#instruction-encoding

Slide 56

Slide 56 text

Mapの部分をSys::Ebpfで埋め込んでみると - 仮にFDが3だと、Sys::Ebpf上でバイトコードの表現するとこんな感じである - srcレジスタにBPF_PSEUDO_MAP_FDを詰めているのが肝 my $High = Sys::Ebpf::Asm->new( code => 0x18, # opcode(lddw) dst_reg => 0x1, # destination register (r1) src_reg => 0x1, # source register(Pseudo map fd) off => 0, # offset imm => 0x3 # immediate value (map fd) ); my $Low = Sys::Ebpf::Asm->new( code => 0, dst_reg => 0, src_reg => 0, off => 0, imm => 0);

Slide 57

Slide 57 text

BPF_PSEUDO_MAP_FDを使う理由は? - 通常命令とMapに関する命令であることを区別するために使われる - パフォーマンスなどの観点から毎回FDを直接読むのではなく、 プログラムがロードされた時点でMapへの実ポインターに置き換わるため - 今回は紹介しないが、BTFでも使われてる模様 static int resolve_pseudo_ldimm64(struct bpf_verifier_env *env){ //(中略) insn[0].imm = (u32)addr; insn[1].imm = addr >> 32; cf. https://elixir.bootlin.com/linux/v6.11/source/kernel/bpf/verifier.c#L18418

Slide 58

Slide 58 text

実際のリロケーションの流れに起こしてみる - 入れるべきデータを得る - eBPF MapをCreateして, eBPF MapのFDを取得する - 変更すべき位置を決定する - .relkprobe~のoffsetを使い修正すべき命令をkprobe/sys_openセクション内から特定する - 何を書き換えるかを決定する - .relkprobe~からシンボルインデックスを取得し、シンボル名を解決し、 どのようなシンボル名のeBPF MapのFDを入れるかを決定する - つまりどのような名前のeBPF Mapなのかを得て、入れるべきFDを決めるということ - 実際に書き換える - 修正すべき命令に、期待したMapのFDを適用して、正しいマップへの参照に書き換える - この際、先程のlddw命令の形式で書き換える必要がある

Slide 59

Slide 59 text

実際にリロケーションをPerlで行う - lddw命令になる部分16byte を取得し、Asmにパースする - 128bitなのでhigh, lowにわけて 取り扱い、MapFDを入れる - 修正結果をシリアライズして 上書きすることでFDを追加 出来る # ここにデバッグログがある(書き換え前を出力) # 指定されたオフセット位置にある `lddw` 命令(16バイト)を取得 my $bpf_insn = substr( $self->{reader}->{raw_elf_data}, $r_offset, 16 ); my $bpf_insn_len = length($bpf_insn); my ( $high, $low ) = Sys::Ebpf::Asm::deserialize_128bit_instruction($bpf_insn); # 即値 (64ビット) にマップFDを設定 $high->set_imm($map_fd); $low->set_imm( $map_fd >> 32 ); # src_reg に PSEUDO_MAP_FD (1) を設定 $high->set_src_reg(1); # 修正後の命令をパックして、元の場所に書き戻す my $new_bpf_insn = Sys::Ebpf::Asm::serialize_128bit_instruction( $high, $low ); substr( $self->{reader}->{raw_elf_data},$r_offset, 16, $new_bpf_insn ); # ここにデバッグログがある(書き換え後を出力) cf.https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Loader.pm#L264

Slide 60

Slide 60 text

再度ロードしてみると...ロードが成功する! sudo perl kprobe_file_open_counter.pl .relkprobe/sys_open Before relocation (offset 104): 18010000000000000000000000000000 After relocation (offset 104): 18110000030000000000000000000000 Before relocation (offset 168): 18010000000000000000000000000000 After relocation (offset 168): 18110000030000000000000000000000 BPF program loaded successfully with FD: 4 Map FD: 3 Prog FD: 4 - Map FDが3なので、リロケーション前と後のデータのログを見てもわかるはず - 無事プログラム側のFDも生成されたのでロード完了! 0から3に変わって いるところからFDの 追加がわかる 動作OK!

Slide 61

Slide 61 text

全体の流れを整理してみると #include #include #include #include SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) Hook Points MapFDをaddressに書き換え済み

Slide 62

Slide 62 text

全体の流れを整理してみると #include #include #include #include SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) Hook Points 1 システムコール でMapを作る MapFDをaddressに書き換え済み

Slide 63

Slide 63 text

全体の流れを整理してみると #include #include #include #include SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) Hook Points 2 Map生成が成功し、 FDを取得できる MapFDをaddressに書き換え済み

Slide 64

Slide 64 text

全体の流れを整理してみると #include #include #include #include SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) Hook Points 3 取得したMapFDを リロケーションで 置き換えて 入れてLoad MapFDをaddressに書き換え済み

Slide 65

Slide 65 text

全体の流れを整理してみると #include #include #include #include SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT MapFDをaddressに書き換え済み Get Prog FD perf_event_open(2) Hook Points 4 Mapの実アドレスに 変換された上で動作

Slide 66

Slide 66 text

全体の流れを整理してみると #include #include #include #include SEC("xdp_drop") int xdp_drop_prog(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; __u16 h_proto; if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP; h_proto = eth->h_proto; if (h_proto == htons(ETH_P_IPV6)) return XDP_DROP; return XDP_PASS; } char _license[] SEC("license") = "GPL"; Cプログラム ELF Object LLVM compile User Kernel bpf(2) Create Map Get map FD ELF リロケーション ELF Section (prog & rel) Map bpf(2) Load Prog Verifier/ JIT Get Prog FD perf_event_open(2) 5 ProgのFDをAttachして BPFプログラムを動作させる (今回は省略) Hook Points MapFDをaddressに書き換え済み

Slide 67

Slide 67 text

eBPF Mapを通じた読み書き 先程まででロードはできたが、 実利用のためにはPerlアプリとのデータ のやり取りができる必要がある。 Mapの読み書き(Read/Write)のAPI としてlookup,update,deleteがあり、 便利に利活用するには最低でも それらを実装する必要がある union bpf_attr { struct { /* Used by BPF_MAP_*_ELEM and BPF_MAP_GET_NEXT_KEY commands */ __u32 map_fd; __aligned_u64 key; union { __aligned_u64 value; __aligned_u64 next_key; }; __u64 flags; }; } __attribute__((aligned(8))); cf. https://man7.org/linux/man-pages/man2/bpf.2.html より、改変及び抜粋

Slide 68

Slide 68 text

PerlでeBPF Mapへの読み書きをする my $attr = pack( "L L Q Q Q", $map_fd, 0, unpack( "Q", pack( "P", $key ) ), defined($value) ? unpack( "Q", pack( "P", $value ) ) : 0, $flags, ); my $result = syscall(    Sys::Ebpf::Syscall::SYS_bpf(), $cmd, $attr, length($attr) ); - またbpf(2)でリクエストすれば実現󰢐 - 実際にsyscallを叩いてるところはこちら - eBPF Mapへのアクセスはだいたい同じ Attributeなので結構簡単にコールできる cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Map.pm#L115

Slide 69

Slide 69 text

利用する側としては簡単ではない😇 具体的には自分でKey情報をpackして、 valueをunpackするという素敵(?)な 実装になってしまうというのはあまり お手軽ではない... なんとかして シリアライズ・デシリアライズを きれいにする必要がある...🤔 PerlでeBPF Mapへの読み書きをする sub raw_lookup { my ( $self, $key, $flags ) = @_; $flags //= 0; my $value = "\0" x $self->{value_size}; my $res = syscall_bpf_map_elem(      BPF_MAP_LOOKUP_ELEM(), $self->{map_fd}, $key,  $value, $flags ); my $key = 1; my $value = $map_kprobe_map->raw_lookup(         pack("L", $key)); print(unpack("Q", $value)) cf. https://github.com/takehaya/Sys-Ebpf/blob/main/lib/Sys/Ebpf/Map.pm#L144 Map lookupに先程のsyscallを共通化した例 今回のサンプルコードでmap lookupできるようにした例

Slide 70

Slide 70 text

key, valueのschemaを定義できるようにした mapの生成後に、mapを操作するクラスでschema名と型を持つことで対応した my ( $map_data, $prog_fd ) = $loader->load_bpf($kprobe_fn); my $map_kprobe_map = $map_data->{kprobe_map}; $map_kprobe_map->{key_schema} = [ [ 'kprobe_map_key', 'uint32' ] ]; $map_kprobe_map->{value_schema} = [ [ 'kprobe_map_value', 'uint64' ] ]; my $key = { kprobe_map_key => 1 }; my $value = $map_kprobe_map->lookup($key); struct bpf_map_def SEC("maps") kprobe_map = { .type = BPF_MAP_TYPE_ARRAY, .key_size = sizeof(__u32), .value_size = sizeof(__u64), .max_entries = 1, }; 実際のCTypeでは Uint32,Uint64で 定義されてる Perl上でも同様に K/Vの名前と型名 をmappingして あげることで解決

Slide 71

Slide 71 text

内部的には型名をパースして解釈 PerlにはCType相当のものがなさそうで チョット涙ぐましい実装になったが 文字列操作が簡単にかけるので代用のコー ドもサクッとかけて良かった sub _match_uint_or_uint_array { my ($type) = @_; return $type    =~ /^uint(\d+)(?:\[(\d+)\])?$/ ? ( $1, $2 ) : (); } sub _get_type_size { my ($type) = @_; if ( my ( $bit_size, $array_size ) = _match_uint_or_uint_array($type) ) { $array_size //= 1; if ( $bit_size =~ /^(8|16|32|64)$/ ) { return ( $bit_size / 8 ) * $array_size; } } die "Unsupported type: $type";

Slide 72

Slide 72 text

ということで、eBPF Loaderの完成! my $kprobe_info= Sys::Ebpf::Link::Perf::Kprobe::attach_kprobe( $prog_fd, $kprobe_fn ); print "Map FD: " . $map_kprobe_map->{map_fd} . "\n"; print "Program FD: $prog_fd\n"; sleep(1); print "Counting file opens. Press Ctrl+C to stop.\n"; while (1) { my $key = { kprobe_map_key => 1 }; my $value = $map_kprobe_map->lookup($key); if ( defined $value ) { printf "Files opened: %d\n", $value->{kprobe_map_value}; } sleep(1); } cf. https://github.com/takehaya/Sys-Ebpf/blob/main/sample/kprobe_file_open_counter/kprobe_file_open_counter.pl Sampleとして用意した「なんかファイルが開かれたらカウンターが増える eBPFプログラム」をハンドルするPerlコードの全貌です!!

Slide 73

Slide 73 text

demo

Slide 74

Slide 74 text

kprobeを動作させてみる 先程の「なんかファイルが開かれたらカウンターが増えるeBPFプログラム」 を実際に動かしてみよう!

Slide 75

Slide 75 text

動作結果の様子 $ sudo perl kprobe_file_open_counter.pl Map FD: 3 Program FD: 4 Counting file opens. Press Ctrl+C to stop. Files opened: 4 Files opened: 4 Files opened: 8 Files opened: 8 Files opened: 12 Files opened: 12 Files opened: 16 Files opened: 16 FDが取得できてる ので無事Load OK open syscallが叩かれた回数 (file openとかされた だけ増える)

Slide 76

Slide 76 text

XDPを動作させてみる では、別のeBPFのプログラムを動かしてみる。 BPFといえばパケットを読み書きするのが定番なので、XDPという高速パケット 処理用の仕組みを利用し、Port 8080への着信パケットの数え上げをする。 WebClient eBPF App Server ⇐ wget http://192.0.2.1:8080 とかを叩いてみる tcp port 8080へ リクエストが飛ぶ interface に XDP Appが アタッチされる

Slide 77

Slide 77 text

動作の様子 $ sudo perl ./xdp_count_8080_port.pl (中略) ens3 にXDPプログラムをアタッチしました。 XDPプログラムをアタッチしました。 Ctrl+Cで停止します。 パケット数 : 0 パケット数 : 0 パケット数 : 0 パケット数 : 1 パケット数 : 2 パケット数 : 3 パケット数 : 9 8080 listen前なので、 portに来てもackが返らず、handshake失敗で終わるの で一回の実行で1つずつしか増えない 8080 listen後なので、handshake終了後、データを転 送してくるので一気に増えてる

Slide 78

Slide 78 text

tcpdumpしてみるとよりわかる IPはマスク してます🙇 8080 listen前なので、一回ずつなことがわかる 8080 listen後は3way handshakeが成功するので数が多い(6回分 右側に8080がある数 + HTTP リクエストの数 を数える と確かに9回になってるのがわかる

Slide 79

Slide 79 text

今後の展望とまとめ

Slide 80

Slide 80 text

- サービスのポータビリティ性を高めるためにBTF対応に関する実装と そこの知見を共有したい。struct opsとかCO-REのためにも対応したい:) - 多くのhook point対応できるコードをいれることでパケット処理や オブザーバビリティに対しての洞察が得られるので進めていきたい - 例えばTC, uprobe, tracepoint, kretprobeとかはまだ... - 他の細かい再配置とかはサボってるのでやりたい... - ガッと書いたのでナウいPerlでは無い気がする ロガーとかエラーとか整備したい... 今後の展望 cf. https://docs.kernel.org/bpf/btf.html#btf-kernel-api

Slide 81

Slide 81 text

まとめ - このようにPure-PerlでeBPFのLoaderが書けることを示しました - よく使われるユースケースであるオブザーバービリティに関することも、 パケット処理のAttachもPure-Perlで実現可能:) - この発表デモからPerlを通じたカーネルのMetric取得や パケット処理が可能であることも学びました - あなたもeBPFに入門してIn Kernelで実行される初めてのソフトウェア を書いてみませんか?:)

Slide 82

Slide 82 text

Thank you !!😻 @takemioIO @takehaya @takehaya

Slide 83

Slide 83 text

Enabling a Connected Future.

Slide 84

Slide 84 text

参考資料 - https://github.com/torvalds/linux - https://docs.kernel.org/userspace-api/ebpf/syscall.html - https://man7.org/linux/man-pages/man2/bpf.2.html - https://speakerdeck.com/yutarohayakawa/ebpfhahe-gaxi-siinoka - https://mechpen.github.io/posts/2019-08-03-bpf-map/index.html - https://blog.yuuk.io/entry/2021/ebpf-tracing - https://atmarkit.itmedia.co.jp/ait/articles/1910/07/news008.html