LGTM

Looks Good To Me

ネットワークは宣言的になりえるか

2020-07-08 追記

はじめに

Kubernetes などのコンテナオーケストレーターとの対比によって、ネットワークの世界でも同じように制御できないか注目されています。Cisco、Apstra、VMWare などが言う "Intent Based Networking" や "Closed Loop Automation" も同じものを指していると思われます。宣言的ネットワーキングは「あるべき状態の維持をプロトコルやソフトウェアに任せられるかもしれない」という点で運用上のメリットがあります。

以前所属していた国際Tier1 ISP *1 で、Kubernetes ほど洗練されてはないものの コンセプトとしてはこれを実践していたり、現在もネットワーク自動化の取り組みの中でゴールをここに設定したりしています。

このエントリーでは、

  • 宣言的ネットワーキングとは何なのか
  • 従来のネットワーク運用を宣言的ネットワーキングに移行できるのか
  • 移行できるとしたら、どういうステップを踏めばよいか

について自分の考えをまとめてみたいと思います。

宣言的ネットワーキングとは何なのか

ざっくりコンセプトとしてはKubernetes と同じで、「あるべき状態を宣言的に記述し API に渡すことで、システムが現在の状態を監視、必要に応じてあるべき状態に収束させてくれるネットワーク制御手法」のことです。

"宣言的" なのはあるべき状態の記述方法・APIの呼び出し方であって、内部的に実行される状態遷移プロセス自体は手続き的になりえます。Kubernetes に詳しくありませんが、おそらくそちらも同じであろうと想像しています。「システムに触れるユーザーが手続きを意識しない」であって「手続き的な何かは一切存在しない」ではない点に注意が必要ですが、これまでの議論 *2 を踏襲し、ここでは「宣言的ネットワーキング」と書くことにします。

実現のためのキーポイントはいくつかあって

  1. あるべき状態の記述方法と、その対象・抽象度の検討
  2. あるべき状態の抽象的な記述 (1) を、ネットワークデバイスが解釈できる設定・パラメーターに変換する機能
  3. 現在のネットワーク状態を取得する機能
  4. ネットワークデバイス上の表現を使って、あるべき状態 (2) と現在 (3) を比較する機能
  5. 差分 (4) を自動投入する機能

が必要だと考えています。それぞれについては、ネットワーキングならではの事情もふまえて後述します。

従来のネットワーク運用との対比

従来のネットワーク運用では、あるべきネットワーク状態を包括的には記述しません。代わりに 障害などのネットワークイベントを検知し、手続き的なアプローチによってあるべき状態に復旧させます。あるいは、あるべき状態が変化した際にその差分だけを反映させます。

従来のネットワーク運用でも 宣言的アプローチを取っている部分はあります。たとえばICMPによる到達性・遅延監視などです。許容できる latency を定義し 現在の状態と比較、超過した場合に通知するような運用はよく行われています。根本原因の自動特定 (Root Cause Analysis)・自動修復(Self Healing / Remediation) まで行えないものの アプローチとしては宣言的であり、部分的にではありますが 宣言的ネットワーキングに必要な要素を実装できていると言えそうです。

一方、宣言的ネットワーキングでは現在のネットワーク状態を取得、あるべき状態との比較を軸にします。比較した結果差分があれば状態に変化を加え、あるべき状態に自動収束させます。これをフィードバックループ的にぐるぐる回すことがポイントです。 (Reconciliation Loop)

もちろん、影響時間の短縮や Root Cause Analysis のために、障害などネットワークイベントを追加のトリガーとしたり、解析のヒントにすることはあると思います。

脱線しますが、ループを回す際、フィードバックが強すぎるなどシステムが発振して収束しないことも考えられますので、安定化のための何かしらの仕組みは別途必要になります。

コンテナオーケストレーションとの対比

Kubernetes をはじめとする宣言的コンテナオーケストレーションと宣言的ネットワーキングについて、若干乱暴ですが次のような違いがあると考えています。

コンテナオーケストレーション ネットワーキング
制御対象 コンテナ群 ネットワーク
あるべき状態は物理を含むか ⭕️
制御対象自体がロバスト ⭕️

両者を比較するために

1. あるべき状態の記述方法と、その対象・抽象度の検討

について触れておく必要があり すこし脱線しますが、「あるべき状態をどのレベルまで抽象化して記述できるか」はとても重要です。 あるべき状態を宣言的に書くと その状態を維持してくれるシステム があるとして、何を記述するでしょう? 究極的には

  • サービスを安価に安定的に提供できること

のように書きたいんじゃないでしょうか? これは残念ながら抽象的すぎて、今のところ現実的ではないため どんどん具体化します。

  • 秒間 1万 HTTPSリクエストを返す。10 ms 以内に。月額コストは1000万円以内
  • 国内ユーザー間のIPトラフィックをトランジットする。自社の網内で latency 20 ms 以内、jitter 0.1 ms 以内、loss rate 0.01% 以内。月額コストは1億円以内

のような、サービス品質を記述するレベルでも厳しいかもしれません。結局のところ、あるべき状態の抽象度として

  • 東京リージョンに、このスペックで動くapp AのPod が5コある
  • データセンターA にいるISP X からトランジット10G x 2 を買い、データセンターB に収容する

くらいまで具体化する必要があるんじゃないでしょうか。もともと宣言的に記述したい対象はビジネス・サービスレベルのものであるはずが 抽象度が高すぎて実現できず、具体化して「ビジネス・サービスを実現するために設計したコンテナ配置、ある相互接続」レベルまでブレイクダウンする必要があります。

さて 話を戻しますが、この抽象度まで下げる場合 ネットワークには物理的な制約が強く現れます。たとえばISP X からトランジットを買い 専用線を借りて相互接続する場合、その接続に替えはありません。「ISP X に繋がるこの専用線トラフィックを乗せる」があるべき状態になります。コンテナのように「すぐ廃棄してどこかに新規作成し、数があってればOK」とすることができません。残念なことに障害時には物理交換が発生するため、復旧プロセスも完全に自動化することは困難です。

このデメリットを補うため、ネットワークプロトコル自体にロバスト性が備わっています。 物理は替えがきかないものの、あらかじめ冗長性を持ちさえすれば、それらをうまく切り替えて自律分散的に動く仕組みがもともとあるわけです。 *3

この性質のため、ネットワーク制御システムはコンテナと比べておそらく薄くできます。コンテナ自体のロバスト性が低いため オーケストレーターが監視・操作する必要があるのに対し、「ネットワークをうまく設計して設定しておけば、あとは自律的に動いてくれる」というのは、おもしろい違いです。

個人的には、宣言的ネットワーキングにおける制御システムの主な役割は QoS と最適化だと考えています。乱暴に言えば、ネットワークは

  • 何かが壊れたら、自律的にそこを使わないようにして予備を使い始める
  • 壊れたものが直ったら、自律的に元に戻る

ので、残る問題としてまず思いつくのが

  • 壊れた判定が難しいもの
  • 計測しづらいもの
  • 自律的な調整が難しいもの

だからです。たとえば

  • ある回線の packet loss rate が許容できる閾値を超えたら、制御システムが自動的に切り離す
  • 10GE が輻輳しないレベルでギリギリまでトラフィックを流す

などです。

従来のネットワーク運用を宣言的ネットワーキングに移行できるのか

さきに

宣言的ネットワーキングにおける制御システムの主な役割は QoS と最適化だと考えています

と書きましたが、ここでは簡単のために「あるべき状態を宣言的に記述でき、何かコマンドを叩くとネットワークがその状態に収束する (ただし物理を除く)」を一歩目のゴールとしましょう。これはKubernetes チュートリアルに出てくるレベルと同じもので、簡単ですが宣言的ネットワーキングだと言っていいと思います。

図示してみると 👇 のような感じです。

一歩目のゴールまでのポイントは3点で、

  1. あるべきネットワークの状態を抽象的に・宣言的に記述する
  2. あるべき状態が変わったら、変化に追随する
  3. 故障したら、あるべき状態の一歩手前 = 冗長度は低いが品質低下のない状態 まで遷移する

しかも 3 はネットワークプロトコルに任せることができるため、宣言的ネットワーキングの一歩目を踏み出すために複雑な制御システムは不要だと考えています。ここでのフォーカスは、

1. あるべき状態の記述方法と、その対象・抽象度の検討
2. 抽象的なあるべき状態の記述 (1) を、ネットワークデバイスが解釈できる設定・パラメーターに変換する機能
5. 差分を自動投入する機能

です。

なお、図中では

  • 故障 (Outage) = ネットワーク自身が、自分で「壊れている」と判定できる状態
  • 品質低下 (Service Degradation) = ネットワーク自身は「壊れている」判定できないが、制御システムから見れば品質を満たせていない状態

を明示的に区別しています。

ネットワーク設定は冪等で宣言的だが、問題は抽象度

さきほど抽象度を下げた宣言的記述「データセンターA にいるISP X からトランジット10G x 2 を買い、データセンターB に収容する」を例にしてみましょう。

ネットワークデバイスへの入力を便宜的に config で書けば

