3PC通信アーキテクチャ 完全解説

Tailscale / ntfy / GDrive — 全レイヤーを理解する

Cockpit MotherShip GateKeeper Tailscale Mesh + ntfy 通知 + GDrive データプレーン

学習資料 — プレゼン用ではなく、深い理解のための技術解説書。

Part 1: Tailscale 技術解説

WireGuardが全てを変えた理由

WireGuard プロトコルの内部構造

「ただのVPN」ではない — 暗号ルーティング基盤

特性 WireGuard OpenVPN IPsec/IKEv2
コードベース 約4,000行 約100,000行以上 約400,000行以上
攻撃対象面 最小限 大きい (OpenSSL) 非常に大きい
実行空間 カーネル空間 ユーザー空間 (tun/tap) カーネル+ユーザー空間
ハンドシェイク 1-RTT (Noise IKpsk2) 2+ RTT (TLS) 4-6メッセージ (IKE)
暗号方式の選択 なし(利点!) 交渉可能 交渉可能

重要な洞察: WireGuardは暗号方式の選択肢がない — 使用する暗号スイートは1種類のみ。これはダウングレード攻撃を防ぐ設計上の利点である。

暗号鍵ルーティング (Cryptokey Routing)

公開鍵=アイデンティティ、AllowedIPs=ルーティングテーブル

ピアCurve25519公開鍵で識別される。ルーティングテーブルは許可IPレンジをピアにマッピングする:

[Interface]
PrivateKey = <自分の秘密鍵>
Address = 100.64.0.1/32

[Peer]
PublicKey = <MotherShipの公開鍵>
AllowedIPs = 100.64.0.2/32

ルーティングの仕組み:

  • 送信時: 宛先IPをAllowedIPsで検索 -> ピア特定 -> 暗号化 -> 送信
  • 受信時: 復号 -> 送信元IPが送信者のAllowedIPs内か検証 -> 許可/破棄

証明書もPKIも不要。公開鍵そのものがアイデンティティとなる。

Noise プロトコル ハンドシェイク

Noise_IKpsk2 による1-RTT鍵交換

Initiator Responder Msg1: ephemeral_pub + encrypted(static_pub + timestamp) Msg2: ephemeral_pub + encrypted(empty) Transport keys derived: 2x ChaCha20-Poly1305

Noise_IKpsk2の意味: I=初回メッセージにイニシエータの静的鍵を含む; K=レスポンダの鍵は事前共有; psk2=2番目のメッセージ後にPSKを混合。

使用プリミティブ: Curve25519 (DH) + ChaCha20-Poly1305 (AEAD) + BLAKE2s (ハッシュ)

Tailscale: コーディネーションサーバー

サーバーは鍵を配布する — トラフィックは通さない

login.tailscale.com 公開鍵+エンドポイントを配布 Cockpit MotherShip 直接WireGuardトンネル (P2P) — サーバーはトラフィックを見ない
  • OAuth (Google/Microsoft等) でデバイスを認証
  • 各デバイスの公開鍵と最後に確認されたエンドポイント (IP:port) を配布
  • ACLポリシーの更新をプッシュ
  • データトラフィックは一切中継しない

DERP: 直接接続が失敗した場合

Designated Encrypted Relay for Packets (暗号化リレー)

① 直接接続 (Direct P2P) ② STUN IP:port 検出 ③ ポート予測 (Port Prediction) ④ バースデー 256port → ~50% ⑤ DERPリレー E2E暗号化維持 最終フォールバック 失敗時に次の手段へフォールバック →

NAT越え: ①直接 → ②STUN → ③ポート予測 → ④バースデー(256port≈50%) → ⑤DERPリレー

DERPも引き続きE2E暗号化 — リレーサーバーは通信内容を読めない。

MagicDNS と Tailnet

プライベートネットワークオーバーレイ

デバイス cockpit.tail…ts.net? 100.100.100.100 ローカルDNSプロキシ .ts.net をインターセプト Tailscale 制御サーバー ホスト名→IPマッピング 100.x.y.z CGNAT (RFC 6598) 解決 100.64.0.0/10 レンジ — パブリックインターネットに出現しない

100.64.0.0/10 (CGNAT, RFC 6598) — ISP内部予約レンジ。パブリックに出現せず、LAN/プライベートレンジと衝突しない。

MagicDNS: cockpit.tail12345.ts.net → 100.x.y.z。ローカルDNSプロキシ (100.100.100.100) が .ts.net をインターセプト。

