ゼロトラストリンク esp32 iot AI

ロリポップ!ゼロトラストリンクがLinuxとESP32に対応したのでStackChanで試してみる

ゼロトラストリンク esp32 iot AI

人とAIを安全につなぐGMOペパボの新サービス「ロリポップ!ゼロトラストリンク」の、Linux版とESP32版クライアントを公開しました。

これまでクライアントがあったのはmacOS、Windows、iOS、Androidで、どれも人が手で触る端末です。今回のLinux版とESP32版のリリースで、サーバとマイコンが加わりました。

  1. PCからサーバ、IoTデバイスまで
  2. これまでのIoTのつなぎ方との違い
  3. StackChanに喋らせる
  4. ESP32版の使い方
    1. 依存を追加する
    2. 接続情報を用意する
    3. つなぐ
    4. 通信する
  5. AIエージェントクラウドとつなぐ
  6. おわりに

PCからサーバ、IoTデバイスまで

ロリポップ!ゼロトラストリンク(以下、ZTL)は、離れた場所にある機器同士をポート公開なしで直接つなぐサービスです。参加した機器にはネットワーク内だけで通用するIPアドレスが振られ、そのアドレスで相手に届きます。

Linux版はサーバやコンテナ用で、systemdで常駐させます。root権限もカーネルのTUNデバイスも使わないモードがあるので、権限を絞ったコンテナの中でも動かせます。

ESP32版はESP-IDFのコンポーネントです。後述する通り、既存のファームウェアに依存を1つ足して関数を3つ呼ぶと、マイコン自身がネットワークに参加します。センサーやロボットにESP32が載ってさえいれば、開発機や本番サーバと対等に通信できます。なお、ESP32版は実験的な位置づけでのリリースです。

これまでのIoTのつなぎ方との違い

IoTデバイスとクラウドのやり取りには、向きが2つあります。デバイスがクラウドのサービスを呼ぶ向きと、外からデバイスへ指示を送る向きです。

前者の定番は、クラウド側のサービスをインターネットへ公開し、デバイスに持たせたAPIキーで認証する構成です。世界中から届く窓口を開けておいて、キーの強さだけで守ることになります。後者は、NATの内側のデバイスに外から直接は届かないので、双方がMQTTのようなブローカーへ接続しておき、そこ経由で指示を押し込むのが定番です。どちらの向きでも、インターネットへの公開か、中継サービスの追加が要ります。

ゼロトラストリンクではデバイス自身がネットワークに参加するので、どちらの向きもアドレス指定の直接通信になります。デバイスはNATの内側のまま、ネットワーク上の他のノードから届くようになります。クラウド側のサービスも、インターネットに公開しておく必要がなくなります。

アクセス許可の単位は通信元のアドレスではなく、認証されたデバイスです。ZTLのダッシュボードからデバイスを一元管理できます。

StackChanに喋らせる

Linux版とESP32版のクライアントの利用例として、StackChanを題材に説明します。StackChanとは、オープンソース発の卓上ロボットで、今回はM5Stackの公式キット(型番K151)を組み立てて使いました。制御部はキット同梱のM5Stack CoreS3で、ファームウェアはここに書き込みます。ソースはkentaro/rino-stackchanにあります。

入出力だけのデバイスと、トンネル越しのホストの構成図

StackChanを上図の構成で動かします。StackChanは話しかけられた声をマイクで拾ってZTLのESP32版クライアントを通じてネットワークに流し、返ってきたAIエージェントによる発話音声を鳴らします。音声認識、発話内容の生成、音声合成をするサーバはネットワークの向こう側にあって、ZTLのLinux版クライアントを通じてつながっています。

ESP32版の使い方

ESP32版の使い方について、より詳しく見ていきましょう。

依存を追加する

idf.py add-dependency "gmo-pepabo/lolipop-ztl"

導入は簡単で、上記の通りでlolipop-ztlコンポーネントとその下で動く通信の実装が入ります。コンポーネントはESP Component Registryで公開しています。

接続情報を用意する

ZTLネットワークへ参加するには、デバイスごとの接続情報が必要です。デバイス認証フロー(RFC 8628のOAuth 2.0 Device Authorization Grant)を通じて接続情報を入手します。テレビでサブスクにログインするとき、画面のコードをスマホで入力するあの方式です。