protocols {
    bgp {
        group x-transit {
            type external;
            metric-out igp;
            export [ transit-out ];
            remove-private;
            neighbor 192.0.2.1 {
                description "Transit X AS65000";
                out-delay 1;
                import [ transit-in AS65000-in ];
                peer-as 65000;
            }               
        }
   }
}

policy-options {
    policy-statement transit-in {
        term default {
            then {
                community add transit;
                local-preference 80;
                next policy;
            }
        }
    }

    policy-statement transit-out {
        term peer-routes {
            from community [ customer ours ];
            then accept;
        }
    }


    policy-statement AS65000-in {
        term match-exact {
            from {
                route-filter 198.51.100.0/24 exact;
            }
            then {
                next policy;
            }
        }
        then reject;
    }
    community ours members XXX:100;
    community customer members XXX:110;
    community transit members XXX:120
}

たとえばこのようになります。中身は適当で特に意味はありませんが、この例のようなネットワークの設定自体、ある条件を満たす限り 宣言的だと考えています。ある条件とは

  1. NETCONF や RESTCONF でいう candidate config を、atomic にcommit できること
  2. 存在しない設定は消えること

で、これを満たす限り ほとんどのネットワークベンダーの設定はあるべき状態の宣言であって、変化を与えるための命令ではないように見えます。 多くのネットワークデバイスは、設定されている状態に収束するよう ネットワークプロトコルを使って自律分散的に動作するからです。 同じ設定を投入しても変化はなく、冪等でもあります。

一方、ネットワーク設定の問題点は抽象度が低いことです。無数にあるパラメーターごとに個別に宣言する必要がありますが、あるべき状態は可能な限り抽象的に記述するのが望ましく、たとえば

peers:
    - type: transit
      neighbor_as: 65000
      neighbor_address: 192.0.2.1   # これが物理と紐づいている
      priority: low

くらいまでは抽象化したいところです。 ここでは便宜的に yaml にしていますがたとえば RDBMS でもよく、データストアは本質ではありません。 「何を」「どのレベルの抽象度で記述するか」が本質です。これは各社のビジネスに深く依存するため、データ構造を共通化するのは困難であり、ビジネスの柔軟性のために自社で定義するのが望ましいと思っています。

さて、ここでは中間表現としてネットワークデバイス設定を例にしましたが、最終的に デバイスに設定を入れる = あるべき状態をAPIに入力する機能 とセットであり、必ずしも あるべき状態記述を設定に変換する必要はありません。 Ansible を使っている場合は あるべき状態を包括的に記述した playbook が中間表現になり、3rd party オーケストレーターを使っている場合は その API が期待する形式に変換する感じになります。

これらの中間表現は、特定業務を自動化するためだけの記述ではなく、ネットワークのあるべき状態を包括的に宣言したものである必要があります。 また、中間表現に冪等性があれば 投入時に差分計算する必要がなくなり、制御システムをより薄く保てます。

ここまでをまとめると、宣言的ネットワーキング (一歩目) に必要な

1. あるべき状態の記述方法と、その対象・抽象度の検討
2. 抽象的なあるべき状態の記述 (1) を、ネットワークデバイスが解釈できる設定・パラメーターに変換する機能
5. 差分を自動投入する機能

は、ざっくり言えば「なにかしらのテンプレートシステムを作りましょう」にすぎません。Reconciliation Loop を回す部分は、ネットワークプロトコルがやってくれます。

移行できるとしたら、どういうステップを踏めばよいか

まずは、抽象的な宣言からネットワークデバイスミドルウェアが解釈できる中間表現を生成するための、テンプレートシステムを作りましょう、ということを書きました。

障害時には物理交換が発生するため、復旧プロセスも完全に自動化することは困難です。
宣言的ネットワーキングにおける制御システムの主な役割は QoS と最適化だと考えています

ということも書きました。最後の2点はこれまで棚上げしていた項目です。両方それなりに複雑になりそうなのですが、前者は実現可能だと思われます。

ひとつの案としては、回線やモジュールごとにビジネスに応じたメトリックを計測します。許容できるサービスレベルを宣言的に記述しておいて、

  1. サービスレベルを下回った場合、制御システムは回線やモジュールをサービスから切り離す
  2. 物理的に復旧した場合、制御システムはメトリックとサービスレベルを比較する
  3. OK であればサービスに組み込む

というアプローチは実装できそうです。

メトリック収集が複雑になりそうですが、これは従来のネットワーク運用でよく見るプロセスで、「人が手順にのっとって実施できることは、システム化できるのでは?」という話です。状態が自動遷移するようなシステムが理想ですが、ネットワークポリシーにそぐわないかもしれません。間にオペレーターの確認アクションを挟んでもよいと思います。

QoS や最適化はより複雑で、具体的なアイデアを持っていません。たとえば、あるIPアドレス間の latency をメトリックとして計測しているとして、原因区間の特定 (Root Cause Analysis) をシステム化するのがまず複雑です。仮に特定でき 原因が回線の輻輳であったして、「解消のためにTEしたところ 移した先で輻輳した」というケースもかなり見ます。ネットワーク全体の評価をスコアリングして、より高いスコアに収束させるような制御システムになるだろうと個人的には想像しています。システムが発散しないような仕組みも必要になります。

まとめ

従来のネットワーク運用を宣言的ネットワーキングに移行するのは、比較的カンタンだと考えています。ネットワークプロトコル自身が持つロバスト性と物理への強い依存を考慮すれば、テンプレートシステムの実装が一歩目で、経験的には、この簡易実装でさえコスト削減効果を上げられます。

一歩目の問題は、宣言の抽象度が低いことです。本来 宣言的に記述したい対象はサービス品質やコスト、さらに高レベルなビジネスの文脈なのですが、従来のネットワーク運用から移行可能なレベルまで宣言の抽象度を下げたにすぎません。しかしながら、こうすることで宣言的ネットワーキング(簡易版) にいったん移行し、徐々に宣言の抽象度を上げるというアプローチを取れます。

抽象度の高いところからスタートしようとすれば、まったく新しい制御システムの導入はもちろん、運用の見直しや 最悪の場合 ネットワークデバイスのリプレースが必要になるかもしれません。費用対効果しだいで こちらのアプローチのほうが有利なケースはもちろんあると思いますが、ネットワークを止めずにマイグレーションを繰り返していくような運用の観点からすれば、変化の歩幅が大きすぎるのでないかと感じています。

2020-07-08 追記

@motonori_shindo さんからフィードバックをいただきました。JANOG45 での議論 に登壇されていた方で、

QoS や最適化はより複雑で、

のところを掘り下げたブログエントリーを書かれています。

理解しやすいネットワーク設計図を描く取り組み

ネットワーク構成をよりよく理解するために、「ネットワーク設計図をプログラムで描く」という取り組みをはじめました。少し前の IEICE ICM研究会 で発表を行う予定でしたが、残念ながら新型コロナウィルス拡大により、研究会が中止になりました。

せっかくなので概要をまとめておこうと思います。

取り組んでいる課題

ネットワーク設計図は「ネットワークがどうあるべきか」を視覚的に理解しやすくするためのものです。記載される内容は

  • 装置・回線の地理的位置・ネットワーク上の論理的位置・種別
  • レイヤーごとの接続関係
  • 動作する技術・プロトコル・パラメーター・そのふるまい

などなど多岐にわたります。正しいネットワーク設計図は理解の助けになる反面、それを正しい状態に維持することはコストがかかる作業です。そこで コストを抑えるために、記述を省略するなど 正しさを犠牲にしたりします。

たとえば、次の図を見てください。

架空のネットワークの「IS-IS 隣接関係・メトリック」「BGP 経路集約」を表現した図です。当然ながら、それぞれの図は表現したいポイントにフォーカスし、それぞれの図の中できれいに表現できるようレイアウトします。一見言いたいことは伝わるかもしれませんが、

  • バックボーン区間・国内区間が別の図なため、俯瞰で捉えにくい
  • 下図で SG / US は省略されているが、JP / HK と同じなのかはっきりしない

など、2つの図を見比べた上で「全体としてネットワークがどのようにふるまうか」を考えるときに理解しやすいとは言えません。しかしながら、両者の関連を明示するのは大変困難です。原因はいくつかあります。

原因1. 図が量産される

複雑なネットワーク設計を表現するにあたり、あらゆる情報を1枚のネットワーク図で表現することはできません。当然ながらフォーカスするポイントが違うネットワーク図が量産されます。キャリアクラスのネットワークになれば、設計書に掲載されるネットワーク図は100を超えてきますが、100枚の図すべてに全ルーターを記載するわけにいきません。そもそも紙面が限られていますし、ひとつの修正 (例: HK から撤退したので、消して回らなければならない) に対するコストが高くなるためです。

実際にはネットワーク上の特定の位置・レイヤー・プロトコルだけに注目し、それ以外を省略することが多いですが、他の図との関連を把握しにくくなってしまうトレードオフがあります。どのようなバランスで表現するかは、多くの場合 書き手の判断で決めていると思われます。

原因2. 組織的な課題

