VXLAN/EVPN on Linux の検証

# モチベーション

データセンタ内のVXLAN/EVPN環境において、VXLAN終端をleafスイッチでなく配下の物理サーバ(Linux)でやりたい.

メリット

- サーバとスイッチ間の接続がL2からL3になる
    - Active/standbyなbondingやMLAG不要
- leafスイッチの設定が簡素化される
    - スイッチはMP-BGPの通信に徹する
    - ネットワークデザインがシンプル化
- leafスイッチのメンテナンスが楽になる
    - BGP graceful shutdown community (使ったことないけど)
    - スイッチのアップグレードを波風立てずに実施したい

デメリット

- ネットワーク性能の低下?(本記事では未検証)
    - leafスイッチのASICで高速処理されていたVXLANのカプセル処理が物理サーバに委譲されるため
        - サーバ用のVXLANオフロードNICがあったような...
- 物理ホスト上でルーティングデーモンを稼働させる必要がある
    - Linux以外のサーバは本構成の対象外になってしまう

[前回の記事](https://qiita.com/tom7/items/472e9871128ef082d1f1)で Cumulus-VX を使用して VXLAN/EVPNのルーティングを検証し、CumulusはDebianとFRRを使っていることがわかった。ということはノーマルなlinuxでも、設定を適切に投入すればVXLAN/EVPNを動作させられるはずである. 結論として動作を確認できた.　

# 背景

データセンタネットワークの歴史的な経緯はこちらの資料を参照.
[(JANOG41) データセンターでのルーティングプロトコル](https://www.janog.gr.jp/meeting/janog41/application/files/5515/1651/9605/janog41-dcrtg-shtsuchi-01.pdf)

IP-fabric デザインにするとスケーラビリティを得る代わりにToR内でVLANが閉じるため、L2-fabricと異なり他のラックに同じVLANをもつマシンを分散配置できない.　ラック内のネットワーク設計によるがネットワーク設定と物理ラックが密結合になるため、管理が必要だったりリソースを有効活用しにくくなる.
これを解決するためにオーバレイネットワークやルーティングプロトコルを駆使してラックを越境するわけだがVXLAN以外にも様々なアプローチがとられている. 

- [(LINE) 大規模サービスを支えるネットワークインフラの全貌](https://www.slideshare.net/linecorp/ss-116867631)
- [(Cybozu) Neco のネットワーク - 実装編](https://blog.cybozu.io/entry/2018/11/01/113000)
- [(Yahoo) データセンタネットワークの取り組みとFacebook Backpack](https://techblog.yahoo.co.jp/advent-calendar-2017/datacenternetwork_backpack/)

サーバエンドでVXLANを終端させるアプローチ自体は既に考えられている.

- [EVPN on the host for multi-tenancy](https://cumulusnetworks.com/blog/evpn-host/)
- [vQFX10000 で VXLAN+EVPN (L2 over L3 編)](http://kakkotetsu.hatenablog.com/entry/2017/05/11/233637)
- [EVPN to the host](https://ripe77.ripe.net/presentations/21-EVPN-to-the-host.pdf)
- [L3 routing to the hypervisor with BGP](https://vincent.bernat.ch/en/blog/2018-l3-routing-hypervisor)

# Network 概要
今回構築するネットワークはこちら. VMが2台あれば容易に検証できるようにした.
![host-vtep-internal.png](https://qiita-image-store.s3.amazonaws.com/0/323524/92d2ef76-0cd1-30c7-6c28-4519a3cd1ded.png)

Linux Networkの詳細な設定は実際の Cumulus-VX の内部設定を参考にした. [NCLU](https://docs.cumulusnetworks.com/display/DOCS/Network+Command+Line+Utility+-+NCLU)は無いので、L2やVNIの設定は`ip`コマンド、L3の設定は`FRR`を直接叩く必要がある.
Cumulus だと[VLAN-aware Bridge](https://docs.cumulusnetworks.com/display/DOCS/VLAN-aware+Bridge+Mode)というひとつのbridgeにすべてのVNIを接続させてVLANでサブネットを分離していた。今回はLXCとの親和性からサブネットごとにbridgeを作成してL2分離した.
Cumulus本家も、[本検証とは異なるアプローチ](https://gitlab.com/CumulusNetworks/evpn-to-the-host#host-architecture)(vlan-aware Bridge + nodeはnamespaceで代用)でサーバVTEPを検証している.

# VTEPの構築

`Ubuntu18.04-LTS`(bionic)を2台用意する. 互いにping疎通可であるとする.
ネットワークの設定方法が`netplan`を使う方法になっているので注意する.

[Netplan の使い方](https://www.komee.org/entry/2018/06/12/181400)

## Linux Kernel のアップグレード
`ip`コマンドの`VRF`機能や`FRR`の要件より、デフォルトより新しめのカーネルにする必要がある.

- [FRR 要件](http://docs.frrouting.org/en/latest/overview.html?highlight=evpn#supported-protocols-vs-platform)
- [Linux Kernel v4.19](http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.19/)

```bash
sudo su -
#-------------------------------------------------------
# Update repository
#-------------------------------------------------------
cat > /etc/apt/sources.list <<EOF
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic main restricted
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic-updates main restricted
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic universe
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic-updates universe
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic multiverse
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic-updates multiverse
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic-backports main restricted universe multiverse
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic-security main restricted
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic-security universe
deb http://ftp.jaist.ac.jp/pub/Linux/ubuntu/ bionic-security multiverse
EOF
apt-get -y update

#-------------------------------------------------------
# Upgrade linux kernel to 4.19
#-------------------------------------------------------
mkdir linux4.19/ && cd $_
wget http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.19/linux-headers-4.19.0-041900_4.19.0-041900.201810221809_all.deb
wget http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.19/linux-headers-4.19.0-041900-generic_4.19.0-041900.201810221809_amd64.deb
wget http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.19/linux-image-unsigned-4.19.0-041900-generic_4.19.0-041900.201810221809_amd64.deb
wget http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.19/linux-modules-4.19.0-041900-generic_4.19.0-041900.201810221809_amd64.deb
dpkg -i *.deb
reboot
 
# Check > 4.19.0-041900-generic
uname -r
```

## sysctl の調整
linux内のbridge間でパケットを転送し合ったりVRFを有効にするために`sysctl`の設定も入れておく. 新しいことをやろうとするとよく嵌まるポイント. 

- [CentOS7 の ip_forward 有効化で rp_filter が邪魔してる話](https://qiita.com/kanejun/items/4f7a5fe7c811a77c0b7b)
- [Linux VRF with L3 Master Device](http://yunazuno.hatenablog.com/entry/2017/06/17/225917)
- [VRF for Linux — a contribution to the Linux Kernel](https://cumulusnetworks.com/blog/vrf-for-linux/)

CumulusからLinuxをルータ/スイッチとして使うにあたり、ARPの挙動をどのように変更したかの解説記事が出ている. 

- [Changing ARP timers in Cumulus Linux](https://support.cumulusnetworks.com/hc/en-us/articles/202012933-Changing-ARP-timers-in-Cumulus-Linux)
- [Address Resolution Protocol](https://docs.cumulusnetworks.com/display/DOCS/Address+Resolution+Protocol+-+ARP) 

```bash
#-------------------------------------------------------
# sysctl
#-------------------------------------------------------
# VRF
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.tcp_l3mdev_accept=1
sysctl -w net.ipv4.udp_l3mdev_accept=1
sysctl -w net.ipv4.conf.default.rp_filter=0
sysctl -w net.ipv4.conf.all.rp_filter=0


# Inspiered by Cumulus
sysctl -w net.ipv4.neigh.default.base_reachable_time_ms=1080000
sysctl -w net.ipv4.conf.default.arp_accept=0
sysctl -w net.ipv4.conf.default.arp_announce=2
sysctl -w net.ipv4.conf.default.arp_filter=0
sysctl -w net.ipv4.conf.default.arp_ignore=1
sysctl -w net.ipv4.conf.default.arp_notify=1

sysctl -p
```

## FRR のインストール

インストールの仕方は色々あるようだ. 今回は`dpkg`でインストール.

- [FRR Github](https://github.com/FRRouting/frr/releases)
- [FRR Install](http://docs.frrouting.org/en/latest/installation.html)
- [FRR ユーザガイド](http://docs.frrouting.org/en/latest/)

```bash
# Install
wget https://github.com/FRRouting/frr/releases/download/frr-6.0.2/frr_6.0.2-0.ubuntu18.04.1_amd64.deb
dpkg -i frr_6.0.2-0.ubuntu18.04.1_amd64.deb
 
# Enable routing daemons
sed -i -e "s/bgpd=no/bgpd=yes/g" /etc/frr/daemons
cat /etc/frr/daemons | grep yes

# Restart
systemctl enable frr
systemctl restart frr
systemctl status frr

# Check version
vtysh -c "show version"
```

## LXD の設定
Bridge接続ができればいいので`KVM`でも問題ないが取り回しが重いのと、Ubuntuなので[LXD](https://lxd.readthedocs.io/en/stable-3.0/)を使う. 

```bash
#-------------------------------------------------------
# LXD 3.0
#-------------------------------------------------------
# Install LXD
apt-get install -y lxd
lxc --version # > 3.0.3
 
# Initialization
lxd init
# Would you like to use LXD clustering? (yes/no) [default=no]:
# Do you want to configure a new storage pool? (yes/no) [default=yes]:
# Name of the new storage pool [default=default]:
# Name of the storage backend to use (btrfs, ceph, dir, lvm) [default=btrfs]: dir
# Would you like to connect to a MAAS server? (yes/no) [default=no]:
# Would you like to create a new local network bridge? (yes/no) [default=yes]: no
# Would you like to configure LXD to use an existing bridge or host interface? (yes/no) [default=no]:
# Would you like LXD to be available over the network? (yes/no) [default=no]:
# Would you like stale cached images to be updated automatically? (yes/no) [default=yes]
# Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]: yes
# config: {}
# networks: []
# storage_pools:
# - config: {}
#   description: ""
#   name: default
#   driver: dir
# profiles:
# - config: {}
#   description: ""
#   devices:
#     root:
#       path: /
#       pool: default
#       type: disk
#   name: default
# cluster: null
 
# Get new container image
lxc init ubuntu:18.04 ubuntu
FINGER_PRINT=$(lxc image list --format json | jq -r .[].fingerprint)
lxc image alias create ubuntu1804 $FINGER_PRINT
lxc image list
 
#-------------------------------------------------------
# Create original container image for PoC
#-------------------------------------------------------
# Deploy a container
lxc launch ubuntu1804 test
lxc exec test bash
 
# SSH
sed -i -e "s/PasswordAuthentication no/PasswordAuthentication yes/g" /etc/ssh/sshd_config
sed -i -e "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
systemctl restart ssh
systemctl status ssh

# Change default root password
echo root:ubuntu | chpasswd
 
# Stop DHCP DISCOVER traffic
# 今回は静的に各コンテナにIPアドレスを設定する
sed -i -e "s/dhcp4: true/dhcp4: false/g" /etc/netplan/50-cloud-init.yaml
 
# Generate new container image
lxc stop test
lxc publish test --alias demo
lxc list
lxc delete test
```

## Linux Network の設定
bridge や VNIを作る. ここから混乱しやすくなるので上図を見ながら設定を投入していく.

各VRFのルーティングテーブルに宛先エントリがない場合、`unreachable`にしてルックアップを終了する設定を入れる必要がある. この設定がないと`ip rule show`にしたがって物理サーバのルーティングテーブルを使って、オーバレイネットワークのパケットがVRFから漏れ出してしまうので蓋をしておく.

[Linux VRF の公式マニュアル](https://www.kernel.org/doc/Documentation/networking/vrf.txt)

MTUの数値は環境に合わせて調整する. UnderlayのNICのMTUは、VXLANのインターフェースのMTUより+50以上大きくしておくこと.


### Tenant の設定
```bash
#-------------------------------------------------------
# Underlay IF MTU (+50 due to VXLAN overhead) 
#-------------------------------------------------------
NIC=ens38
ip link set $NIC mtu 9100
VTEP_IPADDR=$(ip -4 addr show $NIC | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
echo $VTEP_IPADDR

#-------------------------------------------------------
# Tenant
#-------------------------------------------------------
function create_tenant() {
    # VRF
    ip link add ${VRF} type vrf table ${VRF_TABLE_ID}
    ip link set dev ${VRF} up
    ip link set ${NIC}.${VLAN} master ${VRF}
    ip route add table ${VRF_TABLE_ID} unreachable default metric 4278198272 # 蓋
    sysctl -w net.ipv4.conf.${VRF}.rp_filter=0
    sysctl -p

    # Check
    ip vrf show
    ip link show type vrf
    ip route show vrf ${VRF}
    ip -d link show ${VRF}

    # VXLAN (L3VNI)
    ip link add L3VNI${L3VNI} type vxlan id ${L3VNI} local ${VTEP_IPADDR} dstport 4789 nolearning
    ip link set L3VNI${L3VNI} mtu 9000
    ip link set L3VNI${L3VNI} up
    ip -d link show L3VNI${L3VNI}

    # Bridge
    brctl addbr br${L3VNI}
    brctl stp br${L3VNI} off
    brctl addif br${L3VNI} L3VNI${L3VNI}
    bridge link set dev L3VNI${L3VNI} neigh_suppress on
    ip link set br${L3VNI} mtu 9000
    ip link set br${L3VNI} up
    ip link set br${L3VNI} master ${VRF}
    sysctl -p

    # Check
    brctl show
    ip -d link show L3VNI${L3VNI}
    vtysh -c "show interface L3VNI${L3VNI}"
    ip addr show vrf ${VRF}
}

L3VNI=101000 ; VRF=vrf-1 ; VLAN=3001 ; VRF_TABLE_ID=101000 ; create_tenant
L3VNI=102000 ; VRF=vrf-2 ; VLAN=3002 ; VRF_TABLE_ID=102000 ; create_tenant
```

### Subnet の設定
```bash
function create_subnet() {
    # VXLAN (L2VNI)
    ip link add L2VNI${L2VNI} type vxlan id ${L2VNI} local ${VTEP_IPADDR} dstport 4789 nolearning
    ip link set L2VNI${L2VNI} mtu 9000
    ip link set L2VNI${L2VNI} up
    ip -d link show L2VNI${L2VNI}

    # Bridge
    brctl addbr br${L2VNI}
    brctl stp br${L2VNI} off
    brctl addif br${L2VNI} L2VNI${L2VNI}
    bridge link set dev L2VNI${L2VNI} neigh_suppress on
    ip addr add dev br${L2VNI} ${GATEWAY}/24
    ip link set br${L2VNI} mtu 9000
    ip link set br${L2VNI} up
    ip link set br${L2VNI} master ${VRF}
    ip route add ${SUBNET} via ${GATEWAY} dev br${L2VNI}
    sysctl -w net.ipv4.conf.br${L2VNI}.arp_accept=1
    sysctl -w net.ipv4.conf.br${L2VNI}.proxy_arp=1
    sysctl -p

    # Check
    brctl show
    ip -d link show L2VNI${L2VNI}
    vtysh -c "show interface L2VNI${L2VNI}"
    ip addr show vrf ${VRF}
    ip route show vrf ${VRF}
}

L2VNI=101001 ; VRF=vrf-1 ; SUBNET=10.1.1.0/24 ; GATEWAY=10.1.1.254 ; create_subnet
L2VNI=101002 ; VRF=vrf-1 ; SUBNET=10.1.2.0/24 ; GATEWAY=10.1.2.254 ; create_subnet
L2VNI=102001 ; VRF=vrf-2 ; SUBNET=10.2.1.0/24 ; GATEWAY=10.2.1.254 ; create_subnet


#--------------------------------------------
# Sample output
#--------------------------------------------
# root@vtep-1:~# ip route show vrf vrf-1
# unreachable default metric 4278198272
# 10.1.1.0/24 dev br101001 proto kernel scope link src 10.1.1.254
# 10.1.2.0/24 dev br101002 proto kernel scope link src 10.1.2.254
 
# root@vtep-1:~# ip route show vrf vrf-2
# unreachable default metric 4278198272
# 10.2.1.0/24 dev br102001 proto kernel scope link src 10.2.1.254
 
# root@vtep-1:~# brctl show
# bridge name   bridge id       STP enabled interfaces
# br101001      8000.e2a8f93a7056   no      L2VNI101001
# br101002      8000.ea09e6adfcce   no      L2VNI101002
# br102001      8000.6e4881804f2b   no      L2VNI102001
 
# Check VXLAN IF are UP
# root@vtep-1:~# ip link show type vxlan | grep L2VNI
# 13: L2VNI102001: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master br102001 state UNKNOWN mode DEFAULT group default qlen 1000
# 19: L2VNI101001: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master br101001 state UNKNOWN mode DEFAULT group default qlen 1000
# 21: L2VNI101002: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master br101002 state UNKNOWN mode DEFAULT group default qlen 1000
 
# Check bridge are UP
# root@vtep-1:~# ip link show type bridge | grep mtu
# 34: br101001: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master vrf-1 state UP mode DEFAULT group default qlen 1000
# 36: br101002: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master vrf-1 state UP mode DEFAULT group default qlen 1000
# 38: br102001: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master vrf-2 state UP mode DEFAULT group default qlen 1000

# root@vtep-1:~# ip addr show vrf vrf-1
# 16: br101000: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master vrf-1 state UP group default qlen 1000
#     link/ether 92:7c:04:3f:38:8f brd ff:ff:ff:ff:ff:ff
#     inet6 fe80::907c:4ff:fe3f:388f/64 scope link
#        valid_lft forever preferred_lft forever
# 20: br101001: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master vrf-1 state UP group default qlen 1000
#     link/ether e2:a8:f9:3a:70:56 brd ff:ff:ff:ff:ff:ff
#     inet 10.1.1.254/24 scope global br101001
#        valid_lft forever preferred_lft forever
#     inet6 fe80::e0a8:f9ff:fe3a:7056/64 scope link
#        valid_lft forever preferred_lft forever
# 22: br101002: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master vrf-1 state UP group default qlen 1000
#     link/ether ea:09:e6:ad:fc:ce brd ff:ff:ff:ff:ff:ff
#     inet 10.1.2.254/24 scope global br101002
#        valid_lft forever preferred_lft forever
#     inet6 fe80::e809:e6ff:fead:fcce/64 scope link
#        valid_lft forever preferred_lft forever
 
# root@vtep-1:~# ip addr show vrf vrf-2
# 14: br102001: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master vrf-2 state UP group default qlen 1000
#     link/ether 6e:48:81:80:4f:2b brd ff:ff:ff:ff:ff:ff
#     inet 10.2.1.254/24 scope global br102001
#        valid_lft forever preferred_lft forever
#     inet6 fe80::6c48:81ff:fe80:4f2b/64 scope link
#        valid_lft forever preferred_lft forever
# 18: br102000: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master vrf-2 state UP group default qlen 1000
#     link/ether 02:ed:fb:0d:81:e0 brd ff:ff:ff:ff:ff:ff
#     inet6 fe80::ed:fbff:fe0d:81e0/64 scope link
#        valid_lft forever preferred_lft forever
 
# root@vtep-1:~# ip link show type vxlan | grep L3VNI
# 15: L3VNI101000: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master br101000 state UNKNOWN mode DEFAULT group default qlen 1000
# 17: L3VNI102000: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue master br102000 state UNKNOWN mode DEFAULT group default qlen 1000
 
# root@vtep-1:~# brctl show
# bridge name     bridge id               STP enabled     interfaces
# br101000        8000.d202cf1bf95f       no              L3VNI101000 # <-- NEW
# br101001        8000.bef545052a7c       no              L2VNI101001
# br101002        8000.7678fc174cb3       no              L2VNI101002
# br102000        8000.365d009e5a9f       no              L3VNI102000 # <-- NEW
# br102001        8000.ae8e8ac6fba4       no              L2VNI102001
```

## FRR の設定
`FRR`にBGPとEVPNの設定を投入する. 
FRRでBGPピアを設定する際、対向への接続がeBGPマルチホップだとスタティックルートを設定していないと`BGP OPEN`を送信しようとしない(BIRDだと静的ルートなしでピアを張れた). したがってデフォルトルートでping疎通できるのにBGPピアが張れない場合はスタティックルートを入れるようにする.

### FRR VTEP-1 の設定

```bash
# Static route for eBGP multi-hop
# ポイントツー接続なら不要
UNDERLAY_GATEWAY=$(ip -4 route show | grep default | cut -d ' ' -f3)
echo $UNDERLAY_GATEWAY
BGP_PEER_IP_ADDRESS=192.168.0.2
ip route add $BGP_PEER_IP_ADDRESS via $UNDERLAY_GATEWAY
ip route show
 
# Underlay eBGP Neighbor
vtysh
conf t
router bgp 65001
bgp router-id 1.1.1.1
neighbor 192.168.0.2 remote-as external
neighbor 192.168.0.2 ebgp-multihop
address-family ipv4 unicast
redistribute connected
end
write memory
exit

# Enable EVPN
vtysh
conf t
router bgp 65001
address-family l2vpn evpn
neighbor 192.168.0.2 activate
advertise-all-vni
end
write memory
exit

# Mapping VRF and L3VNI
vtysh
conf t
vrf vrf-1
vni 101000
vrf vrf-2
vni 102000
end
write memory
exit
```

### FRR VTEP-2 の設定
```bash
# Static route for eBGP multi-hop
UNDERLAY_GATEWAY=$(ip -4 route show | grep default | cut -d ' ' -f3)
echo $UNDERLAY_GATEWAY
BGP_PEER_IP_ADDRESS=192.168.0.1
ip route add $BGP_PEER_IP_ADDRESS via $UNDERLAY_GATEWAY
ip route show

# Underlay eBGP Neighbor
vtysh
conf t
router bgp 65002
bgp router-id 2.2.2.2
neighbor 192.168.0.1 remote-as external
neighbor 192.168.0.1 ebgp-multihop
address-family ipv4 unicast
redistribute connected
end
write memory
exit
 
# Enable EVPN
vtysh
conf t
router bgp 65002
address-family l2vpn evpn
neighbor 192.168.0.1 activate
advertise-all-vni
end
write memory
exit
 
# Mapping VRF and L3VNI
vtysh
conf t
vrf vrf-1
vni 101000
vrf vrf-2
vni 102000
end
write memory
exit
```

### FRR コマンド出力結果
```bash
# Check
vtysh -c "show running-config"
vtysh -c "show bgp summary"
vtysh -c "show bgp l2vpn evpn route"
vtysh -c "show evpn mac vni all"
vtysh -c "show evpn vni"
vtysh -c "show evpn vni 101001"
vtysh -c "show evpn vni 101002"
vtysh -c "show evpn vni 102001"
vtysh -c "show evpn vni 101000"
vtysh -c "show evpn vni 102000"

# Sample output
# root@vtep-1:~# vtysh -c "show bgp summary"
# 
# IPv4 Unicast Summary:
# BGP router identifier 1.1.1.1, local AS number 65001 vrf-id 0
# BGP table version 3
# RIB entries 5, using 800 bytes of memory
# Peers 1, using 21 KiB of memory
# 
# Neighbor        V         AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd
# 192.168.0.2     4      65002      23      23        0    0    0 00:05:27            2
# 
# Total number of neighbors 1
# 
# L2VPN EVPN Summary:
# BGP router identifier 1.1.1.1, local AS number 65001 vrf-id 0
# BGP table version 0
# RIB entries 19, using 3040 bytes of memory
# Peers 1, using 21 KiB of memory
# 
# Neighbor        V         AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd
# 192.168.0.2     4      65002      23      23        0    0    0 00:05:27            6
# 
# Total number of neighbors 1

# root@vtep-1:~# vtysh -c "show evpn vni"
# VNI        Type VxLAN IF              # MACs   # ARPs   # Remote VTEPs  Tenant VRF
# 102001     L2   L2VNI102001           1        1        1               vrf-2
# 101001     L2   L2VNI101001           1        1        1               vrf-1
# 101002     L2   L2VNI101002           1        1        1               vrf-1
# 101000     L3   L3VNI101000           1        1        n/a             vrf-1
# 102000     L3   L3VNI102000           1        1        n/a             vrf-2
 
 
# root@vtep-1:~# vtysh -c "show evpn vni 101001"
# VNI: 101001
#  Type: L2
#  Tenant VRF: vrf-1
#  VxLAN interface: L2VNI101001
#  VxLAN ifIndex: 7
#  Local VTEP IP: 192.168.0.1
#  Remote VTEPs for this VNI:
#   192.168.0.2
#  Number of MACs (local and remote) known for this VNI: 1
#  Number of ARPs (IPv4 and IPv6, local and remote) known for this VNI: 2
#  Advertise-gw-macip: No
 
# root@vtep-1:~# vtysh -c "show evpn vni 101000"
# VNI: 101000
#   Type: L3
#   Tenant VRF: vrf-1
#   Local Vtep Ip: 192.168.0.1
#   Vxlan-Intf: L3VNI101000
#   SVI-If: br101000
#   State: Up
#   VNI Filter: none
#   Router MAC: f6:b1:a7:d6:84:f6
#   L2 VNIs: 101001 101002
 
# root@vtep-1:~# vtysh -c "show evpn vni 102000"
# VNI: 102000
#   Type: L3
#   Tenant VRF: vrf-2
#   Local Vtep Ip: 192.168.0.1
#   Vxlan-Intf: L3VNI102000
#   SVI-If: br102000
#   State: Up
#   VNI Filter: none
#   Router MAC: b2:e2:2d:18:e5:9b
#   L2 VNIs: 102001
```

## LXD の起動
事前にネットワークプロファイルを作成する.

```bash
# Network Profile
function create_nwprofile () {
    lxc profile create $SUBNET
    lxc profile device add $SUBNET root disk path=/ pool=default
    lxc profile device add $SUBNET eth0 nic name=eth0 nictype=bridged parent=$BRIDGE
    lxc profile list
}

SUBNET=subnet1-1 ; BRIDGE=br101001 ; create_nwprofile
SUBNET=subnet1-2 ; BRIDGE=br101002 ; create_nwprofile
SUBNET=subnet2-1 ; BRIDGE=br102001 ; create_nwprofile
```

### LXC の起動 (VTEP1)

```bash
cat > start_lxc.sh <<EOF
#/bin/bash
function launch_lxc() {
    lxc launch demo ${LXC_NAME} -p ${SUBNET}
    lxc exec ${LXC_NAME} -- ip link set eth0 up
    lxc exec ${LXC_NAME} -- ip addr add ${IPADDR} dev eth0
    lxc exec ${LXC_NAME} -- ip route add default via ${GATEWAY} dev eth0
    lxc list
    ping -I ${BRIDGE} -c 2 ${IPADDR} # Advertise EVPN Type2 Forcibly
}

LXC_NAME=lxc-1  ; SUBNET=subnet1-1  ; IPADDR=10.1.1.1/24  ; GATEWAY=10.1.1.254 ; BRIDGE=br101001 ; launch_lxc
LXC_NAME=lxc-3  ; SUBNET=subnet1-1  ; IPADDR=10.1.1.3/24  ; GATEWAY=10.1.1.254 ; BRIDGE=br101001 ; launch_lxc
LXC_NAME=lxc-5  ; SUBNET=subnet1-2  ; IPADDR=10.1.2.5/24  ; GATEWAY=10.1.2.254 ; BRIDGE=br101002 ; launch_lxc
LXC_NAME=lxc-7  ; SUBNET=subnet1-2  ; IPADDR=10.1.2.7/24  ; GATEWAY=10.1.2.254 ; BRIDGE=br101002 ; launch_lxc
LXC_NAME=lxc-9  ; SUBNET=subnet2-1  ; IPADDR=10.2.1.9/24  ; GATEWAY=10.2.1.254 ; BRIDGE=br102001 ; launch_lxc
LXC_NAME=lxc-11 ; SUBNET=subnet2-1  ; IPADDR=10.2.1.11/24 ; GATEWAY=10.2.1.254 ; BRIDGE=br102001 ; launch_lxc
EOF

cat > delete_lxc.sh <<EOF
#!/bin/bash
function delete_lxc() {
    lxc delete -f ${LXC_NAME}
    ip -4 neighbor del ${IPADDR} dev ${BRIDGE} # Cleanup host ARP table
    lxc list
}

LXC_NAME=lxc-1  ; IPADDR=10.1.1.1  ; BRIDGE=br101001 ; delete_lxc
LXC_NAME=lxc-3  ; IPADDR=10.1.1.3  ; BRIDGE=br101001 ; delete_lxc
LXC_NAME=lxc-5  ; IPADDR=10.1.2.5  ; BRIDGE=br101002 ; delete_lxc
LXC_NAME=lxc-7  ; IPADDR=10.1.2.7  ; BRIDGE=br101002 ; delete_lxc
LXC_NAME=lxc-9  ; IPADDR=10.2.1.9  ; BRIDGE=br102001 ; delete_lxc
LXC_NAME=lxc-11 ; IPADDR=10.2.1.11 ; BRIDGE=br102001 ; delete_lxc
EOF
 
chmod 755 start_lxc.sh delete_lxc.sh
sh -x start_lxc.sh
 
# root@vtep-1:~# lxc list
# +--------+---------+------------------+------+------------+-----------+
# |  NAME  |  STATE  |       IPV4       | IPV6 |    TYPE    | SNAPSHOTS |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-1  | RUNNING | 10.1.1.1 (eth0)  |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-11 | RUNNING | 10.2.1.11 (eth0) |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-3  | RUNNING | 10.1.1.3 (eth0)  |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-5  | RUNNING | 10.1.2.5 (eth0)  |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-7  | RUNNING | 10.1.2.7 (eth0)  |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-9  | RUNNING | 10.2.1.9 (eth0)  |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
 
# Enter lxc
lxc exec lxc-1 bash
ip addr show
ping 10.1.1.254
exit
```

### LXC の起動 (VTEP2)

```bash
cat > start_lxc.sh <<EOF
#/bin/bash
function launch_lxc() {
    lxc launch demo ${LXC_NAME} -p ${SUBNET}
    lxc exec ${LXC_NAME} -- ip link set eth0 up
    lxc exec ${LXC_NAME} -- ip addr add ${IPADDR} dev eth0
    lxc exec ${LXC_NAME} -- ip route add default via ${GATEWAY} dev eth0
    lxc list
    ping -I ${BRIDGE} -c 2 ${IPADDR} # Advertise EVPN Type2 Forcibly
}

LXC_NAME=lxc-2  ; SUBNET=subnet1-1  ; IPADDR=10.1.1.2/24  ; GATEWAY=10.1.1.254 ; BRIDGE=br101001 ; launch_lxc
LXC_NAME=lxc-4  ; SUBNET=subnet1-1  ; IPADDR=10.1.1.4/24  ; GATEWAY=10.1.1.254 ; BRIDGE=br101001 ; launch_lxc
LXC_NAME=lxc-6  ; SUBNET=subnet1-2  ; IPADDR=10.1.2.6/24  ; GATEWAY=10.1.2.254 ; BRIDGE=br101002 ; launch_lxc
LXC_NAME=lxc-8  ; SUBNET=subnet1-2  ; IPADDR=10.1.2.8/24  ; GATEWAY=10.1.2.254 ; BRIDGE=br101002 ; launch_lxc
LXC_NAME=lxc-10 ; SUBNET=subnet2-1  ; IPADDR=10.2.1.10/24 ; GATEWAY=10.2.1.254 ; BRIDGE=br102001 ; launch_lxc
LXC_NAME=lxc-12 ; SUBNET=subnet2-1  ; IPADDR=10.2.1.12/24 ; GATEWAY=10.2.1.254 ; BRIDGE=br102001 ; launch_lxc
EOF

cat > delete_lxc.sh <<EOF
#!/bin/bash
function delete_lxc() {
    lxc delete -f ${LXC_NAME}
    ip -4 neighbor del ${IPADDR} dev ${BRIDGE} # Cleanup host ARP table
    lxc list
}

LXC_NAME=lxc-2  ; IPADDR=10.1.1.2  ; BRIDGE=br101001 ; delete_lxc
LXC_NAME=lxc-4  ; IPADDR=10.1.1.4  ; BRIDGE=br101001 ; delete_lxc
LXC_NAME=lxc-6  ; IPADDR=10.1.2.6  ; BRIDGE=br101002 ; delete_lxc
LXC_NAME=lxc-8  ; IPADDR=10.1.2.8  ; BRIDGE=br101002 ; delete_lxc
LXC_NAME=lxc-10 ; IPADDR=10.2.1.10 ; BRIDGE=br102001 ; delete_lxc
LXC_NAME=lxc-12 ; IPADDR=10.2.1.12 ; BRIDGE=br102001 ; delete_lxc
EOF
 
chmod 755 start_lxc.sh delete_lxc.sh
sh -x start_lxc.sh
 
# root@vtep-2:~# lxc list
# +--------+---------+------------------+------+------------+-----------+
# |  NAME  |  STATE  |       IPV4       | IPV6 |    TYPE    | SNAPSHOTS |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-10 | RUNNING | 10.2.1.10 (eth0) |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-12 | RUNNING | 10.2.1.12 (eth0) |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-2  | RUNNING | 10.1.1.2 (eth0)  |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-4  | RUNNING | 10.1.1.4 (eth0)  |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-6  | RUNNING | 10.1.2.6 (eth0)  |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
# | lxc-8  | RUNNING | 10.1.2.8 (eth0)  |      | PERSISTENT | 0         |
# +--------+---------+------------------+------+------------+-----------+
 
# Check Ping
lxc exec lxc-2 bash
ifconfig
ping 10.1.1.254
exit
```
 
# 動作検証
今回は`Route Leaking`設定をしていないので異なるVRF配下のネットワーク(Case-3)には到達できない.

```bash
# VTEP-1
lxc exec lxc-1 bash
ping 10.1.1.2  # Case-1: 同じsubnet --> OK
ping 10.1.2.6  # Case-2: 異なるsubnet --> OK
ping 10.2.1.10 # Case-3: 異なるVRF --> Destination Unreachable
exit
```

## 確認コマンド
```bash
#-----------------------------------------
# common
#-----------------------------------------
tcpdump -i any port 4789       # VXLAN のパケット監視
tcpdump -i any port 179        # BGP/EVPN のパケット監視

#-----------------------------------------
# L2
#-----------------------------------------
bridge fdb show                # bridge の Forwarding DB の確認
bridge link                    # bridge 配下のIF一覧
bridge monitor link            # bridge のイベント監視
bridge monitor fdb
ip neighbor                    # ARPテーブル確認
ip neighbor show dev br101001  # IF 
ip -s neigh flush all          # ARPテーブル初期化
ip -d link show type vxlan     # vrf-1 に属するIFの詳細情報
ip -d link show vrf vrf-1      # VXLAN IF の詳細情報

#-----------------------------------------
# L3
#-----------------------------------------
ip route show vrf vrf-1        # ルーティングテーブルの確認
ip -d addr show vrf vrf-1      # vrf-1 に属するIFの詳細情報
ip -d addr show type vxlan     # VXLAN IF の詳細情報

#-----------------------------------------
# FRR
#-----------------------------------------
show running-config            # FRR の設定
show bgp summary               # BGP 情報
show bgp vrf vrf-1 summary     # Overlay BGP の情報
show bgp vrf vrf-1 ipv4 unicast summary 
show bgp ipv4 unicast neighbors 192.168.0.1 received-routes    # Overlay で受信するルート情報
show bgp ipv4 unicast neighbors 192.168.0.1 advertised-routes　# Overlay で広告しているルート情報
show bgp evpn route            # EVPN 情報
show bgp l2vpn evpn summary    # EVPN の状態確認
show bgp l2vpn evpn vni        # VNIとVRFの情報
```

# 考察: EVPN で Type-2 ルートが広報される条件
cumulus がleafスイッチの場合、配下のコンテナやVMで通信を発生させれば自動的に送信元と宛先のType-2ルート(/32)がルーティングテーブルに載ってくるのだが今回は挙動が異なる.
例えば上記動作検証のCase-1において、対向のVTEP配下のノードに通信を発生させても、送信元と宛先両方ともEVPNで経路広報の対象にならない. Case-2の場合は送信元`10.1.1.1@VTEP1`のType-2がVTEP-2に広報され、`10.1.2.6@VTEP-2`の経路はVTEP-1に広報されていない. 

```bash
# Case-2 実施後のルーティングテーブル
root@vtep-1:~# ip route show vrf vrf-1
unreachable default metric 4278198272
10.1.1.0/24 dev br101001 proto kernel scope link src 10.1.1.254
10.1.2.0/24 dev br101002 proto kernel scope link src 10.1.2.254
# 10.1.2.6/32 のエントリが存在しない

root@vtep-2:~# ip route show vrf vrf-1
unreachable default metric 4278198272
10.1.1.0/24 dev br101001 proto kernel scope link src 10.1.1.254
10.1.1.1 via 192.168.0.1 dev br101000 proto bgp metric 20 onlink # <-- ホストルート
10.1.2.0/24 dev br101002 proto kernel scope link src 10.1.2.254
```

結論から言うと、FRRが稼働する物理サーバ側の`ip neighbor`(ARPテーブル)に`REACHABLE`で一度エントリが載らないとEVPNで経路が広報されないということがわかった.

```bash
root@vtep-1:~# ip neighbor show dev br101001
10.1.1.1 lladdr 00:16:3e:c9:30:44 STALE # <-- エントリが存在
root@vtep-1:~# ip neighbor show dev br101002
10.1.2.6 lladdr 00:16:3e:e0:59:6b STALE

# エントリが存在しない
root@vtep-2:~# ip neighbor show dev br101001
root@vtep-2:~# ip neighbor show dev br101002
```

`ip neighbor`にエントリを載せるにはそのサブネットのgatewayに対して`ARP Request/Reply`の通信を発生させる必要があるようだ. `Case-1`の場合、同サブネット間の通信の通信はgatewayを経由しないので物理ホスト側のARPテーブルにはエントリが載らない. `Case-2`の場合、`10.1.1.1`から`10.1.1.254`へARPリクエストが発生するためARPテーブルにエントリが載る.これによりEVPN Type-2で対向VTEPに`10.1.1.1/32`の経路が載るようだ. しかしこの理屈だと、戻りの通信(`10.1.2.6@VTEP2` --> `10.1.1.1@VTEP1`)でも`10.1.2.254`へのARPリクエストが発生するので`10.1.2.6/32`の経路もEVPN Type2でVTEP1に広報されてもいい気がするのだが、なぜそうならないのかはコードをよく読まないとわからない.

このEVPN Type2がスムーズに広報されない問題を回避する最も簡単な方法は、コンテナ起動時に物理ホストから各コンテナに対して通信を意図的に発生させることである.  [Cumulus本家のPoC](https://gitlab.com/CumulusNetworks/evpn-to-the-host/blob/master/poc-environment/roles/hypervisor/tasks/main.yaml) ではハイパーバイザ側からVMにpingしてEVPN Type2を強制的に広報させているように見える. 別のアプローチでこの問題をスマートに解決する有効なオプションや設定を現時点では見つけることができなかった(ので知ってる方いたらおしえてほしい).

LXCコンテナを削除した際には、`BGP UPDATE(Withdrawn Routes)`が広報されることにより、該当コンテナのホストルートはルーティングテーブルから削除される.[この辺りの実装を見ると](https://github.com/FRRouting/frr/blob/master/zebra/rt_netlink.c) `bridge fdb show`や`ip neighbor`を監視することでコンテナやVMの削除を検知していることがわかる. 

# Source NAT
VXLAN/EVPN に関係ないが [GitLab: evpn-to-the-host](https://gitlab.com/CumulusNetworks/evpn-to-the-host#host-architecture) の Future work にあった NAT もついでに検証する. LinuxでVRFを前提にNATするのは初ケースなので動作する設定を見つけ出すのに苦労した. 説明しづらいので図も添付する.

![host-vtep-snat.png](https://qiita-image-store.s3.amazonaws.com/0/323524/a3ded392-6388-eb7a-2b26-701acc72a091.png)

まず物理ホストにて外部との接続を持つインターフェースをブリッジ(br0)に所属させる.　次にOverlay NetworkのパケットをどうにかしてUnderlay Networkに持っていくために、UnderlayのVRF(VRF-PUBLIC)を作成し仮のインターフェースを作成する(vrf_br0). FRRでRoute Leakingを設定し、デフォルトルートをVRF-PUBLICに向ける. あとはiptablesでSource NATをしてやれば、物理ホストのインターフェースを使用して外に出ることができる. 
デフォルトルートでRoute Leakingをするのは[Cumulus のマニュアル](https://docs.cumulusnetworks.com/display/DOCS/Virtual+Routing+and+Forwarding+-+VRF)によると非推奨だがとりあえず動作はした. デフォルトルートで無闇にリークしてしまうとVRFによる隔離が弱くなる可能性があるため、実際に使う際はよく考えてから設定する必要がある. 

```bash
#----------------------------------------------------
# br0
#----------------------------------------------------
# Create a bridge
brctl addbr br0
brctl stp br0 off
ip link set dev br0 up
ip addr add 172.16.25.100/24 dev br0
sysctl -w net.ipv4.conf.br0.rp_filter=0
sysctl -p

# 管理インターフェースの ens33 を br0 に参加させる
# ens33 で接続していた通信は切れるので注意
ip addr flush dev ens33
brctl addif br0 ens33
ip link set dev ens33 up
ip route add default via 172.16.25.254 dev br0

#----------------------------------------------------
# VRF-PUBLIC
#----------------------------------------------------
# Set value
VRF_TABLE_ID=103000
PUBLIC_GATEWAY=172.16.25.254
VETH_PUBLIC_IP=172.16.25.101/24

# Create VRF
ip link add vrf-public type vrf table $VRF_TABLE_ID
ip link set vrf-public up

# Check (VRF status is UP)
ip -d link show vrf-public
ip route show vrf vrf-public
ip vrf show

# Create a Virtual cable (veth)
ip link add name vrf_br0 type veth peer name br0_vrf

# Connect (br0)*<-- (br0_vrf)
brctl addif br0 br0_vrf
ip link set br0_vrf up

# Connect (vrf_br0) -->*(VRF)
ip addr add $VETH_PUBLIC_IP dev vrf_br0
ip link set dev vrf_br0 master vrf-public
ip link set vrf_br0 up

# Set default route in VRF
ip route add table $VRF_TABLE_ID default via $PUBLIC_GATEWAY dev vrf_br0

# Check
brctl show
ip addr show vrf_br0
ip addr show vrf vrf-public
ip link show br0_vrf
ip route show vrf vrf-public
ping -I vrf_br0 $PUBLIC_GATEWAY


#-------------------------------------------------------
# (FRR) RouteLeaking
# vrf-1 && vrf-2 --> vrf-public
#-------------------------------------------------------
vtysh
conf t
vrf vrf-1
ip route 0.0.0.0/0 vrf-public nexthop-vrf vrf-public
vrf vrf-2
ip route 0.0.0.0/0 vrf-public nexthop-vrf vrf-public
end
write memory
exit

# Check
vtysh -c "show ip route vrf vrf-1"
vtysh -c "show ip route vrf vrf-2"
ip route show vrf vrf-1
ip route show vrf vrf-2

#--------------------------------------------------------
# SNAT (overlay --> public)
#--------------------------------------------------------
SNAT_IPADDR=$(ip -4 addr show br0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
echo $SNAT_IPADDR
iptables -t nat -A POSTROUTING -p all -s 10.1.1.0/24 -j SNAT --to-source $SNAT_IP_ADDR
iptables -t nat -A POSTROUTING -p all -s 10.1.2.0/24 -j SNAT --to-source $SNAT_IP_ADDR
iptables -t nat -A POSTROUTING -p all -s 10.2.1.0/24 -j SNAT --to-source $SNAT_IP_ADDR
iptables -t nat -L

# ping check
lxc list
lxc exec lxc-1 bash
ping 8.8.8.8
exit
```

物理サーバでルーティングできるので、任意のVRFをBGPでサーバまで伝搬させれば分散NATが可能となる.


# おわり
結論として、ただのLinuxにFRRをインストールすることで、従来leafスイッチでやっていたEVPN+VXLANの処理をサーバエンドにオフロードすることができた. 数年前にOpenStack NeutronとNamespaceを駆使して実現できたことをBGPやVRFで置き換えているだけなのだが、SPOFが存在しないのでより洗練されたアーキテクチャになった. VXLANのオーバヘッドに懸念があるのでネットワーク性能は試験する必要がある. またセキュリティ(ACL)や細かい動作などを考慮していないので実用にはまだ遠い.