ACLポリシーエンジン

WireGuardレベルで強制される暗号的アイデンティティ

タグはWireGuardレベルで強制される暗号的アイデンティティpc-mesh タグのデバイスは他の pc-mesh デバイスのみに到達可能。「ラベルを信頼」ではなく、ルーティングテーブルで強制される。

Tailscaleのリソース消費

マシンへの影響と実行時の挙動

項目 App Store版 スタンドアロン (brew)
VPN実装 Network Extension (サンドボックス) utunデバイス (カーネル)
権限 sudo不要 sudo必要
メモリ 約280 MB 約250 MB
CPU アイドル時1%未満、ハンドシェイク時に一時的スパイク 同上

なぜCPU消費が少ないのか?

WireGuardはアイドル時にゼロワーク — keepaliveポーリングも、ハートビートも、バックグラウンド鍵ローテーションもない。ChaCha20-Poly1305はハードウェアアクセラレーションなしでも約2 Gbpsで動作する。

メモリ内訳: Goランタイム(80) + Network Extension(100) + 状態(20) + WG(5) + バッファ

Part 2: ntfy 技術解説

HTTPベースのPub/Sub通知サービス

ntfy: サブスクリプション方式

メッセージを受信する3つの方法

Poll (GET) GET /topic/json?poll=1 キャッシュ済みメッセージ返却 接続は即座に終了 Stream (SSE) GET /topic/json チャンク転送 接続を維持し続ける WebSocket GET /topic/ws 双方向通信 最低レイテンシ
  • Poll: cronジョブに最適 / Stream: デーモンに最適 / WebSocket: インタラクティブに最適

pc_mesh.py readPollモードを使用。将来の pc_mesh.py listen はリアルタイムSSEのStreamモードを採用予定。

ntfy: メッセージのライフサイクル

POSTから配信まで

Client POST SQLiteキャッシュ TTL = 12時間 サブスクライバー HTTP ボディ送信 ファンアウト (接続中) FCM/APNs (モバイル) 12時間後: キャッシュ削除

実際のメッセージJSON

{ "id": "sPs6v9cXIkY8", "time": 1743897600,
  "expires": 1743940800, "event": "message",
  "topic": "<secret-prefix>-cockpit",
  "message": "タスク完了: PDF生成",
  "title": "pc_mesh: mothership", "priority": 3 }

レート制限(公開サーバー): 1IPあたり1日250メッセージ、メッセージ本文最大64KB。

ntfy: セキュリティモデル

公開サーバー (ntfy.sh) の正直な評価

脅威 保護されているか 詳細
トピック列挙 弱い トピックURLが唯一の秘密
通信中のメッセージ はい ntfy.shへのHTTPS/TLS
保存時のメッセージ いいえ サーバー上のSQLite、運営者が読取可能
送信者認証 いいえ トピックを知る誰もが投稿可能
リプレイ攻撃 いいえ ノンスやシーケンス番号なし

我々の対策: 通知とデータの分離

ntfyは「メールが届いた」という通知のみを運ぶ — メール本文は決して運ばない。 タスクの詳細はGDrive(保存時暗号化、アクセス制御済み)に置く。

セルフホストならユーザー/パスワード認証、トピック単位のACL、完全なデータ管理が可能 — ただしサーバー運用が必要。

ntfy: できないこと

理解すべき重要な制約

  • 配信保証なし — HTTPはベストエフォート; メッセージは12時間で期限切れ
  • 確認応答なし — 送信者は受信者がメッセージを読んだか知る手段がない
  • 処理機能なし — フィルタ、変換、ルーティングロジックなし
  • アクション起動なし — メッセージ受信だけでは何も起こらない
機能 ntfy RabbitMQ Redis Pub/Sub NATS
配信保証 なし At-least-once なし At-most-once
確認応答 なし あり (ACK/NACK) なし あり
永続化 12時間キャッシュ 永続キュー 任意 (AOF) JetStream

ntfyは我々の用途に最適: メッセージ消失が致命的でない軽量通知に向いている。

Part 3: 通信ギャップの正体

通知が終わり、アクションが必要になる場所

現状と理想状態

「通知された」と「処理された」の間のギャップ

現在の状態 Mac送信 ntfy.sh 待機中... 人間がセッション開始+読取必要 理想の状態 Mac送信 ntfy.sh 自動処理