ネットワーク規模が大きくなるにつれ、組織も大きくなります。ひとつのチームでは設計・構築・運用できないため、ネットワークレイヤー別に分けたり、サービス毎に分けたりします。大規模事業者になれば、ポリシー設計・リソース管理・工事を別々の組織が担当することも普通です。「あるポートにIPアドレスを設定する」という例でいえば

  1. IPアドレスの採番ルールを決めるチーム
  2. 1 のルールにしたがって、アドレスを採番するチーム
  3. 2 のアドレスを設定するチーム

に分かれているイメージです。組織横断でネットワーク設計図を維持する場面が増えるほど、書き手は他の図との関連を意識しにくくなりがちです。

研究内容

上記の課題を解決するには、ネットワーク図を人が描くのではなく、プログラムによって描画する必要があると考えています。

  • 注目するポイントが違う個々のネットワーク図、および全体像を低コストで描けること
  • 注目するポイントに関連する情報を、選択的に表示できること

Webブラウザ上で実現するプロトタイプを実装、実環境におけるネットワーク設計書に適用して評価しました。

たとえば先ほどの経路集約の例でいえば、

  1. ベースとなるネットワーク図を自動描画できる
  2. ネットワーク図の一部を非表示にしたり、ノード配置を修正できる
  3. ベースとなるネットワーク図に、経路ごとの集約・被集約をマッピングできる。表示する対象を切り替えられる
  4. 関連情報を表示できる。表示・非表示を切り替えられる

1~2 の結果をリセットすることなく、3~4 を行います。

f:id:codeout:20200514204514p:plain

本研究はソフトバンク株式会社との共同研究です。

ネットワークの全体把握を可能にする構成図描画方式の検討

実現方法

データソースとなる設計データは、構成管理データベース (CMDB) に格納されているものを流用します。ただし、

  • 設計要旨を表現するのに十分な範囲、今回は地理的に限定された2 エリアのみを描画
  • 2 エリア内の設計情報は全て描画し、さらに何かを省略することはしない
  • 基本設計から逸脱するイレギュラーを手動で取り除き、「ネットワークがどうあるべきか」をJSON で表現する

設計データには「ルーターAとルーターBが100GEインターフェイスで繋がっている」レベルの接続情報を持ちますが、自動描画するための座標などは含まれていません。そこで、ネットワーク図を描画するための inet-henge というライブラリを利用します。

github.com

このライブラリは、前述のような「AとBが繋がっている」レベルの構成情報から自動レイアウトでネットワーク図を描画します。出力形式は SVG であり、CSSJavaScript によって、ユーザープログラムからノードやリンクのメタデータを表示・操作するための工夫がされています。

今回、このライブラリを使ってSVG 操作するプログラムを JavaScript および CSS で実装し、実際の設計書と同等のネットワーク設計図を描画しました。その後、オリジナルである MS PowerPoint 版と比較・評価しました。

効果

詳細は省略しますが、今回の題材において、先に述べた「ネットワーク設計図の維持コスト」「他の図と関連させて表現させるためのトレードオフ」の課題は大幅に改善されています。ネットワーク図の自動描画によって、設計変更 → ドキュメント内のネットワーク図変更であった作業が、設計変更 → CMDB・プログラム修正 で代用可能です。ネットワーク図の体裁が俗人化してしまうという課題も改善しています。

しかしながら 変更のあった設計項目によっては、CMDB上でのデータ作成・プログラム修正コストが必ずしも小さくなく、全体として低負荷にならない場合がありそうです。

まとめ

最後に、技術報告のまとめを引用します。

本稿では,OSSライブラリのinet-hengeを用いてデータからNW構成図を自動作成し,個々の設計要旨とNWの全体像を同時に表現することで,よりわかりやすいNW構成図を低負荷で作成する方式を検討した.図を作成する過程には課題が残っているが,できたNW構成図は従来の図より利点が多く有望な方式と考える.今後は,4.2.で述べた改善と,適用範囲を設計書だけでなく詳細設計や構築済みNWに拡大した検証を行う予定である.

inet-henge が出力する SVG の変更点

TL; DR

  • リンクラベルをノードの上に描画するため、SVG DOM 構造を変えました
  • CSS を適用している場合は、修正する必要があるかもしれません
  • いまさらですがVersioning 始めます

問題: リンクラベルがノードの裏に回ってしまう

これまでのinet-hengeには、上のように「リンクラベルがノードの裏に回ってしまって読めない」問題がありました。

下の2点を満たすためにやむなくこうなっていたものです。

  1. SVGの仕様では、後に定義された要素が表に描画される
  2. CSSでうまく選択できるよう、リンク(線) とリンクラベル(テキスト) を近くに配置したい
    • 例: 線をポイントしたとき、ラベルを強調したい

線をノードの裏に隠すために先に定義すると、リンクラベルは線にひきずられてノードの裏に回ってしまっていました。

解決策: リンクを 線レイヤー / ラベルレイヤー に分割する

次のような修正を入れました。

  1. リンク(線)
  2. ノード
  3. リンクラベル(テキスト)

の順に定義することで、リンクラベルだけをノードの表に描画します。SVGz-index プロパティがないためこうするしかありませんが、

例: 線をポイントしたとき、ラベルを強調したい

のようなことをCSSで実現できなくなってしまいます。 この部分は後述します。

この修正により、inet-henge が出力する SVG DOM が変わっています。
CSS を適用している場合は、修正する必要があるかもしれません。

SVG DOM

参考までに、SVG DOM の変更点を抜粋します。

これまで

<svg width="960" height="600">
    <g>
        <g transform="translate(-1339.7071035665983,-516.1007538206986) scale(3.4533582342976468)">
            <rect width="9600" height="6000" transform="translate(-4800, -3000)" style="opacity: 0;"></rect>

            <!-- グループ定義 -->
            <g class="group pop03" transform="translate(462.13786327372185, 31.609395906680902)">
                <rect rx="8" ry="8" width="224.02789064247673" height="218.51666231118884" style="fill: rgb(255, 127, 14);"></rect>
                <text>POP03</text>
            </g>

            <!-- リンク定義 -->
            <g class="path-group">
                <line class="link pop03-bb01 pop03-bb02 pop03-bb01-pop03-bb02 " x1="493.13786327372185" y1="183.41381777944147" x2="574.8771135543029" y2="229.12605821786974" stroke="#7a4e4e" stroke-width="3" id="link2" transform="translate(0, 0)"></line>
                <path class="path" d="M 493.13786327372185 183.41381777944147 L 574.8771135543029 229.12605821786974" id="path2" transform="translate(0, 0)"></path>
                <text class="path-label" pointer-events="none" style="visibility: visible;" transform="rotate(0)">
                    <textPath xlink:href="#path2">
                        <tspan x="20" dy="1.5em" class="interface">ge-0/0/0</tspan>
                    </textPath>
                </text>
                <text class="path-label" pointer-events="none" style="visibility: visible;" transform="rotate(0)">
                    <textPath xlink:href="#path2" class="reverse" text-anchor="end" startOffset="100%">
                        <tspan x="-20" dy="1.5em" class="interface">Te0/0/0/0</tspan>
                    </textPath>
                </text>
            </g>

            <!-- ノード定義 -->
            <g id="pop03-bb02" name="POP03-bb02" transform="translate(547.8771135543029, 212.12605821786974)" class="node rect pop03-bb02 ">
                <rect width="54" height="34" rx="5" ry="5" style="fill: rgb(255, 187, 120);"></rect>
                <text text-anchor="middle" x="30" y="20">
                    <tspan x="30">POP03-bb02</tspan>
                </text>
            </g>
            ...
        </g>
    </g>
</svg>

<line> 要素と <text> 要素が隣接していたため、CSSによる操作が簡単でした。

/* リンクにマウスオーバーしたとき、対応するラベルを操作 */
.link:hover ~ .path-label {
   ...
}

これから

<svg width="960" height="600">
    <g>
        <g transform="translate(-1336.8725605988434,-477.80660514393185) scale(3.448574180070414)">
            <rect width="9600" height="6000" transform="translate(-4800, -3000)" style="opacity: 0;"></rect>

            <!-- グループ定義 -->
            <g id="groups">
                ...
                <g class="group pop03" transform="translate(462.13786327372185, 31.609395906680902)">
                    <rect rx="8" ry="8" width="224.02789064247673" height="218.51666231118884" style="fill: rgb(255, 127, 14);"></rect>
                    <text>POP03</text>
                </g>
            </g>

            <!-- リンク定義 -->
            <g id="links">
                ...
                <g class="link pop03-bb01 pop03-bb02 pop03-bb01-pop03-bb02 ">
                    <line x1="493.13786327372185" y1="183.41381777944147" x2="574.8771135543029" y2="229.12605821786974" stroke="#7a4e4e" stroke-width="3" id="link2" transform="translate(0, 0)"></line>
                    <path d="M 493.13786327372185 183.41381777944147 L 574.8771135543029 229.12605821786974" id="path2" transform="translate(0, 0)"></path>
                </g>
                ...
            </g>

            <!-- ノード定義 -->
            <g id="nodes">
                ...
                <g id="pop03-bb02" name="POP03-bb02" transform="translate(547.8771135543029, 212.12605821786974)" class="node rect pop03-bb02 ">
                    <rect width="54" height="34" rx="5" ry="5" style="fill: rgb(255, 187, 120);"></rect>
                    <text text-anchor="middle" x="30" y="20">
                        <tspan x="30">POP03-bb02</tspan>
                    </text>
                </g>
                ...
            </g>

            <!-- リンクラベル定義 -->
            <g id="link-labels">
                ...
                <g class="link pop03-bb01 pop03-bb02 pop03-bb01-pop03-bb02 ">
                    <text class="path2" transform="rotate(0)" style="visibility: visible;">
                        <textPath xlink:href="#path2">
                            <tspan x="20" dy="1.5em" class="interface">ge-0/0/0</tspan>
                        </textPath>
                    </text>
                    <text class="path2" transform="rotate(0)" style="visibility: visible;">
                        <textPath xlink:href="#path2" class="reverse" text-anchor="end" startOffset="100%">
                            <tspan x="-20" dy="1.5em" class="interface">Te0/0/0/0</tspan>
                        </textPath>
                    </text>
                </g>
                ...
            </g>
        </g>
    </g>
