お腹.ヘッタ。

進捗つらぽよ

XDPでBUMを運べるわけないじゃん、ムリムリ!(※ムリじゃなかった!?)

目的

XDP_CLONE: Efficient In-Driver Packet Duplication for eBPF-Accelerated Systemsという論文が eBPF'26で出ていました、一章では、bpf_redirect_map()とBPF_F_BROADCASTによる複製を説明しています。

その直後に続く、個別の書き換えについての記述を抜粋します。

the program has no way to rewrite each copy independently

(訳: そのプログラムには、各コピーを個別に書き換える手段がない。)

同様に3章でも、複製したパケットを変更できないという説明を繰り返しています。

しかし、この説明は誤りです。 devmap broadcastとdevmap egress programを組み合わせれば、既存のXDPでもパケットを複製し、コピーごとに書き換えられます。

BUMは、ブロードキャスト、宛先不明のユニキャスト、マルチキャストをまとめた呼び方です。 例えば、VXLANやSRv6でL2ネットワークを構成し、BUMを複数の対向へユニキャストで配信する場面で、この書き換えが必要になります。

元のEthernetフレームは同じでも、コピーごとに外側の宛先を変える必要があります。 この記事ではVXLANを例に、この処理を実装します。

私は以前からXDPでこうした処理を実現できると知っていたので、論文の説明を読み、この使い方が十分に知られていないのだと感じました。 実際、私がAIに尋ねたときにも、XDPで分類し、TCのbpf_clone_redirect()で複製する構成が提案されました。 その構成でも動作しますが、TCではskbを扱うため、XDPで複製まで完結させる場合との性能差も気になります。 そこで、既存のXDPによる複製と個別の書き換えを、動くコードで説明することにしました。

性能については、TC併用版も用意し、後半で測定結果を示します。

説明より先にコードが欲しい人は、拙作のgoxdp-templateを見てください。 XDPを書くためのテンプレートとして作り始めましたが、最近は動くサンプル集に近くなっています。 今回のブログの元になったVXLANの実装も置いているので、BUMを実装するときや、AIに具体的なコードを渡して相談するときの参考になると思います。 参考になったら、スターを付けてもらえるとうれしいです。

github.com

まずは、受信フレームをそのまま複製する基本形から見ていきましょう。

XDPで単純なBroadcastを実現するには?

基本形は、devmapに登録した出力インターフェースへ、受信フレームをそのまま複製して送る処理です。 BPF_MAP_TYPE_DEVMAPのマップを用意し、BPF_F_BROADCASTを指定してリダイレクトします。 次のプログラムが、その基本形になります。

#include <linux/bpf.h>
#include <linux/if_ether.h>

#include <bpf/bpf_helpers.h>

struct {
  __uint(type, BPF_MAP_TYPE_DEVMAP);
  __uint(max_entries, 64);
  __type(key, __u32);
  __type(value, __u32);
} tx_ports SEC(".maps");

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

  if (data + sizeof(struct ethhdr) > data_end) return XDP_DROP;

  return bpf_redirect_map(&tx_ports, 0,
                          BPF_F_BROADCAST | BPF_F_EXCLUDE_INGRESS);
}

char __license[] SEC("license") = "Dual MIT/GPL";

ユーザーランドからtx_portsの各スロットへ出力先のifindexを設定すると、そのインターフェースが複製先になります。 例えば、受信ポート以外の二つの出力先を登録すれば、同じ内容のフレームをそれぞれに一つずつ送ります。 受信したフレームのMACやIP、TTLを変更する処理は入れていません。

ここで指定した二つのフラグには、次の意味があります。

  • BPF_F_BROADCASTを指定すると、カーネルはマップに登録された出力へフレームを複製します。このとき第2引数のキーは使いません。
  • BPF_F_EXCLUDE_INGRESSを指定すると、受信したインターフェースを送信先から外します。受信ポートをグループに含めても、そこへ送り返すコピーは作りません。

ここでいうbroadcastは、マップに登録した出力への複製を指します。 Ethernetの宛先MACを変更する指定ではないため、入力フレームの宛先がユニキャストでも使えます。 グループごとにdevmapを用意すれば、送信先をグループで選ぶマルチキャスト転送も構成できます。 この使い方は、カーネルのdevmapドキュメントでも説明されています。

