---
title: "Batfishとは？ネットワーク設定を本番前に検証するOSSの仕組みと使い方"
url: "https://www.issoh.co.jp/tech/details/5185/"
published: 2025-02-05
updated: 2026-10-13
categories: ["インフラ・クラウド"]
publisher: "株式会社一創"
---

# Batfishとは？ネットワーク設定を本番前に検証するOSSの仕組みと使い方

Batfishは、ルータやファイアウォールの設定ファイルを読み込み、そのネットワークでパケットがどこへ届き、どのACL行に当たり、BGPセッションが張れるかを計算するオープンソースの検証ツールです。実機にもエミュレータにも接続しないため、変更案の設定ファイルを用意するだけで、本番に入れる前に「SSHを全開放してしまう」「上の行に隠れて効かないACL」を見つけられます。この記事では2026年8月27日公開のv2026.08.27とpybatfish 2026.9.17.3748で実行した出力を載せながら、導入、ACLの検査、リンク障害の影響の洗い出し、CIへの組み込みまでを説明します。

なお、英語で batfish と呼ばれる魚（アカグツやツバメウオの仲間）の話ではなく、ネットワーク構成解析ツールの記事です。

## まとめ：Batfishでできることと導入の要点

- 設定ファイルからベンダー非依存のモデルを作り、到達性・ACL・経路・BGPを計算する。実機への接続は不要
- 最新はBatfish v2026.08.27（2026年8月27日）とpybatfish 2026.9.17.3748（2026年9月18日）。pybatfishはPython 3.10以上、サーバはJDK 21以上が必要
- 起動は `docker run -p 9996:9996 batfish/batfish` で足りる。9997番の公開はpybatfish 2022.9.7以降不要
- 設定ファイルはスナップショット直下の `configs/` に置く。フォルダを直下に置かないと読み込まれない
- ACLの死に行は `filterLineReachability`、変更前後のACL差分は `compareFilters`、到達性は `traceroute` で調べる
- `bf.fork_snapshot` でリンクや機器を止めた状態を作り、`differentialReachability` で比べると、障害時に届かなくなる通信や、ACLを迂回する通信が見つかる
- `pybatfish.client.asserts` をpytestから呼べば、危険な変更でCIを失敗させられる

以下、仕組み・導入・使い方・障害解析・CI・向かない場面の順に説明します。

## Batfishの仕組み：設定ファイルから挙動を計算する流れ

Batfishは、Cisco IOSやJuniper Junosなどの設定文をベンダーごとのパーサで読み、共通のデータモデルへ変換します。そのモデルからルーティングプロトコルの収束結果（データプレーン）を計算し、利用者が投げた「質問（question）」に表形式で答えます。GitHubの `batfish/batfish` はApache License 2.0で公開され、2026年10月13日時点のGitHub APIが返すスター数は1,502です。

処理は次の4段階です。

1. スナップショット（設定ファイル一式）をアップロードし、ベンダーを自動判定してパースする
2. パース結果をベンダー非依存のモデルへ変換する。未対応の構文はここで警告になる
3. OSPF・BGP・静的経路などを収束させ、各機器のRIBと転送表を計算する
4. 質問を受けて、到達性・ACL評価・セッション状態などをpandasのDataFrameで返す

計算対象は論理的な振る舞いです。パケットを実際に流すわけではないので、帯域や遅延、機器のソフトウェア不具合は結果に現れません。

### Batfishの起源と開発体制：NSDI 2015の研究からIntentionet、AWSへ

