結論から言うと、コアルータは EVPN から完全に離脱し、overlay ネットワークはハイパーバイザ(HV)側で完全に自己完結する構成になりました。前回の記事で触れた「論理トンネル(lt-)を使った戻り経路」も、そっくり不要になっています。
アーキテクチャの変化 (Before / After)
| 項目 | 旧構成 (OSPF + EVPN) | 新構成 (BGP fabric) |
|---|---|---|
| Underlay | OSPF | BGP unnumbered (IPv6 LL / RA) |
| EVPN Control Plane | コア (MX240) が RR 兼 ゲートウェイ | HV 同士の eBGP multihop Full-mesh |
| VRF Leak / 外部接続 | 内部論理トンネル lt- + 自己 eBGP |
Distributed Exit-node + VRF Import |
旧構成で抱えていた課題
旧構成では、Juniper MX240 が EVPN の Route Reflector(RR)とゲートウェイを兼ねていました。しかし、overlay の VRF とグローバルテーブル(inet.0)の間で経路をやり取りする際、JunOS の仕様上、素直に VRF 間リークを行うのが難しく、論理トンネル lt- を使って自分自身と eBGP を張る(AS 64998 ⇄ 64999)という力技で実現していました。
inet.0 ←→ lt-0/2/0.0 ←(eBGP 64998/64999)→ lt-0/2/0.1 ←→ EVPN-VRF
動きはするのですが、この論理トンネルと自己 eBGP を組み合わせた構成は、overlay のトラブルシュートのたびにこの迂回路を頭の中で辿る必要があって、じわじわと負債感がありました。自分で組んだのに、半年後の自分が読んで分かるかというと怪しい。
そんな中、Proxmox VE 9.2 で SDN に BGP fabric が入ったのを見て、これなら作り替えられそうだと思って着手しました。
Underlay の BGP unnumbered 化
いきなり全変更はリスクが高いため、HV はコアルータ2台にそれぞれ繋がっていることを活かし、片方のコアとのリンクだけを先に fabric 化することにしました。もう片方は旧構成(OSPF + EVPN)のまま残しておけば、失敗しても overlay は生きています。
設定とピアリング
GUI から BGP fabric を作成し、ノードごとの ASN(65021 / 65022 / 65023)、IPv6 Loopback、コア向けの NIC を割り当てます。
生成された FRR の設定を確認すると、BGP unnumbered(IPv6 リンクローカル + RA によるピア自動発見)が動いていることが分かります。IP アドレスを一切書かずに eBGP が張れるのは、やはり気持ちがいい。
! FRR (HV側)
neighbor bgpv6 peer-group
neighbor bgpv6 remote-as external
neighbor bgpv6 local-as 65021 no-prepend replace-as
neighbor nic5 interface peer-group bgpv6
対する JunOS(コア)側は dynamic-neighbor の peer-auto-discovery で受けます。FRR 側が RA を見て peer を発見するので、JunOS 側から RA を出す設定も要ります。
# Junos (コア側)
set protocols bgp group FABRIC type external
set protocols bgp group FABRIC peer-as-list PVE-FABRIC
set protocols bgp group FABRIC family inet unicast extended-nexthop
set protocols bgp group FABRIC family inet6 unicast
set protocols bgp group FABRIC dynamic-neighbor DYN-PVE peer-auto-discovery family inet6 ipv6-nd
set protocols bgp group FABRIC dynamic-neighbor DYN-PVE peer-auto-discovery interface xe-0/2/0.0
set protocols router-advertisement interface xe-0/2/0.0 default-lifetime 0
default-lifetime 0 は「RA は出すが自分をデフォルトルータとしては広告しない」という意味です。
ハマりポイント:コアが別の AS を名乗る必要があった
これで上がるはずと思って commit したのですが、セッションが張れませんでした。ログを見ると FRR 側からこれが返っていました。
bad peer AS number, value 45689
HV 側の FRR は基底の AS が 45689(EVPN の controller ASN)で、そこに local-as 65021 を被せている状態です。そこへコアが素の 45689 を名乗ると、FRR から見れば「eBGP なのに相手が自分と同じ AS」になります。
コア側にも別の AS を持たせて解決しました。
set protocols bgp group FABRIC local-as 65001 no-prepend-global-as
言われてみればその通りなのですが、コアに fabric 専用の AS 番号を振るという発想が最初は出てきませんでした。
underlay ができたので EVPN を移す
片系の fabric が上がって、VTEP の loopback がお互いに見えるようになりました。ここまでは旧構成と並走しているので無停止です。
次が本番で、EVPN を fabric の上に載せ替えます。SDN の EVPN controller に fabric を参照させると、VTEP 同士が eBGP multihop の full-mesh を組む形になります。
leaf の AS がバラバラでも、route-target には controller の ASN が使われるため、per-node ASN にしても import が成立します。
同時に VTEP も IPv6 に切り替えました。VXLAN の外側ヘッダが v6 になります。
Apply すると、コアとの EVPN セッションが消えて、HV 同士の full-mesh に置き換わりました。コアは EVPN の経路を一つも持たなくなります。
Note: 生成された設定は正しいのに kernel の VXLAN インターフェースが古い (v4 のままの) 状態になることがありました。
ifreload -aで作り直したら反映されました。
v4 が流れてこない
fabric は v6 で作りました。ルータ層を IS-IS の v6 only にしたときの流れで、こちらも v4 のアドレスを持たせない方針にしたためです。
ただ overlay のサービスは v4 が主で、外への default route も v4 が必要です。ここで詰まりました。v6 の default は届くのに、v4 の default が届きません。
コア側は family inet unicast extended-nexthop を入れて v4 も広告しているのですが、HV 側で受け取れていませんでした。
SDN が生成した FRR の設定を見て分かりました。IPv6 の address-family でしか peer を activate していません。
BGP は address-family ごとに activate しないとその種類の経路が流れません。fabric を v6 で作ったので、SDN は「v6 の経路しか流れない」と判断して v4 の activate を出さなかったわけです。
とりあえず frr.conf.local で activate を足す
# /etc/frr/frr.conf.local
router bgp 45689
address-family ipv4 unicast
neighbor bgpv6 activate
exit-address-family
exit
これで v4 の default が v6 のリンクローカルを next-hop として入ってきました。underlay には v4 のアドレスが一つも無いまま、v4 の経路だけが流れています。
そもそも v6 only にこだわる必要が無かった
動いたので一旦は満足していたのですが、後から考えると 前提の方がおかしかったと気づきました。
v4 のアドレスを持たない方針にしたのは、ルータ層で JunOS の IS-IS が v4 の経路を v6 の next-hop で解決してくれなかったからです。IGP で運べないなら v4 loopback を持っていても仕方がない、という消去法でそうなっていました。
fabric に v4 と v6 の両方の prefix を設定し直しました。こうすると SDN が v4 の activate も loopback も生成してくれるので、frr.conf.local に activate を足す必要はなくなります。リンクに v4 が付かないことは変わらないので、unnumbered は成立したままです。
「GUI だけだと完結しない」と思っていたのは、自分が変わったことをやろうとしていただけでした。
exit-node を全台にする
overlay から外へ出る経路は SDN の exit-node で作ります。最初は1台だけ exit-node にしていましたが、他の HV 上の VM は、いったん exit-node まで VXLAN で運ばれてから外に出ることになり遠回りです。
3台とも exit-node にしました。これで各 HV が自分のリンクから直接外に出ます。
PVE の exit-node は「落とす」仕組みだった
そうしたら、exit-node にした HV 上の VM が外に出られなくなりました。調べると exit-node 自身の VRF に default route が入っていません。
default-originate は他のノードに配る機能なので、自分の VRF には入らない。ではどうやって出るつもりなのだろう、と気になって SDN のコードを読みました。
# Zones/EvpnPlugin.pm
if (!$is_evpn_gateway) {
"post-up ip route add vrf $vrf unreachable default metric 4278198272"
} else {
"post-up ip route del vrf $vrf unreachable default metric 4278198272"
}
Linux の VRF は、VRF のテーブルで引けなかったパケットを main のテーブルに落とします (ip rule が l3mdev の次に main を見るため)。PVE はこれを使っていました。 通常のノードには VRF テーブルに unreachable default を刺して落下を塞ぎ、exit-node ではそれを消す。exit-node の定義は「default を配られるノード」ではなく、「VRF のルックアップを main に素通りさせるノード」だったわけです。
戻りは別で用意されていて、overlay の VRF を default VRF の BGP に import vrf する設定が生成されます。そこから外へ広告して戻ってくる。NAT ではなく素直なルーティングです(subnet ごとの SNAT オプションもありますが、そちらは既定で無効の逃げ道という位置づけに見えます)。
つまり PVE の前提は 「ホストの main の default が、そのまま外への道である」 ことです。1本のブリッジで管理もデータも兼ねている普通の構成なら、これで素直に成立します。
うちは成立しませんでした。 main の default は管理 LAN のゲートウェイを向いていて、AS の外向き経路ではありません。しかも kernel が入れた管理系の default は、BGP が入れた fabric の default より優先されます。素通りさせたら overlay の通信が全部管理系から出ていってしまいます。
なので、VRF の中に BGP の default を持たせることにしました。
router bgp 45689 vrf vrf_evpn
address-family ipv4 unicast
import vrf default
これで default VRF が fabric から受け取った default が、そのまま overlay の VRF に入ります。管理系は経由しません。
ここで PVE の想定から一歩外れました。 これが後で効いてきます。
戻りの経路をどうするか
全台を exit-node にしたところで、「exit-node にした HV 上の VM は外に出られるのに、他の HV 上の VM が出られない」 という症状が出ました。
パケットを追いかけると、こうなっていました。
VM (HV-C) → HV-C のリンク → 外 ← 行きは正常
外 → コア → ECMP → HV-A のリンクに着弾 ← 戻りは別の HV へ
HV-A → VXLAN → HV-C ← ここで止まる
分散 exit-node は本質的に非対称経路になります。 行きは VM が居る HV から出ますが、コアは subnet の集約しか知らないので、戻りがどの HV に着地するかは ECMP 次第です。着地した HV が VXLAN で本来の HV へ転送する必要があります。
つまり考えることが二つあります。着地した HV がちゃんと転送できるかと、そもそも別の HV に着地させるべきかです。
転送が止まっていた
前者から潰しました。
まず net.ipv4.ip_forward が 0 でした。以前 fabric を立てたときに IPv6 の forwarding だけ有効にして、v4 を見落としていた単純な取りこぼしです。
もう一つが rp_filter で、これは少し考えました。
rp_filter は逆経路のチェックで、既定値は 2 (loose) です。loose は「送信元アドレスがどこかの interface 経由で到達可能なら通す」という判定なので、default route が一本でもあれば実質何も落としません。それなのに落ちている。
VRF の中の default を見て分かりました。
default nhid 454 proto bgp metric 20
nexthop via inet6 fe80::… dev nic5
nexthop via inet6 fe80::… dev nic2
next-hop の interface が vrf_evpn に属していません。 さっき自分で入れた import vrf default の結果で、VRF の中の経路が VRF の外を指しています。逆引きがそこで VRF の外に出てしまい、有効な逆経路として数えられていないようでした。
PVE の想定どおり main に素通りさせていれば、逆引きも同じく main に落ちて普通に解決していたはずです。想定から外れた分の皺寄せがここに来た、という感じでした。
0 にして通るようになりました。loose は元から何も落としていないので、失ったものはありません。ただ、根っこは 管理系と外向きの経路が同じテーブルに同居していることで、そこを分ければまとめて消える話です。管理系を専用の VRF に隔離するのは別のタスクにして、ここでは先へ進みました。
コアに何を広告するか
後者は設計の判断です。各 HV は EVPN 経由で他の HV 上の VM の /32 も学習しています。これをそのままコアに広告すれば、コアは VM 単位で居場所を知ることになり、遠回りは起きなくなります。
一度はそうしようかと考えました。ただ、それをやると コアが VM 一台ずつの居場所を知ることになります。overlay の中で完結させるために組み直したのに、コアがその中身を持ち始めるのでは本末転倒です。
なので subnet の集約だけを広告することにしました。移動は EVPN の type-2 (MAC mobility) で overlay の中に閉じ、コアは何も知らないままでいられます。
! /32 ホスト経路と Default Route を除外し、集約サブネットのみを通す
ip prefix-list LEAK-SUBNET-ONLY seq 5 deny 0.0.0.0/0
ip prefix-list LEAK-SUBNET-ONLY seq 10 permit 0.0.0.0/0 le 31
プレフィックス長で判定する形にしているので、subnet を追加しても設定を触らなくて済みます。遠回りが残るのは承知の上で、そちらを選びました。
できあがったもの
- コアから EVPN が消えました。 route-reflector も VRF も論理トンネルも全部削除
- overlay は HV 3台で自己完結。 VTEP は IPv6、EVPN は full-mesh
- 各 HV が自分のリンクから外に出ます。 遠回りなし
- コアが知る overlay の情報は subnet の集約だけ。 VM が増えても移動してもコアは無関係
前の記事で「論理トンネルで戻り経路を作る」と説明した部分が丸ごと不要になりました。あれは JunOS で VRF 間を繋ぐための回避策で、そもそもコアが overlay のゲートウェイを兼ねていたことに起因する複雑さでした。役割を leaf 側に寄せたら、回避策ごと消えたことになります。
小規模な AS なので、こういう構成を実機で試して作り替えられるのはありがたい環境だと思っています。