</svg>

リンク側 <path id="path2"> とラベル側 <text class="path2"> を対応させています。 ひとつの <text/> に複数 <line/> というケースがあるため、<text/> に対応するのはあくまで <path/> です。

「個別のラベル(テキスト)がどのリンク(線)に対応するか」は表現できますが、「マウスオーバーした線に対応するラベルだけを操作」がCSSだけでは実現できなくなります。

inet-henge に入れたハック

上記の対処のため、線にマウスオーバーした場合は対応するリンクラベル要素に hover クラスを付与する処理を入れました。十分ではありませんが、とりあえず。

前述のCSSは、👇のように変えれば動きます。

/* リンクにマウスオーバーしたとき、対応するラベルを操作 */
.link text.hover {
   ...
}

サンプル: http://inet-henge.herokuapp.com/issue09.html

また、リンク(線) のスタイルには .link ではなく .link line を、ラベルテキストのスタイルには .path-label ではなく .link text を使ってください。バージョンアップ後は、おそらく以前の.css ではうまく動かないと思います。

Versioning 始めました

SVG DOM が変更になることで、下位互換が崩れてしまいました 😢

互換性のない変更をいれたのはおそらく初めてのはずですが、いまさらながらCHANGELOG をきちんと書くために今後はVersion を振っていきます。

SVG の z-index property

2009年にProposalが出され、当初は、いまCandidate Recommendationまで進んでいるSVG2で入る予定であったようです。 History を辿ってみると、2018-08-07版 で省かれてしまっています。

当時の議論によれば、「重要なプロパティなのはわかるし、よくリクエストを貰うんだけど、render順 = 定義順 と仮定してしまってる既存コードをチェックして回るのが超大変。SVG2.1 でやるわ」ということのようでした。

z-index が入ればこのように泥臭いことをやらなくてよくなります。ぜひ欲しいですね! 楽しみに待ちたいと思います。

Vagrant Box の Juniper vQFX を、VMWare ESXi で起動するメモ

Juniper の vQFX10000 トライアル版community supported project として Vagrant で利用可能です。ただ VirtualBox provider のみのサポートであり、他ベンダーの VM と組み合わせてテストすることを考えると、ネットワーク設定が簡単な VMWare ESXi などで起動できると便利です。

設定を忘れて何度か試行錯誤してしまったため、メモしておきます。

本来はイメージを Juniper から直接入手すべきなのですが、

  • 評価版をすんなりダウンロードできない。サポートに連絡しないといけない
  • 面倒なので、Vagrant Cloud にあるバージョンで OK な場合は流用する

というだけの話です。

.vmdk のダウンロード

RE / PFE イメージをダウンロードします。

$ vagrant box add juniper/vqfx10k-re
$ vagrant box add juniper/vqfx10k-pfe

少ないですがバージョン選択肢があって、

box version RE Junos version PFE Junos version
0.3.0 17.4R1.16 17.4R1.16
0.2.0 15.1X53-D63.9 revoked
0.1.0 15.1X53-D60.4 15.1X53-D60.4

から選べます。

~/.vagrant.d/boxes/juniper-VAGRANTSLASH-vqfx10k-{re,pfe}/0.3.0/virtualbox/ に .vmdk が展開されます。同じ場所に VirtualBox 向け .ovf があり、これを参考にしつつ ESXi に設定する流れになります。

vSwitch / Port Group の作成

RE -- PFE 間で使う、内部接続の準備をします。

標準仮想スイッチの作成。VM 側で Mac Address をアサインできるよう、セキュリティポリシーを設定します。

セキュリティ ポリシー
無差別モードを許可 はい
偽装転送を許可 はい
MAC 変更を許可 はい

イメージのアップロード

後ほど vqfx-re / vqfx-pfe という名前で VM を作るとしてディレクトリーを作っておき、各々アップロードしておきます。

f:id:codeout:20191006070955p:plain

ディスク上にあまりファイルを書かないため さほど意味ないかもしれませんが、 thin provisiong に変換しつつイメージを置き換えます。

# ESXi にssh して

[root@esxi:~] cd /vmfs/volumes/datastore1/vqfx-re
[root@esxi:/vmfs/volumes/5d34e31a-944e599a-5f49-94c691ae50a2/vqfx-re] vmkfstools -i packer-virtualbox-ovf-1520879272-disk001.vmdk vqfx-re.vmdk -d thin
[root@esxi:/vmfs/volumes/5d34e31a-944e599a-5f49-94c691ae50a2/vqfx-re] rm packer-virtualbox-ovf-1520879272-disk001.vmdk

[root@esxi:/vmfs/volumes/5d34e31a-944e599a-5f49-94c691ae50a2/vqfx-re] cd ../vqfx-pfe/
[root@esxi:/vmfs/volumes/5d34e31a-944e599a-5f49-94c691ae50a2/vqfx-pfe] vmkfstools -i packer-virtualbox-ovf-1520878605-disk001.vmdk vqfx-pfe.vmdk -d thin
[root@esxi:/vmfs/volumes/5d34e31a-944e599a-5f49-94c691ae50a2/vqfx-pfe] rm packer-virtualbox-ovf-1520878605-disk001.vmdk

VM の作成

RE VM

「新規仮想マシンの作成」を行います。

ESXi 5.5 仮想マシン ( virtualHW.version = "10" ) にする必要があることに注意してください。

設定
互換性 ESXi 5.5 仮想マシン
ゲスト OS ファミリ その他
ゲスト OS のバージョン FreeBSD 11 より前のバージョン (64 ビット)

一旦ストレージを追加しておいて…

後から削除、さきほどアップロードした .vmdk を新規追加します。
( 追加直後はシックプロビジョニングと表示されますが、一度起動すればシンプロビジョニングとして認識されます )

CPU やメモリ設定は .ovf のものを転記。

設定
CPU 1
メモリ 1024MB
新規ハードディスク コントローラの場所 IDE コントローラ 0 マスター

後からの NIC 追加が非常に面倒なため、必要分を先に作成します。

設定
アダプタタイプ E1000

それぞれの NIC は、Junos 上からは上から順に 👇 のような対応になります。

  • 上から 1つめ、3つめがマネージメントインターフェイス
  • 上から 2つめが先ほど作成した RE--PFE 接続
  • xe-0/0/x は上から4 つめ以降

なことに注意してください。

NIC # Junos から見たInterface メモ
1 em0 マネージメント
2 em1 RE--PFE間接続
3 em2 マネージメント
4 em3, xe-0/0/0 10GE インターフェイス
5 em4, xe-0/0/1 10GE インターフェイス
... ... ...

( RE 起動直後は em3 として認識した NIC が、PFE 接続後 xe-0/0/0マッピングされる )

PFE VM

こちらも「新規仮想マシンの作成」

設定
互換性 ESXi 5.5 仮想マシン
ゲスト OS ファミリ LInux
ゲスト OS のバージョン Ubuntu Linux (64 ビット)

同じくディスクを追加しておいて削除、アップロードした .vmdk を新規追加します。

CPU やメモリ設定は .ovf のものを転記。

設定
CPU 1
メモリ 2048MB
新規ハードディスク コントローラの場所 IDE コントローラ 0 マスター

NIC は👇 のとおり。

設定
アダプタタイプ E1000

それぞれの NIC は、Junos 上からは上から順に 👇 のような対応になります。

NIC # Junos から見たInterface メモ
1 em0 マネージメント
2 em1 RE--PFE間接続

起動

RE へのログイン情報: root / Juniper

ライセンスについて

"community supported project" とは何か調べてませんが、

  1. vQFX イメージ自体は Juniper がライセンスし、 Vagrant Cloud に公開している
  2. Vagrantfile は Apache License 2 として github にホストされ、 コミュニティでメンテしている

というふうに見えます。

1 の EULA には

k . Other Use Restrictions and Prohibitions. You > shall not, directly or indirectly: i. Decompile, disassemble or reverse engineer the Software or modify, change, unbundle, or create derivative works based on the Software, except as expressly permitted by applicable law without the possibility of contractual waiver.

のような条項がありますが、 .vmdk をそのまま使うぶんにはおそらく大丈夫だろうと思っています。

