家のMatter機器——Nanoleafの照明やスマートプラグ——は、僕がつくってGitHubで公開しているCLI mat で動かしています。

mat on --node 5                # 照明をつける
mat off --node 5 --endpoint 2  # 特定のエンドポイントだけ消す
mat color --node 5 --hue 300 --sat 80  # 色を変える

全体構成の記事で「6本の中でいちばん薄くないCLI」と書いたのがこの mat です。当初は公式ツール chip-tool を子プロセスとして呼ぶだけのラッパーでした。それがいまは、TLV(Matterの全データのやり取りに使われるバイナリ形式)の読み書きからBLE経由のペアリングまでプロトコルを自前実装したRustコントローラになっています。行数にすると、コントローラ部分のcrate(crates/mat-controller/src)だけで約2万行あります。

なぜラッパーをやめてフルスクラッチしたのか。この記事は、その設計判断と、決断から8日間の実装の記録です。

第一形態:chip-toolのラッパー

Matterは、スマートホーム機器の新しい共通規格です。仕様は公開されていますが、中身はかなり重量級です。機器との通信路を作るだけで、証明書チェーンの検証、鍵交換、暗号化セッションの確立が要ります。

だから最初の mat は、プロトコルを1行も書きませんでした。Matter規格の参照実装(CHIP SDK、connectedhomeip)に同梱されている chip-tool というCLIを子プロセスとして呼び、その長大なテキストログをパースして、純粋なJSONに整形して返す。mat は「皮」に徹する設計です。

これ自体は合理的な判断だったと思っています。プロトコルの正しさは公式実装が保証してくれるし、mat は自分の関心事——stateless / one-shot / stdoutは純粋なJSON——に集中できる。実際この形で、機器のペアリングも、読み書きも、グループ制御も動いていました。

綻びはCPUメーターに出た

問題は常駐で顕在化しました。

Matterはセッション確立(CASEという鍵交換手続き)が重く、コマンドのたびに毎回払うと応答に秒単位のラグが出ます。そこで確立済みのwarmなセッションを保持する常駐デーモン matd を別バイナリで用意し、中で chip-tool をinteractive serverモードのまま温存する構成にしていました。

すると、ラズパイのCPU1コアが常時ほぼ100%に張り付きました。