docs.kernel.org

この基本形をVXLANへ応用するには、複製先の選択と、コピーごとの書き換えを考える必要があります。 VNIはVXLANの論理ネットワークを識別する値、VTEPはVXLANトンネルの端点です。 以下では、VXLANの外側のヘッダーをOuter、内側のEthernetフレームをInnerと呼びます。

  • 課題1: VNIごとに複製先のポートを選ぶには?
    • 全ポートを一つのdevmapへ入れると、別のVNIに属するポートへもコピーを送ってしまいますのでなんとかする必要があります。
  • 課題2: コピーごとにVXLANの送信先を変えるには?
    • 複数のVTEPへユニキャストでBUMを配信するとき、Innerは同じ内容に保ちつつも、Outerの宛先は、コピーごとに変える必要があります。

では、この二つの課題をどう解決するか、順に見ていきましょう。

課題1: VNIごとに複製先のポートを選ぶには?

これの解決方法は、VNIごとにdevmapを作り、受信したパケットのVNIに対応するマップを選ばせて、複製先をそのVNIのメンバーに限定させることで解決させます。

具体的には マップの値として別のマップへの参照を持つmap-in-mapを使うことです。

まずやりたいことを考えてみましょう。VNI 100のフレームはVNI 100のメンバーへ、VNI 200のフレームはVNI 200のメンバーへ送ります。 BPF_F_BROADCASTで複製するときに全ポートへ送らないよう、呼び出し前に対象のdevmapを選んでおきます。

このmapはmap-in-mapになっており、 - 外側のvni_groupsはBPF_MAP_TYPE_HASH_OF_MAPSで、VNIをキーに内側のマップを選びます。 - 内側はBPF_MAP_TYPE_DEVMAP_HASHで、そのVNIのメンバーだけを登録します。 - 内側のキーには出力ifindexを使い、値には出力ifindexとegress programを持たせます。

マップの定義はxdp_vxlan.c、VNIごとのdevmapの作成と登録はGo側のUpdateMembers()にあります。

転送時には、対象VNIのdevmapを取り出してからBPF_F_BROADCASTを指定します。

void *members = bpf_map_lookup_elem(&vni_groups, &vni);
if (!members)
    return XDP_DROP;

return bpf_redirect_map(members, 0,
                       BPF_F_BROADCAST | BPF_F_EXCLUDE_INGRESS);

この呼び出しでは、membersが指すdevmapに登録された出力へコピーを送ります。 第2引数のキーを使わなくても、先にVNIでマップを選ぶことで複製先を限定できます。 内側のマップへの参照を取得する方法は、カーネルのmap-in-mapドキュメントにも説明されています。

イメージとしては以下の様な図になります。

外側のマップがVNIごとのdevmapを参照し、各devmapがそのVNIの出力を保持し、access1とaccess2は端末側、uplink0はVXLANの送受信に使うインターフェースです。 同じuplink0を両方に登録しても、VNI 100からaccess2へのコピーは作りません。

flowchart LR
    accTitle: VNI-scoped BUM replication with map-in-map
    accDescr: The outer vni_groups hash maps VNI 100 and VNI 200 to separate devmaps. Each devmap contains one access port and the shared uplink0. Broadcast uses only the selected devmap, so the shared uplink does not merge the VNI groups. The arrows show map references and membership.
    subgraph outer["vni_groups: HASH_OF_MAPS"]
        v100["key: VNI 100"]
        v200["key: VNI 200"]
    end
    v100 -->|map reference| a["DEVMAP_HASH A<br/>VNI 100 members"]
    v200 -->|map reference| b["DEVMAP_HASH B<br/>VNI 200 members"]
    a --> access1["access1<br/>VNI 100"]
    a --> uplink["uplink0<br/>shared tunnel interface"]
    b --> uplink
    b --> access2["access2<br/>VNI 200"]
    classDef default fill:#ffffff,stroke:#666666,color:#222222;
    class v100,v200,a,b map;
    classDef map fill:#edf4ee,stroke:#7b9180,color:#222222;

同じ出力ifindexを複数VNIのdevmapへ登録しても、各VNIの複製先は別々に管理できます。

