🐝

ebpf-goによるパケットフィルタリング(XDP)入門

2024/01/02に公開

ebpf-goによるパケットフィルタリング(XDP)入門

ebpf-goを使用したパケットフィルタリング(XDP)について紹介します。

具体例として、MACアドレスおよびIPv4アドレスのフィルタリングについて説明します。
これは、接続元アドレス制限機能に相当します。多くの商用ルータにこの機能が実装されているため、既にご存知の方も多いかと思います。

本ドキュメントで説明する例の仕様は以下の通りです:

  • 接続元MACアドレス制限 有効・無効
  • 接続元MACアドレス 1個
  • 接続元IPv4アドレス制限 有効・無効
  • 接続元IPv4アドレス 1個

XDP

XDPの機能概要は以下のドキュメントで説明されていますので、本記事では簡潔に紹介したいと思います。

XDPでできることを簡潔に言えば、eBPFはLinuxカーネルのネットワークスタックがパケットを受け取る前に、パケットの処理を可能にします。これはData Plane Development Kit(DPDK)と多少の似ている点がありますが、DPDKとは異なり、Linuxカーネルのネットワークスタックの既存の資産をそのまま利用できるという利点があります。一方で、eBPFにはeBPF VMの制限が存在します。
また、XDPは主に受信パケットのハンドリングに限定されます。パケットのドロップや他のインターフェースへの転送などが可能ですが、送信パケットの制御、例えば送信量制御(QoS)などはXDPでは実現できません。これらの機能を実現するためには、eBPFのTC(Traffic Control)プログラムタイプを使用する必要があります。

パケットの流れ一例

NIC -> XDP -> Linuxカーネルネットワークスタック -> アプリケーション

まとめると

  • パケットは、Linuxカーネルのネットワークスタックに到達する前にXDPによって処理される
  • XDPによるパケットの処理後、必要ならばパケットはLinuxカーネルのネットワークスタックでさらに処理される

ebpf-goを用いたカーネルとユーザランド間のデータ転送

本記事で一番説明したい点です。

  • ebpf-goによるコード生成はどうなるか
  • C言語(eBPFコード)のデータ構造をGo言語からどうやって読み書きするのか

ブロック図

データ転送を図に示すと下記の通りです。

ブロック図

eBPFコード

接続元アドレス制限機能のデータ構造の定義は下記の通り。

eBPFコード(addr_restrictions.bpf.c)抜粋

struct addr_restrictions {
	__u32 saddr;           // source ipv4 address
	unsigned char smac[6]; // source mac address
	__u8 enable_saddr;     // enable source ip address restriction
	__u8 enable_smac;      // enable source mac address restriction
};

struct {
	__uint(type, BPF_MAP_TYPE_LRU_HASH);
	__uint(max_entries, MAX_MAP_ENTRIES);
	__type(key, __u32);
	__type(value, struct addr_restrictions);
} addr_restrictions_map SEC(".maps");

自動生成コード

以下は、addr_restrictions.bpf.cからbpf2goによって自動生成されたコードの一部です。eBPFコードのデータ構造がGo言語の構造体として表現されています。

bpf_bpfel.go抜粋

type bpfAddrRestrictions struct {
	Saddr       uint32
	Smac        [6]uint8
	EnableSaddr uint8
	EnableSmac  uint8
}

アプリケーションから自動生成コード呼び出し部分

eBPFのマップは、Put()やLookup()などのメソッドを使用して読み書きが可能です。これらの操作は、Go言語の標準的なマップ操作と同様に行うことができます。

アプリケーションコード(addr_restriction.go)抜粋

// Load pre-compiled programs into the kernel.
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
	log.Fatalf("loading objects: %s", err)
}
defer objs.Close()

// Attach the program.
l, err := link.AttachXDP(link.XDPOptions{
	Program:   objs.XdpAddrRestrictions,
	Interface: iface.Index, // en160, eth0など
})

// GoからのeBPF マップ操作例
// Put
// - objs.AddrRestrictionsMap.Put(key, value)
// Get
// - objs.AddrRestrictionsMap.Lookup(key, &value)

restrictions := bpfAddrRestrictions{
	Saddr:       convertIPStringToUint32("192.168.10.17"),
	Smac:        convertMACStringToUint8Array("5c:1b:f4:7b:1c:49"),
	EnableSaddr: enable,
	EnableSmac:  enable,
}

if err := objs.AddrRestrictionsMap.Put(keyNum, restrictions); err != nil {
	log.Fatalf("putting into map: %s\n", err)
}

XDPによるパケットフィルタリング

処理の流れは下記の通り。

  • ① Etherヘッダをパース
  • ② 接続元アドレス制限のデータをユーザ空間から取得
  • ③ 接続元 MACアドレス制限処理
  • ④ EtherのヘッダからペイロードがIPヘッダか確認
  • ⑤ IPヘッダをパース
  • ⑥ 接続元 IPアドレス制限処理

これは、ヘッダの解析とペイロードの処理を繰り返します。XDPでパケットのハンドリングする場合のパターンです。

SEC("xdp")
int xdp_addr_restrictions(struct xdp_md *ctx)
{
	void *data_end = (void *)(long)ctx->data_end;
	void *data = (void *)(long)ctx->data;

	// ① First, parse the ethernet header.
	struct ethhdr *eth = data;
	if ((void *)(eth + 1) > data_end) {
		return XDP_ABORTED;
	}

	// ②
	int key = KEY_NUM;
	struct addr_restrictions *restrictions = bpf_map_lookup_elem(&addr_restrictions_map, &key);
	if (restrictions == NULL) {
		return XDP_ABORTED;
	}

	// ③
	if (restrictions->enable_smac) {
		for (int i = 0; i < 6; i++) {
			if (eth->h_source[i] != restrictions->smac[i]) {
				// bpf_printk("MAC addresses: %x != %x\n", eth->h_source[i], restrictions->smac[i]);
				return XDP_DROP;
			}
		}
	}

	// ④
	if (eth->h_proto != bpf_htons(ETH_P_IP)) {
		// The protocol is not IPv4, so we can't parse an IPv4 source address.
		return XDP_ABORTED;
	}

	// ⑤ Then parse the IP header.
	struct iphdr *ip = (void *)(eth + 1);
	if ((void *)(ip + 1) > data_end) {
		return XDP_ABORTED;
	}

	// ⑥
	if (restrictions->enable_saddr) {
		if (ip->saddr != restrictions->saddr) {
			// bpf_printk("source IP address: %x != %x\n", ip->saddr, restrictions->saddr);
			return XDP_DROP;
		}
	}

	return XDP_PASS;
}

これは基本的な例ですが、実際にコードを作成し試行することで、理解が深まるでしょう。

参考

本コード

パケットハンドリングの改善

この例でも、プロトコルの手動解析は複雑な作業であることが感じませんでしたか?

この解決策として、p4lang/p4c: P4言語があります。
詳細な説明は略しますが、DSLでプロトコルのハンドリングが可能になります。
P4言語がらeBPFコードに変換する事も可能です。

TCPや独自のプロトコルのパケットの手動解析は現実的な工数では難しいため、このようなツールの使用するのが現実的と思われます。

Discussion