根本的な問題: ntfyは「何かが起きた」と通知する。しかし受信側でリスニング・パース・アクション実行するプロセスが存在しない。人間がグルーレイヤーになっている。

ギャップの本質: 「通知は行動ではない (Notification does not equal action)」

ギャップを埋める: アーキテクチャの選択肢

4つのアプローチ、それぞれにトレードオフ

選択肢 仕組み レイテンシ 複雑度 信頼性
A: Cronポーリング */5 * * * * pc_mesh.py read 5分 非常に低い
B: 常駐リスナー pc_mesh.py listen をハンドラにパイプ リアルタイム 中-高
C: Discord Botブリッジ Discord -> Bot -> Claude CLI 約10秒 高(構築済)
D: GDrive fswatch fswatch Agent_Inbox/ -> トリガー 30秒-5分 低-中
# 選択肢A: crontab -e
*/5 * * * * python3 /path/to/pc_mesh.py read 10 \
  | python3 handler.py

# 選択肢B: systemd/launchdサービス
pc_mesh.py listen \
  | while read -r line; do python3 dispatch.py; done

確認応答 (ACK) の問題

Fire-and-forget(撃ちっぱなし)では不十分

現状のフロー(ACKなし)

Cockpit --[送信]--> ntfy "secret-mothership" --> MotherShip
         (届いたことを祈る)                    (読むかもしれない)

理想のフロー(ペアトピックによるACK)

Cockpit --[送信]--> ntfy "secret-mothership" --> MotherShip
Cockpit <--[ACK]--- ntfy "secret-cockpit"    <-- MotherShip
Cockpit <--[完了]-- ntfy "secret-cockpit"    <-- MotherShip
# 実装スケッチ: ペアトピックACK
msg_id = send("mothership", "PDF batch-42を処理せよ")
msg = read("mothership")                # 受信側
send("cockpit", f"ack:{msg['id']}")     # ACK+完了通知

ダムパイプ上の規約 — ntfyにACK機能なし。ペアトピックで自前構築。

Part 4: GDrive データプレーン

通知とペイロードの分離

GDrive 同期の内部動作

Google Drive File Streamの実際の仕組み

ローカル書込 (Local write) ライトバック キュー (非同期) アップロード 30s–5min Google サーバー (保存・暗号化) 同期シグナル 他デバイスへ通知 ダウンロード on-demand ローカル キャッシュ 小ファイル: 30–60秒 / 大ファイル(>10MB): 1–5分 競合時: filename (1).md 作成

Google Drive File Streamは全ファイルをダウンロードしない。仮想ファイルシステムを提示する:

  • ストリーミングモード(デフォルト): Finderに表示されるが、初回アクセス時に取得
  • オフライン利用可能: 明示的にピン留めされたファイルはローカルキャッシュ
  • ライトバック: ローカル変更は非同期でアップロード

GDrive 同期の遅延と競合

実際の遅延とconflict解決ポリシー

シナリオ 典型的な遅延 最悪の場合
小ファイル (<1KB) 作成 30-60秒 5分
大ファイル (>10MB) 1-5分 10分以上
オフライン復帰時 即座にキュー同期 大量キュー時は数分

競合解決: GDriveは競合時に filename (1).md を作成する — そのためclaim-then-editプロトコルを採用。

制御プレーンとデータプレーンの分離

実際の分散システム設計パターン

制御プレーン (ntfy) 小さく、速く、信頼性は低い 「Agent_Inboxに新タスク到着」 データプレーン (GDrive) 大きく、遅く、信頼性は高い タスク詳細+添付ファイル 同じパターン: Kafkaメタデータ + S3データブロブ Kubernetes APIサーバー (制御) + etcd (データ)
観点 単一チャネル 分離構成
通知速度 大きなペイロードで律速 ntfyが1秒未満で配信
データ耐久性 チャネル断で消失 GDriveが無期限に永続化
セキュリティ 全データが1経路を通過 機密データは暗号化ストレージへ
オフライン時 何も機能しない GDriveがキュー、ntfyがリプレイ

Part 5: 統合アーキテクチャ

3PC通信の完全な構造

3PC フルアーキテクチャ

全レイヤー、全コンポーネント