例えばuplink0をVNI 100と200で共有しながら、それぞれのVNIに属するaccessポートを分けられます。 ただし、uplink0へ出すコピーの宛先VTEPはVNIごとに異なるため、ポートの選択に加えてOuterの書き換えも必要になります。

課題2: コピーごとにVXLANの送信先を変えるには?

これの解決方法は、コピーごとの書き換えには、devmapに関連付けたegress programを使います。

基本形では、devmapの値に出力先のifindexだけを登録し、受信フレームをそのまま複製して送りました。 コピーごとに書き換えるには、出力先のifindexに加え、送信前に実行するXDPプログラムを同じエントリーへ登録します。 このプログラムがdevmap egress programです。

受信時のXDPプログラムは、基本形と同じくbpf_redirect_map()とBPF_F_BROADCASTで複製を指示します。 カーネルは各出力へフレームを振り分けた後、各コピーの送信前に、その出力のエントリーに登録したegress programを実行します。 そこでOuterの宛先などを書き換えます。

同じegress programを複数のエントリーに登録して使えます。 プログラム内では、ctx->egress_ifindexから、そのコピーがどのインターフェースへ送られるかを調べられます。 例えば、複製先の二つの出力インターフェースをp2とp3とします。 同じVNIのフレームを両方へ複製すると、p2向けのコピーではこの値がp2のifindex、p3向けではp3のifindexになります。

その値に応じて、p2向けのOuterの宛先IPを192.0.2.2、p3向けを192.0.3.2に変えられます。 p2向けのコピーを書き換えても、p3向けのコピーの内容には影響しません。

マップ定義の値の型も、基本形の__u32から、ifindexとプログラムを持つstruct bpf_devmap_valへ変えます。 この構造体のbpf_prog.fdに、アタッチタイプがBPF_XDP_DEVMAPのXDPプログラムを指定します。 BPF側では、このプログラムをSEC("xdp/devmap")で定義します。

Goから登録する場合、devmapの値は出力ifindexとプログラムFDの二つの32ビット値になります。 次の例のmembersは対象VNIのdevmap、egressはロード済みのegress programを指します。

value := [2]uint32{uint32(ifindex), uint32(egress.FD())}
if err := members.Update(value[0], value, ebpf.UpdateAny); err != nil {
    return err
}

この登録処理は、Go側のUpdateMembers()で行っています。

複製したフレームに対して別のXDPプログラムを実行する仕組みは、既存カーネルに含まれています。 複製とプログラム実行は、Linux 7.3のdev_map_enqueue_multi()とdev_map_bpf_prog_run()でも確認できます。

今回の実装では、VNIとctx->egress_ifindexの組から相手のVTEPを検索し、Outerの送信元と宛先を書き換えます。 次のコードは、ヘッダーの検証とpeerの検索を終えた後の、vxlan_rewrite()の書き換え部分です。

__builtin_memcpy(eth->h_source, port.mac, ETH_ALEN);
__builtin_memcpy(eth->h_dest, target.mac, ETH_ALEN);
ip->saddr = port.local_ip;
ip->daddr = target.remote_ip;
checksum(ip);
return XDP_PASS;

ここで返すXDP_PASSは、devmapで選んだ出力への送信を続ける指示です。 受信時のXDPプログラムで返す場合とは意味が異なり、Linuxのネットワークスタックへ渡す処理ではありません。 XDP_DROPを返すと、その出力向けのコピーを破棄します。

書き換えの対象はOuterだけで、Innerフレームは各コピーで同じ内容を保ちます。 UDPとVXLANのヘッダーも共通にできます。 Outer UDP送信元ポートはInnerのフローハッシュから決めるため、同じフローでは安定し、異なるフローでは値を変えられます。

一旦図で見てみましょう。 このように複製後の各egressでOuterを付け、同じInnerを異なるVTEPへ送ります。

