gmo-pepabo/lolipop-ztl

0.1.0

Latest
uploaded 7 hours ago
ロリポップ!ゼロトラストリンクの ESP32 組み込みクライアント (ts2021 + WireGuard + DERP + DISCO)

Readme (ja)

# lolipop-ztl — ロリポップ!ゼロトラストリンク ESP32 クライアント

English documentation: [README.md](README.md)

ESP32 デバイスを[ロリポップ!ゼロトラストリンク](https://ztna.lolipop.jp/)のネットワークに「1ノード」として参加させる ESP-IDF コンポーネントです。マイコン上で制御プロトコル (ts2021) + WireGuard + DERP + DISCO が動き、ロボット・IoT デバイス・センサー類を、ポート公開なしで安全にクラウドや他のノード (PC / スマホ / サーバ) と通信させられます。

ZTL 専用ファームウェアではなく**ライブラリ**です。既存の ESP-IDF アプリに依存を1つ足して関数を呼ぶだけで組み込めます。ホストアプリの責務は Wi-Fi の管理と、デバイス認証コードの表示 (画面 / シリアル / QR) だけです。

## 必要なもの

- ESP-IDF v5.0 以降 (v5.4 で検証)
- ESP32-S3 + 8MB PSRAM 推奨 (スタックチャン = M5Stack 公式キット「K151」/ 本体 M5Stack CoreS3 で動作確認)
- ゼロトラストリンクの契約 (接続情報はデバイス認証フローで自動発行されるため、キーや URL を手で用意する必要はありません)

## 使い方

接続情報の入手は**デバイス認証フロー** (OAuth 2.0 Device Authorization Grant, RFC 8628) で行います。キーや URL をデバイスに入力する作業はありません:

```
デバイス: 短いコードを表示            ユーザー: スマホ / PC のブラウザで承認ページを
          (画面 / シリアルログ)  --->           開き、コードを入力。デバイス名を目視で
                                                確認して承認
デバイス: 承認をポーリング       <---  サービスが接続情報を発行
デバイス: NVS に保存して接続 — 完了
```

```c
#include "ztl.h"

// Wi-Fi (STA) を接続した後で:
static void show_code(const char *user_code, const char *verification_uri,
                       const char *verification_uri_complete, void *arg) {
    // 表示手段はデバイス次第 (LCD / シリアルログ / QR など)
    printf("%s を開いてコード %s を入力してください\n", verification_uri, user_code);
}

ztl_config_t cfg;
if (!ztl_config_resolve(&cfg)) {
    // 初回起動: デバイス認証フローを実行。起動直後は回線が未確立な
    // ことがあるため、成功するまでリトライする
    while (ztl_device_auth("my-device", "esp32", show_code, NULL, &cfg) != ESP_OK) {
        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}
ztl_t *z = ztl_connect(&cfg, NULL);

// 以降はトンネル越しにピアと通信する
//   ztl_tcp_connect(z, ip, port, timeout_ms)
//   ztl_udp_create(z, port)
//   ztl_resolve(z, "peer-hostname")
```

- 接続情報は NVS (namespace `ztlcfg`) に保存され、以後は電源投入だけで再接続します
- トークン応答にコントローラ URL が含まれるため、手で設定するものはありません
- コントローラの Noise 公開鍵は初回接続時に `/key` から自動取得してキャッシュします
- 接続状態は `ztl_callbacks_t` の `state_cb` (`ZTL_STATE_CONNECTED`) か `ztl_is_connected()` で確認できます
- 保存済みキーが拒否された場合 (`ZTL_STATE_AUTH_FAILED`) は `ztl_device_auth` を再実行します — 同じフローが「再ログイン」経路を兼ねます
- 承認待ちを途中でやめるには別タスクから `ztl_device_auth_cancel()` を呼びます (セットアップ画面から抜ける導線など)
- `ztl_device_auth` / `ztl_connect` はスタックが PSRAM のタスクからも安全に呼べます (ESP32 はフラッシュ書き込み中に PSRAM へアクセスできないため NVS 保存はそのままでは PSRAM スタックのタスクからクラッシュしますが、コンポーネント内部で内部 RAM スタックの一時タスクへ委譲しています)
- 実装の実体は同梱のプロトコルスタック [microlink](https://github.com/pepabo/microlink) です。`ztl_*` API にない高度な機能は、同じハンドルで `microlink.h` の API を直接呼べます

Wi-Fi の確立からデバイス認証・接続・状態表示までを通しで実装した完全なサンプルは [`examples/basic`](../../examples/basic/) にあります (`idf.py build` でそのままビルドできます)。

## 処理の流れ

電源投入から定常運用までの全体像:

```
電源投入 (ホストアプリが Wi-Fi STA を確立)
  │
  ├── NVS に接続情報あり? ── あり ─────────────────────────────┐
  │                                                            │
  なし                                                         │
  │                                                            │
  ▼                                                            │
デバイス認証フロー (ztl_device_auth)                           │
  POST /api/auth/device/code ── user_code を発行               │
  コードと承認 URL をユーザーに表示 (prompt_cb)                │
  /api/auth/device/token をポーリング ── ユーザーの承認まで    │
  接続情報を NVS に保存                                        │
  │                                                            │
  ▼                                                            ▼
ztl_connect ◄──────────────────────────────────────────────────┘
  │
  コントローラの Noise 公開鍵を /key から取得
  (初回接続のみ。証明書検証つき。以後は NVS のキャッシュを使用)
  │
  ▼
制御プレーン: ts2021 Noise ハンドシェイク → ノード登録 → streaming netmap
  │
  ▼
データプレーン: DERP リレー (TLS) + DISCO 直結経路の探索
  │
  ▼
WireGuard トンネル確立 ══ ZTL_STATE_CONNECTED ══ アプリ通信 (TCP/UDP)
  │
  ├── リンク断 ──► 指数バックオフで自動再接続 (ZTL_STATE_RECONNECTING)。
  │                無限にリトライし、リンクが戻ればトンネルを復旧
  │
  └── キー失効 ──► ZTL_STATE_AUTH_FAILED: ホストアプリが ztl_device_auth を
                   再実行 (ユーザーがブラウザで新しいコードを承認)
```

## 必須の sdkconfig

```
CONFIG_ML_CTRL_TLS=y                  # コントローラは HTTPS (443) のみ公開
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y   # /key 取得時の証明書検証に必要
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y   # TLS バッファを PSRAM から確保 (下記)
CONFIG_SPIRAM=y
```

`CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC` は重要です。TLS の I/O バッファは1接続あたり約 20KB で、コントローラと DERP リレーの TLS を内部 RAM で2本張ると枯渇し、`mbedtls_ssl_setup` が失敗します (エラーは "Bad input parameters" に化けます)。

## セキュリティ

- 通常接続の TLS はサーバ証明書を検証せず、**ピン留めした Noise 公開鍵**によるハンドシェイクでコントローラの真正性を担保します (同じ制御プロトコルを 80 番平文で話す Tailscale クライアントと同じ信頼モデル。ここでの TLS は「443 しか通らない経路への適合」のためにあります)
- ピン留め前で信頼の根がない**初回の `/key` 取得だけは証明書検証必須** (証明書バンドル・リダイレクト追従なし)
- 認証キーは既定では NVS に平文で保存されます。フラッシュを読み出せる人はキーを取り出せるため、**デバイスを手放すときはダッシュボードからそのデバイスを削除(キーを失効)**してください。量産・配布するデバイスでは ESP-IDF の [flash encryption](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/security/flash-encryption.html) と [NVS 暗号化](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/storage/nvs_encryption.html)(`CONFIG_SECURE_FLASH_ENC_ENABLED=y` + `CONFIG_NVS_ENCRYPTION=y`)を有効にして、フラッシュからキーを読み出せないようにしてください
- デバイスを再セットアップに戻すときは `ztl_config_erase()` を呼びます

## 可用性・再接続

- 制御プレーンの切断 (ストリーム断・登録失敗・netmap 失敗) は指数バックオフ (1秒から倍々で上限16秒) の再接続ループに入り、**無限にリトライ**します。スタック側から諦めて止まることはありません
- DERP リレーは I/O タスクが自動再接続し、WireGuard は周期的に再ハンドシェイクします
- ノード鍵が失効した場合は保存済みの認証キーで自動再登録を試みます。ただしデバイス認証フローで発行されるキーは単回・短命のため、通常は `ZTL_STATE_AUTH_FAILED` になります — 下記の再ログイン導線で復帰してください
- 起動時に NTP に到達できなくても時刻同期はバックグラウンドで再試行を続け、時刻が入り次第 WireGuard ハンドシェイクが追従します
- **Wi-Fi の復旧はホストアプリの責務**です (コンポーネントは Wi-Fi を管理しません)。`examples/basic` のように `WIFI_EVENT_STA_DISCONNECTED` で再接続してください。リンクが戻れば上記の再接続ループがトンネルを復旧します
- 再接続の進行は `state_cb` (`ZTL_STATE_RECONNECTING` → `ZTL_STATE_CONNECTED`) で通知されます
- **認証キーが拒否された場合** (失効・無効化) は `state_cb` に `ZTL_STATE_AUTH_FAILED` が届きます。これはデスクトップ版の「再ログインボタン」に相当する合図で、ホストアプリは `ztl_device_auth` を再実行して新しいコードをユーザーに見せてください。スタック側はサーバ側でキーが再有効化された場合に備えて最大バックオフで再試行を続けます
- デバイス認証のエンドポイントへは証明書検証必須・リダイレクト追従なしでアクセスします。承認画面にはデバイスの自己申告名が表示され、ユーザーが目視照合してから承認します (フィッシング耐性)

## 動作検証環境

| | |
|---|---|
| 実機 | スタックチャン (M5Stack 公式キット「K151」)。本体は M5Stack CoreS3 (ESP32-S3、16MB Flash / 8MB PSRAM) |
| ホストアプリ | 「[スタックチャン](https://github.com/stack-chan/stack-chan)」構成の会話ロボットファームウェアに本コンポーネントを組み込んで検証 (LVGL・音声入出力と同居) |
| 接続先 | ロリポップ!ゼロトラストリンクのサービス環境 (本コンポーネントは同サービスの利用が前提) |
| ESP-IDF | v5.4.4 (CI は `espressif/idf:release-v5.4` コンテナでビルド) |
| 検証内容 | `/key` 取得 → control plane TLS + Noise ハンドシェイク → ノード登録 → streaming netmap → DERP リレー TLS + DISCO direct path → WireGuard トンネル上の継続的な TCP/UDP アプリケーション通信 (音声・API の往復) |

## 既知の制約

- ホストアプリは ESP-IDF ネイティブであること (Arduino ベースは "Arduino as an ESP-IDF component" 構成が別途必要)
- subnet router / exit node にはなりません (自ノード宛の通信のみ)
- 保存済みキーが失効・無効化された場合は `ztl_device_auth` を再実行します (ユーザーがブラウザで再承認)

## サポート・不具合報告

バグ報告・質問・サービスに関するお問い合わせは、いずれも[サポートフォーム](https://support.ztna.lolipop.jp/hc/ja/requests/new)からお願いします(ヘルプセンター: [support.ztna.lolipop.jp](https://support.ztna.lolipop.jp/hc/ja))。脆弱性の報告も同じフォームで受け付けます(公開の場に詳細を書かないでください)。

## ライセンス

MIT([LICENSE](LICENSE) 参照)。プロトコル実装として [pepabo/microlink](https://github.com/pepabo/microlink)([CamM2325/microlink](https://github.com/CamM2325/microlink) の fork に ZTL 対応を加えたもの、MIT)と wireguard-lwip (BSD-3-Clause) を同梱しています。各ディレクトリの LICENSE を参照してください。

Links

  • Repository Repository
  • License MIT

Supports all targets

To add this component to your project, run:

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

download archive

Stats

  • Archive size
    Archive size ~ 33.10 KB
  • Downloaded in total
    Downloaded in total 0 times
  • Downloaded this version
    This version: 0 times

Badge

gmo-pepabo/lolipop-ztl version: 0.1.0
|