XDP 入門 -- ブリッジ/レイヤー 2 スイッチ転送機能を実現する eBPF プログラム

この記事の高度な部分またはこの記事では、Linux ブリッジをセットアップし、そのブリッジに複数のイーサネット インターフェイスを追加して、最も基本的なレイヤ 2 スイッチのレイヤ 2 スイッチングおよび転送機能を実現する方法について説明しましたLinux ブリッジは、ブリッジの各ポートに接続されている各デバイスの MAC アドレスを学習し、MAC/ポート マッピング テーブルを生成し、MAC/ポート マッピング テーブルに従って受信データ パケットのレイヤ 2 転送を実行できます。

大きく以下の2つの工程に分かれます。

  • MAC アドレスの学習とエージング、MAC/ポート マッピング テーブルの生成と更新 (コントロール プレーン プロセス)
  • MAC/ポートマッピングテーブルに従って、受信したメッセージを正しいポートから転送します(データプレーンプロセス)

Linux ブリッジを構成すると、この機能が自動的に追加されることはわかっていますが、Linux ブリッジを使用してこの機能を実現する場合、各データ パケットは XDP、Qdisc、Bridge_check、netfilter のリンク層を通過する必要があることがわかっています。各テーブル/チェーンの処理後 (詳細についてはこの記事を参照)、正しい出口に転送できますが、転送リンクは非常に長く、効率とパフォーマンスは比較的低くなります。

ここでは、BPF フレームワークによって実装された Linux ブリッジの基本機能を使用して、Qdisc、bridge_check、netfilter などのカーネル プロトコル スタック ハンドラーをバイパスし、よりパフォーマンスの高い Linux ブリッジを実現します。
この例では、データ プレーンとコントロール プレーンの分離が実現されます (読者はユーザー モード プログラムで eBPF バイトコードを自動的にロードおよびアンロードする方法を理解していることが前提となります)。

  • BPF プログラムは、XDP フレームワークの MAC アドレスとポートの対応テーブルを照会して、メッセージ転送を実現します。
  • MACアドレスとポートの対応表の更新とエージングは​​、ユーザーモードで動作するアプリケーションプログラムによって管理されます。

これを利用して、BPF、BPF マップのフレームワーク、および BPF プログラムの動作原理をさらに理解することができます。

1. テスト環境

テスト環境は、前の記事「XDP の使用開始 – ユーザー モード プログラムを使用したネットワーク カードへの eBPF プログラム バイトコードの自動ロードとアンロード」と同じです

ハードウェア: Raspberry Pi Zero + 2 つのイーサネット カードを備えた拡張ベースボードに基づく ---- 図の RPi ネットワーク
: 以下の図に示すように

                                                     +- RPi -------+          +- old pc1----+
                                                     |         Eth0+----------+ Eth0        |    
                 +- Router ----+                     |  DHCP server|          | 10.0.0.10   |
                 | Firewall    |                     |   10.0.0.1  |          |             |