flowchart TB
    accTitle: VXLAN replication with per-output rewriting
    accDescr: One inner frame is replicated to two tunnel outputs and one access output. Tunnel copies get different outer IP addresses while the inner frame remains identical.
    inner["Inner F"] --> prepare["Prepare Outer template<br/>header_cache[key]"]
    prepare --> group["DEVMAP broadcast<br/>Inner F + metadata"]
    group --> b["p2: egress rewrite<br/>attach Outer"]
    group --> c["p3: egress rewrite<br/>attach Outer"]
    group --> access["Access egress<br/>remove metadata"]
    b --> packetB["Outer dst: 192.0.2.2<br/>UDP / VXLAN / Inner F"]
    c --> packetC["Outer dst: 192.0.3.2<br/>UDP / VXLAN / Inner F"]
    access --> local["Access output<br/>Inner F"]
    classDef default fill:#ffffff,stroke:#666666,color:#222222;
    class prepare,b,c,access process;
    classDef process fill:#edf3fa,stroke:#74849a,color:#222222;
    class group map;
    classDef map fill:#edf4ee,stroke:#7b9180,color:#222222;

現在のサンプルでは、VNIと出力ifindexの組ごとに一つのunicast peerを設定します。

同じVNIのフレームを複数peerへ複製するために、異なる出力インターフェースを使っています。 これはサンプルの構成上の制約であり、カーネルがdevmapのキーをifindexに限定しているわけではありません。 同じVNIから同じ出力インターフェースを通して複数VTEPへ複製するには、コピーごとに宛先VTEPを識別する仕組みを追加する必要があります。

これで、複製先の選択とコピーごとの書き換えがそろいました。 二つの処理をつないだ全体像を確認してみましょう。

複製と書き換えをつないだ全体像

ざっくり言葉でまとめると、、、

  • VNIごとのdevmapで複製先を限定し、各エントリーに登録したegress programでコピーを書き換えれば、同じInnerを複数のVTEPへ送れます
  • ingressが複製先のグループを選び、devmap egressが各出力向けにパケットを組み上げます

図としてはこの様な感じで、ingressで対象VNIのdevmapを選び、複製後の各egressで出力先に応じた処理を行います。

flowchart TB
    accTitle: VNI selection, replication, and per-copy rewriting
    accDescr: Ingress selects a VNI and its devmap. Broadcast replicates the Inner frame to the group's members. Each tunnel egress adds Outer headers and rewrites its destination, while access egress sends the original Inner frame.
    input["Received frame"] --> ingress["Ingress XDP<br/>validate / select VNI"]
    ingress --> group["DEVMAP for selected VNI<br/>replicate to members"]
    group -->|"tunnel copy"| tunnel["Devmap egress<br/>attach Outer<br/>rewrite destination"]
    group -->|"access copy"| access["Devmap egress<br/>keep Inner unchanged"]
    tunnel --> peers["Unicast to each VTEP<br/>same Inner / different Outer"]
    access --> endpoint["Access output<br/>original Inner"]
    classDef default fill:#ffffff,stroke:#666666,color:#222222;
    class ingress,tunnel,access process;
    classDef process fill:#edf3fa,stroke:#74849a,color:#222222;
    class group map;
    classDef map fill:#edf4ee,stroke:#7b9180,color:#222222;

ここからは、じゃあ具体的にVNIを与えるための処理を考えてみましょう。 例えば、Outerの共通部分をどこで準備するか、ingressで決めたVNIや処理内容を各egressへどう渡すかを考える必要があります。

この部分はいろんな実装が考えられますので、あくまで一例としてみてもらえればと思います。

プログラム間のデータと処理指示の受け渡し

各egressで呼ばれるプログラムには、書き換えに使うヘッダー情報と、実行する処理や引数を渡す必要があります。 このサンプルでは、共通ヘッダーを複製前にマップへ保存し、処理IDと引数を32バイトのdata_metaで引き継ぎます。

格納先 内容
data_metaの先頭16バイト 処理IDのprogramと検証用のmagicを各64ビットで保持します。
data_metaの後半16バイト vni、flags、inner_len、flow_hashを各32ビットで保持します。
header_cache Ethernet、IPv4、UDP、VXLANの共通テンプレートです。

ingressではPROGRAM_FORWARDで転送処理を進め、複製前にPROGRAM_REWRITEへ切り替えます。 egressは共通のディスパッチャーでコマンドの長さ、magic、処理IDを検証し、書き換えハンドラーへテールコールします。 コマンドが不正な場合やハンドラーが未登録の場合は、パケットを破棄します。

