eBPF tail call 使い方メモ
eBPFにはtail callと呼ばれるプログラム呼び出し方法が存在する。 これは、ある実行中のBPFプログラムから別のBPFプログラムにジャンプするものである。あくまでジャンプであって、通常の関数呼び出しと違って呼び出し元に処理は戻らない。ユースケースとしては例えば、役割が違う複数のプログラムの独立性を保ったまま連携させたい場合に役立つ。
bpf: introduce bpf_tail_call() helper [LWN.net]
また、tail callの面白い点として、ジャンプ先のルックアップは BPF_MAP_TYPE_PROG_ARRAY
という種類のBPF mapを使う点が挙げられる。このmapは通常のBPF map同様、プログラム実行中にユーザ空間側から動的に書き換えることができる。したがって、ジャンプ先のプログラムを動的に入れ替えたり、あるいはジャンプを取りやめるといったことが容易に実現できる。
通常の使用例
tail callを使うには、BPF_MAP_TYPE_PROG_ARRAY
のBPF mapを用意した上で bpf_tail_call()
helperを使えばよい。カーネルの samples/bpf/sockex3_kern.c
が参考になる。
linux/sockex3_kern.c at master · torvalds/linux · GitHub
該当部分を抜粋する*1:
#define PARSE_VLAN 1 #define PARSE_MPLS 2 #define PARSE_IP 3 #define PARSE_IPV6 4 static inline void parse_eth_proto(struct __sk_buff *skb, u32 proto) { switch (proto) { case ETH_P_8021Q: case ETH_P_8021AD: bpf_tail_call(skb, &jmp_table, PARSE_VLAN); break; case ETH_P_MPLS_UC: case ETH_P_MPLS_MC: bpf_tail_call(skb, &jmp_table, PARSE_MPLS); break; case ETH_P_IP: bpf_tail_call(skb, &jmp_table, PARSE_IP); break; case ETH_P_IPV6: bpf_tail_call(skb, &jmp_table, PARSE_IPV6); break; } }
jmp_table
がprogram arrayである。proto
が ETH_P_8021Q
であれば jmp_table[PARSE_VLAN]
が示すファイルディスクリプタのBPFプログラムにジャンプする。なお、もしここでjmp_table[n]
が有効なBPFプログラムでない場合、そのまま処理が続行(fall-through)される点に注意。逆にこの特性を利用すれば、トラブルシューティングの時だけデバッグ用のBPFプログラムを登録する、といったことが可能になる。
bccでの使用例
ここではiovisor/bccを使った場合の動作を試してみることにする。bccにはprog_array.call()
という構文のsyntax sugarが用意されており、bpf_tail_call()
と等価な処理を容易に実現できる。
ここでは例として、通常時は全てのパケットを通過するが、コマンドによって全てのパケットをドロップするXDPプログラムを考える*2。
XDP program
カーネル空間側で実行されるXDPのコードは下記の通り。通常時はprog_entry
を実行するが、パケットをドロップさせたい時はprog_drop
にジャンプさせる。jmp_table
がprogram arrayである。
なお、events.perf_submit()
はログをカーネル空間からユーザ空間に送るのに使用する。これもbccのsyntax sugarで、bpf_perf_event_output()
と等価である。
// xdp_chain.c BPF_TABLE("prog", int, int, jmp_table, 8); BPF_PERF_OUTPUT(events); struct event_t { u64 timestamp; char message[128]; }; int prog_drop(struct xdp_md *ctx) { struct event_t e = { .timestamp = bpf_ktime_get_ns(), .message = "Enter prog_drop\0", }; events.perf_submit(ctx, &e, sizeof(e)); return XDP_DROP; } int prog_entry(struct xdp_md *ctx) { struct event_t e_enter = { .timestamp = bpf_ktime_get_ns(), .message = "Enter prog_entry\0", }; events.perf_submit(ctx, &e_enter, sizeof(e_enter)); jmp_table.call(ctx, 0); struct event_t e_exit = { .timestamp = bpf_ktime_get_ns(), .message = "Exit prog_entry\0", }; events.perf_submit(ctx, &e_exit, sizeof(e_exit)); return XDP_PASS; }
動作を観察する
ここでは簡単のため、ユーザ空間側の処理はipython
経由で逐次実行する。
まずは下準備としてプログラムのロードまでを実施する。ここでevents.open_perf_buffer()
は前述したログを読み出すためのもので、bpf.kprobe_poll()
を呼び出してログが到着するとprint_event()
がコールバックされる。
$ sudo ipython In [1]: %cpaste Pasting code; enter '--' alone on the line to stop or use Ctrl-D. :import ctypes as ct :from bcc import BPF : :class Event(ct.Structure): : _fields_ = [ : ("timestamp", ct.c_uint64), : ("message", ct.c_char * 128), : ] : :def print_event(cpu, data, size): : event = ct.cast(data, ct.POINTER(Event)).contents : print event.timestamp, event.message : : :bpf = BPF(src_file="./xdp_chain.c") : :events = bpf["events"] :events.open_perf_buffer(print_event) : :prog_entry = bpf.load_func("prog_entry", BPF.XDP) :prog_drop = bpf.load_func("prog_drop", BPF.XDP) :<EOF>
prog_entry
をインタフェースens9
にアタッチ。XDPでのパケット処理が開始される。
In [2]: bpf.attach_xdp("ens9", prog_entry)
ログは以下のようになっている。jmp_table
に何も登録されていないためfall-throughされていることが確認できる。
In [3]: bpf.kprobe_poll() 87485224130884 Enter prog_entry 87485224144293 Exit prog_entry 87487208201839 Enter prog_entry 87487208220583 Exit prog_entry
この状態で、jmp_table[0]
に prog_drop
を登録してみる。
In [4]: jmp_table = bpf["jmp_table"] In [5]: jmp_table[0] = prog_drop.fd
外部からens9
宛のpingが通らなくなり、パケットがドロップされるようになったことが確認できる。ログを確認すると、prog_drop
が実行され、かつprog_entry
の末尾部分に処理が戻っていないことも確認できる。
In [6]: bpf.kprobe_poll() 87669224146031 Enter prog_entry 87669224160730 Enter prog_drop 87671208094900 Enter prog_entry 87671208110055 Enter prog_drop
jmp_table[0]
を削除すれば、再び最初の状態に戻る。
In [7]: del jmp_table[0] In [8]: bpf.kprobe_poll() 87875240128813 Enter prog_entry 87875240144164 Enter prog_drop 87877224181681 Enter prog_entry 87877224199698 Enter prog_drop 87879208128601 Enter prog_entry 87879208145440 Exit prog_entry