(Internet)---WAN-+ DHCP server +-WLAN AP-+-)))   (((-+ WLAN        |          +-------------+
                 | 192.168.3.1 |                     |             |          
                 +-------------+                     |             |          +- old pc2----+
                                                     |         Eth1+----------+ Eth0        |   
                                                     |             |          | 10.0.0.4    |                                                       
                                                     +-------------+          |             |
                                                                              +-------------+

ここに画像の説明を挿入

2. eBPF バイトコード ソース コードの実装

実装の原理は非常に単純で、受信した各メッセージのターゲット MAC アドレスを確認し、MAC アドレスとポートの対応テーブルでクエリを実行します。

  1. 見つからない場合は、処理のためにカーネルの tcp/ip プロトコル スタックに送信されます。
  2. 見つかった場合は、ターゲット MAC アドレスに対応するポートに従ってメッセージを直接転送します。
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include "/usr/include/bpf/bpf_helpers.h"

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif


// mac_port_map保存该目标MAC地址/端口的映射关系表,以目标MAC地址为key, 以端口为value
struct bpf_map_def __section("maps") mac_port_map = {
    
    
        .type = BPF_MAP_TYPE_HASH,
        .key_size = sizeof(long long),
        .value_size = sizeof(int),
        .max_entries = 100,
};

__section("prog")
int xdp_bridge_prog(struct xdp_md *ctx)
{
    
    
        void *data_end = (void *)(long)ctx->data_end;
        void *data = (void *)(long)ctx->data;
        long dst_mac = 0;
        int in_index = ctx->ingress_ifindex, *out_index;
        // data即数据包开始位置
        struct ethhdr *eth = (struct ethhdr *)data;
        char info_fmt[] = "Dst Addr:0x%llx From:[%d]---Redirect to:[%d]\r\n";
        char info_fmt1[] = "xdp_pass";
        char info_fmt2[] = "xdp_drop";
        
        // 错误包检查,必选 
        if (data + sizeof(struct ethhdr) > data_end) {
    
    
                return XDP_DROP;
        }

        // 获取目标MAC地址
        __builtin_memcpy(&dst_mac, eth->h_dest, 6);

        // 目标MAC地址/端口的映射表里查找目标端口
        out_index = bpf_map_lookup_elem(&mac_port_map, &dst_mac);
        if (out_index == 0) {
    
    
                // 如若找不到,则上传到内核TCP/IP协议栈处理
                bpf_trace_printk(info_fmt1, sizeof(info_fmt1));
                return XDP_PASS;
        }
        // 错误报文,进出同一个端口的,丢弃
        if (in_index == *out_index) {
    
    
                bpf_trace_printk(info_fmt2, sizeof(info_fmt2));
                return XDP_DROP;
        }

        // 打印转发信息到/sys/kernel/tracing/trace_pipe
        bpf_trace_printk(info_fmt, sizeof(info_fmt), dst_mac, in_index, *out_index);

        // 转发到目标端口
        return  bpf_redirect(*out_index, 0);
}

char _license[] SEC("license") = "GPL";

3. ユーザー状態アプリケーション層の管理および制御プログラムのソース コード実装

この関数も単純です。

  1. 指定された eBPF バイトコードを 2 つの NIC すべてにロードします
  2. MAC アドレス通知の更新をリッスンし、情報を削除し、eBPF バイトコード クエリの mac_port_map テーブルに対するターゲット MAC アドレス/ポートのマッピング関係を更新します。
  3. 終了時に eBPF バイトコードを自動的にアンロードする
#include <stdio.h>
#include <signal.h>
#include <sys/socket.h>
#include <net/if.h>
#include <bpf/bpf.h>
#include <linux/bpf.h>
#include <linux/rtnetlink.h>
#include "/usr/src/linux-6.1/tools/testing/selftests/bpf/bpf_util.h"

int flags = XDP_FLAGS_UPDATE_IF_NOEXIST;
static int mac_port_map_fd;
static int *ifindex_list;

// 退出时卸载XDP
static void int_exit(int sig)
{
    
    
        int i = 0;
        for (i = 0; i < 2; i++) {
    
    
                bpf_set_link_xdp_fd(ifindex_list[i], -1, 0);
        }
        exit(0);
}

int main(int argc, char *argv[])
{
    
    
        int sock, i;
        char buf[1024];
        char filename[64];
        static struct sockaddr_nl g_addr;
        struct bpf_object *obj;
        struct bpf_prog_load_attr prog_load_attr = {
    
    
                .prog_type      = BPF_PROG_TYPE_XDP,
        };
        int prog_fd;
        printf("we are starting...\r\n");
        snprintf(filename, sizeof(filename), "bridge.o");
        prog_load_attr.file = filename;

        // 载入eBPF代码
        if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd)) {
    
    
                return 1;
        }

        mac_port_map_fd = bpf_object__find_map_fd_by_name(obj, "mac_port_map");
        ifindex_list = (int *)calloc(2, sizeof(int *));

        //通过ifname查询所有二个网卡的ifindex
        ifindex_list[0] = if_nametoindex(argv[1]);
        ifindex_list[1] = if_nametoindex(argv[2]);

        for (i = 0; i < 2; i++) {
    
    
                // 将eBPF字节码注入到所有网卡
                if (bpf_set_link_xdp_fd(ifindex_list[i], prog_fd, flags) < 0) {
    
    
                        printf("link set xdp fd failed\n");
                        return 1;
                }
        }
        // 设置CTRL+C退出程序时要执行的卸载函数
        signal(SIGINT, int_exit);

        bzero(&g_addr, sizeof(g_addr));
        g_addr.nl_family = AF_NETLINK;
        g_addr.nl_groups = RTM_NEWNEIGH;
        printf("we are starting socket...\r\n");
        if ((sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)) < 0) {
    
    
                int_exit(0);
                return -1;
        }

        if (bind(sock, (struct sockaddr *) &g_addr, sizeof(g_addr)) < 0) {
    
    
                int_exit(0);
                return 1;
        }


        // 持续监听socket,捕获更新信息,更新删除MAC/端口对应表
        while (1) {
    
    
                int len;
                struct nlmsghdr *nh;
                struct ndmsg *ifimsg ;
                int ifindex = 0;
                unsigned char *cmac;
                unsigned long long lkey = 0;

                len = recv(sock, buf, sizeof(buf), 0);
                printf("recv...\r\n");
                if (len <= 0) continue;
                printf("get mac notification\r\n");
                for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) {
    
    
                        ifimsg = NLMSG_DATA(nh) ;
                        if (ifimsg->ndm_family != AF_BRIDGE) {
    
    
                                continue;
                                printf("not AF_BRIDGE\r\n");
                        }

                        printf("AF_BRIDGE\r\n");
                        // 获取notify信息中的端口
                        ifindex = ifimsg->ndm_ifindex;
                        for (i = 0; i < 2; i++) {
    
    
                                printf("find ifindex = %d\r\n", ifindex);
                                if (ifindex == ifindex_list[i]) break;
                        }
                        if (i == 2) continue;

                        printf("i=%d,  ifindex=%d\r\n",i,ifindex);
                        // 获取notify信息中的MAC地址
                        cmac = (unsigned char *)ifimsg + sizeof(struct ndmsg) + 4;

                        memcpy(&lkey, cmac, 6);
                        printf("sizeof lkey %d\r\n", sizeof(lkey));
                        printf("2nd i=%d, ifindex=%d\r\n",i,ifindex);
                        if (nh->nlmsg_type == RTM_DELNEIGH) {
    
    
                                bpf_map_delete_elem(mac_port_map_fd, (const void *)&lkey);
                                printf("Delete XDP item from [HW Address:Port] table-------------[0x%llx]:[%d]\r\n", lkey, ifindex);
                        } else if (nh->nlmsg_type == RTM_NEWNEIGH) {
    
    
                                bpf_map_update_elem(mac_port_map_fd, (const void *)&lkey, (const void *)&ifindex, 0);
                                printf("Update XDP item from [HW Address:Port] table-------------[0x%llx]:[%d]\r\n", lkey, ifindex);
                        }
                }
                printf("out of for()\r\n");
        }
}