図を見てみましょう。複製時のパケット本体はInnerのままで、tunnel向けのコピーだけが、マップから取り出した50バイトのOuterを付ける事が想像できると思います。

%%{init: {"theme":"base","look":"classic","fontFamily":"DejaVu Sans, sans-serif","themeVariables":{"lineColor":"#666666","clusterBkg":"#fafafa","clusterBorder":"#aaaaaa","edgeLabelBackground":"#ffffff"},"flowchart":{"htmlLabels":false,"curve":"linear"},"themeCSS":".edgeLabel rect, .edgeLabel .background { fill: #ffffff !important; opacity: 1 !important; } .labelBkg { background-color: #ffffff !important; }"}}%%
flowchart TB
    accTitle: Shared Outer template and metadata handoff
    accDescr: A 1514-byte Inner frame stays unchanged while its shared 50-byte Outer template is saved in a Per-CPU LRU map. Devmap checks output MTUs and replicates the Inner frame with a 32-byte command. Access egress keeps the Inner frame. Tunnel egress retrieves or rebuilds the template, checks the final length, prepends Outer, and rewrites addresses.
    inner["Inner frame: 1514 B"] --> prepare["Ingress forwarding<br/>prepare shared Outer template"]
    prepare -. "store template" .-> cache[("Per-CPU LRU: header_cache<br/>key: (VNI, Inner length)<br/>Outer template: 50 B")]
    prepare -->|"32 B metadata<br/>REWRITE + arguments"| group["DEVMAP<br/>check MTU per output<br/>replicate Inner frame"]
    group -->|"access MTU: 1500"| access["Access egress<br/>Inner frame: 1514 B"]
    group -->|"tunnel MTU: 1600"| tunnel["Tunnel egress<br/>check final length<br/>prepend 50 B / rewrite"]
    cache -. "read / validate<br/>rebuild on miss" .-> tunnel
    tunnel --> output["Outer 50 B + Inner 1514 B<br/>frame: 1564 B<br/>Outer IP packet: 1550 B"]
    classDef default fill:#ffffff,stroke:#666666,color:#222222;
    class prepare,access,tunnel process;
    classDef process fill:#edf3fa,stroke:#74849a,color:#222222;
    class group,cache map;
    classDef map fill:#edf4ee,stroke:#7b9180,color:#222222;

devmapはegress実行前にMTUを検査するため、複製前にはOuterを付けません。 access向けはInnerの長さで検査し、tunnel向けはegressでOuterを含む最終長も検査します。 VXLAN_OVERHEADの50バイトは、Ethernet 14、IPv4 20、UDP 8、VXLAN 8バイトの合計です。

vxlan_rewrite()では、32バイトのコマンドを外し、先頭に50バイト確保してテンプレートをコピーします。 次の抜粋では、headerに取得して検証を済ませたテンプレートが入っています。

if (bpf_xdp_adjust_meta(ctx, sizeof(struct xdp_command)) ||
    bpf_xdp_adjust_head(ctx, -VXLAN_OVERHEAD))
  return XDP_DROP;
data = (void*)(long)ctx->data;
end = (void*)(long)ctx->data_end;
if (data + VXLAN_OVERHEAD > end) return XDP_DROP;
__builtin_memcpy(data, header.bytes, VXLAN_OVERHEAD);

VLANとVNIによるBUMの分離

ここまでは、対象のVNIが決まっているものとして複製先を選びました。 同じtrunkで複数のVLANを扱う場合は、その前に入力フレームが属するVNIを決める必要があります。 今回のVLAN対応ポートでは、入力ifindexとVIDの組からVNIを選び、そのVNIのdevmapへBUMを複製します。 例えば、一つのtrunkでVID 10をVNI 100へ、VID 20をVNI 200へ割り当てられます。 分類にはVIDだけでなく入力ifindexも使うため、別のポートのVID 10をVNI 300へ割り当てても、VNI 100の複製先とは混ざりません。

ポートごとの対応はvlansに設定します。 次の設定では、一つのtrunkで二つのVIDを受け入れます。 各VNIのグループには、このtrunkと必要な出力ポートを登録します。

