はじめに
瀧澤です。
前回、EgressViewという、自宅・SOHOネットワークの外向き通信を可視化するOSSを作った記事を書きました。もともとYamaha RTX専用でしたが、前回の記事の公開後、実際の利用者から「自分でコードに手を入れてCiscoルーターで動かした」という報告をいただきました。ユーザーがフォークして自力対応するほどの需要があるなら、こちらで安全に使える形を提供すべきだと思い、Cisco IOSに正式対応しました。
さらに、Ciscoを利用するSOHOで想定される冗長化や複数回線にも備えるため、Yamaha RTXとCisco IOSを任意に組み合わせて最大10台まで登録できるようにしました。
- GitHub: https://github.com/yo1t/egressview
- Project Page: https://yo1t.github.io/egressview/
- Release v1.3.5: https://github.com/yo1t/egressview/releases/tag/v1.3.5
この記事は「Yamaha 対応のツールを Cisco に広げるとき、何が流用でき、何に留意が必要だったか」の記録です。結論から書くと、新たに必要な Cisco 固有コードは全体の5%弱でした。
できるようになったこと
同じグラフマップ・統計・通信ログを、Cisco の NAT テーブル由来の通信に対しても使えます。宛先ホスト名・組織名・国・脅威フィード照合(Feodo Tracker / ThreatFox / URLhaus / Spamhaus DROP)、MCP 経由の AI エージェント連携も従来どおりです。
| 構成 | 動作 |
|---|---|
| Yamaha RTX のみ | 従来どおり |
| Cisco IOS のみ | NAT変換テーブルから同等の可視化 |
| Yamaha + Cisco 併用 | 同じ通信を重複計上せず、観測した全ルーターを保持 |
| Cisco複数台 / Yamaha複数台 | 同一メーカーを含め、任意の組み合わせで最大10台 |
設定画面もこの通りです。
流用できたもの・書いたもの
| 分類 | 規模(概算) | Cisco対応での扱い |
|---|---|---|
| 保存層(SQLite、履歴、集計) | 大 | そのまま流用 |
| エンリッチ(逆引きDNS、RDAP組織名、Geo) | 大 | そのまま流用 |
| 脅威フィード照合 | 中 | そのまま流用 |
| Web UI(グラフマップ、統計、通信ログ、端末一覧) | 大 | そのまま流用 |
| MCP サーバー / Slack 通知・検出ログ | 中 | そのまま流用 |
| Cisco 固有コード(SSH対話、パーサー、アダプタ) | 841行 | 新規 |
Cisco 専用モジュールは841行で、JavaScript 全体(テスト除く18,105行)の**約4.6%**です。この比率は専用モジュールの規模であり、既存ファイルへの変更量を含む流用率ではありません。それでも、保存・分析・可視化を作り直さずに済みました。
もともと、「ルーター」を抽象化してあったのが良かったと考えています。EgressView の内部では、ルーターは「定期的に接続レコードの配列を返すもの」でしかありません。
// ルーターアダプタが返す共通の接続レコード(概念図)
{ src: '192.168.1.42', dst: '203.0.113.10', dport: 443, proto: 'tcp', srcMac: 'aa:bb:cc:11:02:00' }
この形に変換するアダプタさえ書けば、その先は機種を知らないまま動きます。「対応機種を増やすかもしれないなら、1機種目のうちにデータの正規形と境界を決めておく」——今回いちばん効いた設計判断は、Cisco 対応前に済んでいました。
ただし機種の抽象化だけでは台数を増やせません。kind: cisco という種類と routerId: cisco1 という個体を分離し、scheduler が個体ごとに独立ポーリングすることで、cisco1 / cisco2 / yamaha1 のような構成を特別扱いなしで表現しています。複数ルーターが同じ通信を観測した場合は、通信は自然キー (src, dst, dport, proto) で1件のまま、観測元だけを connection_observations テーブルに routerId 単位で保持します。
留意点 — CLI の出力の仕方
設計レベルの困難ではなく「出力の仕方」への対処でした。対処自体は数行ですが、どれも失敗が沈黙する(エラーにならず、止まる・0件になる)ため、注意深い対応が必要です。
| # | Yamaha との違い | 症状 | 対処 |
|---|---|---|---|
| 1 |
--More-- ページャー |
出力が途中で止まりタイムアウト | 冒頭で terminal length 0 + --More-- 検出時にスペース送信 |
| 2 |
enable の Password: 応答 |
プロンプト待ち正規表現に永遠にマッチしない | 「何を待つか」を差し替え可能にし、Password: とプロンプト両方を待つ |
| 3 | enable 失敗時も一部コマンドは通る | 接続は「成功」なのにデータが永遠に0件 |
# への遷移を検証し、% Invalid input をエラー扱い |
| 4 | MAC 表記が aabb.cc11.0200 形式 |
同じ端末が二重登録される | パーサー入口でコロン区切りへ正規化 |
| 5 | ポーリングループが複数になる | 差分配信の漏れ・二重カウント | 個体ごとに状態を分離し、観測元を junction table に保存 |
なお、2と3は privilege 15 のユーザーでテストしていると気づけません。ログイン直後から特権モードに着地し、enable の分岐が一度も実行されないためです。「手元では動くのにユーザーの環境では動かない」の典型でした。
留意点 — NATテーブルの細かな差
同じ「NAT テーブル」でも、載っている情報が違います。
| 情報 | Yamaha RTX | Cisco IOS |
|---|---|---|
| コマンド | show nat descriptor address N detail |
show ip nat translations |
| プロトコル | TCP / UDP / ICMP / GRE | TCP / UDP / ICMP |
| LAN側IP・ポート | あり | Inside local としてあり |
| 通信先IP・ポート | あり | Outside global としてあり |
| セッションの残存時間 | 実測TTLを取得 |
verbose の left から取得(未対応機は既定値) |
| セッション生成時刻 | なし | verbose の create から取得 |
Cisco は inside/outside × local/global の「NAT の前後」を軸にした語彙、Yamaha は「LAN 側とその通信先」という素直な形で代わりに実測 TTL を持つ——情報の多寡ではなく軸が違うのです。アダプタの仕事は形式変換だけでなく「共通の正規形に何を残すか」の判断で、EgressView では「どの端末がどこへ出ているか」を核にし、片方にしかない情報はオプション扱いにしました。共通レコードを両機種の和集合にすると、3機種目でまた壊れるからです。
この違いは機種固有の強みにもなります。show ip nat translations verbose の create / left を取り込み、Cisco 版ではセッション開始時刻を NAT エントリ生成の推定時刻まで遡れるようになり、TTL も既定値から実測値になりました。verbose 未対応機は自動フォールバックし、Yamaha 側のコードには一切手を入れていません。
3機種目を見据えたリファクタリング
対応作業と同時に、次の機種を簡単に追加できるように4つの改善を行いました。
① アダプタ契約のコード化 — 実装すべき22メソッドを契約として定義し、起動時に検証。3機種目の実装漏れは起動した瞬間にエラーメッセージ付きでわかります。
// router-interface.js(抜粋)
const REQUIRED_METHODS = ['configure', 'connect', 'fetchSessions', /* ...計22 */];
function validateRouterPoller(adapter) {
for (const name of REQUIRED_METHODS) {
if (typeof adapter[name] !== 'function') {
throw new TypeError(`adapter "${adapter.kind}" is missing method: ${name}`);
}
}
}
② SSH 対話ロジックの状態機械(ステートマシン)化 — enable 遷移は SSH コールバックに埋まった「実機がないとテストできない」コードでした。I/O を注入できる純粋な状態遷移関数に切り出し、「enable パスワードが間違っていた場合」のような再現しにくいケースもユニットテストで固定しています。
③ ポーリング状態のポーラー単位分離 — 留意点5(差分配信の漏れ・二重カウント)の恒久対策。「前回配信時刻」などをポーラーごとに持ち、観測点が増えてもループ同士が干渉しません。
④ 負荷と障害分離をゲート化 — fake router を Yamaha 5台 + Cisco 5台・各1,000セッションで動かす自動試験を追加。同時実行は最大3、共通100通信の重複排除でユニーク9,100件、1台を意図的に落としても残り9台の9,000セッションを収集できることを CI で確認します。あわせて履歴を「SQLite に全件・メモリに新しい100,000件」の二層にし、実データ216,582件で常駐メモリを約995MB→約604MB(約39%減)にしました。
結果はテストの数字に出ています: ユニットテスト1,132件(うち Cisco 関連64件) + Playwright E2E。特に②により「実機がないと確かめられない」領域を対話の外周だけに追い込めたことが、品質面のいちばんの成果です。
それでも自動化できないもの
実機での接続確認(SSH の実挙動、実機の NAT 出力形式、ホスト鍵の初回受け入れ)だけは手動で残ります。用意できた実機は Cisco 1台と Yamaha 1台。補足試験として各実機を異なる routerId で2重登録して4個体として並列収集し、同じ Cisco 通信17件・Yamaha 通信178件が両 routerId から観測され、観測元を失わず重複排除できることを確認しました。ただしこれは SSH 並列処理・個体別状態・重複排除の確認であり、異なる物理機器2台の構成や HSRP/VRRP、実フェイルオーバーを検証したものではありません。
ここは対応機種を増やすほど重くなるので、「この機種・この IOS で動いた/動かなかった」という動作報告をもらえれば、と思います。
試し方
| 項目 | 内容 |
|---|---|
| EgressView | v1.3.5(GitHub Release) |
| Node.js | 22 以上 |
| ルーター側 | SSH ログイン + show ip nat translations が実行できること |
| 権限 | privilege 15 のユーザー、または enable パスワード(設定画面に入力欄あり) |
| 動作確認済み環境 | Cisco C841M-4X-JSEC/K9 / IOS 15.5(3)M9 |
詳細手順は Cisco IOS — SSH 設定ガイドにあります。
git clone https://github.com/yo1t/egressview.git
cd egressview
npm install
npm start
ルーターが手元になくてもデモモードで UI を確認できます。
DEMO_MODE=true DEMO_ADMIN_TOKEN=my-token npm start
「この機種・この IOS バージョンで動いた/動かなかった」の報告だけでも大変ありがたいです。Issue・PR も歓迎です。
参考
- ssh2(Node.js の SSH クライアント): https://github.com/mscdex/ssh2
- Netmiko(Python のネットワーク機器自動化。CLI の違いの対処の実装が参考になります): https://github.com/ktbyers/netmiko
- 前回記事: Yamaha RTX配下の通信をWebで可視化し、MCPでAIエージェントからアクセスできるようにした話
おわりに
2機種目の対応は、1機種目で対応した抽象化の答え合わせのようなものでした。データの正規形と境界を先に決めてあったおかげで、Cisco 対応の中心は「CLI の出力差異を吸収するアダプタを書く」ことに収まり、専用コードは全体の5%弱となりました。
さらに、将来の対応機種追加と冗長構成を見据え、最大10台のルーターを共通のschedulerとregistryで扱えるようにしました。機種固有の差はアダプタの内側へ閉じ込め、保存・分析・可視化は共通化する。この境界を保てるかぎり、3機種目以降も同じ設計で追加していけると考えています。
免責
こちらの内容にもとづく環境の構築は、自己の責任でお願いします。各種サービス、AIクライアント、APIを利用する場合は、それぞれの利用条件に従ってください。また、ここに記載の全ての内容については個人の意見であり、所属する企業や団体とは関係ありません。
商標について
Cisco、Cisco IOS、Yamaha などの製品名は、各社の商標または登録商標です。EgressView はこれらの企業と提携・承認・後援関係にありません。