Batfishは大学の研究から始まったツールです。最初の論文は2015年のNSDIで発表された[「A General Approach to Network Configuration Analysis」](https://www.usenix.org/conference/nsdi15/technical-sessions/presentation/fogel)で、著者はUCLA、南カリフォルニア大学、Microsoft Researchの7人です。設定ファイルから、その設定を入れたときにできるデータプレーンを導き出し、適用前に誤りと「もし〜なら」の問いに答えるという、今のBatfishの考え方はこの論文で示されました。論文では2つの大学のネットワークを解析し、多数の設定ミスを見つけています。

その後はIntentionetという会社が開発を担いました。2023年のSIGCOMMで同社のメンバーが発表した[「Lessons from the evolution of the Batfish configuration analysis tool」](https://www.cs.ucla.edu/~todd/research/sigcomm23b.pdf)は、Batfishが研究用の試作から実運用に耐える製品へ育った経緯をまとめています。初期のDatalogによる実装には転送状態の生成と解析で大きな制約があったため、データプレーンの生成はJavaで書いた手続き型のエンジンに、データプレーンの検証はBDD（二分決定グラフ）を使うエンジンに置き換えられました。BDDのエンジンが本番の実装になったのは2018年初めです。同じ論文には「著者は全員、現在Amazon Web Servicesにも所属している」と記されています。

ライセンスはApache License 2.0のまま、GitHubの `batfish/batfish` で開発が続いています。2026年10月10日から12日にかけてもBGPやJunosの処理を直すコミットが続いており、開発は止まっていません。

### 仮想ラボ・NetBox・Ansibleとの役割分担

仮想ルータを立てて実際に動かすラボは、ベンダーOSそのものの挙動を確かめられる代わりに、イメージの入手と起動に手間がかかります。Batfishは機器イメージを起動せず設定ファイルだけで計算するので、変更のたびに毎回回す検査に向いています。

管理系のOSSとは担当範囲が違います。[NetBox](/tech/details/3571/)はIPアドレスや機器の「あるべき姿」を記録する台帳で、[Ansible](/tech/details/6533/)は設定を機器へ配る実行役です。Batfishはその間に入り、配る前の設定が意図どおりに振る舞うかを判定します。台帳から設定を生成し、Batfishで検査し、合格したものだけをAnsibleで適用する、という並べ方が自然です。

## 最新版v2026.08.27の変更点と動作要件

Batfishは日付を版番号にしており、2026年10月13日時点の[GitHub Releases](https://github.com/batfish/batfish/releases)の最新は2026年8月27日のv2026.08.27です。pybatfishの版と対応Pythonは[PyPI](https://pypi.org/project/pybatfish/)で確認できます。その前のv2025.07.07から約1年あいたため、変更の幅が大きくなっています。

| 項目           | 2026年10月13日時点                   |
| ------------ | ------------------------------- |
| Batfish（サーバ） | v2026.08.27（ビルド2026.08.27.3685） |
| pybatfish    | 2026.9.17.3748（2026年9月18日）      |
| Python       | 3.10以上（分類上は3.10〜3.14）           |
| Java         | JDK 21以上（推奨はJDK 25）             |
| Dockerイメージ   | linux/amd64・linux/arm64         |
| ライセンス        | Apache License 2.0              |

リリースノートのうち、運用に響く変更は次のとおりです。

- Nokia SR OS（MD-CLI）に新たに対応。長く要望が多かったベンダーで、インターフェース、OSPF、IS-IS、BGP、VPRNまで変換する
- Broadcom FASTPATH（管理プレーンから）と、Azureの実験的サポートを追加
- Dockerイメージがamd64とarm64のマルチプラットフォームになり、Apple Silicon機でもネイティブに動く
- pybatfishにベータ版のMCPサーバが入った。`pip install 'pybatfish[mcp]'` で導入し `batfish-mcp` で起動すると、36個のツールをAIエージェントから呼べる。ツール名と引数は今後変わる可能性がある
- 破壊的変更として、`routes`・`bgpRib`・`evpnRib` の結果から旧 `Next_Hop_IP`・`Next_Hop_Interface` 列が消えた。`Next_Hop` 列を参照するスクリプトへ書き換える必要がある

既存のスクリプトで `Next_Hop_IP` を読んでいる場合、サーバだけ更新するとKeyErrorで止まります。サーバとpybatfishは同じ時期の版へまとめて上げてください。

Docker Hubの `batfish/batfish` には `test-2026.10.12.3823` のような `test-` 付きのタグがほぼ毎日追加されますが、これは開発中のビルドです。2026年10月13日時点の `latest` タグは `2026.08.27.3685` と同じイメージを指しているので、検証に使うのは `latest` か版番号のタグにしてください。

## 対応ベンダーと設定ファイルの取り方

pybatfishの[公式ドキュメント（formats）](https://batfish.readthedocs.io/en/latest/formats.html)が挙げる対応ベンダーは、A10、Arista、AWS、Azure、Cisco、Check Point、Cumulus Linux、F5 BIG-IP、Fortinet、Juniper、Palo Alto Networks、SONiCです。v2026.08.27で加わったNokia SR OSは、2026年10月13日時点でこの一覧にまだ載っていません。ベンダーごとに、どのコマンドの出力を渡すかが決まっています。

| ベンダー                              | 渡す内容                                      | 置き場所                |
| --------------------------------- | ----------------------------------------- | ------------------- |
| Cisco IOS・IOS-XE・IOS-XR・NX-OS・ASA | show running-config                       | configs/            |
| Arista EOS                        | show running-config                       | configs/            |
| Juniper Junos                     | show configuration \| display set（階層形式も可） | configs/            |
| Fortinet FortiOS                  | show                                      | configs/            |
| Palo Alto                         | Panoramaのset形式または機器単体のshow出力              | configs/            |
| SONiC                             | config\_db.json・frr.conf                  | sonic\_configs/機器名/ |
| AWS                               | describe系APIのJSON                         | aws\_configs/       |
| Azure                             | リソースのJSONビュー                              | azure\_configs/     |

Junosは `display set` の平坦な形式が基本ですが、階層形式の設定も受け付け、内部でset形式へ変換します。機器から取った設定には版を示す行が含まれるのでベンダーは自動判定されますが、テンプレートから生成した設定では判定に失敗することがあります。その場合はファイル先頭に `!RANCID-CONTENT-TYPE: arista` のような行を足してベンダーを明示します。IOS系の違いは[IOS-XRとCisco IOS・IOS-XEの違い](/tech/details/5187/)を参照してください。

## 導入手順：Dockerでサーバを起動しpybatfishから接続

Batfishはサーバ（Java）とクライアント（Python）に分かれています。サーバはDockerで動かし、手元のPythonにpybatfishを入れて接続するのが公式の手順です。

### サーバの起動とポート

Docker Hubには2種類のイメージがあります。`batfish/batfish` はサーバ単体、`batfish/allinone` はサーバにpybatfishとサンプルのJupyterノートブックを同梱したものです。検証を自動化するならサーバ単体で十分です。

```
docker run -d --name batfish \
  -v batfish-data:/data \
  -p 9996:9996 \
  batfish/batfish
```

pybatfish 2022.9.7以降は9996番（V2 API）だけで全機能を使えます。`batfish/batfish` イメージ自体もEXPOSEしているのは9996番だけです。README には9997番を公開する例が残っていますが、現行の組み合わせでは不要です。ノートブックを試すときは `batfish/allinone` を使い、`-p 8888:8888` を足してブラウザから開きます。ノートブックはコンテナ内で同居するサーバへ接続するので8888番だけで動きますが、ホスト側のPythonからpybatfishで接続するには9996番の公開が別に必要です。

起動が済んだかは `curl http://localhost:9996/v2/version` で確かめられます。v2026.08.27のサーバは `{"Batfish":"2026.08.27.3685","api_version":"0.0.0"}` を返します。MacでDocker Desktopを使わずに動かす場合は[Colima](/tech/details/17842/)でも起動できます。

### pybatfishのインストール

```
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pybatfish
```

最新のpybatfishはPython 3.10未満に対応していません。Python 3.9の環境でインストールすると、3.9で入る最後の版である2025.7.7.2423が選ばれ、v2026.08.27のサーバと版がずれます。

Pythonを書かずに端末から質問だけ投げたい場合は、pybatfishを包んだCLIの bfqry も使えます。`bfqry trace -b simple --node dev1 --daddr 10.45.0.5` のように1行で経路やACLを確認できますが、bfqry 2024.12.1をv2026.08.27のサーバにつなぐと、`route` コマンドだけが列名の変更（`Next_Hop_IP` の削除）でKeyErrorになります。

### Batfishサーバの推奨メモリ容量

公式のシステム要件は、サンプルノートブックを動かすPCに8GB、実ネットワークを処理するサーバに最低32GBのRAMを推奨しています。`batfish/batfish` イメージのJavaはコンテナが使えるメモリの80%までをヒープに使う設定（`-XX:MaxRAMPercentage=80`）で起動します。Dockerの `--memory` は上限の指定なので、ホストや、Docker Desktop・Colimaの仮想マシンに割り当てたメモリ自体が足りなければ、指定しても使える量は増えません。先に仮想マシン側の割り当てを確認してください。

## スナップショットのフォルダ構成

Batfishは「スナップショット」というフォルダ単位で設定を読み込みます。ベンダー設定はスナップショット直下の `configs/` に置くのが決まりで、フォルダの直下にファイルを並べただけでは読み込まれません。`configs/` の中はサブフォルダを切っても再帰的に読まれ、ベンダーが混在していても構いません。

```
snapshot/
├── configs/             # 機器の設定ファイル（必須）
│   ├── edge1.cfg
│   └── core/core1.cfg
├── hosts/               # 端末をモデル化するJSON（任意）
├── aws_configs/         # AWSのdescribe結果（任意）
├── batfish/
│   ├── layer1_topology.json   # 物理結線（任意）
│   ├── isp_config.json        # ISPのモデル（任意）
│   └── runtime_data.json      # IF速度など実行時情報（任意）
└── external_bgp_announcements.json
```

物理結線を示す `layer1_topology.json` を渡さない場合、BatfishはIPアドレスから隣接を推定します。たとえば `192.168.1.1/24` と `192.168.1.2/24` を持つインターフェースは隣り合っているとみなします。同じアドレス空間を複数の場所で使い回している構成や、リンクローカルアドレスで接続している構成ではこの推定が働かないため、結線ファイルを渡す必要があります。

## pybatfishによるACLの誤り検出と変更差分の検証

ここからは、1台のCisco IOSルータを例に、よくある誤りを検出します。インターネット側から443番だけを許可し、SSHは管理用の198.51.100.0/24からだけ通すつもりで書いた設定です。

```
hostname edge1
!
interface GigabitEthernet0/0
 ip address 203.0.113.1 255.255.255.0
 ip access-group INBOUND in
!
interface GigabitEthernet0/1
 ip address 192.168.10.1 255.255.255.0
!
ip access-list extended INBOUND
 permit tcp any any eq 443
 permit tcp any host 192.168.10.10 eq 443
 deny   tcp any host 192.168.10.10 eq 22
 permit tcp 198.51.100.0 0.0.0.255 host 192.168.10.10 eq 22
 deny   ip any any
```

この内容を `base/configs/edge1.cfg` として保存します。

### 読み込み結果の確認：fileParseStatus・initIssues

最初に、全ファイルがパースできたかを確かめます。ここで失敗や警告が出ている状態で後の質問に答えさせても、結果は信用できません。

```
from pybatfish.client.session import Session

bf = Session(host="localhost")
bf.set_network("edge-demo")
bf.init_snapshot("base", name="base", overwrite=True)

print(bf.q.fileParseStatus().answer().frame())
print(bf.q.initIssues().answer().frame())
```

```
           File_Name  Status File_Format      Nodes
0  configs/edge1.cfg  PASSED   CISCO_IOS  ['edge1']
Empty DataFrame
Columns: [Nodes, Source_Lines, Type, Details, Line_Text, Parser_Context]
```

StatusがPASSEDで、initIssuesが空なら読み込みは正常です。未対応の構文があるとStatusがPARTIALLY\_UNRECOGNIZEDになり、initIssuesに該当行が並びます。

### 効かないACL行の検出：filterLineReachability

```
df = bf.q.filterLineReachability().answer().frame()
print(df[["Unreachable_Line", "Blocking_Lines"]].to_string())
```

```
                                             Unreachable_Line                               Blocking_Lines
0  permit tcp 198.51.100.0 0.0.0.255 host 192.168.10.10 eq 22  ['deny   tcp any host 192.168.10.10 eq 22']
1                    permit tcp any host 192.168.10.10 eq 443                ['permit tcp any any eq 443']
```

管理用のSSH許可は、その前の `deny` にすべて吸われて一度もマッチしません。つまり管理者もSSHできない設定です。2行目の443番許可は1行目と重複しているだけなので実害はありませんが、ACLの整理対象になります。目視のレビューでは見落としやすい行の順序の誤りを、Batfishは行単位で示します。

古い解説記事には `bf.q.aclReachability()` という書き方が残っていますが、現行のサーバにこの質問はなく、呼ぶと `AttributeError: 'Questions' object has no attribute 'aclReachability'` になります。同じ目的には `filterLineReachability` を使います。

### 変更前後の比較：compareFilters・searchFilters・testFilters

次に、誰かが「SSHが通らない」と言われて、1行目の直後に `permit tcp any any eq 22` を足した変更案を `cand/configs/edge1.cfg` として用意したとします。変更前後を比べると、何が変わるかがわかります。

```
from pybatfish.datamodel.flow import HeaderConstraints

bf.init_snapshot("cand", name="cand", overwrite=True)

print(bf.q.compareFilters(filters="INBOUND")
      .answer(snapshot="cand", reference_snapshot="base").frame())

ssh = HeaderConstraints(srcIps="0.0.0.0/0", dstIps="192.168.10.10",
                        applications=["ssh"])
print(bf.q.searchFilters(headers=ssh, filters="INBOUND", action="permit")
      .answer(snapshot="cand").frame())
```

`compareFilters` は、新しい2行目が旧設定の `deny tcp any host 192.168.10.10 eq 22` と `deny ip any any` の2行と異なる判定をすると返しました。`searchFilters` は、条件に当てはまる具体的なパケットを1つ探して見せます。変更案では `8.8.8.8:49152->192.168.10.10:22` のSSHが `permit tcp any any eq 22` で許可されました。管理者を通すつもりの変更で、このACLは送信元を問わずSSHを許可するようになっています。経路まで含めて外部から届くかどうかは、`traceroute` や `reachability` で別に確かめます。

`searchFilters` が「条件に合うパケットがあるか」を探すのに対し、`testFilters` は送信元や宛先を1つに決めたパケットがどの行で判定されるかを返します。管理用サブネットの198.51.100.20からのSSHを、変更前後の両方で判定させると違いがはっきりします。

```
admin_ssh = HeaderConstraints(srcIps="198.51.100.20", dstIps="192.168.10.10",
                              applications=["ssh"])
for snap in ["base", "cand"]:
    df = (bf.q.testFilters(headers=admin_ssh, nodes="edge1", filters="INBOUND")
          .answer(snapshot=snap).frame())
    print(snap, df.loc[0, "Action"], df.loc[0, "Line_Content"])
```

```
base DENY deny   tcp any host 192.168.10.10 eq 22
cand PERMIT permit tcp any any eq 22
```

変更案で管理者のSSHは通るようになりましたが、許可したのは管理用の行ではなく、全開放の行です。`testFilters` だけを見ると「直った」と判断しかねないので、`searchFilters` や後述のpytestで「通ってはいけない通信」も必ず確かめます。

### 経路とBGPの確認：routes・bgpSessionStatus・traceroute

ルーティングを含むネットワークでは、次の質問をよく使います。

| 目的         | 質問名                                              |
| ---------- | ------------------------------------------------ |
| 読み込みの確認    | fileParseStatus・initIssues・parseWarning          |
| ACLの検査     | filterLineReachability・searchFilters・testFilters |
| ACLの変更比較   | compareFilters                                   |
| 到達性        | traceroute・reachability                          |
| 変更前後の到達性差分 | differentialReachability                         |
| 経路とBGP     | routes・bgpSessionStatus                          |
| トポロジの抽出    | layer3Edges・bgpEdges・ospfEdges・layer1Edges       |

Batfishに同梱されているサンプルネットワーク（ルータ13台と端末2台の構成）の場合、Intel Core i9-9880HのMac上のv2026.08.27で、スナップショットの初期化が3.0秒、37本のBGPセッションの判定が3.0秒です。結果は確立34本、設定の不一致3本です。`traceroute` の1回の問い合わせは0.5秒でした。計算時間は機器の台数と質問の種類で変わるため、CIに組み込む前に自社の設定一式で一度測っておくと、1回の検査にかかる時間を見積もれます。

## 障害時の影響分析：fork\_snapshotによる単一リンク障害の検証

設定の変更だけでなく、「このリンクが切れたら何が届かなくなるか」も設定ファイルから計算できます。pybatfishの `bf.fork_snapshot` は既存のスナップショットを複製し、指定したインターフェースや機器を停止した状態の新しいスナップショットを作ります。停止前後を `differentialReachability` で比べると、既定では一方のスナップショットで配送に成功し、もう一方では成功しないフローの例が返ります。両方で配送に成功する経路変更をすべて列挙する質問ではありません。ここではpybatfishのGitHubリポジトリにあるサンプル `failure-analysis`（3つのASにまたがるルータ13台と、宛先の端末1台）を使います。

### トポロジの抽出：layer3Edges・bgpEdges・ospfEdges

事前にGitHubの `batfish/pybatfish` リポジトリから `jupyter_notebooks/networks/failure-analysis` をフォルダごと取得し、Pythonの作業ディレクトリ直下に置いてください。pipでpybatfishをインストールするだけでは、このサンプルフォルダは用意されません。前節のACL検証とは別のネットワークを選び、以下を実行してから隣接関係を取得します。

```
from pybatfish.client.session import Session

bf = Session(host="localhost")
bf.set_network("failure-demo")
bf.init_snapshot("failure-analysis", name="base", overwrite=True)
```

落とすリンクの一覧は、Batfishが計算した隣接関係から取ります。[公式ドキュメント](https://batfish.readthedocs.io/en/latest/notebooks/topology.html)の定義では、`layer3Edges` はネットワーク内のレイヤ3のエッジ、`bgpEdges` と `ospfEdges` はそれぞれBGPとOSPFの隣接関係を一覧にする質問です。これらは有向の隣接関係を返します。このサンプルのlayer3Edgesは両方向がそろうため42行を21組にまとめられますが、BGP・OSPFの隣接関係の件数を物理リンク数として数えることはできません。

```
edges = bf.q.layer3Edges().answer().frame()
print(edges.head(2).to_string())
```

```
                      Interface          IPs               Remote_Interface   Remote_IPs
0  hongkong[GigabitEthernet0/0]  ['3.0.2.2']  singapore[GigabitEthernet1/0]  ['3.0.2.1']
1  hongkong[GigabitEthernet1/0]  ['3.0.1.2']      tokyo[GigabitEthernet0/0]  ['3.0.1.1']
```

このサンプルでは `layer3Edges` が42行（リンク21本）、`bgpEdges` が36行、`ospfEdges` が24行でした。同じ表はトポロジ図を描く材料にもなります。物理結線のファイル（`layer1_topology.json`）を渡している場合は、`layer1Edges` で物理リンクの一覧も取れます。

### 単一リンク障害の総当たり：differentialReachability

21本のリンクを1本ずつ落とし、宛先の端末がいる2.128.0.0/24への到達性が変わるリンクを探します。`deactivate_interfaces` には `pybatfish.datamodel.Interface` のリストを渡します。`"hongkong[GigabitEthernet0/0]"` のような文字列を渡すと、サーバが `400 Bad Request` を返して止まります。

```
from pybatfish.datamodel import Interface
from pybatfish.datamodel.flow import HeaderConstraints

edges = bf.q.layer3Edges().answer().frame()
links = {tuple(sorted([(e.Interface.hostname, e.Interface.interface),
                       (e.Remote_Interface.hostname, e.Remote_Interface.interface)]))
         for e in edges.itertuples()}

for a, b in sorted(links):
    bf.fork_snapshot("base", "fail", overwrite=True,
                     deactivate_interfaces=[Interface(*a), Interface(*b)])
    diff = bf.q.differentialReachability(
        headers=HeaderConstraints(dstIps="2.128.0.0/24"), maxTraces=1
    ).answer(snapshot="fail", reference_snapshot="base").frame()
    if len(diff):
        print(a, b, len(diff))
```

Intel Core i9-9880HのMacで、21本の総当たりは40〜44秒（1本あたり約2秒）で終わりました。到達性が変わったのは6本です。

| 落としたリンク            | 返されたフロー例の件数 | 変化の内容              |
| ------------------ | ----------- | ------------------ |
| seattle〜tokyo      | 11          | 香港・シンガポール・東京から届かない |
| hongkong〜tokyo     | 7           | 香港・シンガポールから届かない    |
| hongkong〜singapore | 3           | シンガポールから届かない       |
| london〜paris       | 3           | ACLで止まっていた通信が届く    |
| london〜newyork     | 3           | ACLで止まっていた通信が届く    |
| houston〜pop        | 54          | 宛先の端末の接続そのもの       |

最後の1本は宛先の端末の唯一の接続なので、落ちれば届かないのは当然です。問題は上の3本です。アジアの3台は香港〜パリのリンクでヨーロッパ経由の迂回路を持っているのに、シアトル〜東京の1本が切れただけで、2.128.0.0/24への経路が消えて `NO_ROUTE` になります。

原因は香港の設定にあります。パリから受け取る経路に `route-map as1_to_as3 in` がかかっており、その `deny` が参照する `access-list 102` に2.128.0.0/16が含まれています。ヨーロッパ経由の経路を捨てる設定なので、平常時は問題が見えず、迂回が必要になったときに初めて効きます。`bf.q.bgpRib(network="2.128.0.0/16")` を2つのスナップショットで比べると、アジアの3台だけ経路が消えていることも確かめられます。公式ノートブックは2本同時の障害をランダムに選んでこの問題を見つけていますが、総当たりにすると1本の障害で起きることまでわかります。

### 機器の停止で見つかるACLの迂回

到達性の変化は「届かなくなる」方向だけではありません。`deactivate_nodes=["london"]` でロンドンのルータを止めると、ミラノとパリから出るICMPの3フローが、平常時はニューヨークのACL `AS1_TO_AS2` で止まっていた（`DENIED_IN`）のに、アジアと米国西海岸を経由して届く（`EXITS_NETWORK`）ようになります。ロンドン〜パリ、ロンドン〜ニューヨークのリンク障害でも同じことが起きます。

フィルタを特定の経路上の1台にしか置いていないと、迂回路ができた瞬間にそのフィルタを通らなくなります。冗長化の設計と、セキュリティのためのフィルタの置き場所は一緒に確かめる必要があり、そのための検査として障害の総当たりが役に立ちます。2本同時の組み合わせ（このサンプルなら210通り）まで広げると、1シナリオ約2秒の実測から単純計算で7分前後かかります。CIで毎回回すなら単一障害に絞り、組み合わせは定期実行に回すのが現実的です。障害注入の実験そのものの設計は[カオステストの設計とツール選定](/tech/details/16269/)で扱っています。

## pytestによるCI/CDへの組み込みと危険な設定変更の検出

`pybatfish.client.asserts` には、検査に失敗すると例外を投げる関数がそろっています。[pytest](/tech/details/7527/)から呼べば、設定リポジトリへのプルリクエストごとに検査を回せます。

```
import os
import pytest
from pybatfish.client.session import Session
from pybatfish.datamodel.flow import HeaderConstraints
from pybatfish.client.asserts import (
    assert_filter_denies,
    assert_filter_permits,
    assert_filter_has_no_unreachable_lines,
    assert_no_undefined_references,
)

SNAPSHOT_DIR = os.environ.get("SNAPSHOT_DIR", "cand")


@pytest.fixture(scope="module")
def bf():
    s = Session(host=os.environ.get("BATFISH_HOST", "localhost"))
    s.set_network("edge-ci")
    s.init_snapshot(SNAPSHOT_DIR, name="candidate", overwrite=True)
    status = s.q.fileParseStatus().answer().frame()
    assert not status.empty and (status["Status"] == "PASSED").all(), status
    issues = s.q.initIssues().answer().frame()
    assert issues.empty, issues
    return s


def test_no_undefined_references(bf):
    assert_no_undefined_references(session=bf)


def test_ssh_from_internet_is_denied(bf):
    ssh = HeaderConstraints(srcIps="0.0.0.0/0 \\ 198.51.100.0/24",
                            dstIps="192.168.10.10", applications=["ssh"])
    assert_filter_denies("INBOUND", ssh, session=bf)


def test_ssh_from_admin_is_permitted(bf):
    ssh = HeaderConstraints(srcIps="198.51.100.0/24",
                            dstIps="192.168.10.10", applications=["ssh"])
    assert_filter_permits("INBOUND", ssh, session=bf)


def test_acl_has_no_dead_lines(bf):
    assert_filter_has_no_unreachable_lines("INBOUND", session=bf)
```

fixtureの中で、全ファイルのパース成功と `initIssues` が空であることを先に確かめています。解釈できない行を残したままACLの検査だけが通る事態を防ぐためです。SSHの拒否を確かめる送信元は `0.0.0.0/0 \ 198.51.100.0/24` と書き、管理用サブネットを除いています。`\` は集合の差を表す指定子の記法です。

3つの設定に対して実行した結果は次のとおりです。元の設定（base）は管理者のSSHが通らず、変更案（cand）はインターネットからのSSHが通るため、どちらも `2 failed, 2 passed` で終了コード1になります。管理用の許可行を `deny` より上へ移し、重複した443番の行を消した次の設定だけが `4 passed` になります。

```
ip access-list extended INBOUND
 permit tcp any any eq 443
 permit tcp 198.51.100.0 0.0.0.255 host 192.168.10.10 eq 22
 deny   ip any any
```

失敗時に投げられるのは `BatfishAssertException` で、Pythonの `AssertionError` を継承していません。自前のコードで `except AssertionError` と書いても捕まらない点に注意してください。pytestは例外の種類を問わず失敗として扱います。

GitHub Actionsでは、Batfishをサービスコンテナとして起動し、APIが応答してからテストを流します。プルリクエストごとに走らせる指定（`on: pull_request`）の書き方は[GitHub Actionsのトリガー（on）](/tech/details/16923/)で詳しく説明しています。リポジトリには、上のテストを `tests/test_edge.py`、変更案の設定を `snapshots/candidate/configs/`、ワークフローを `.github/workflows/batfish.yml` として置く想定です。

```
name: batfish-check
on: pull_request
jobs:
  verify:
    runs-on: ubuntu-latest
    services:
      batfish:
        image: batfish/batfish:2026.08.27.3685
        ports:
          - 9996:9996
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"
      - run: pip install pybatfish==2026.9.17.3748 pytest
      - name: wait for batfish
        timeout-minutes: 2
        run: until curl -sf --max-time 5 http://localhost:9996/v2/version; do sleep 2; done
      - run: pytest -q tests/test_edge.py
        env:
          SNAPSHOT_DIR: snapshots/candidate
```

Ansibleから呼ぶための公式ロール `batfish.base` もありましたが、リポジトリ `batfish/ansible` は2023年8月22日にアーカイブされ、読み取り専用になっています。新しく組むなら、上のようにpybatfishをテストコードから直接呼ぶ形にしてください。ネットワーク設定のリポジトリにテストとパイプラインを組み込む作業を外部に任せる場合は、[DevOps・CI/CD導入支援](/service/system/devops/)も相談先になります。

## Batfishの適用限界と導入前の判定基準

Batfishは設定の論理を検査する道具で、ネットワークが実際に動くことを保証するものではありません。次の用途には使わないでください。

- 帯域・遅延・輻輳の見積もり。トラフィック量は計算に入らない
- 障害からの復旧時間の見積もり。`fork_snapshot` の結果は収束し終えた後の状態で、BGPやOSPFが再収束するまでの秒数は出ない
- 機器のソフトウェア不具合やハードウェア故障の検出。Batfishはベンダーの仕様どおりに動く前提で計算する
- 未対応の構文が多い機器の検査。Batfishは解釈できない行を無視することがあるため、その行が効いている前提の結論は誤りになる

未対応構文の見落としを防ぐため、CIに組み込むときは、`initIssues` が空であること、`fileParseStatus` がすべてPASSEDであることを最初のテストにしてください。これが通らない機器の結果は、合格でも信用しないという線を先に決めておきます。実ネットワークを載せるサーバは、公式推奨の32GB以上のメモリで用意してください。

MCPサーバ経由でAIエージェントに調査させる使い方も始まっていますが、v2026.08.27の時点ではベータで、ツール名と引数が変わる可能性があると明記されています。CIの合否判定のように結果を固定したい用途は、pybatfishのAPIで書くほうが安全です。

## よくある質問

### Batfishは無料で使えますか？

無料で使えます。Apache License 2.0のOSSで、GitHubの `batfish/batfish` でソースと、リリースノートを公開しています。商用利用も可能です。

### BatfishのGitHubリポジトリはどこですか？

サーバ本体は `github.com/batfish/batfish`、Pythonクライアントは `github.com/batfish/pybatfish` です。最新リリースはv2026.08.27で、サンプルのノートブックはpybatfish側の `jupyter_notebooks` にあります。

### Cisco機器の設定はそのまま読み込めますか？

読み込めます。IOS、IOS-XE、IOS-XR、NX-OS、ASAの `show running-config` の出力を `configs/` に置けば、ベンダーは自動判定されます。NX-OSは可能なら `show run all` の出力を渡すよう公式ドキュメントが推奨しています。

### Apple SiliconのMacでDockerイメージは動きますか？

動きます。v2026.08.27から `batfish/batfish` と `batfish/allinone` はlinux/arm64のイメージも配布しており、エミュレーションを介さずに動作します。

### pybatfishはどのPythonのバージョンで動きますか？

pybatfish 2026.9.17.3748はPython 3.10以上が必要で、3.10から3.14までが対応版として記載されています。サーバとpybatfishは同じ時期の版にそろえてください。

## 関連記事

- [NetBoxとは？IPAM・DCIMを一元管理するOSSの機能と使い方【v4.7対応】](/tech/details/3571/)
- [Ansibleとは？エージェントレスの仕組みとメリット・デメリット、Red Hat AAPの制約](/tech/details/6533/)
- [pytestの使い方と書き方｜fixture・parametrize・docstringの扱いまで【9.1対応】](/tech/details/7527/)
- [IOS-XRとは？Cisco IOS・IOS-XEとの違いと対応機器・最新版26.3.1【2026年10月】](/tech/details/5187/)
- [カオステストとは？障害注入の実験設計とツール選定・本番実施の判断を実装者向けに解説](/tech/details/16269/)

---

出典: [Batfishとは？ネットワーク設定を本番前に検証するOSSの仕組みと使い方](<https://www.issoh.co.jp/tech/details/5185/>)（株式会社一創）