デバイスは起動するとまず保存済みの接続情報を探します。なければサービスへコードを要求し、受け取ったコードと承認URLを画面に出します。ユーザーがブラウザの承認ページでコードを入力すると、ポーリングしていたデバイスに接続情報が発行され、デバイスはそれを保存して接続します。

デバイス認証フローのシーケンス図

コードにするとこうなります。

ztl_config_t cfg;

if (!ztl_config_resolve(&cfg)) {
    // 初回起動。接続情報がまだ無いのでデバイス認証を実行する。
    // 起動直後は回線が未確立なことがあるため、成功するまで繰り返す
    while (!run_device_auth(&cfg)) {
        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}

ztl_config_resolveが保存済みの接続情報を読みます。2回目以降の起動はこれだけです。

初回だけztl_device_authを呼びます。コードの表示手段はデバイスごとに違うので、そこはコールバックでホストアプリに任される作りです。

static void device_auth_prompt(const char *user_code,
                               const char *verification_uri,
                               const char *verification_uri_complete,
                               void *cb_arg) {
    const char *url = verification_uri_complete[0] ? verification_uri_complete
                                                    : verification_uri;
    face_show_auth_code(user_code, url);   // 画面にQRコードとコードを出す
}

static bool run_device_auth(ztl_config_t *cfg) {
    esp_err_t err = ztl_device_auth("rino-stackchan", "esp32 (ESP-IDF v5.4)",
                                    device_auth_prompt, NULL, cfg);
    face_hide_auth_code();
    return err == ESP_OK;
}

StackChanには画面があるので、今回作成した例では、デバイス承認のためのURLのQRコードと確認用コードを画面に表示しました。ユーザーがそれをスマホで読んで承認する流れです。画面のないデバイスなら、シリアルログにコードを出すことになります。

つなぐ

ztl_callbacks_t cbs = { .state_cb = on_state_change };
ztl_t *ztl = ztl_connect(&cfg, &cbs);

state_cbが状態変化のコールバックです。StackChanを用いた今回の例では、接続中や再接続中を画面のステータス行に出しています。

通信する

つながったら、相手のアドレスを指定して通信します。

// TCP
ztl_tcp_socket_t *sock = ztl_tcp_connect(ztl, host_ip, port, 15000);

// UDP
ztl_udp_socket_t *usock = ztl_udp_create(ztl, 9000);
ztl_udp_set_rx_callback(usock, on_udp_rx, NULL);

トンネルを通るのは、このztl_*のAPIで書いた通信だけです。ふだんのlwIPソケットは従来どおりの経路に出るため、esp_http_clientもトンネル越しには使えません。StackChanが叩く先はどれもHTTPなので、このTCPのAPIの上に、ヘッダを組んでContent-Lengthぶん読み切るだけの小さなクライアントを書きました。

UDPは、ネットワーク上の他のノードから話しかけるための口として開けています。

echo "こんにちは、調子はどう?" | nc -u <デバイスのアドレス> 9000

これをMacから打つと、返事が声で返ってきます。

コードの全体はkentaro/rino-stackchanにあります。CoreS3でつまずいた設定(PSRAMやTLSのバッファ、マイクとスピーカーがI2Sのクロックを共有する件)もREADMEに書いてあります。

AIエージェントクラウドとつなぐ

StackChanのAIとしての本体は、ロリポップ!AIエージェントクラウドで動いているHermes Agentです。ロリポップ!AIエージェントクラウドは、OpenClawやHermes Agentをクラウド上で動かせるサービスです。クラウドでの常時稼働と安全性を両立するために、外部にポートを公開せず、操作はWebの管理画面かSlackやDiscordなどのチャットに限っています。

そこでZTLの出番です。筆者は、Hermes Agentの動く環境にLinux版クライアントを入れて、ZTLのネットワークへ参加させています。参加すると、環境の中で127.0.0.1に閉じたままのチャットAPIや音声合成が、同じネットワークのStackChanからはアドレス指定で見えるようになります。どこにもポートを開けないまま、机の上のロボットとクラウドのAIが会話しています。

おわりに

Linux版とESP32版で、「ロリポップ!ゼロトラストリンク」はPCやモバイルだけでなく、サーバからIoTデバイスまで使えるようになりました。ESP32版の導入は簡単です。手元にESP32があれば、ぜひ何かひとつZTLのネットワークを通じてつないでみてください。