cilium/ebpfを使ってLightweight Tunnel (LWT)フックでeBPFを実行してみた話
Segment Routing over IPv6 (SRv6) のLinux実装について調べているときに,LightWeight Tunnel (lwtunnel, LWT) というインフラストラクチャを使ってSRv6といったトンネリングが実装されていることを知りました.もう少し調べてみると,LWTではeBPFが実行でき,SRv6のEnd.BPFではLWTのeBPFがベースとなっているようでした.
LWTのeBPFについては記事が少ないので,自分が調べた/やったことについて共有します.
LightWeight Tunnel (lwtunnel, LWT)
LightWeight Tunnel (lwtunnel, LWT)は,ネットワークのトンネリングを実装するためのインフラストラクチャです.MPLSやSRv6(SEG6/SEG6LOCAL)のトンネリングの機能は,LWTがベースになっています.
LWTでは,eBPFを実行できます(LWTのBPFの機能についてはこの記事が参考になります).eBPFが実行できるLWTの種類には,基本的に以下の3つがあります.
- BPF_PROG_TYPE_LWT_IN
- BPF_PROG_TYPE_LWT_OUT
- BPF_PROG_TYPE_LWT_XMIT
bpf: BPF for lightweight tunnel infrastructureによると,lwt_inとlwt_outのプログラムでは,無効なヘッダや不正なヘッダが構築されるのを防ぐためにskbのデータは読み取りのみ,lwt_xmitでは,書き込み可能であるようです.
アプリケーションを作ってみる
まずは,簡単にパケットキャプチャするアプリケーションの作成します.Goのcilium/ebpfのライブラリを使用してプログラムを作成します.cilium/ebpfライブラリで,LWTのeBPFプログラムを書いている人がいなかったので,ライブラリのコードを見て作っています.なので,一部に冗長な書き方があるかもしれません.
今回使用するソースコードは,https://github.com/shu1r0/lwt_ebpf_capture にあります.
動作確認の環境は以下の通りです.
- OS: Ubuntu 22.04(5.19.0-43-generic)
- Go: go1.18.3 linux/amd64
- clang: Ubuntu clang version 14.0.0-1ubuntu1
eBPF側のコード
eBPF側のコードは,フックで実行する関数を定義して,ユーザスペース側にパケットを送るだけです.SEC("lwt_xmit/capture")
では,プログラムタイプがcilium/ebpfで認識できるようにlwt_xmit
を指定します.
コードの流れとして,LWTフックではまずskbを受け取ります.このskbはIPヘッダからのデータになっています.その後,パケット解析を行うのですが,今回はbpf_perf_event_output
でパケットをユーザスペースに送ります.そのときにメタデータも共に送っています.
LWTフックでは,TCやXDPとは返り値の指定が異なります.以下の三種が指定できます.
- BPF_OK
- BPF_DROP
- BPF_REDIRECT
#include <linux/bpf.h>
#include "bpf_helpers.h"
#include "bpf_trace_helpers.h"
struct metadata
{
__u16 cookie;
__u16 pkt_len;
} __attribute__((packed));
// Perf Map
struct bpf_map_def SEC("maps") perf_map = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(__u32),
.max_entries = 128,
};
static __always_inline long perf_event_packet(void *ctx, __u16 cookie, __u16 pkt_len)
{
struct metadata meta = {
.cookie = cookie,
.pkt_len = pkt_len};
__u64 flags = BPF_F_CURRENT_CPU | ((__u64)pkt_len << 32);
return bpf_perf_event_output(ctx, &perf_map, flags, &meta, sizeof(meta));
}
SEC("lwt_xmit/capture")
int capture_xmit(struct __sk_buff *skb)
{
bpf_trace("Enter packet on lwt_xmit");
void *data_end = (void *)(long)skb->data_end;
void *data = (void *)(long)skb->data;
__u16 cookie = 0xbeef;
__u16 pkt_len = data_end - data;
long r = perf_event_packet(skb, cookie, pkt_len);
return BPF_OK;
}
char _license[] SEC("license") = "GPL";
Go側のコード
上のeBPFプログラムをgo generateでコンパイルするために,以下のような文面をGoのソースコードに記載します.詳細は https://github.com/shu1r0/lwt_ebpf_capture を見てください.
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go capture ../../ebpf/lwt_capture.c -- -I../../ebpf/ -I/usr/include/
コンパイルしたeBPFプログラムをロードしてアタッチします.LWTフックでは,eBPFはルートとしてアタッチします.vishvananda/netlinkを使用した場合,bpfEncapの構造体を初期化して,プログラムをセットします.lwt_xmitのフックでセットする場合,nl.LWT_BPF_XMIT
を指定します.その後,ルートとして,eBPFを設定します.
注意点として,vishvananda/netlinkをgo get
でインストールした場合,bpfEncapは実装されていないバージョンになっています(2023/06/12現在).なので,githubの最新のものをインストールする必要があります.
// eBPFのコードを取得(https://github.com/shu1r0/lwt_ebpf_capture/pkt/ebpfを参照)
bpfObj, err := cap.NewEBpfObjects(nil)
if err != nil {
panic(fmt.Errorf("Failed new eBPF Object: %v\n", err))
}
// eBPFプログラムをセット
if err := bpfEncap.SetProg(nl.LWT_BPF_XMIT, bpfObj.CaptureXmit.FD(), "lwt_xmit/capture"); err != nil {
panic(fmt.Errorf("set prog error : %s", err))
}
// Link indexを取得
oif, err := netlink.LinkByName("<link_name>")
if err != nil {
panic(fmt.Errorf("link by name error : %s", err))
}
// destination address
_, dst, err := net.ParseCIDR("<destination>")
if err != nil {
panic(fmt.Errorf("parse cidr error : %s", err))
}
// ルートを追加
route := netlink.Route{LinkIndex: oif.Attrs().Index, Dst: dst, Encap: &bpfEncap}
if err := netlink.RouteAdd(&route); err != nil {
panic(fmt.Errorf("route add error : %s", err))
}
iproute2を使ってeBPFをセットする場合例は以下になります.
ip route add <DESTINATION> encap bpf [in | out | xmit] obj <program.o> section <sec_name> dev <interface>
ルートを設定すると例えば以下のように表示されます.
# ip -6 r
2001:db8:10::/48 dev r1_h1 proto kernel metric 256 pref medium
2001:db8:20::2 encap bpf xmit lwt_xmit/capture[fd:11] dev r1_h2 metric 1024 pref medium
2001:db8:20::/48 dev r1_h2 proto kernel metric 256 pref medium
fe80::/64 dev r1_h1 proto kernel metric 256 pref medium
fe80::/64 dev r1_h2 proto kernel metric 256 pref medium
eBPFプログラム側ではbpf_perf_event_output
を使ってパケットを送るので,それを受け取るコードが必要となります.eBPFから受け取ったイベントのデータから,メタデータ分を抜いた部分がパケットのデータです.パケットはgopacketで解析します.layers.LayerTypeIPv6
としているように,得られるデータはIPヘッダからとなっています.
ctrlC := make(chan os.Signal, 1)
signal.Notify(ctrlC, os.Interrupt)
go func() {
var event perfEventItem
for {
evnt, err := perfEvent.Read()
if err != nil {
if errors.Unwrap(err) == perf.ErrClosed {
break
}
panic(fmt.Errorf("perf event read error : %s", err))
}
reader := bytes.NewReader(evnt.RawSample)
if err := binary.Read(reader, binary.LittleEndian, &event); err != nil {
panic(fmt.Errorf("binary read error : %s", err))
}
pkt := gopacket.NewPacket(evnt.RawSample[4:], layers.LayerTypeIPv6, gopacket.Default)
fmt.Println(pkt.Dump())
}
}()
<-ctrlC
if err := perfEvent.Close(); err != nil {
panic(fmt.Errorf("close error : %s", err))
}
pkt.Dump()
では,パケットの中身とフィールドの値を確認することができます.なので,pingのパケットをキャプチャした場合,以下のようなデータを得ることができます.
-- FULL PACKET DATA (104 bytes) ------------------------------------
00000000 60 0a 0a 78 00 40 3a 3f 20 01 0d b8 00 10 00 00 |`..x.@:? .......|
00000010 00 00 00 00 00 00 00 02 20 01 0d b8 00 20 00 00 |........ .... ..|
00000020 00 00 00 00 00 00 00 02 80 00 46 5d ff 9b 00 03 |..........F]....|
00000030 76 bd 86 64 00 00 00 00 1c ee 05 00 00 00 00 00 |v..d............|
00000040 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f |................|
00000050 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f | !"#$%&'()*+,-./|
00000060 30 31 32 33 34 35 36 37 |01234567|
--- Layer 1 ---
IPv6 {Contents=[..40..] Payload=[..64..] Version=6 TrafficClass=0 FlowLabel=658040 Length=64 NextHeader=ICMPv6 HopLimit=63 SrcIP=2001:db8:10::2 DstIP=2001:db8:20::2 HopByHop=nil}
00000000 60 0a 0a 78 00 40 3a 3f 20 01 0d b8 00 10 00 00 |`..x.@:? .......|
00000010 00 00 00 00 00 00 00 02 20 01 0d b8 00 20 00 00 |........ .... ..|
00000020 00 00 00 00 00 00 00 02 |........|
--- Layer 2 ---
ICMPv6 {Contents=[128, 0, 70, 93] Payload=[..60..] TypeCode=EchoRequest Checksum=18013 TypeBytes=[]}
00000000 80 00 46 5d |..F]|
--- Layer 3 ---
ICMPv6Echo {Contents=[] Payload=[] Identifier=65435 SeqNumber=3}
おわりに
今回は,LWTフックのeBPFの簡単なアプリケーションを作成しました.LWTのプログラムの制限があるので,LWTがどういったユースケースで使えるのかが気になります.今回は使っていないですがLWTのみで使用できるヘルパーは何があるのかを今後調べていきたいです.また,End.BPF
もいつか実行してみたいと思ってます.
参考資料
- bpf: BPF for lightweight tunnel infrastructure
- BPF: A Tour of Program Types, Oracle Linux Blog
- BPF for lightweight tunnel infrastructureについて調べてみた, Qiita
- Mathieu Xhonneux, "An interface for programmable IPv6 Segment Routing network functions in Linux", UCL - Ecole polytechnique de Louvain, 2018
- Lightweight & flow based tunneling, LWN.net
- lwtunnel: add support for encap type bpf, vishvananda/netlink
- eBPF Dynencap + Reflection
Discussion