EVPN - VLAN Based と VLAN Aware Bundle の相互接続

TL; DR

EVPN は仕様が標準化され、Control / Data Plane が分離されているにも関わらず、Service Interface が異なる場合 ふつうは相互接続できません。 これがマルチベンダー EVPN を困難にする一因になっています。

この記事では Juniper vQFX (VLAN Aware Bundle) と vEOS (VLAN Based) 間で EVPN 接続し、実際に MAC 学習できないことを確認します。

それから、両者を橋渡しできるような Route Reflector を書くことで MAC 学習させ、通信させてみます。

EVPN とは

L2VPN を実現するプロトコルのひとつで、RFC7209 ( *1 ) で要件整理され、RFC7432 ( *2 ) で仕様策定されました。簡単に言うと、

Control Plane

EthernetMAC 情報をMP-BGP を使って伝搬させ、MAC 学習する

Data Plane

データであるEthernet フレームは、いろいろなトンネリングプロトコルによってカプセル (encapsulate) し、伝送する

というプロトコルです。 トンネリングプロトコルとセットにして、EVPN / MPLS や EVPN / VXLAN などと記載されることが多いと思います。

RFC7432 策定当時は EVPN / MPLS を前提としていて、VPLS の代替と位置付けられていましたが、オーバーレイの encapsulation は MPLS でなくても構いません。 RFC8365 ( *3 ) には、いくつかの encapsulation に関する記述があります。MPLS 同様よく使われているであろう VXLAN ( *4 ) を使う場合についてもこちらに定義されています。

マルチベンダーで EVPN 使いたい!

そもそも本当にマルチベンダーにしたいのかについては一考の余地があります。

  • ベンダーを統一するとできることが増えるのでは?
  • 単一API で機能を呼べることで、開発コストが減るのでは?
  • スイッチとしてのハードウェアのふるまい、ソフトウェアのデザインが一致していることで運用コストが減るのでは?
  • ...

いろんな理屈があって どれもそれはそうなんですが、「マルチベンダー」という選択肢が減るのは悪だと思っています。比較のうえ捨てるのは OK ですが、比較すらできないのはよくない。

さて、「マルチベンダーを考えましょう」という立場にたったとき、EVPN が標準化され Control / Data Plane が綺麗に分離されているにも関わらず、相互接続性に問題があります。

もっとも大きい問題が「Service Interface の相互接続性」です。