アプリケーション層: Claude Code / OpenClaw / Discord Bot ストレージ/データプレーン: GDrive (Agent_Inbox, feedback, shared-memory) 通知/制御プレーン: ntfy.sh (HTTP pub/sub, 12時間TTL) ネットワーク層: Tailscale Mesh (WireGuard, 100.x.y.z) 物理層: Cockpit (Mac M4) + MotherShip (Win/RTX) + GateKeeper (HP/WSL2)
レイヤー 役割 障害時の挙動
物理層 計算+ローカルストレージ PC停止 -> タスクがキューに待機
ネットワーク層 暗号化P2P接続 NAT問題 -> DERPフォールバック
通知層 「何かが起きた」アラート ntfy障害 -> GDrive直接ポーリング
ストレージ層 タスク詳細+共有状態 GDriveオフライン -> ローカルキュー

タスクの追跡: ステップバイステップ

pc_mesh.py send gatekeeper "Inboxバッチを処理せよ"

Step 1: PythonがHTTP POSTボディを構築
  {"topic": "<UUID>-gatekeeper", "message": "Inboxバッチを処理せよ"}

Step 2: ntfy.shにHTTPS POST (TLS, Tailscale非経由)
  ntfy.shがSQLiteに保存 (TTL=12時間)

Step 3: GateKeeperにアクティブなサブスクライバーがいれば即時配信 (<1秒)
  いなければ: キャッシュで待機

Step 4: 並行して(別チャネル):
  MacがAgent_Inbox/TASK_2026-04-06_inbox-batch.mdをGDriveに書込
  GDriveがGateKeeperに同期 (30秒-5分)

Step 2-3 (ntfy) と Step 4 (GDrive) は異なるチャネルで並行して実行される。

タスクの追跡(続き)

受信から処理完了まで

Step 5: GateKeeperが pc_mesh.py read 実行 -> 通知を確認
  Agent_Inbox/TASK_*.md を読み、タスクの全詳細を取得

Step 6: GateKeeperが処理 (OpenClaw + GLM-5.1)
  結果 -> Agent_Outbox/ -> GDrive同期でMacに返送

重要なポイント

  • 通知チャネル (ntfy)データチャネル (GDrive) が完全に独立
  • どちらか一方が障害でも、もう一方で回復可能
  • ntfyが死んでもGDriveのfswatch/ポーリングで検出可能
  • GDriveが遅延してもntfyで「到着予告」を先行受信可能

この二重チャネル設計が3PC通信の耐障害性の鍵である。

短期ロードマップ (今週中)

即時実施アクション

アクション 対象PC 工数
pc_mesh.py read をセッション開始チェックリストに追加 全PC 5分
Agent_Inbox/fswatch 用launchd plist作成 Mac 30分
pc_mesh.py にACK規約を追加(ペアトピック) 全PC 1時間

中期ロードマップ (今月中)

自動化の深化

アクション 対象PC 工数
pc_mesh.py listen デーモンモード実装 GateKeeper 2時間
リスナーをOpenClawタスクディスパッチャーに接続 GateKeeper 3時間
pc_mesh.py read--since フラグ追加 全PC 1時間

長期展望: TailscaleによるPC間直接HTTP APIが実現すれば、PC-to-PC通信にntfyは不要になる。ntfyはモバイルプッシュ用途のみに残る。

意思決定マトリクス

各PCに最適な自動化パス

PC 最適な選択肢 理由 フォールバック
Cockpit (Mac) D: GDriveのfswatch モバイル端末; GDriveはオフラインで動作 A: Cronポーリング
MotherShip (Win) C: Discord Botブリッジ 構築済み; 24/7稼働 B: リスナー
GateKeeper (HP) B: 常駐リスナー Discordゲートウェイにリアルタイム必要 D: fswatch

PCの役割に合わせた自動化を選ぶ。 Cockpitはモバイル — GDriveキューが堅牢。MotherShipは常時稼働 — 既存Botを活用。GateKeeperはリアルタイム必要 — 常駐リスナー。

全PCに共通で必要なもの: 統一タスクフォーマット

GDrive Agent_Inbox/TASK_YYYY-MM-DD_<topic>.md にid・from・to・priority・statusフィールドを標準化する。

Appendix: 用語集 (Glossary)

本スライドで使用した技術用語の解説

用語集 構成 暗号・プロトコル WireGuard / Noise Curve25519 / BLAKE2s ネットワーク CGNAT / DERP MagicDNS / ACL 通信パターン Pub/Sub / SSE Fire-and-forget 分散システム 冪等性 / リース 制御/データプレーン

用語集: 暗号・プロトコル基盤

