SONiCでVXLAN 2022 L2VNI編

# はじめに

けっこう前にVXLANの記事を書いたのですが、当時は不勉強だったこともあり「よくわかりません」的な感じだったと思います。いろいろ調べたり聞いたり試したりして、だいたいこんな感じかというのが把握できたので、共有したいと思います。

# VXLAN? EVPN?

VXLANは4789/udpを使ってオーバーレイ・ネットワークを構成する技術で、[RFC7348](https://www.rfc-editor.org/rfc/rfc7348)に詳細が、というかたい説明はすっとばして、ざっくりいうとL3到達性のある複数のVTEP(VXLAN Tunnel End Point)の間でL2トンネルを張るというものになります。もう少し踏み込んだ概要については、[JANOG37資料 VXLANチュートリアル](https://www.janog.gr.jp/meeting/janog37/download_file/vxlan.pdf)あたりを読まれるといいかと思います。

初期の素朴なVXLANはVTEPを見つけるのにマルチキャストを使う、あるいは静的に設定を投入するというので導入障壁が高かったと聞きます。

近年ではこの問題をMP-BGP EVPNを使って解決する(BGPを使ってVTEPやリモートホストの情報を互いに交換する)のが一般的なようで、SONiCでいえばFRRoutingへの設定投入と合わせて使う形になります。

EVPNについては[ここらへんのPDF](https://www.nic.ad.jp/ja/materials/iw/2016/proceedings/t05/t5-kamitani-2.pdf)を読んだり、ふつうに検索して解説が出てくると思うのでお読みいただければと思います。

# SONiCでVXLAN

最初(つまり前回の記事を書いたくらい)のころ、`Ethernet*`につけたIPアドレスをVTEPのIPアドレスとして設定し、リモートVTEPも静的に指定するなどやっていて、仮想マシン同士ではうまく行くものの実機では動かず、しばらく首をひねってました。

結論からいうと

- VTEPはLoopbackのIPを指定。
- VTEPを複数使うことは想定されてない模様。
- EVPNを使う前提。
- VNI (Virtual Network Instance) はVLANにマッピングする。
- Broadcomは機種によって動く動かないが違う(ASICの設定ファイル`config.bcm`次第)
- コミュニティ版をBroadcomで動かす場合、さらにちょっとした改造が必要そうだった

というふうです。試していてつまづいたところは後述します。

# 試験構成

VXLANを試そうとすると、最小構成は

- 両端にホスト
- 間にVTEP2台

の合計4台になります。VTEPは当然ですがSONiCが動作するスイッチとなります。
大まかに絵にすると、こんなかんじです。

```
+-------+          +--------------+               +--------------+          +-------+
| HostA |--(VLAN)--| VTEPA(SONiC) |====(VXLAN)====| VTEPB(SONiC) |--(VLAN)--| HostB |
+-------+          +--------------+               +--------------+          +-------+
```

なお、参考にしたURLはこちらになります。対象はベンダーサポートのあるEdgecore SONiCとのことですが、概ねコミュニティ版でも変わりません。状態確認などはこの記事より細かいので、参考になればいいかと思います。

https://support.edge-core.com/hc/en-us/articles/900002720523--Edgecore-SONiC-EVPN-L2-VxLAN

# ホスト間の通信はL2かL3か

2つのホストが同一L2セグメントか。言い換えると、同一のIPサブネットに属しているかしていないか。単純なのは同一セグメント。VNI (Virtual Network Instance) を使いL2で到達性があるためL2VNIと呼ばれたりします。

つまり異なるセグメント間の通信を実現するL3VNIというのもあるのですが、ひとまず置いておきます。

L2VNIの場合、ホストのIPアドレスはたとえば下記のような感じになります。

- HostA: 192.168.2.1/24
- HostB: 192.168.2.2/24 

そしてSONiCは、VLAN-VXLANのブリッジ動作をするということになります。

# 試験構成(詳細)

横向きだと必要な情報を追記するのに見づらかったので縦に変えてみます。

```
   +-------+
   | HostA |
   +---+---+
       | ens4.100 192.168.2.1/24
       |
    (VLAN id:100)
       |
       | Ethernet0 Vlan100
+--------------+
| VTEPA(SONiC) | Loopback0 1.1.1.1/32
+--------------+
       || Ethernet4 198.51.100.1/24
       ||
   (VXLAN VNI=3000)
       ||
       || Ethernet4 198.51.100.2/24
+--------------+
| VTEPB(SONiC) | Loopback0 2.2.2.2/32
+--------------+
       | Ethernet0 Vlan200
       |
    (VLAN id:200)
       |
       | ens4.100 192.168.2.2/24
   +-------+
   | HostB |
   +---+---+
```

機器としては

- HostA, HostB: PC (Ubuntuを使用)
- VTEPA, VTEPB: DELTA AG9032V2A (Broadcom Trident3搭載ホワイトボックススイッチ)

を使いました。Edgecore AS7726-32Xや、DELL EMC S5232F-ONでも試しています。

SONiCのバージョンですが、2022年11月時点のmasterブランチに少し改造を施したものを使っています。改造については後述します。


# L2VNIの設定

## HostA

tag VLANを作ってIPアドレスを振ってlink upするだけです。
Ubuntu想定で書きますので、別のOSを使うケースなどはうまく読み替えてください。

```shell-session
sudo ip link add name ens4.100 link ens4 type vlan id 100
sudo ip addr add 192.168.2.1/24 dev ens4.100
sudo ip link set ens4.100 up
```

## HostB

```shell-session
sudo ip link add name ens4.200 link ens4 type vlan id 200
sudo ip addr add 192.168.2.2/24 dev ens4.200
sudo ip link set ens4.200 up
```
## VTEPA

いよいよSONiCの設定です。CLIでの設定と、bgpdの設定があります。

### CLIでVXLANの設定

ざっくり説明すると

1. VTEP用のIPアドレスをループバックインタフェースに設定
1. VNIをマップするVLANを設定
1. BGPでpeerと通信するインタフェースを設定
1. VXLAN VTEPを作成
1. EVPNオーバーレイを作成
1. VLANとVNIをマッピング

となります。

```shell-session
sudo config loopback add Loopback0
sudo config interface ip add Loopback0 1.1.1.1/32
sudo config vlan add 100
sudo config vlan member add Ethernet0
sudo config interface ip add Ethernet4 198.51.100.1/24
sudo config vxlan add vtep 1.1.1.1
sudo config vxlan evpn_nvo nvo vtep
sudo config vxlan map add 100 3000
```

### vtyshあるいはbgpd.confを使いEVPNの設定

この例ではAS番号101で、VTEPのIPを広告した上で、VNIの広告を設定します。

```:bgpd.conf
router bgp 101
  neighbor 198.51.100.2 remote-as internal
  address-family ipv4 unicast
    network 1.1.1.1/32
  exit-address-family
  address-family evpn l2vpn
    neighbor 198.51.100.2 activate
    advertise-all-vni
  exit-address-family
```

まっさらな状態であれば、これを流し込めばVTEPAの設定は完了です。

`vtysh`を起動して対話的に設定する場合はとくに気にする必要はないのですが、ファイルを流し込む場合は注意が必要です。どういうことかというと、SONiCでは、FRRoutingはbgp dockerの中で動いていますので、ファイルを流し込む場合はコンテナ内にファイルが存在する必要があるということです。


```
docker cp bgpd.conf bgp:.
vtysh -c bgpd.conf
```

## VTEPB

VTEPAと同様に設定します。

### CLIでVXLANの設定

```shell-session
sudo config loopback add Loopback0
sudo config interface ip add Loopback0 2.2.2.2/32
sudo config vlan add 200
sudo config vlan member add Ethernet0
sudo config interface ip add Ethernet4 198.51.100.2/24
sudo config vxlan add vtep 2.2.2.2
sudo config vxlan evpn_nvo nvo vtep
sudo config vxlan map add 200 3000
```

### vtyshあるいはbgpd.confを使いEVPNの設定

```:bgpd.conf
router bgp 101
  neighbor 198.51.100.1 remote-as internal
  address-family ipv4 unicast
    network 2.2.2.2/32
  exit-address-family
  address-family evpn l2vpn
    neighbor 198.51.100.1 activate
    advertise-all-vni
  exit-address-family
```

VTEPAと同じ手順で`bgpd`に流し込みます。

```
docker cp bgpd.conf bgp:.
vtysh -c bgpd.conf
```

以上で、すべての設定は完了です。

# 動作確認 その前に

気にしておいたほうがいいことを書いておきます。

- ログを見る
- ステータスをみる
- BGPでVTEPの情報を交換できているか見る
- CPUで処理していないか確認する
- トンネルの外を通っていないか確認する


## ログを見る

重要です。なぜかというと、SONiCではCLIのsyntaxとして問題がなければ、コマンドの実行結果は成功となります。しかし動作として内部でエラーが発生することがあり、エラーが出ているかどうかはログを見ないとわからないからです。

```
show logging | less
```

## ステータスをみる

`show`コマンドを使って状態確認します。

```
show ip interfaces
show vxlan tunnel
show vxlan remotevtep
show vxlan remtoevni all
show vxlan vlanvnimap all
show ip route
```

## BGPでVTEPの情報を交換できているか見る

vtyshで

```
show bgp summary
show evpn
```

## CPUで処理していないか確認する

VXLANトンネルを貼っている側のインタフェース (今回の試験ではどちらのスイッチも`Ethernet4`)で`tcpdump`します。もしVXLANのencap/decap処理がCPUで実行されている場合、VXLANのパケットを観測することができます。ASICがハードウェア処理している場合はVXLANパケットを観測することはありません。

## トンネルの外を通っていないか確認する

L2VNIではこれを心配する必要はありませんが、ASICで処理されている場合`tcpdump`ではパケットの内容を観察できないため、トンネルの内側を通っているのかトンネルの外側を通っているのかわかりません。

観察する方法としては

- 間に中継ホストを挟み、観察する
- `mirror_session` を設定して他ホストにパケットを転送し、転送先のホストで観察する

のいずれかの手段をとる必要があります。

# 動作確認

HostAから、HostBに向かってpingします。

```
ping 192.168.2.2
PING 192.168.2.2 (192.168.2.2) 56(84) bytes of data.
From 192.168.2.1 icmp_seq=1 Destination Host Unreachable
From 192.168.2.1 icmp_seq=2 Destination Host Unreachable
From 192.168.2.1 icmp_seq=3 Destination Host Unreachable
```

あれ?

## 調査と対応

ログをみるとエラーが。(日時は冗長なので削っています)

```
NOTICE swss#orchagent: :- create_tunnel: create_tunnel:encapmaplist[0]=0x290000000006f5 
NOTICE swss#orchagent: :- create_tunnel: create_tunnel:encapmaplist[1]=0x290000000006f7 
ERR syncd#syncd: [none] SAI_API_TUNNEL:_brcm_sai_vxlan_create_vpn:890 create tunnel initiator setup for net port failed with error Invalid parameter (0xfffffffc). 
ERR syncd#syncd: [none] SAI_API_TUNNEL:_brcm_sai_vxlan_enable:1182 create a vxlan decap tunnel failed with error -5. 
syncd#syncd: [none] SAI_API_TUNNEL:brcm_sai_create_tunnel_map_entry:2682 Can't create brcm vxlan tunnel 
ERR syncd#syncd: :- sendApiResponse: api SAI_COMMON_API_CREATE failed in syncd mode: SAI_STATUS_FAILURE 
```

設定が悪いのか、コミュニティ版は動かないけどベンダーサポートされてるイメージなら動くのかよくわからなかったのだけど、MLに症状を報告したところ、Broadcomのひとからヒントをいただけました。

https://github.com/sonic-net/sonic-buildimage/pull/9491

いわく、`config.bcm`にトンネルを使う設定が必要、と。具体的には下記を追加するといい、と。

```
#vxlan
use_all_splithorizon_groups=1
riot_enable=1
sai_tunnel_support=1
riot_overlay_l3_intf_mem_size=4096
riot_overlay_l3_egress_mem_size=32768
riot_overlay_ecmp_resilient_hash_size=16384
flow_init_mode=1
```

なるほど。つまるところASICに依存する問題(MellanoxとかBarefootでは発生しないかも?)ということのようです。数字が適切かはあまり考えないことにします。

riotというのは Routing In and Out of Tunnelの頭文字。それに限らず、VXLANを動かすのに必要な設定の模様。確認すると、`config.bcm`のなかでこれが書かれているものと書かれていないものが混在していました。書かれてるものとピックアップするとこのような感じです。

```shell-session
$ rg -l riot
alphanetworks/x86_64-alphanetworks_bes2348t-r0/Alphanetworks-BES2348T/bes2348t.config.bcm
quanta/x86_64-quanta_ix7_rglbmc-r0/Quanta-IX7-32X/td3-ix7-32x100G.config.bcm
celestica/x86_64-cel_belgite-r0/CELESTICA-BELGITE/belgite.config.bcm
quanta/x86_64-quanta_ix8a_bwde-r0/Quanta-IX8A-BWDE-56X/td3-ix8a-bwde-48x25G+8x100G.config.bcm
quanta/x86_64-quanta_ix7_bwde-r0/Quanta-IX7-BWDE-32X/td3-ix7-bwde-32x100G.config.bcm
quanta/x86_64-quanta_ix8_rglbmc-r0/Quanta-IX8-56X/td3-ix8-48x25G+8x100G.config.bcm
arista/x86_64-arista_7050dx4_32s/Arista-7050DX4-32S/td4-a7050dx4-32s-32x400G.config.bcm
arista/x86_64-arista_7050cx3_32s/Arista-7050CX3-32S-C32/config.bcm.j2
arista/x86_64-arista_7050cx3_32s/Arista-7050CX3-32S-D48C8/config.bcm.j2
dell/x86_64-dellemc_s5212f_c3538-r0/DellEMC-S5212f-P-25G/td3-s5212f-25g.config.bcm
dell/x86_64-dellemc_n3248pxe_c3338-r0/DELLEMC-N3248PXE/td3-x5-n3248pxe-48x10GCU+4x25G-2x100G.config.bcm
dell/x86_64-dellemc_s5224f_c3538-r0/DellEMC-S5224f-P-25G/td3-s5224f-25g.config.bcm
```

おそらくベンダーサポートのあるイメージには記載があるのだろうと思います。さっそく取り込んでみることに。

DELTA AG9032V2Aの場合、変更する`config.bcm`の正確なPATHは

`/usr/share/sonic/device/x86_64-delta-ag0032v2a-r0/Delta-ag9032v2a-r0/td3-ag9032v2a-32x100G+1x10G.config.bcm`


実機上で編集して再起動すれば、ソースコードを`git clone`してコードに手を加えてビルド、といった手間はかからず、すぐに変更を適用できます。

# 再度動作確認

設定。ログを確認。

エラーはありません。ではHostAからHostBに向かってpingします。

```
ping 192.168.2.2
PING 192.168.2.2 (192.168.2.2) 56(84) bytes of data.
From 192.168.2.1 icmp_seq=1 Destination Host Unreachable
From 192.168.2.1 icmp_seq=2 Destination Host Unreachable
From 192.168.2.1 icmp_seq=3 Destination Host Unreachable
```

あれ?

## 調査と対応

`show`コマンドで状態を確認しましたが問題なさそうだったので、パケットが出ているのかキャプチャしてみます。VTEPの間にPCを挟むことができたので、ブリッジを作って`tcpdump`してみます。いろいろ確認したいので`-v`つきで(日時は省きます)

```
IP (tos 0x0, id 56036, offset 0, flags [none], proto UDP (17), length 96) 
    198.51.100.1.39255 > 198.61.100.2.4789: VXLAN, flags [I] (0x08), vni 3000 
```

出てますね。VXLANでカプセリングもされてます。

が、、、あれ?

ttlは? (他の通信を確認するとtosの次にttl 246とかついてる)

**は? 0なの? 馬鹿なの?**

TTLが0だと捨てられるのは道理です。
そして検索などして見つけたのがこれ。

https://github.com/sonic-net/sonic-buildimage/issues/10050

注目すべきは最後の方にあるコメント。下記コードの追加で問題が解消されたとのこと。

```c++
        attr.id = SAI_TUNNEL_ATTR_ENCAP_TTL_MODE;
        attr.value.s32 = SAI_TUNNEL_TTL_MODE_PIPE_MODEL;
        tunnel_attrs.push_back(attr);

        attr.id = SAI_TUNNEL_ATTR_ENCAP_TTL_VAL;
        attr.value.u8 = 64;
        tunnel_attrs.push_back(attr);
```

他プラットフォームではわかりませんが、Broadcom Trident系だとこれがもろに効いてるような感じです。
ソースコードをみると、上記コードを設定する部分はあったのですが条件があって、当該関数のパラメータでTTLが指定された場合でした。指定は省略可能だったのですが、省略するとどういう値かというと

```c++
static sai_object_id_t
create_tunnel(
    struct tunnel_ids_t* ids,
    sai_ip_address_t *src_ip,
    sai_ip_address_t *dst_ip,
    sai_object_id_t underlay_rif,
    bool p2p,
    sai_uint8_t encap_ttl=0)
```

`encap_ttl=0` これはC++の記法でデフォルト値を表します。0を指定した場合は、`SAI_TUNNEL_ATTR_ENCAP_TTL_VAL`は指定されません。結果としてTTL=0のパケットを相手に送るということのようでした。

0じゃない値を代入する箇所はEVPNでは存在せず。この場合、0以外の値を渡して`SAI_TUNNEL_ATTR_ENCAP_TTL_VAL`つきでSAIのAPIを呼び出すには、ソースコードを変更してビルドし直すしかありません。

このあたり、ベンダーサポートのあるイメージではどうなってるのか、ソースコードはおそらく閲覧できないので確認のしようがないのですが、気になるところではあります。

# ソースコードを変更してビルド、そして実機にインストール

このような差分を用意しました。Broadcom以外での影響が不明のためPR出せませんぐぬぬ。

```diff
diff --git a/orchagent/vnetorch.h b/orchagent/vnetorch.h
index 4f63764..3bad266 100644
--- a/orchagent/vnetorch.h
+++ b/orchagent/vnetorch.h
@@ -19,7 +19,6 @@
 #define VNET_TUNNEL_SIZE 40960
 #define VNET_ROUTE_FULL_MASK_OFFSET_MAX 3000
 #define VNET_NEIGHBOR_MAX 0xffff
-#define VXLAN_ENCAP_TTL 128
 #define VNET_BITMAP_RIF_MTU 9100
 
 extern sai_object_id_t gVirtualRouterId;
diff --git a/orchagent/vxlanorch.cpp b/orchagent/vxlanorch.cpp
index 1995675..2d6ec2a 100644
--- a/orchagent/vxlanorch.cpp
+++ b/orchagent/vxlanorch.cpp
@@ -274,7 +274,7 @@ create_tunnel(
     sai_ip_address_t *dst_ip,
     sai_object_id_t underlay_rif,
     bool p2p,
-    sai_uint8_t encap_ttl=0)
+    sai_uint8_t encap_ttl=VXLAN_ENCAP_TTL)
 {
     sai_attribute_t attr;
     std::vector<sai_attribute_t> tunnel_attrs;
diff --git a/orchagent/vxlanorch.h b/orchagent/vxlanorch.h
index 9529a86..9a8befb 100644
--- a/orchagent/vxlanorch.h
+++ b/orchagent/vxlanorch.h
@@ -47,6 +47,8 @@ typedef enum
 
 #define MAX_VNI_ID 16777215
 
+#define VXLAN_ENCAP_TTL 128
+
 typedef enum
 {
     TNL_CREATION_SRC_CLI,
@@ -196,7 +198,7 @@ public:
 
     bool deleteMapperHw(uint8_t mapper_list, tunnel_map_use_t map_src);
     bool createMapperHw(uint8_t mapper_list, tunnel_map_use_t map_src);
-    bool createTunnelHw(uint8_t mapper_list, tunnel_map_use_t map_src, bool with_term = true, sai_uint8_t encap_ttl=0);
+    bool createTunnelHw(uint8_t mapper_list, tunnel_map_use_t map_src, bool with_term = true, sai_uint8_t encap_ttl=VXLAN_ENCAP_TTL);
     bool deleteTunnelHw(uint8_t mapper_list, tunnel_map_use_t map_src, bool with_term = true);
     void deletePendingSIPTunnel();
     void increment_spurious_imr_add(const std::string remote_vtep);
```

ソースコードをひっぱってきて、上記の変更を適用してビルドします。

```shell-session
git clone https://github.com/sonic-net/sonic-buildimage
cd sonic-buildimage
make init
cd src/sonic-swss
patch -p1 < それ
cd -
make configure PLATFORM=broadcom
make target/sonic-broadcom.bin
```

4時間とかその倍とか時間がかかるので、ゆっくり待ちます。一眠りして、翌朝確認とかでいいと思います。

ビルドが終わったら実機に`scp`等でコピーして(`scp`メンテされないんで`rsync`使おうという話がありましたね)、インストールして再起動。

```shell-session
sudo sonic-installer install -y sonic-broadcom.bin
sudo shutdown -r now
```

# さらなる動作確認

`show version`を使い、自分がビルドしたバージョンが起動したか確認した上で、VXLANとBGPを設定し、HoatAからHostBにpingしてみます。

```shell-session
ping 192.168.2.2
PING 192.168.2.2 (192.168.2.2) 56(84) bytes of data.
64 bytes from 192.168.2.2: icmp_seq=1 ttl=255 time=0.246 ms
64 bytes from 192.168.2.2: icmp_seq=2 ttl=255 time=0.176 ms
64 bytes from 192.168.2.2: icmp_seq=3 ttl=255 time=0.219 ms
64 bytes from 192.168.2.2: icmp_seq=4 ttl=255 time=0.189 ms
```

とおりました！

# まとめ

VXLAN L2VNIでの通信が実機で確認できました。

ただし、実機がBroadcom Traident3搭載スイッチの場合、SONiCコミュニティ版無改造では動作せず、ちょっとした改造が必要でした。ベンダーサポート版だったり他ベンダーのASIC(Mellanox, Barefoot等)では改造は不要でそのまま動作するかもしれません。お手元にある方はお試しいただいて、どうだったか教えていただけると嬉しく思います。