
2026/03/09 21:38
**UniFi Inform プロトコルの逆解析**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
UniFi は、各デバイスの inform トラフィックを MAC アドレスに基づいてルーティングすることで、単一の VPS を収益性の高いマルチテナントサービスへと変換できます。軽量な Go プロキシ(約 200 行)は、inform パケットの最初の 40 バイトを読み取り、MAC アドレス(オフセット 8–13)を抽出し、事前登録または IP 範囲照合で構成されたインメモリテーブルからテナントを検索し、パケット全体を変更せずに適切なバックエンドコントローラへ転送します。バイト 42 の後のペイロードは AES‑128‑CBC 暗号化されたままであるため、プロキシはデータを復号したりデータベースを照会したりする必要がなく、単にトラフィックをルーティングします。
各テナントには引き続き専用の UniFi コントローラインスタンスが提供され、独自のデータベースと分離されたポートバインディング(8443 HTTPS、3478 UDP STUN、6789 TCP スピードテスト、27117 TCP MongoDB、10001 UDP L2 ディスカバリ)が割り当てられます。初期状態ではデバイスはデフォルトキーを使用しますが、採用後には各デバイスにユニークなキーが発行されます。
デバイスが採用され
set‑inform http://tenant.subdomain/inform で構成された後は、通常の Host ヘッダーによるルーティングでプロキシを完全に置き換えることができ、設定を簡素化します。この手法は、多数のテナント間でインフラストラクチャを共有しつつ、完全な分離とセキュリティを維持したまま、低マージン(顧客あたり 4–6 USD の VPS)運用を収益性の高いモデルへと転換します。
本文
数年前、私は小規模な UniFi ホスティングサービスを運営していました。
自社でサーバーを立てたくない MSP や IT ショップ向けに、クラウド上のコントローラーを管理・提供していたわけです。
各顧客には専用 VPS が割り当てられ、その VPS で専用コントローラーが稼働していました。
サービスは需要がありました。人々はハードウェアやポート転送、バックアップの手間を省くためにホスト型コントローラーを望んでいたのです。しかし経済面で問題が発生しました。
顧客ごとに VPS が必要だったので、DigitalOcean のドロップレットは月 4〜6 ドル、私はそれに対して 7〜8 ドルを請求していました。つまり顧客ひとりあたりの利益は 1〜2 ドルしかありませんでしたし、サポートリクエストが入るたびにマージンはゼロになってしまいます。結局ボランティア的な状態だったわけです。
明らかな解決策はマルチテナント化です。すべての顧客に専用 VM を与える代わりに、複数のコントローラーを共有インフラ上で稼働させること。しかし UniFi コントローラーはマルチテナント設計になっていません。各インスタンスは独自のデータベースとポートバインディングを持つ完全に分離された環境です。そのため、受信トラフィックを見てどの顧客に属するか判別できるルーティング層が必要になります。
Web UI(ポート 8443)は簡単です。リバースプロキシでサブドメインを割り当てれば十分です。
しかし、実際に面白くなるのは inform プロトコル(ポート 8080)です。
inform が何をしているか
UniFi デバイス(アクセスポイント・スイッチ・ゲートウェイ)は 10 秒ごとに HTTP POST を使って自分のコントローラーへ「電話」をします。
この通信でデバイス統計、設定同期、ファームウェアバージョン、クライアント数などを管理しています。
ペイロードは AES‑128‑CBC で暗号化されています。そのため、トラフィックを有効に扱うには各デバイスごとの鍵が必要だと考えられます。つまりコントローラーのデータベース(1 つのインスタンス)にアクセスしなければならないということです。
そこで私は生のバイト列を調べました。
パケット構造
| オフセット | サイズ | フィールド |
|---|---|---|
| 0 | 4B | Magic: “TNBU” (0x544E4255) |
| 4 | 4B | Packet version(現在は 0) |
| 8 | 6B | デバイス MAC アドレス |
| 14 | 2B | Flags(暗号化・圧縮等) |
| 16 | 2B | AES IV 長さ |
| 18 | 16B | AES IV |
| 34 | 4B | Data version |
| 38 | 4B | Payload length |
| 42+ | 可変 | 暗号化ペイロード(AES‑128‑CBC) |
オフセット 8 はデバイスの MAC アドレスで、完全に暗号化されていません。
実際のパケットは次のようになります。
54 4E 42 55 # Magic: "TNBU" 00 00 00 00 # Version: 0 FC EC DA A1 # MAC: fc:ec:da:a1:b2:c3 B2 C3 01 00 # Flags ...
“TNBU” は単に “UBNT” を逆さにしたもので、Ubiquiti のティッカーシンボルであり、同社デバイスのデフォルト SSH 資格情報でもあります。
MAC アドレスがヘッダーに入っている理由は、コントローラーが暗号化を解読する前にどのデバイスかを特定する必要があるためです。鍵は採用時に割り当てられるので、正しいキーを取得するにはまず「誰が話しているか」を知る必要があります。これはセキュリティ上の欠陥ではなく実務上の要件です。しかしこれだけで inform トラフィックを暗号化を触れずにルーティングできます。
MAC の読み取り
抽出はほぼ何も書かないコードです:
header := make([]byte, 40) if _, err := io.ReadFull(conn, header); err != nil { return err } if string(header[0:4]) != "TNBU" { return fmt.Errorf("not an inform packet") } mac := fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", header[8], header[9], header[10], header[11], header[12], header[13])
14 バイトを読むだけで、どのデバイスが通信しているか分かります。暗号化は不要です。
プロキシ構築
MAC を取得したらルーティングは簡単です。
「どの MAC がどのテナントに属するか」の表を保持し、その表で一致した後、ヘッダー+暗号化ペイロード全体をそのまま正しいバックエンドへ転送します。
デバイス (MAC: aa:bb:cc:dd:ee:ff) | v +-----------------------------------+ | | | Inform Proxy | | | | Read MAC from bytes 8‑13 | | | | Lookup: | | aa:bb:cc:... -> tenant-7 | | 11:22:33:... -> tenant-3 | | fe:dc:ba:... -> tenant-12 | | | | Forward to correct backend | +-----------------------------------+ | | | v v v Tenant 7 Tenant 3 Tenant 12
全体で約200行程度の Go コードで、MAC→テナントマップをメモリに保持するだけです。
マップへの登録方法
では MAC をどうやってその表に入れるのでしょうか?
デバイスに SSH して
set-inform http://acme.tamarack.cloud:8080/inform を実行すれば、プロキシは不要です。Host ヘッダーで通常の Ingress がルーティングを担当します。これは既存デバイスをホスト型コントローラーに移行する際に主に使われます。
プロキシが必要になるケースは、DHCP オプション 43 によってデバイスがコントローラーを発見するときです。この場合、デバイスは IP アドレスしか受け取らず、ホスト名や Host ヘッダーもありません。HTTP リクエストにテナントを識別する情報は inform ペイロードの MAC だけです。
対策として次の2つがあります:
- テナントがデバイスをオンラインにする前に MAC アドレスを事前登録させる、または
- ソース IP 範囲(例: オフィスのパブリック IP)を追加し、既知の範囲から inform してきたデバイスをマッチングできるようにする。
採用後は、デバイスをテナント専用サブドメインへ
set-inform すれば、Host ヘッダーによるルーティングが機能します。プロキシは初期採用と工場出荷時リセットのみに使用されます。
他のポート
inform が最も難しいですが、残りのコントローラーのポートは比較的単純です:
| ポート | プロトコル | 目的 |
|---|---|---|
| 8080 | TCP/HTTP | Inform(デバイス電話) |
| 8443 | TCP/HTTPS | Web UI と API |
| 3478 | UDP | STUN |
| 6789 | TCP | スピードテスト(内部) |
| 27117 | TCP | MongoDB(内部) |
| 10001 | UDP | L2 発見(ローカル専用) |
8443 は Web UI ですので、サブドメインごとの標準 HTTPS Ingress で十分です。
3478 の STUN はステートレスなので、単一の共有 coturn インスタンスが全テナントをカバーします。
その他はコンテナ内または L2 限定のためホスト外に出ることはありません。
暗号化ペイロード内部
興味深い点として、42 バイト以降は AES‑128‑CBC で暗号化されています。採用直後のデバイスは Ubiquiti が公開しているデフォルトキー(
ba86f2bbe107c7c57eb5f2690775c712)を使用します。このキーはコントローラーのソースコードに組み込まれています。採用後はコントローラーが各デバイス専用キーを割り当てます。
暗号化されたペイロードにはデバイス統計や設定データが入っています。コントローラ開発者なら興味深い情報ですが、ルーティング目的では無意味です。
何が得られるか?
テナントごとに専用のコントローラーを維持しつつ、顧客一人あたり VPS を割り当てる必要はなくなります。1〜2 ドルのマージンでボランティアだった運営を、実際に利益を上げられるビジネスへと変えることができます。
もし MAC が暗号化ペイロード内にあったなら、プロキシレイヤーでデバイスキーを管理する必要があり、その結果再び「顧客ごとのインスタンス」になってしまいます。14 バイトのプレーンテキストがパケットヘッダーにあるだけで、全体を実現できるわけです。
Ubiquiti がこのように設計したのは第三者が構築しやすくするためではなく、暗号化前にデバイスを特定する必要性から来ています。しかしその副作用として、inform プロトコルは「TCP 接続の 14 バイトだけ読めれば」誰でもルーティング可能になっています。
もし inform プロトコルを実際に調査したことがあれば、ぜひ教えてください。
[info@tamarack.cloud]