🌐

cilium/ebpfを使ってLightweight Tunnel (LWT)フックでeBPFを実行してみた話

2023/06/12に公開

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/netlinkgo 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もいつか実行してみたいと思ってます.

参考資料

Discussion