WireGuard 約4,000行の軽量VPNプロトコル。カーネル空間で動作し、暗号方式の選択肢を排除することでダウングレード攻撃を防ぐ。 使用箇所: スライド3, 4, 5, 15 Curve25519 楕円曲線Diffie-Hellman鍵交換アルゴリズム。公開鍵=デバイスアイデンティティとして機能し、証明書不要の認証を実現する。 使用箇所: スライド4, 5 ChaCha20-Poly1305 AEAD(認証付き暗号化)方式。ハードウェアアクセラレーションなしで約2 Gbpsを実現。WireGuardの唯一の暗号スイートで選択肢を排除することでダウングレード攻撃を防ぐ。 使用箇所: スライド5, 10 BLAKE2s / Noise Protocol (IKpsk2) BLAKE2s: WireGuard使用の高速ハッシュ関数。Noise IKpsk2: 1-RTTで完了する鍵交換フレームワーク。I=イニシエータ鍵含む、K=レスポンダ鍵既知、psk2=PSK混合。 使用箇所: スライド5

用語集: ネットワーク・インフラ

CGNAT (100.64.0.0/10) キャリアグレードNAT。RFC 6598でISP内部用に予約されたIPレンジ。パブリックインターネットに出現しないためLAN・他プライベートレンジと衝突せずオーバーレイに利用可能。 使用箇所: スライド8 DERP (Designated Encrypted Relay for Packets) 直接P2P接続が失敗した際のTailscale暗号化リレー。E2E暗号化は維持されるためリレーサーバーは通信内容を読めない。世界約20ノードで運用、セルフホストも可能。 使用箇所: スライド7 MagicDNS Tailscaleが提供する自動DNS。各デバイスに `hostname.tail12345.ts.net` 形式の名前を付与し100.x.y.z IPに解決する。ローカルDNSプロキシは100.100.100.100で動作。 使用箇所: スライド8 ACL (Access Control List) アクセス制御リスト。TailscaleではWireGuardレベルで強制される暗号的アイデンティティに基づくタグポリシー。「ラベルを信頼する」のではなくルーティングテーブルで強制される。 使用箇所: スライド8

用語集: 通信パターン

Pub/Sub (パブリッシュ/サブスクライブ) 送信者(Publisher)はトピックにメッセージを投稿し、受信者(Subscriber)はトピックを購読して受信する非同期通信パターン。送受信者は互いを直接知らなくてよい。 使用箇所: スライド11, 12, 14 SSE (Server-Sent Events) サーバー送信イベント。HTTPコネクションを維持しサーバーからクライアントへリアルタイムにデータを送信する技術。ntfyのStreamモードで使用。WebSocketより実装が軽量。 使用箇所: スライド11 Fire-and-forget (送りっぱなし通信) メッセージを送信したら応答を待たない通信パターン。シンプルだが配信確認ができない。ntfyの現状実装がこのパターン。ペアトピックによるACKで改善可能。 使用箇所: スライド16 制御プレーン / データプレーン (Control / Data Plane) 制御プレーン: 小さく速い通知チャネル(ntfy)。データプレーン: 大きく信頼性の高いデータチャネル(GDrive)。Kafka+S3やKubernetes APIサーバー+etcdと同じ設計パターン。 使用箇所: スライド19

用語集: 分散システム概念

冪等性 (Idempotency) 同じ操作を何度繰り返しても結果が変わらない性質。タスクIDによる重複排除に使用。ネットワーク障害でリトライが発生しても二重実行を防ぐ分散システムの基本原則。 使用箇所: タスクフォーマット設計 (スライド24) リース (Lease) タスク占有権の有効期間。処理中タスクを一定時間ロックしハートビートで更新する。タイムアウト時に他のワーカーへ再割り当て可能にすることで孤立タスクを防ぐ。 使用箇所: タスク管理設計 (スライド24) ACK (Acknowledgement / 確認応答) 受信側が送信側にメッセージ受領を通知する仕組み。ntfyはACK機能を持たないためペアトピック(`secret-cockpit` / `secret-mothership`)で自前実装する設計が必要。 使用箇所: スライド16 ハートビート (Heartbeat) プロセスやサービスが生存していることを定期的に通知する信号。リースと組み合わせ処理中タスクの占有権を継続更新することで障害検知とフェイルオーバーを実現する。 使用箇所: Tailscaleリソース消費 (スライド10), タスク管理設計