マルチレイヤなNWトポロジ情報から検証環境を作ってみる(2) - L3/OSPF/BGP

## はじめに

[前回 (1)](https://qiita.com/corestate55/items/ba966cc1c73e877f5bee) は、やりたいことと、だいたいどんなものができたのか、大枠の話をしました。今回 (2) では、その中身について。複数レイヤで表現されたトポロジ情報を元にどうやって検証環境の情報に落としていくのか、各レイヤごとに説明していきます。

### 使っているもの

- [batfish は 2021−05-05 時点の batfish/allinone:latest 最新](https://hub.docker.com/layers/batfish/allinone/latest/images/sha256-0f38df2a03f8b0157fa58e31bfb80b51aaa661bfd546be16ee18cd15620c94e9?context=explore)です

```
hagiwara@dev02:~/nwmodel/netomox-examples$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.2 LTS"
hagiwara@dev02:~/nwmodel/netomox-examples$ uname -a
Linux dev02 5.8.0-50-generic #56~20.04.1-Ubuntu SMP Mon Apr 12 21:46:35 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
hagiwara@dev02:~/nwmodel/netomox-examples$ docker image ls | grep batfish
batfish/allinone                latest                         f2ba5422a4ef   3 weeks ago     1.06GB
hagiwara@dev02:~/nwmodel/netomox-examples$
```

## L3 topology to tinet config

元データは毎度の RFC8345・RFC8346 ベースの JSON (トポロジ情報) です。これをもとに tinet に食わせるトポロジや設定情報 (spec.yaml) を作っていきます。とはいっても L3 topology の段階では実はそれほど困ることがないですね。JSON データを読んで node/interface, link 情報を元に tinet config を作っていきます。

下記のコマンドで L3 設定だけの環境を作れます (ospf/bgp ナシ; デバッグ用)

```
hagiwara@dev02:~/nwmodel/netomox-examples$ bundle exec ruby config_defs/topo2config.rb --debug layer3
```

### インタフェース名の制約

気をつけないといけないのは Cisco のインタフェース名がそのまま veth には適用できないことくらいです。

```
+ ip netns exec core02 ip link set Fa0/0 up
Error: argument "Fa0/0" is wrong: "dev" not a valid ifname
```

Linux のインタフェース名制限として以下があります。(ref. [Allowed chars in Linux network interface names? - Unix & Linux Stack Exchange](https://unix.stackexchange.com/questions/451368/allowed-chars-in-linux-network-interface-names/532650#532650)) 

- 最大15文字 (末尾 `\0` を足して 16byte まで)
- `/` と whitespace は NG
- 大文字小文字は区別する

当然ですが、トポロジデータを扱う中で、内部で参照されるインタフェース名には一貫性が必要です。最初はインタフェース名を変換しないといけないのを意識せずに作っていたので、JSONデータを読んで tinet config を出力する段でインタフェース名の置換を入れていました。が、tinet config には独自記法やあるいは直接コマンド書くようなところもあって、どこがインタフェース名に相当するのかを都度指定しながら置換してやらないといけない。

これはとても面倒なので、JSONデータを読んだ直後でインタフェース名の置換をかけるように変えています。RFC8345 ベースの JSON データであれば、どこにインタフェース名が入ってくるのかが明示的に指定できるし、かつ最初にまとめて変換することで一貫性を担保しやすくなるためです。元コンフィグからトポロジデータを出力するとき・トポロジデータを読んで模擬環境コンフィグを生成するとき、モデルデータの直近で変換をかけることでデータの一貫性を保証しつつインタフェース名を扱えるようになります。

![convert interface name](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/eec37d45-278a-359c-2f10-6022e3a7e804.png)


### Loopback interfaceの取扱

地味ですが要注意。元環境は Cisco IOS で検証環境は Linux/FRR なので当然 loopback interface の持ち方とかも異なります。Linux/FRR 上ではデフォルトで `lo` があるので、IOS `Loopback0` 相当のものとして使っています。

### テスト

tinet config には `test` セクションがあります。各コンテナ等を起動した後にチェックのためのコマンドをこのセクションに書いておいて、まとめて実行することができます。L3 トポロジデータに対しては、各リンクが全て /30 p2p なリンクなので、対向のインタフェース IP に対して ping を実行するようなコマンドを生成して入れてあります。

こうしたテストが自動生成できるのもトポロジ情報を元にコンフィグを作成した利点ですね。従来であれば、NW構成図やパラメタシートを見ながら、各デバイスごとに、どこに対して ping を実行すればよいかを手作業でリストアップしていたような内容です。それをスクリプトでまとめて生成・実行できます。

## OSPF topology to tinet config

つぎは OSPF の設定を作りましょう。もとの JSON には ospf-proc / ospf-area の2つのレイヤが含まれています。今作りたいのは各ルータ上で実行する ospf (process) の設定なので ospf-proc の情報を主体に操作することになります。

下記のコマンドで L3 + OSPF 設定だけの環境を作れます。 (bgp ナシ; デバッグ用)

```
hagiwara@dev02:~/nwmodel/netomox-examples$ bundle exec ruby config_defs/topo2config.rb --debug ospf
```

### OSPF トポロジ定義の考え方

[以前の記事](https://qiita.com/corestate55/items/fb18066d1105010758d9) では Cisco config から OSPF 情報を抽出しています。その時は完全に失念していたのですが、[RFC 8346 - A YANG Data Model for Layer 3 Topologies](https://tools.ietf.org/html/rfc8346) の "Example OSPF Topology" で Layer3 topology model (RFC8346のメインテーマ, RFC8345 の拡張) を更に拡張して OSPF topology を考えてみるが入っています。オレオレ OSPF 構成情報と RFC8346 Example (RFC8346exとします) の違いについて先にちょっと見ておきます。

RFC8346ex とオレオレ定義の一番の違いは OSPF Area をどう取り扱うかにあります。オレオレでは、全てのエリアの OSPF proc 間接続と、エリア間接続を別にして 2 枚のレイヤで管理するイメージで作っていました。

一方 RFC8346ex では 1 エリア 1 レイヤにするイメージのようです。というのも、RFC8346ex では ospf-type network (layer) でネットワークの属性 (attribute) としてひとつの area-id を持つようになっているからです。今回対象にしているネットワークではみっつの ospf area があるので、RFC8346ex にしたがうと下記のような 3 枚のトポロジに分割して OSPF トポロジを管理することになります。

![rfc8346ex ospf](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/4677f912-dad7-06a6-18d0-49a0ccb0ace5.png)

オレオレの方では、OSPF の全域 (全エリア) がどういう接続関係になっているのかを見たかったので、複数エリアのプロセスもひとつにまとめてどう接続されているのかを 1 枚にしています。(ただ、いま改めてみてみるとやっぱり ospf-area のトポロジと ospf-proc とのノード間依存関係が微妙。)

問題は、ここからNW機器コンフィグを起こそうとしたときに、ABR間 (core01-core02 間) リンクがどのエリアに属しているのかを決定できないことでした。この図のようなエリア～ノード間の依存関係の情報だけだと、core01-core02 間リンクはどのエリアに属していてもおかしくないように見えてしまう。なのでABRのインタフェースごとのエリア情報か、ospf の `network` コマンド相当の情報 (どのネットワークでOSPFをしゃべるか) がないと、どのリンクがどのエリアに属するのかを明示できない。

![self-defined ospf model](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/4c61a9c0-415b-2daf-e468-b1fcf4deed8c.png)

なので、今回は ospf-proc ノードのインタフェース (term-point) にエリア情報を入れることにしてしまいました。どうせそれをやるなら、ABRかどうかによらず、すべての インタフェースに入れてしまえば、ospf-proc の情報だけで ospf config が起こせてしまう [^ospf-area]。本来は ospf network type をちゃんと定義して必要な属性をつくってやらないといけないんですが、実験なので暫定的な対応にしてしまっていて、IPアドレスしか入れられない (l3-network の) attribute にムリヤリ足してしまっています……(下図)。

![embed ospf area info](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/f7ada394-227c-8148-8a72-7bb90cbe6354.png)

[^ospf-area]: ospf-area レイヤの存在意義があまりないな…。いちおうどのエリアにどのルータ (ospf proc) が属しているのをグループ化してはいるものの、結局 ABR みたいに複数エリアにまたがるものとかをうまく扱えていない。そういう観点では、RFC8346にあるように 1Area-1Network として分割して入れていくのが良いのかもしれない。でも 1 エリア単位でトポロジ見たいかといわれるとそうでもないような……。見(せ)たいもののモデルと作りたいもののモデルをどうバランスさせるかが課題です。

### テスト

OSPF (IGP) については、L3 NW のテストで作った隣接ノード間 ping がちゃんとできてればだいたい動くので、ここでは ospf neighbor および ospf routing table の表示だけにしてあります。(やるのであれば、この後解説する BGP のテストと役割を対比させて、OSPF エリア間通信とかを生成する形かな…)

## BGP topology to tinet config

BGP やるうえで何が難しいかというと、いくつかの知識を組み合わせて、何が必要か・どういう形に持っていけばよいのかを考えるところでした。

- 機器コンフィグの理解 : Cisco + FRR どちらに対しても
    - FRR ほとんど触ったことがないので、元環境と同等のことをやるためにはどういうコンフィグになるのかを確認するところから開始
- Cisco config を Batfish に食わせた結果、どんな情報が得られているか
    - 元 config との対応関係の理解
    - トポロジ情報にそれらのデータをどう埋め込むか → トポロジデータにどんな情報が入っていれば tinet/FRR 用の設定に変換しやすいか

### ノードとASの関係

AS 間のトポロジ (bgp-as network) と BGP speaker のトポロジ (bgp-proc network) の関係性: bgp-proc 内のノードに所属している AS 番号の情報とか埋めちゃってもいいんだけど、せっかく複数レイヤに分けているので、bgp-as node (AS 自体を指す) をもとに、bgp-proc node のAS番号とかを出すようにしています。

で、ここで気にしないといけないのが次の confederation の話。

### Confederation + Route reflector

Confederation を設定していると、外から見える public (global) AS と、その内部で使う private (local) AS とでふたつの AS 番号を持つことになります。それらがコンフィグ上でどう扱われているのか → Batfish に読ませるとどういうデータとして抽出されているかを見ないといけない。これを見ていくと

- confederation しているところでは基本的には local AS の番号が使われる
- [`config_bgp_peer` query](https://github.com/corestate55/netomox-examples/blob/develop/model_defs/bf_l3trial/csv/sample1b/config_bgp_peer.csv) で global AS との紐づけ情報が出てくる

ということがわかりました。このへんちょっと見落としがあって、BGP peer 情報って基本的に [`edges_bgp` query で取れる情報](https://github.com/corestate55/netomox-examples/blob/develop/model_defs/bf_l3trial/csv/sample1b/edges_bgp.csv)ばかり見ていたんですよね。BGP neighbor 設定なんかは iBGP, confederation eBGP, 通常の eBGP とかタイプに応じて変えてやらないといけないので `config_bgp_peer` の情報もちゃんと見る必要があります。

Route reflector (RR) も同様で `config_brp_peer` の情報が必要です。RR については基本的に server 側での設定になるので、表にあらわれてくるのも server として動作するノードだけ。とはいえ、「可視化」して環境構成を理解したい観点を考えると、RR client がどの RR server を見ているのかも知りたいので、client 側にもデータを埋め込んでいます。

![embed bgp confederation/RR info](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/d0f0cbfe-2b69-d81a-890a-94b50a850288.png)

### Advertise network

さて予想外に困ったのが BGP の `network` コマンドの設定でした。というのも `network` コマンドの情報が batfish の BGP 関連クエリで取れてきてないんですよね…。広告するネットワークの情報とか重要なのではという気がするんだけど。何か他のクエリを投げないといけないんだろうか? 

いま、元環境のコンフィグでは、経路広告しているルータでは、広告しているブロックの null route を設定しているので、それが `routes` クエリの結果として入ってきています。今回はそれを使って bgp `network` の設定としています。うーん

### テスト

OSPF のテストと同様に経路情報や neighbor の情報を取得するのあそれはやるとして、せっかくなので AS 間の通信確認 (ping) コマンドを生成しています。ある AS を対象としたときに、その AS に含まれる BGP speaker (process) からその外部 AS の境界インタフェースの IP アドレスに対して ping していきます。隣接あるいは接続関係のわかっている外部 AS の境界点への通信確認ですね。これは AS 間トポロジ情報 (bgp-as network) が元になっているので、上位レイヤトポロジがうまく定義できていればちょっと観点の違うテストなんかもできるって例になるかなと思い、試してみました。

例: AS65531 (の内部にあるルータ: core01) から、外部ASの境界インタフェースへの ping

```yaml
test:
  cmds:
# 省略
  - cmd: docker exec core01 ping -c2 10.0.0.17
  - cmd: docker exec core01 ping -c2 10.0.0.45
  - cmd: docker exec core01 ping -c2 10.0.1.10
  - cmd: docker exec core01 ping -c2 10.0.1.13
  - cmd: docker exec core01 ping -c2 10.0.1.14
# 省略
```

![inter-AS ping test](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154230/915dcd9a-65f9-ba63-d9a2-8e272ebacbce.png)

## まとめ

トポロジ情報から検証環境設定を出力するにあたって、実装上の検討点をレイヤ別に出してみました。今回はちょっと個別の実装の話を見ていったので、[次回はもうちょっと全体の話とか今後の話とか](https://qiita.com/corestate55/items/567a4693abd67e1a5531)を見ていきたいと思います。