{
  "device": "trunk0",
  "vlans": [
    {"vid": 10, "vni": 100},
    {"vid": 20, "vni": 200}
  ]
}

ingressでは、802.1Qタグを検証してVIDを取り出し、vlan_bindingsからVNIを取得します。 検索部分を抜き出すと、次のようになります。

struct vlan_key key = {.ifindex = ingress, .vid = vid};
__u32* binding = bpf_map_lookup_elem(&vlan_bindings, &key);
if (!binding) return XDP_DROP;
vni = *binding;

取得したVNIを使い、vni_groupsからdevmapを選びます。 分類したVNIはメタデータで引き継ぎ、VIDはInnerに残したタグからegressで読み直します。 このため、32バイトのコマンド領域を増やす必要はありません。

共有trunkへの出力では、VNIへの所属とInnerのVIDを組み合わせて検証します。 例えばVNI 100と200の両方に属するtrunkでも、VNI 100のコピーにはVID 10を要求します。 VNI 100のパケットにVID 20が入っていた場合は、そのtrunkへ出しません。 この検証には、port_vnisに登録したVNIと出力ifindexの組、および期待するVIDを使います。

VXLAN受信時はOuterを外して同じVNIのaccessへ送り、他のtunnelへは転送しないsplit horizonでループを防ぎます。 その際も、Innerに同じegress検証を適用します。 さらに、受信tunnelのifindexとVXLANのVNIが許可された組であることを、カプセル化解除前に確認します。 VNIがホスト全体で設定されていても、別のtunnelポートからそのVNIを受信できるとは限りません。

%%{init: {"theme":"base","look":"classic","fontFamily":"DejaVu Sans, sans-serif","themeVariables":{"lineColor":"#666666","clusterBkg":"#fafafa","clusterBorder":"#aaaaaa","edgeLabelBackground":"#ffffff"},"flowchart":{"htmlLabels":false,"curve":"linear"},"themeCSS":".edgeLabel rect, .edgeLabel .background { fill: #ffffff !important; opacity: 1 !important; } .labelBkg { background-color: #ffffff !important; }"}}%%
flowchart TB
    accTitle: VLAN-scoped BUM forwarding across VXLAN
    accDescr: A tagged frame on trunk0 selects VNI 100 for VID 10 or VNI 200 for VID 20. Each devmap sends local copies and a tunnel copy with its original inner tag. The remote VTEP checks tunnel admission, decapsulates, and validates output membership and the inner VID before sending to a shared trunk.
    input["trunk0: one 802.1Q frame<br/>Inner VID 10 or VID 20"] --> classify["vlan_bindings<br/>(ingress ifindex, VID) → VNI"]
    classify -->|VID 10| a["vni_groups[100]<br/>DEVMAP broadcast"]
    classify -->|VID 20| b["vni_groups[200]<br/>DEVMAP broadcast"]
    a --> local10["Local VID 10 output<br/>Inner unchanged"]
    b --> local20["Local VID 20 output<br/>Inner unchanged"]
    a --> tunnel10["Shared uplink<br/>Outer VNI 100 / Inner VID 10"]
    b --> tunnel20["Shared uplink<br/>Outer VNI 200 / Inner VID 20"]
    decap["Remote VTEP ingress<br/>check allowed tunnel / VNI<br/>then decapsulate"] --> policy["Remote egress: port_vnis<br/>(VNI, egress ifindex)<br/>require expected Inner VID"]
    policy -->|Match| output["Remote trunk<br/>Inner tag unchanged"]
    policy -->|Mismatch| drop["Drop this output copy"]
    tunnel10 --> decap
    tunnel20 --> decap
    class drop rejected;
    classDef default fill:#ffffff,stroke:#666666,color:#222222;
    classDef rejected fill:#ffffff,stroke:#666666,color:#222222,stroke-dasharray:4 3;
    class classify,decap,policy process;
    classDef process fill:#edf3fa,stroke:#74849a,color:#222222;
    class a,b map;
    classDef map fill:#edf4ee,stroke:#7b9180,color:#222222;