原因は chip-tool 側のbusy-loopです。WebSocketサーバの実装がイベント待ちでスリープせずに回り続けるもので、上流に2023年10月からissue(connectedhomeip#29971)として報告されたまま、未修正でした。さらに180秒アイドルが続くとWebSocket接続が勝手に切れる癖もある。keepaliveの送信とidle-timeoutの短縮で緩和はしましたが、これは延命であって解決ではありません。

決定的だったのは、chip-tool の立ち位置を知ったことです。これはテスト・デバッグ用のツールであって、本番システムのバックエンドとして組み込むことは想定されていません。テキストログの特定の行(Data = ...)を拾ってJSON化する mat のパース層も、上流のバージョン更新で静かに壊れうる構造でした。

家の照明は毎日動きます。その基盤が「テストツールのログのパース」と「未修正のbusy-loop」の上に載っている。これはいただけない、となりました。

既製の選択肢を全部並べた

移行先の候補は、思いつく限り並べて比較しました。要点だけ書きます。

候補見送った理由
chip-tool をforkしてbusy-loopだけ直す延命であって移行ではない。パースの脆さと上流追従の負担が残る
CHIP SDK に直接リンクするC++デーモンを自作プロトコルの正しさは最強。ただしC++が恒久的にrepo入りし、巨大なcheckoutとビルドの税を払い続ける
matter.js を常駐サービスにHome Assistantが本番採用しており実戦量は十分。ただしNodeデーモン+プロセス間通信という、いまのchip-tool構成と同型の複雑さが残る
rust-matc(個人開発のRust実装)をforkプロトコルの核心が手書きで実在し参考になる。ただし個人プロトタイプへの依存は避けたい
rs-matter(公式のRust実装)コントローラ側の機能がまだない

それで残ったのが、Rustでフルスクラッチでした。

決め手は性能でも工数でもなく、所有性です。家が動かなくなったとき、原因のコードを全行自分で説明できる状態にしたい。それは既製のどれを選んでも手に入りません。加えて、Matterコントローラを自分で書くこと自体が面白そうだ、という趣味プロジェクトとしての選好も明示的にありました。工期より純度を取る、と決めたわけです。

見積もりは楽観的でした。必要な機能はon/off・色変更・グループ一斉制御だけ。暗号プリミティブ(P-256 / AES-CCM / SHA-256 / HKDF)はRustCryptoの既製crateを使い、自分で書くのはプロトコルの状態機械だけ。それなら6,000〜10,000行で収まるだろう、と。

移行を支えた設計:fabricへの「相乗り」

フルスクラッチで怖いのは、動くものを壊すことです。家はすでに chip-tool 経由で毎日動いています。作り直しの間、家を止めるわけにはいきません。

これを解いたのが、chip-toolのfabricに相乗りするという設計です。

Matterでは、コントローラと機器の信頼関係を「fabric」という単位で管理します。ペアリング(コミッショニング)時に証明書が機器に書き込まれ、以後その証明書を持つコントローラだけが操作できる。つまり資格情報さえ同じなら、実装が変わっても機器から見れば同じコントローラです。

chip-tool は資格情報をKVS(キーバリューストア。実体はただのファイル)に保存しています。そこからroot証明書・操作用証明書・秘密鍵・グループ鍵を読み出して、自作コントローラが同じfabricの一員として振る舞う。再ペアリング一切なしで、既存の全機器がそのまま新実装から操作できました。

もうひとつの保険は、切替の単位です。mat のJSONスキーマ・サブコマンド・exit codeは一切変えず、バックエンドをadapter1枚の差し替えにする。切替はコマンド単位のフラグ運用ではなくマイルストーン単位で行い、途中で詰んでも既存経路は無傷で撤退できる。実際、初期マイルストーンの間は chip-tool 経路が既定のまま、裏で新実装を実機検証していました。

8日間の駆け抜け

方向性を決めてspecを書いたのが7月10日。そこからマイルストーンを刻んで進めました。

  1. TLVとメッセージ層——Matterの全データが乗るバイナリ形式のcodecと、暗号化セッションの土台。相手はローカルPCで動く公式のexampleデバイス
  2. CASE——証明書ベースの鍵交換(Sigma1/2/3の3往復)を自前で喋り、unicastのread/invokeが通る
  3. KVS相乗り——実機のNanoleafに同一fabricでon/off・色変更が通る
  4. matdのin-process化——子プロセスもWebSocketも介さず、ライブラリとしてwarmセッションを保持。子プロセス・ws切断・孤児ポートという障害クラスが構造ごと消え、CPU100%もここで消滅
  5. groupcast——epoch鍵からグループ鍵を導出し、AES-CCMで暗号化してマルチキャスト送信。リビング照明7台のグループで7/7配達をE2E確認
  6. コミッショニング——PASE(SPAKE2+)、BLE経由のトランスポート(BTP)、機器証明書の検証まで自作。玄関のライトをnative実装だけでBLEペアリングし、点滅確認まで通した

決定から8日、コミット数にして50。「6,000〜10,000行で収まる」はずの見積もりは、コントローラcrateのsrcだけで約2万行(実測19,700行・25ファイル)になりました。X.509証明書チェーンの検証、mDNSでの機器発見、attestation(機器が本物かの証明書検証)——規格書の目次で1行のものが、実装では1ファイルずつ増えていく。crateのファイル一覧が、そのままMatterの重さの証明になっています。

crates/mat-controller/src/
├── tlv.rs          # バイナリcodec
├── message.rs      # メッセージ層
├── session.rs      # セッション暗号
├── case.rs         # 鍵交換 (運用時)
├── pase.rs         # 鍵交換 (ペアリング時)
├── spake2p.rs      # PASEの暗号プロトコル
├── btp.rs / ble.rs # BLEトランスポート
├── x509.rs / cert.rs / attestation.rs  # 証明書まわり
├── dnssd.rs        # mDNSでの機器発見
├── im.rs           # read/write/invoke
├── group.rs        # groupcast
├── kvs.rs          # chip-tool互換ストア
└── commissioning.rs # ペアリング一式

正直に書くと、まだ終わっていません。個別の機能は出揃いましたが、既定経路をnativeに切り替えて chip-tool を完全撤去する最終マイルストーンが残っています。137MBの chip-tool バイナリがrepoから消える日は近い、という現在地です。

おわりに

ECHONET Liteの記事で書いた enl は、UDPパケットを1発送るだけの小さなCLIで済みました。Matterはその対極で、通信路を開くだけに暗号と証明書の行列ができます。プロトコルの重さは選べません。

でも、CLIの顔つきは選べます。mat はこれだけ中身が入れ替わっても、外から見える形——one-shotで、stdoutは純粋なJSONで、状態は持たない——を一度も変えていません。プロトコルの重さはバックエンドのcrateに閉じ込めて、道具としての姿は enl と同じに保つ。UNIX哲学の分担がここでも効きました。

そして何より、毎晩リビングの照明を消しているコードの全行を、いまは自分で説明できます。趣味として最高の贅沢だと思っています。

次は、このwarmセッションを預かる常駐デーモン matd の中身や、CLI群を束ねる casa の話を書いていく予定です。自分の家が自分のコードで動いていく記録として、また覗きに来てもらえたら嬉しいです。