( 抜粋: マルチベンダ環境におけるEVPN構築のノウハウ~Interop Tokyo 2016 ShowNetでの相互接続検証を元に~ https://www.nic.ad.jp/ja/materials/iw/2016/proceedings/t05/t5-ohkubo.pdf )

Service Interface については、こちらもInternet Week 2016 スライドがわかりやすいので抜粋しますが、

「ひとつの EVPN Instance (EVI) が何個のBridge Domain / Broadcast Domain (VLAN) をカバーするか」の分類を表します。

( 抜粋: EVPN 技術紹介 https://www.nic.ad.jp/ja/materials/iw/2016/proceedings/t05/t5-kamitani-2.pdf )

Arista、Juniper MX など一部プラットフォームを除き、多くの機器でService Interfaceは実質固定です。 これは各NOSのEPVN Instance がどのようなテーブルを持っているか、ASIC / チップがどのような制約を持っているかによりますが、Service Interface が違うと相互接続できない点がマルチベンダーを困難にしています。

わたしが触る範囲では VLAN Based なプラットフォームが多いですが、そんなに多種多様な機器を触っているわけではないので…偏っているかもしれません。Juniper QFX など、よくみるプラットフォームで VLAN Aware Bundle なものはあります。 個人的には、VLAN Bundle に触れたことはありません。

ほんとうに相互接続できないか試す

  • EVPN / VXLAN
  • Juniper vQFX (VLAN Aware Bundle)
  • Arista vEOS (VLAN Based 設定)
    • Arista 自体は VLAN Aware Bundle いけますが、ここでは VLAN Based で設定

を 👇 のように接続し、Route Reflector 経由で EVPN BGP を張ります。
( わざわざ Control / Data Place を分けた理由は後述 )

  • IP アドレス記載のあるポートはL3、それ以外はL2 ポート
  • vQFX xe-0/0/0 と vEOS Et1 は VLAN100 (VNI 1100) に所属
  • vQFX xe-0/0/1 と vEOS Et2 は VLAN200 (VNI 1200) に所属
  • vQFX config
  • vEOS config

やはり通信できない

いくつかのコマンド結果を記載します。

vQFX 側は対向からの EVPN route type 2 ( MAC/IP )、route type 3 ( IMET ) を受信・解釈し、MAC 学習できているのに対し、vEOS 側は受信できているものの解釈・MAC 学習できていません。

vQFX
koji@vqfx1-re> show ethernet-switching table

MAC flags (S - static MAC, D - dynamic MAC, L - locally learned, P - Persistent static
           SE - statistics enabled, NM - non configured MAC, R - remote PE MAC, O - ovsdb MAC)


Ethernet switching table : 4 entries, 4 learned
Routing instance : default-switch
   Vlan                MAC                 MAC      Logical                Active
   name                address             flags    interface              source
   vlan100             00:0c:29:88:49:aa   D        vtep.32769             10.0.0.2  # ← vEOS
   vlan100             00:0c:29:fa:ac:f7   D        xe-0/0/0.0
   vlan200             00:0c:29:88:49:b4   D        vtep.32769             10.0.0.2  # ← vEOS
   vlan200             00:0c:29:fa:ac:01   D        xe-0/0/1.0
koji@vqfx1-re> show route table default-switch.evpn.0 detail

default-switch.evpn.0: 8 destinations, 8 routes (8 active, 0 holddown, 0 hidden)
2:10.0.0.1:1::1100::00:0c:29:fa:ac:f7/304 MAC/IP (1 entry, 1 announced)
        *EVPN   Preference: 170
                Next hop type: Indirect, Next hop index: 0
                Address: 0xb4d8570
                Next-hop reference count: 6
                Protocol next hop: 10.0.0.1
                Indirect next hop: 0x0 - INH Session ID: 0x0
                State: <Active Int Ext>
                Age: 6:36:18
                Validation State: unverified
                Task: default-switch-evpn
                Announcement bits (1): 1-BGP_RT_Background
                AS path: I
                Communities: encapsulation:vxlan(0x8)
                Route Label: 1100
                ESI: 00:00:00:00:00:00:00:00:00:00

2:10.0.0.1:1::1200::00:0c:29:fa:ac:01/304 MAC/IP (1 entry, 1 announced)
        *EVPN   Preference: 170
                Next hop type: Indirect, Next hop index: 0
                Address: 0xb4d8570
                Next-hop reference count: 6
                Protocol next hop: 10.0.0.1
                Indirect next hop: 0x0 - INH Session ID: 0x0
                State: <Active Int Ext>
                Age: 6:36:16
                Validation State: unverified
                Task: default-switch-evpn
                Announcement bits (1): 1-BGP_RT_Background
                AS path: I
                Communities: encapsulation:vxlan(0x8)
                Route Label: 1200
                ESI: 00:00:00:00:00:00:00:00:00:00

2:10.0.0.2:1100::0::00:0c:29:88:49:aa/304 MAC/IP (1 entry, 1 announced)
        *BGP    Preference: 170/-101
                Route Distinguisher: 10.0.0.2:1100
                Next hop type: Indirect, Next hop index: 0
                Address: 0xb4d8690
                Next-hop reference count: 8
                Source: 192.168.10.3
                Protocol next hop: 10.0.0.2
                Indirect next hop: 0x2 no-forward INH Session ID: 0x0
                State: <Secondary Active Int Ext>
                Local AS: 65000 Peer AS: 65000
                Age: 2:13       Metric2: 11
                Validation State: unverified
                Task: BGP_65000.192.168.10.3
                Announcement bits (1): 0-default-switch-evpn
                AS path: I (Originator)
                Cluster list:  192.168.10.3
                Originator ID: 10.0.0.2
                Communities: target:65000:1100 encapsulation:vxlan(0x8)
                Import Accepted
                Route Label: 1100
                ESI: 00:00:00:00:00:00:00:00:00:00
                Localpref: 100
                Router ID: 192.168.10.3
                Primary Routing Table bgp.evpn.0

2:10.0.0.2:1200::0::00:0c:29:88:49:b4/304 MAC/IP (1 entry, 1 announced)
        *BGP    Preference: 170/-101
                Route Distinguisher: 10.0.0.2:1200
                Next hop type: Indirect, Next hop index: 0
                Address: 0xb4d8690
                Next-hop reference count: 8
                Source: 192.168.10.3
                Protocol next hop: 10.0.0.2
                Indirect next hop: 0x2 no-forward INH Session ID: 0x0
                State: <Secondary Active Int Ext>
                Local AS: 65000 Peer AS: 65000
                Age: 2:02       Metric2: 11
                Validation State: unverified
                Task: BGP_65000.192.168.10.3
                Announcement bits (1): 0-default-switch-evpn
                AS path: I (Originator)
                Cluster list:  192.168.10.3
                Originator ID: 10.0.0.2
                Communities: target:65000:1200 encapsulation:vxlan(0x8)
                Import Accepted
                Route Label: 1200
                ESI: 00:00:00:00:00:00:00:00:00:00
                Localpref: 100
                Router ID: 192.168.10.3
                Primary Routing Table bgp.evpn.0

3:10.0.0.1:1::1100::10.0.0.1/248 IM (1 entry, 1 announced)
        *EVPN   Preference: 170
                Next hop type: Indirect, Next hop index: 0
                Address: 0xb4d8570
                Next-hop reference count: 6
                Protocol next hop: 10.0.0.1
                Indirect next hop: 0x0 - INH Session ID: 0x0
                State: <Active Int Ext>
                Age: 6:40:17
                Validation State: unverified
                Task: default-switch-evpn
                Announcement bits (1): 1-BGP_RT_Background
                AS path: I
                Communities: encapsulation:vxlan(0x8)
                Route Label: 1100
                PMSI: Flags 0x0: Label 1100: Type INGRESS-REPLICATION 10.0.0.1

3:10.0.0.1:1::1200::10.0.0.1/248 IM (1 entry, 1 announced)
        *EVPN   Preference: 170
                Next hop type: Indirect, Next hop index: 0
                Address: 0xb4d8570
                Next-hop reference count: 6
                Protocol next hop: 10.0.0.1
                Indirect next hop: 0x0 - INH Session ID: 0x0
                State: <Active Int Ext>
                Age: 6:40:17
                Validation State: unverified
                Task: default-switch-evpn
                Announcement bits (1): 1-BGP_RT_Background
                AS path: I
                Communities: encapsulation:vxlan(0x8)
                Route Label: 1200
                PMSI: Flags 0x0: Label 1200: Type INGRESS-REPLICATION 10.0.0.1

3:10.0.0.2:1100::0::10.0.0.2/248 IM (1 entry, 1 announced)
        *BGP    Preference: 170/-101
                Route Distinguisher: 10.0.0.2:1100
                PMSI: Flags 0x0: Label 68: Type INGRESS-REPLICATION 10.0.0.2
                Next hop type: Indirect, Next hop index: 0
                Address: 0xb4d8690
                Next-hop reference count: 8
                Source: 192.168.10.3
                Protocol next hop: 10.0.0.2
                Indirect next hop: 0x2 no-forward INH Session ID: 0x0
                State: <Secondary Active Int Ext>
                Local AS: 65000 Peer AS: 65000
                Age: 3:53       Metric2: 11
                Validation State: unverified
                Task: BGP_65000.192.168.10.3
                Announcement bits (1): 0-default-switch-evpn
                AS path: I (Originator)
                Cluster list:  192.168.10.3
                Originator ID: 10.0.0.2
                Communities: target:65000:1100 encapsulation:vxlan(0x8)
                Import Accepted
                Localpref: 100
                Router ID: 192.168.10.3
                Primary Routing Table bgp.evpn.0

3:10.0.0.2:1200::0::10.0.0.2/248 IM (1 entry, 1 announced)
        *BGP    Preference: 170/-101
                Route Distinguisher: 10.0.0.2:1200
                PMSI: Flags 0x0: Label 75: Type INGRESS-REPLICATION 10.0.0.2
                Next hop type: Indirect, Next hop index: 0
                Address: 0xb4d8690
                Next-hop reference count: 8
                Source: 192.168.10.3
                Protocol next hop: 10.0.0.2
                Indirect next hop: 0x2 no-forward INH Session ID: 0x0
                State: <Secondary Active Int Ext>
                Local AS: 65000 Peer AS: 65000
                Age: 3:53       Metric2: 11
                Validation State: unverified
                Task: BGP_65000.192.168.10.3
                Announcement bits (1): 0-default-switch-evpn
                AS path: I (Originator)
                Cluster list:  192.168.10.3
                Originator ID: 10.0.0.2
                Communities: target:65000:1200 encapsulation:vxlan(0x8)
                Import Accepted
                Localpref: 100
                Router ID: 192.168.10.3
                Primary Routing Table bgp.evpn.0
vEOS
veos2#sh mac address-table
          Mac Address Table
------------------------------------------------------------------

Vlan    Mac Address       Type        Ports      Moves   Last Move
----    -----------       ----        -----      -----   ---------
 100    000c.2988.49aa    DYNAMIC     Et1        1       0:00:44 ago
 200    000c.2988.49b4    DYNAMIC     Et2        1       0:00:33 ago
Total Mac Addresses for this criterion: 2

          Multicast Mac Address Table
------------------------------------------------------------------

Vlan    Mac Address       Type        Ports
----    -----------       ----        -----
Total Mac Addresses for this criterion: 0
veos2#sh bgp evpn detail
BGP routing table information for VRF default
Router identifier 10.0.0.2, local AS number 65000
BGP routing table entry for mac-ip 000c.2988.49aa, Route Distinguisher: 10.0.0.2:1100
 Paths: 1 available
  Local
    - from - (0.0.0.0)
      Origin IGP, metric -, localpref -, weight 0, valid, local, best
      Extended Community: Route-Target-AS:65000:1100 TunnelEncap:tunnelTypeVxlan
      VNI: 1100 ESI: 0000:0000:0000:0000:0000
BGP routing table entry for mac-ip 000c.2988.49b4, Route Distinguisher: 10.0.0.2:1200
 Paths: 1 available
  Local
    - from - (0.0.0.0)
      Origin IGP, metric -, localpref -, weight 0, valid, local, best
      Extended Community: Route-Target-AS:65000:1200 TunnelEncap:tunnelTypeVxlan
      VNI: 1200 ESI: 0000:0000:0000:0000:0000
BGP routing table entry for mac-ip 1100 000c.29fa.acf7, Route Distinguisher: 10.0.0.1:1
 Paths: 1 available
  Local
    10.0.0.1 from 192.168.10.3 (192.168.10.3)
      Origin IGP, metric -, localpref 100, weight 0, valid, internal, best
      Originator: 10.0.0.1, Cluster list: 192.168.10.3
      Extended Community: Route-Target-AS:65000:1100 TunnelEncap:tunnelTypeVxlan
      VNI: 1100 ESI: 0000:0000:0000:0000:0000
BGP routing table entry for mac-ip 1200 000c.29fa.ac01, Route Distinguisher: 10.0.0.1:1
 Paths: 1 available
  Local
    10.0.0.1 from 192.168.10.3 (192.168.10.3)
      Origin IGP, metric -, localpref 100, weight 0, valid, internal, best
      Originator: 10.0.0.1, Cluster list: 192.168.10.3
      Extended Community: Route-Target-AS:65000:1200 TunnelEncap:tunnelTypeVxlan
      VNI: 1200 ESI: 0000:0000:0000:0000:0000
BGP routing table entry for imet 10.0.0.2, Route Distinguisher: 10.0.0.2:1100
 Paths: 1 available
  Local
    - from - (0.0.0.0)
      Origin IGP, metric -, localpref -, weight 0, valid, local, best
      Extended Community: Route-Target-AS:65000:1100 TunnelEncap:tunnelTypeVxlan
      VNI: 1100
BGP routing table entry for imet 10.0.0.2, Route Distinguisher: 10.0.0.2:1200
 Paths: 1 available
  Local
    - from - (0.0.0.0)
      Origin IGP, metric -, localpref -, weight 0, valid, local, best
      Extended Community: Route-Target-AS:65000:1200 TunnelEncap:tunnelTypeVxlan
      VNI: 1200
BGP routing table entry for imet 1100 10.0.0.1, Route Distinguisher: 10.0.0.1:1
 Paths: 1 available
  Local
    10.0.0.1 from 192.168.10.3 (192.168.10.3)
      Origin IGP, metric -, localpref 100, weight 0, valid, internal, best
      Originator: 10.0.0.1, Cluster list: 192.168.10.3
      Extended Community: Route-Target-AS:65000:1100 TunnelEncap:tunnelTypeVxlan
      VNI: 1100
BGP routing table entry for imet 1200 10.0.0.1, Route Distinguisher: 10.0.0.1:1
 Paths: 1 available
  Local
    10.0.0.1 from 192.168.10.3 (192.168.10.3)
      Origin IGP, metric -, localpref 100, weight 0, valid, internal, best
      Originator: 10.0.0.1, Cluster list: 192.168.10.3
      Extended Community: Route-Target-AS:65000:1200 TunnelEncap:tunnelTypeVxlan
      VNI: 1200

BGP Update Message を覗く

それぞれの type 2 route のサンプルです。

EVPN route はすべて MP_REACH_NLRI にエンコードされます。

の値が微妙に違うのがわかるでしょうか。 両方 VNI 1100 の経路です。

vQFX (VLAN Aware Bundle) → vEOS (VLAN Based)

設定:

  • VLAN Aware Bundle なので、 RD は VLAN (VNI) によらず 10.0.0.1:1 (固定値)

vQFX (VLAN Aware Bundle) ← vEOS (VLAN Based)

設定:

  • VLAN Based なので、RD を区別して
    • VLAN 100 ( VNI 1100) → 10.0.0.2:1100
    • VLAN 200 ( VNI 1200) → 10.0.0.2:1200

MAC 学習させるには

今回の RD / VNI 番号設計に限った話ではありますが、

Service Interface RD E-Tag ID
VLAN Aware Bundle <VTEP endpoint address>:1 <VNI>
VLAN Based <VTEP endpoint address>:<VNI> 0

を相互に変換すればよさそうです。

RFC7432 によれば、

6.1. VLAN-Based Service Interface

... The Ethernet Tag ID in all EVPN routes MUST be set to 0.

6.3. VLAN-Aware Bundle Service Interface

... The Ethernet Tag ID in all EVPN routes MUST be set to the normalized Ethernet Tag ID assigned by the EVPN provider.

あるいは RFC8365 には、

For the VLAN-Aware Bundle Service (multiple VNIs per MAC-VRF with each VNI associated with its own bridge table), the Ethernet Tag field in the MAC Advertisement, Ethernet A-D per EVI, and IMET route MUST identify a bridge table within a MAC-VRF; the set of Ethernet Tags for that EVI needs to be configured consistently on all PEs within that EVI. For locally assigned VNIs, the value advertised in the Ethernet Tag field MUST be set to a VID just as in the VLAN-aware bundle service in [RFC7432]. Such setting must be done consistently on all PE devices participating in that EVI within a given domain. For global VNIs, the value advertised in the Ethernet Tag field SHOULD be set to a VNI as long as it matches the existing semantics of the Ethernet Tag, i.e., it identifies a bridge table within a MAC-VRF and the set of VNIs are configured consistently on each PE in that EVI.

という記述があり、確かにそれでよさそう。

経路変換するにあたり、RD、E-Tag ID は MP_REACH_NLRI なため、一般的な経路フィルターでは操作できません。 Route Reflector を書く必要がありそうです。

例として gobgpd ( *5 ) をカスタマイズし、

  • BGP Neighbor ごとに VLAN Aware Bundle / VLAN Based の別を設定できる
  • RIB には VLAN Aware Bundle の形で保持し、VLAN Based ピアには変換して広告する

ようにしてみます。

VXLAN Header 自体は 変数としてVNI しか持たないため、Control Plane だけの変換ですむはず。

通信できた 🎉

パッチを一部 掲載しますが、

# pkg/server/server.go

1022 func (s *BgpServer) processOutgoingPaths(peer *peer, paths, olds []*table.Path) []*table.Path {
...
1029         for idx, path := range paths {
1030                 var old *table.Path
1031                 if olds != nil {
1032                         old = olds[idx]
1033                 }
1034                 if p := s.filterpath(peer, path, old); p != nil {
1035                         // Hack
1036                         if !peer.fsm.pConf.Config.VlanAwareBundle {
1037                                 if p != nil && p.GetRouteFamily() == bgp.RF_EVPN {
1038                                         nlri := p.GetNlri().(*bgp.EVPNNLRI)
1039
1040                                         switch nlri.RouteType {
...
1057                                         case bgp.EVPN_ROUTE_TYPE_MAC_IP_ADVERTISEMENT:
1058                                                 route := nlri.RouteTypeData.(*bgp.EVPNMacIPAdvertisementRoute)
1059
1060                                                 // For VLAN based router, RD needs to vary, otherwise only one route will be active per RD
1061                                                 rd := route.RD.(*bgp.RouteDistinguisherIPAddressAS)
1062                                                 rd.Assigned = uint16(route.ETag)  // E-Tag ID をRD 下位16bit にコピー
1063
1064                                                 new := &bgp.EVPNMacIPAdvertisementRoute{
1065                                                         RD:               rd,
1066                                                         ESI:              route.ESI,
1067                                                         ETag:             0,  // 0 で上書き
1068                                                         MacAddressLength: route.MacAddressLength,
1069                                                         MacAddress:       route.MacAddress,
1070                                                         IPAddressLength:  route.IPAddressLength,
1071                                                         IPAddress:        route.IPAddress,
1072                                                         Labels:           route.Labels,
1073                                                 }
1074
1075                                                 p = table.NewPath(p.GetSource(), bgp.NewEVPNNLRI(nlri.RouteType,      new),
...
1154 func (s *BgpServer) propagateUpdate(peer *peer, pathList []*table.Path) {
...
1263                 if path.GetRouteFamily() == bgp.RF_EVPN {
1264                         nlri := path.GetNlri().(*bgp.EVPNNLRI)
1265                         switch nlri.RouteType {
...
1269                         case bgp.EVPN_ROUTE_TYPE_MAC_IP_ADVERTISEMENT:
1270                                 evpnRoute := nlri.RouteTypeData.(*bgp.EVPNMacIPAdvertisementRoute)
1271                                 if len(evpnRoute.Labels) > 0 {
1272                                         evpnRoute.ETag = evpnRoute.Labels[0]  # MPLS Labels1 から転記
1273                                 }

さほど複雑ではありません。 これを使うと…さきほど MAC学習できていなかった vEOS 側で無事 MAC 学習できました!!

veos2#sh mac address-table
          Mac Address Table
------------------------------------------------------------------

Vlan    Mac Address       Type        Ports      Moves   Last Move
----    -----------       ----        -----      -----   ---------
 100    000c.2988.49aa    DYNAMIC     Et1        1       0:05:17 ago
 100    000c.29fa.acf7    DYNAMIC     Vx1        1       0:00:21 ago
 200    000c.2988.49b4    DYNAMIC     Et2        1       0:05:06 ago
 200    000c.29fa.ac01    DYNAMIC     Vx1        1       0:00:21 ago
Total Mac Addresses for this criterion: 4

          Multicast Mac Address Table
------------------------------------------------------------------

Vlan    Mac Address       Type        Ports
----    -----------       ----        -----
Total Mac Addresses for this criterion: 0

EVPN Table のほうは、vEOS から見た diff です。

@@ -15,7 +15,7 @@
       Origin IGP, metric -, localpref -, weight 0, valid, local, best
       Extended Community: Route-Target-AS:65000:1200 TunnelEncap:tunnelTypeVxlan
       VNI: 1200 ESI: 0000:0000:0000:0000:0000
-BGP routing table entry for mac-ip 1100 000c.29fa.acf7, Route Distinguisher: 10.0.0.1:1
+BGP routing table entry for mac-ip 000c.29fa.acf7, Route Distinguisher: 10.0.0.1:1100
  Paths: 1 available
   Local
     10.0.0.1 from 192.168.10.3 (192.168.10.3)
@@ -23,7 +23,7 @@
       Originator: 10.0.0.1, Cluster list: 192.168.10.3
       Extended Community: Route-Target-AS:65000:1100 TunnelEncap:tunnelTypeVxlan
       VNI: 1100 ESI: 0000:0000:0000:0000:0000
-BGP routing table entry for mac-ip 1200 000c.29fa.ac01, Route Distinguisher: 10.0.0.1:1
+BGP routing table entry for mac-ip 000c.29fa.ac01, Route Distinguisher: 10.0.0.1:1200
  Paths: 1 available
   Local
     10.0.0.1 from 192.168.10.3 (192.168.10.3)
@@ -45,7 +45,7 @@
       Origin IGP, metric -, localpref -, weight 0, valid, local, best
       Extended Community: Route-Target-AS:65000:1200 TunnelEncap:tunnelTypeVxlan
       VNI: 1200
-BGP routing table entry for imet 1100 10.0.0.1, Route Distinguisher: 10.0.0.1:1
+BGP routing table entry for imet 10.0.0.1, Route Distinguisher: 10.0.0.1:1100
  Paths: 1 available
   Local
     10.0.0.1 from 192.168.10.3 (192.168.10.3)
@@ -53,7 +53,7 @@
       Originator: 10.0.0.1, Cluster list: 192.168.10.3
       Extended Community: Route-Target-AS:65000:1100 TunnelEncap:tunnelTypeVxlan
       VNI: 1100
-BGP routing table entry for imet 1200 10.0.0.1, Route Distinguisher: 10.0.0.1:1
+BGP routing table entry for imet 10.0.0.1, Route Distinguisher: 10.0.0.1:1200
  Paths: 1 available
   Local
     10.0.0.1 from 192.168.10.3 (192.168.10.3)

まとめ & 考察

  • EVPN Service Interface 間の相互接続は、Control Plane に手を加えるだけで達成できそうです。ただし、
    • 確認したのは Single Home のみ
    • ビジネスロジック = RD / VNI 番号設計 を前提にする必要があります。汎用的な変換をするのは難しそう
  • SDN コントローラーのような、Route Reflector としてふるまうコンポーネントでは、経路変換は実装・運用可能に見えます
    • 一方、Spine-Leaf BGP EVPN な設計のところで Spine に手を入れられるかというと…難しい場合が多そう
  • 単純なしくみで Control Plane を作れます。「相互接続は難しい」とされる理由について、他に何かありそうと感じます。
    • なんだろう? Multihoming?
  • そもそもマルチベンダーしたいかという点について、もちろん各社の方針はあるでしょう

EVPN、さほど詳しくありません。間違いがありましたら、バンバンご指摘いただけると嬉しいです!

inet-henge 利用例: Batfish ネットワークトポロジーの可視化

inet-henge を使うと、ネットワーク図を簡単に描画できます。

github.com

必要なのは「このデバイスは、あのデバイスとつながっている」という構成情報のみ。

{
  "nodes": [
    { "name": "A" },
    { "name": "B" }
  ],

  "links": [
    { "source": "A", "target": "B" }
  ]
}

このエントリーでは、Batfish が持っているネットワークトポロジーをinet-henge をつかって可視化してみます。 たとえば、👇のようなネットワーク図が描けます。

Batfish とは

Batfish はネットワークコンフィグ分析ツールで、実機に投入せずとも confiuration validation / 各種プロトコルの状態確認 / ルーティングテーブルやACL分析が可能です。

その一環として、IPアドレスを元に layer-3 interface adjacencies を判定しネットワーク構成をデータ化する、ということもやっています。

詳細は こちら をご覧ください。

API で、Batfish ネットワークトポロジーを取得する

Batfish が持っているネットワークトポロジーは、👇のようなURLからAPIで取得できます。

http://<batfish_server>:9996/v2/networks/<network>/snapshots/<snapshot>/topology

チュートリアルGetting Started with Batfish を試す場合でいえば、Batfish サーバーである 9996/tcp を公開しておけばOKです。

$ docker run --rm -p 8888:8888 -p9996:9996 batfish/allinone
[I 17:02:05.649 NotebookApp] Writing notebook server cookie secret to /data/.local/share/jupyter/runtime/notebook_cookie_secret
[I 17:02:05.892 NotebookApp] Serving notebooks from local directory: /notebooks
[I 17:02:05.892 NotebookApp] The Jupyter Notebook is running at:
[I 17:02:05.892 NotebookApp] http://(5df00b13e028 or 127.0.0.1):8888/?token=2a0b09ed8f6a1aea73fc1f4e9360696224ec1d4bcbe646b5
[I 17:02:05.892 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[W 17:02:05.895 NotebookApp] No web browser found: could not locate runnable browser.
[C 17:02:05.895 NotebookApp]

    To access the notebook, open this file in a browser:
        file:///data/.local/share/jupyter/runtime/nbserver-9-open.html
    Or copy and paste one of these URLs:
        http://(5df00b13e028 or 127.0.0.1):8888/?token=2a0b09ed8f6a1aea73fc1f4e9360696224ec1d4bcbe646b5

APIリクエストにX-Batfish-Version: 0.36.0 のようなHTTPヘッダーが必要な点にもご注意ください。

レスポンス例

$ curl -H "X-Batfish-Version: 0.36.0" http://localhost:9996/v2/networks/example_network/snapshots/example_snapshot/topology
[ {
  "node1" : "as1border1",
  "node1interface" : "GigabitEthernet0/0",
  "node2" : "as1core1",
  "node2interface" : "GigabitEthernet1/0"
}, {

...

inet-henge による可視化

Batfish が吐くデータ構造を、inet-henge が期待する形に変換します。

サンプルをこちらに掲載しますが、さほど複雑ではありません。 https://gist.github.com/codeout/07f3f711a7f0b5adec7fc8065b0f3958

コマンド等による前処理が面倒なため、ブラウザからBatfish APIを呼んでいます。

inet-henge によるネットワーク図

拡大するとインターフェイス名が現れます。

チュートリアル中にあるネットワーク図

(https://github.com/batfish/pybatfish/blob/master/jupyter_notebooks/Getting%20started%20with%20Batfish.ipynb)

まとめ

inet-henge によるネットワーク図は オートレイアウトのため、ひょっとすると好みの出力を得られないかもしれません。しかしながら「見てわかる」程度のネットワーク図を描くことが可能です。

HTMLをひとつ置いておくだけで、前処理なしでBatfish のネットワーク図が見れるというのは便利ですね!

設定ファイルエディターをつくる方へ

JANOG43 でライトニングトークしてきました

speakerdeck.com]

  • NETCONF向けのコマンド(xml) とCLI向けのコマンド(text) が似ているため、XMLスキーマから抽象構文木が作れる
  • 抽象構文木からエディターを作れる
  • ためしにJunos向けの実装をしてみたが、他ベンダー向けもできるはず

簡単にいうと このような内容の発表をしてきました。

時間の関係で話せなかったことなどを、こちらにまとめておきます。

頂いた質問に答えます

いくつか質問を頂きました。ありがとうございます!

Q: オフラインでも使えますか?

A: 使えます。

任意のCLI階層におりたとき、次に続くかもしれないキーワードのリストをXMLスキーマを参照して取得すると…

発表ではこのような説明をしましたが、実際にはXMLスキーマを扱いやすい形 *1 に変換し、vscode拡張の中に同梱しています。ネットワーク機器との通信は不要で、オフラインでもOKです。

Q: XSDとYANG、どちらを使いました?

A: XSDを使いました。

詳しくは後述しますが、結果的にJunosではXSDを使ったものの、他でやるならまずYANGを試してみると思います。

設定ファイルエディターをつくる方へ

さて 本題ですが、あまりに誰得すぎて発表前に消したスライドがあります。実際にエディターを書く人向け。

せっかくなので、このスライドについて 補足します。

XSDとYANG、どちらを取るか

とりあえずはYANGがオススメです。

  • 標準化されたフォーマットだから
    • 抽象構文木を作成するにあたり、単一手法で複数プラットフォーム対応できる可能性がある
  • パーサー実装があるから
  • サイズが小さいから
    • たとえばJUNOS 17.4 の場合、XSD 102MB に対して YANG は18MB しかありません。だいぶ楽
しかしながらJunosの場合、XSDのほうが便利そうだった

一番のポイントは、大量のメタデータが使えることでした。下は一例ですが、エディター実装時にはとても役立つ情報です。

                <xsd:annotation>
                  <xsd:documentation>Policy Name</xsd:documentation>
                  <xsd:appinfo>
                    <flag>mustquote</flag>
                    <flag>identifier</flag>
                    <flag>nokeyword</flag>
                    <flag>current-product-support</flag>
                    <regex-match deprecate="deprecate">^.{1,64}$</regex-match>
                    <regex-match-error deprecate="deprecate">Must be string of 64 characters or less</regex-match-error>
                    <match>
                      <pattern>^.{1,64}$</pattern>
                      <message>Must be string of 64 characters or less</message>
                    </match>
                      <identifier/>
                  </xsd:appinfo>
                </xsd:annotation>
  • それが識別子であるか、クォートが必要かどうか、など
  • バリデーション失敗時のメッセージ
    <xsd:annotation>
      <xsd:documentation>Source address filters</xsd:documentation>
      <xsd:appinfo>
        <flag>oneliner</flag>
        <flag>homogeneous</flag>
        <flag>autosort</flag>
  • CLI で、一行表現可能かどうか
    • これがないと正しくCLIコマンドをパースできません
                    <xsd:annotation>
                      <xsd:documentation>List of captive portal content delivery rules</xsd:documentation>
                      <xsd:appinfo>
                        <flag>current-product-support</flag>
                        <products>
                          <product>mx960</product>
                          <product>mx480</product>
                          <product>mx240</product>
                          <product>mx80</product>
                          <product>mx80-48t</product>
                          <product>mx5-t</product>
                          <product>mx10-t</product>
                          <product>mx40-t</product>
                          <product>mx80-t</product>
                          <product>mx80-p</product>
                          <product>mx2020</product>
                          <product>mx2010</product>
                          <product>ex9204</product>
                          <product>ex9208</product>
                          <product>ex9214</product>
                          <product>mx104</product>
                          <product>vmx</product>
                          <product>vrr</product>
                          <product>mx2008</product>
                          <product>mxtsr80</product>
                          <product>mx10001</product>
                          <product>mx10002</product>
                          <product>ex9251</product>
                        </products>
                    </xsd:appinfo>
                  </xsd:annotation>
  • サポートするプラットフォーム一覧
一方、IOS-XR はYANGがよさそう
  • メタ情報が少ない
  • ファイル構造がほとんどYANGと同じ

このような理由で、もしIOS-XRでやるなら YANGを試すと思います。

NETCONFコマンドとCLIコマンドに差分がある場合の対応

Junosの場合 感覚的には「95% くらい同じ」と言えますが、やはり若干の差があります。

代表的なものは隠しコマンドの扱いです。XSD や YANG に隠しコマンドは記述されていません。

抽象構文木(AST) 生成元であるXSD / YANG は、ベンダーの都合で更新されます。それに引きずられることなく文法修正するために、一旦オレオレ記法で中間ファイルを作っておき、そこにパッチしていくと便利です。

XSDの場合には、大幅にサイズ削減できるという効果も狙っています。エディターに同梱するのは最終形態なので結果同じにはなりますが、開発中に何度もXSD本体をパースする必要はありません。

ライセンス

今回発表したやり方でいくと XSD / YANG をエディター処理系のデータ構造に変換し、エディターに同梱し、二次利用することになります。

問題ないことが多いとは思いますが、これがベンダーのライセンスに抵触するかどうかについて 確認しておくと安心です。

*1:JavaScriptで、巨大なObject