分岐はVID 10と20の別々の入力フレームを表し、各フレームは一つのVNIを選びます。 tunnel向けにはInnerのタグを保持したコピーを送り、受信側は出力ポートのVNIとVIDを検証します。

この実装は、単一の802.1Qタグを保持したまま転送する構成に絞りました。 同じVNIに属するVLAN対応ポートには、同じVIDを設定します。 対向VTEPも、そのVIDを受け入れるように設定します。 タグの付け外しやVID変換は実装していません。 同じVNI内に、従来の固定VNIポートとVLAN対応ポートを混在させる設定も拒否します。 従来の固定VNIポートだけを使う設定では、Innerのタグをそのまま転送します。

VLAN対応ポートは、未登録VID、タグなし、VID 0と4095、途中で切れたタグ、802.1ad、二重タグを破棄します。 PCPとDEIは分類に使わず、タグのビットをそのまま保持します。 実NICのRX VLAN strippingでタグが取り除かれた場合も、そのフレームをタグなしとして破棄します。そのため、転送するには、strippingを無効にする必要があります。

UpdateMembers()では、当初設定したグループからメンバーを外したり、外したメンバーを戻したりできます。 この更新ではVLANの分類や受信許可は変わりません。VIDとVNIの対応を変える場合は、設定を再ロードします。

ちなみにですが、完全なJSON設定例と許可条件はVLAN対応の説明にまとめてたりしてます。(割と端折った)

ここまでの構成が実際に動くか、netnsとvethで確認してみましょう。

netnsでの動作確認

3つのVTEPと、VNI 100と200に属する端末をvethでつなぎ、このサンプルを動かしました。 Linux bridgeやLinuxのVXLANデバイスは使わず、カプセル化とカプセル化解除もXDPで行います。

以下のトポロジー図を見るとわかる通り、VTEPと端末をそれぞれ独立したnetnsへ置き、vethで接続しています。

%%{init: {"theme":"base","look":"classic","fontFamily":"DejaVu Sans, sans-serif","themeVariables":{"lineColor":"#666666","clusterBkg":"#fafafa","clusterBorder":"#aaaaaa","edgeLabelBackground":"#ffffff"},"flowchart":{"htmlLabels":false,"curve":"linear"},"themeCSS":".edgeLabel rect, .edgeLabel .background { fill: #ffffff !important; opacity: 1 !important; } .labelBkg { background-color: #ffffff !important; }"}}%%
flowchart LR
    accTitle: Three-VTEP VXLAN network namespace topology
    accDescr: VTEP A connects to VTEPs B and C through two direct veth underlay links. Each VTEP has separate VNI 100 and VNI 200 access endpoints, and A has one extra VNI 100 access endpoint. Every endpoint and VTEP has its own namespace.
    a100["a-100 namespace<br/>VNI 100"] ---|"eth0 / p0"| A["VTEP A namespace<br/>goxdp / XDP VXLAN"]
    a200["a-200 namespace<br/>VNI 200"] ---|"eth0 / p1"| A
    alocal["a-local namespace<br/>VNI 100"] ---|"eth0 / p4"| A
    A ---|"p2 / p2<br/>192.0.2.1 / 192.0.2.2<br/>VXLAN over veth<br/>MTU 1600"| B["VTEP B namespace<br/>goxdp / XDP VXLAN"]
    A ---|"p3 / p2<br/>192.0.3.1 / 192.0.3.2<br/>VXLAN over veth<br/>MTU 1600"| C["VTEP C namespace<br/>goxdp / XDP VXLAN"]
    B ---|"p0 / eth0"| b100["b-100 namespace<br/>VNI 100"]
    B ---|"p1 / eth0"| b200["b-200 namespace<br/>VNI 200"]
    C ---|"p0 / eth0"| c100["c-100 namespace<br/>VNI 100"]
    C ---|"p1 / eth0"| c200["c-200 namespace<br/>VNI 200"]
    classDef default fill:#ffffff,stroke:#666666,color:#222222;
    class A,B,C process;
    classDef process fill:#edf3fa,stroke:#74849a,color:#222222;

VNIごとのBUM配信と、VXLANで対向VTEPの端末まで配送できることを確認しました。 確認環境はLinux 7.0.0-38-genericとvethで、テストの詳細と実行手順はtesting.mdにまとめています。