4. コンパイルして実行する

  • コンパイル
sudo clang -O2 -Wall -target bpf -c bridge.c -o bridge.o
gcc main.c -lbpf

現在のディレクトリに 2 つのファイル a.out とbrideg.o を生成します

  • 走る
 sudo ./a.out eth0 eth1

5. 試験結果

このコードを、ブリッジの機能をオフにしてオンにしてテストしました。

次のように進めます。

  1. 初期状態ではブリッジ br0 が有効になっており、プログラムが実行されていないときに、10.0.0.4 から 10.0.0.10 に ping を実行し、tcpdump コマンドを使用してネットワーク ポート 10.0.0.4 上のパケットをキャプチャします。
    見られます:
  • pingは機能します
    ここに画像の説明を挿入

  • tcpdump は icmp 要求/応答メッセージを確認できます
    ここに画像の説明を挿入

  1. プログラムを実行し、最初にブリッジを閉じてから開きます。
  • プログラムを実行する
sudo ./a.out eth0 eth1
  • 別の端末でブリッジのクローズとオープンを実行して、MAC アドレス ポートの更新通知をトリガーします
sudo ifconfig br0 down
sudo ifconfig br0 up

ユーザー インターフェイス プログラムは次のように出力します。最初に 2 つのエントリを削除し、次に 2 つのエントリを追加します。

get mac notification
AF_BRIDGE
find ifindex = 3
i=0,  ifindex=3
sizeof lkey 8
2nd i=0, ifindex=3
Delete XDP item from [HW Address:Port] table-------------[0x26513558577c]:[3]
out of for()
recv...
get mac notification
AF_BRIDGE
find ifindex = 4
find ifindex = 4
i=1,  ifindex=4
sizeof lkey 8
2nd i=1, ifindex=4
Delete XDP item from [HW Address:Port] table-------------[0x40f046730318]:[4]
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
AF_BRIDGE
find ifindex = 3
i=0,  ifindex=3
sizeof lkey 8
2nd i=0, ifindex=3
Update XDP item from [HW Address:Port] table-------------[0x26513558577c]:[3]
out of for()
recv...
get mac notification
AF_BRIDGE
find ifindex = 4
find ifindex = 4
i=1,  ifindex=4
sizeof lkey 8
2nd i=1, ifindex=4
Update XDP item from [HW Address:Port] table-------------[0x40f046730318]:[4]
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()

  • ping と tcpdump の状態を確認する
    この時点で ping は接続されていますが、icmp の req/reply メッセージが tcpdump でキャッチできません
    ここに画像の説明を挿入
    ここに画像の説明を挿入
  • カーネル印刷ログレコードの表示

10.0.0.4 と 10.0.0.10 の間の ping パケットが XDP で直接転送されたことがわかります。

sudo cat /sys/kernel/tracing/trace_pipe

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/meihualing/article/details/130842827