このように XDPでも BUMがサポートできてそうな事がわかりました。やったね!

動作を確認できたので、次はTC併用版との性能差を見てみましょう。

TC併用版との性能比較

同じ転送設定で--backend tcへ切り替え、既定の--backend xdpと比較しました。 TC併用版では、検証とカプセル化解除をXDPで行い、XDP_PASSでskbへ渡してからTCで複製します。

access向けを先に送り、tunnel向けにはOuterを一度付けて宛先ごとに書き換えます。VLANからVNIへの分類と、VNIやVIDの検証の処理は、XDP版と共通の仕組みを使ってます。

%%{init: {"theme":"base","look":"classic","fontFamily":"DejaVu Sans, sans-serif","themeVariables":{"lineColor":"#666666","clusterBkg":"#fafafa","clusterBorder":"#aaaaaa","edgeLabelBackground":"#ffffff"},"flowchart":{"htmlLabels":false,"curve":"linear"},"themeCSS":".edgeLabel rect, .edgeLabel .background { fill: #ffffff !important; opacity: 1 !important; } .labelBkg { background-color: #ffffff !important; }"}}%%
flowchart TB
    accTitle: XDP and XDP plus TC VXLAN replication paths
    accDescr: Both backends classify and decapsulate in XDP. The XDP backend replicates through a devmap and rewrites each tunnel copy in an egress program. The TC backend passes an skb to TC, sends access copies first, prepends Outer once, and rewrites each tunnel destination. Both preserve the Inner frame.
    subgraph xdp["XDP backend"]
        direction TB
        xi["Ingress XDP<br/>validate / decap"] --> group["DEVMAP broadcast"]
        group --> rewrite["Per-copy egress XDP<br/>attach Outer / rewrite"]
        rewrite --> xo["Tunnel copies<br/>same Inner / varied Outer"]
    end
    subgraph tc["XDP + TC backend"]
        direction TB
        ti["Ingress XDP<br/>validate / decap"] --> pass["XDP_PASS<br/>create skb"]
        pass --> clone["TC ingress<br/>access copies first<br/>prepend Outer once"]
        clone --> target["Rewrite each destination<br/>N-1 clones + redirect"]
        target --> tout["Tunnel copies<br/>same Inner / varied Outer"]
    end
    classDef default fill:#ffffff,stroke:#666666,color:#222222;
    class xi,rewrite,ti,pass,clone,target process;
    classDef process fill:#edf3fa,stroke:#74849a,color:#222222;
    class group map;
    classDef map fill:#edf4ee,stroke:#7b9180,color:#222222;

図のフローで示した通り、XDP版はdevmapで複製してから各egressで加工し、TC併用版はTC ingressで書き換えと複製を行います。

このveth環境では、XDP版はTC併用版の約1.6〜1.8倍の受信合計スループットを達成しました。 XDPerfの4ワーカーと4キューのvethを使い、一つのVNIの64個のUDPフローを2個のunicast VTEPへ複製しました。 表は各3回の受信合計の中央値で、VLAN対応を追加する前のバイナリでの測定結果です。

入力フレーム長 XDP(Mpps) XDP + TC(Mpps) XDP / TC
64バイト 1.861 1.056 1.76倍
1514バイト 1.454 0.896 1.62倍

この値は、同じKVMホストで送信器、転送処理、受信側を動かしたときの処理量です。 実NICでの性能や無損失転送の上限、未測定のXDP_CLONEとの性能差を示すものではありません。 測定条件と生データは測定レポートにまとめました。

終わりに

今回の記事では既存のXDPでも、devmap broadcastとegress programを組み合わせれば、BUMを複製し、同じInnerを異なるOuterで送れることを示しました。

論文ではコピーを個別に書き換えられないと説明していますが、devmap egress programを使えば、既存のXDPでも実現できます。

一方、XDP_CLONEには、コピー番号を直接扱えることや原本を直接スタックへ渡せることなど、既存の構成より便利な点もありそうです。 性能や使い勝手でどんな差が出るのか、今後の比較に期待しています。

ちなみにLinkedinに流してみるかと言う事でLLMに翻訳してもらった英語版はこちら。 takeio.hatenablog.com