目次を表示する

ngrokの魔法を解き直す ── 仕組みを理解し、Goで自作してみる

本物の ngrok と答え合わせする —— GSLB・Traffic Policy・Agent Endpoint

本物の ngrok と答え合わせする —— GSLB・Traffic Policy・Agent Endpoint

ここまでで手元にあるもの

第 3 章で 80 行のリバーストンネルを書いた。第 4 章で muxado を入れ、1 本の TLS の上で複数ストリームが流れるようになった。第 5 章で公開ホスト名でリクエストを振り分け、x-forwarded-* を付けて upstream に渡せるようにした。第 6 章で TypedStreamSession を使い、コントロールチャネルとデータチャネルを 1 本の接続の中で型付きに分離した。

ここまでで、手元の mintunnel には以下のものが揃っている。

  • ローカルから edge への単一 TLS 接続
  • その上の muxado による多重化
  • ストリーム型でコントロール / データを分離
  • ホスト名ベースのルーティング
  • HTTP リクエストのヘッダー書き換え

「使ったことのある人の感覚で言うと、これでだいたい ngrok っぽいもの」が動く状態だ。だが、本物の ngrok にはまだ追いついていない要素がたくさんある。この章では、本物の ngrok を手元の mintunnel の隣に並べ、答え合わせをする。何を省略していたのか、何を別の方法で実装していたのか、そして「実は本物も同じことをやっていた」のはどこか。

伏線回収の章でもある。第 5 章で「自分で書いた粗いルーター」と呼んでいたものが、本物では宣言的な設定言語として書けるようになっている。その全貌を見ていく。

並べてみる:mintunnel と本物の ngrok

まずは図で全体像を並べる。左が手元の mintunnel、右が本物の ngrok だ。

graph TB
  subgraph mintunnel["自作 mintunnel(第 6 章まで)"]
    direction TB
    mtClient[Browser] --> mtEdge[mintunnel-edge<br/>単一ホスト]
    mtEdge -- muxado/TLS --> mtAgent[mintunnel-agent]
    mtAgent --> mtUp[localhost:3000]
  end

  subgraph ngrok["本物の ngrok"]
    direction TB
    nClient[Browser] --> nGSLB{GSLB<br/>最寄り PoP へ}
    nGSLB --> nEdge1[PoP A<br/>TLS 終端]
    nGSLB -.failover.-> nEdge2[PoP B]
    nEdge1 --> nPolicy[Traffic Policy<br/>add-headers/rate-limit<br/>jwt-validation/terminate-tls]
    nPolicy -- muxado/TLS --> nAgent[ngrok agent<br/>Agent Endpoint]
    nAgent --> nUp[localhost:3000]
  end

左右を見比べて、本物にあって自作にないものを列挙すると次の通りだ。

  1. GSLB:クライアントを最寄り PoP に振り分け、TLS は最も近いエッジで終端する仕組み
  2. PoP の冗長性:エッジが地理的に分散し、ダウン時には別 PoP に failover する
  3. Traffic Policy:エッジでの振る舞い(ヘッダー追加・レート制限・JWT 検証・TLS 終端位置の選択)を宣言的に書く設定言語
  4. Cloud Endpoint と Agent Endpoint の 2 軸抽象:旧来の「tunnel」を再整理した概念
  5. agent config v3:設定ファイルのスキーマ刷新
  6. MCP / AI Gateway 用途:2025〜2026 年のユースケース拡張

一方、並べて初めて気づくこともある。実は muxado による多重化、x-forwarded-* の付与、コントロール / データの分離 ── これらは本物も自作もやり方が一致している。「写経で再現したつもりが本物のミニチュア版になっていた」というポジティブな答え合わせも、この章の見どころだ。

それでは 1 つずつ見ていく。

GSLB —— 単一ホストから「最寄り PoP」へ

mintunnel-edge は 1 台のホストで動かしてきた。読者の手元では多分 localhost か AWS の 1 リージョンに置いた VM だろう。本物の ngrok は違う。世界中に PoP(Point of Presence)が分散して配置されており、ユーザーは「最も近い PoP」に自動的に接続される

この振り分けを担うのが GSLB(Global Server Load Balancing) で、2023 年 9 月に GA したサービスだ。公式リリースには次の 3 つの特徴が明示されている。

  • クライアントと agent のトラフィックを最も低遅延の PoP へインテリジェントにルーティング
  • TLS は client に最も近いエッジで終端
  • PoP 全体がダウンした場合、別の PoP へ failover

mintunnel では mintunnel.example.com を引いたら 1 つの IP が返って終わりだった。ngrok では *.ngrok-free.app のような公開 URL に対して、世界のどこから引いても「その人にとって最寄りの PoP」が応答する。GSLB の内部実装が DNS ベースの地理的ルーティングなのか、それとも別の仕組みなのかは公式に明示されていないので、ここでは踏み込まない。読者として押さえておくべきは 「自作の単一ホストモデルを地理的に拡張したものが本物のエッジ層」 という対応関係だ。

さらに 2026 年 3 月には region pinning(リージョン固定)機能がリリースされた。GSLB の自動振り分けに任せず、「このエンドポイントは必ず東京リージョンを使う」と固定できる。GDPR・データ主権・特定ベンダーと同じリージョンに置きたい等の要件への応答だ。

mintunnel の mintunnel-edge を地理的に分散させ、最寄り PoP に振る GSLB を入れ、必要なら region pinning も足す ── ここまで揃えると、ようやく本物の冗長性に近づく。手元で書いた「単一ホストの edge」は、本物の最小単位 1 つに相当する、というのが正確な対応関係だ。

Traffic Policy —— 第 5 章で書いた粗いルーターの最終形

第 5 章で mintunnel-edge の中に書いた、ルーティングとヘッダー付与のコードを思い出してほしい。if r.Host == ... { ... } のような分岐を、Go のコードとして手で書いていた。ホスト名でバックエンドを選び、x-forwarded-for を追記し、特定パスだけ別の挙動にしたいときは switch を増やしていく。

本物の ngrok では、これが「設定ファイル」として書ける。それが Traffic Policy だ。公式ドキュメントの言葉を借りると次のようになる。

「endpoint に流れるトラフィックを filter, match, manage, orchestrate するための configuration language」

YAML(または JSON)で書き、add-headers / rate-limit / jwt-validation / terminate-tls 等のアクションを宣言的に並べる。たとえばこんな書き方になる。

# Traffic Policy のミニサンプル
on_http_request:
  - actions:
      - type: add-headers
        config:
          headers:
            X-Tenant: "${vars.tenant_id}"
            Host: "internal.example.com"   # Host は置換セマンティクス
  - expressions:
      - "req.url.path.startsWith('/api/')"
    actions:
      - type: rate-limit
        config:
          name: api-limit
          algorithm: sliding_window
          capacity: 100
          rate: 1m
      - type: jwt-validation
        config:
          issuer:
            allow_list:
              - value: "https://auth.example.com/"

on_tcp_connect:
  - actions:
      - type: terminate-tls
        config:
          mutual_tls_certificate_authorities:
            - "${secrets.client_ca}"

第 5 章で if r.Host == ... と手で書いていた分岐が、ここでは expressions: に式として並ぶ。x-forwarded-* の追記は add-headers アクションが標準で持っている。レート制限・JWT 検証・mTLS のクライアント証明書検証 ── ぜんぶ「アクションを足す」というモデルで書ける。

${vars.tenant_id}${secrets.client_ca} のところに注目してほしい。Traffic Policy には Variables(リクエストメタデータや変数を埋め込む仕組み)と Macros(設定の再利用機構)があり、さらに 2025 年 11 月に Secrets がダッシュボード対応で GA した。CA 証明書や API key を policy 中に直接書かず、Secrets として外に出して参照できる。

Traffic Policy の重要な性質をもう 1 つ。Agent Endpoint の場合、Traffic Policy は任意だ。policy で terminate されなかったトラフィックは、これまで通り agent に流れる。つまり「mintunnel を Traffic Policy なしで動かす」状態と「ngrok を Traffic Policy なしで動かす」状態は、エッジでの挙動がほぼ等価になる。手元で書いた粗いルーターは、Traffic Policy を空っぽにしたときの本物と同じレイヤー ── これが第 5 章 → 第 7 章の答え合わせだ。

Cloud Endpoint と Agent Endpoint —— 「tunnel」という言葉の死

mintunnel では mintunnel-agent が edge に接続している間だけ公開 URL が生きていた。プロセスを止めれば URL は消える。ngrok でも基本は同じ振る舞いだ ── と思っていると、実は本物には 2 種類のエンドポイントがある。

旧来の「tunnel」概念は廃止され、現在は Cloud EndpointAgent Endpoint の 2 軸抽象に整理されている。

  • Cloud Endpoint:ngrok クラウド側に常駐するエンドポイント。agent が起きていなくても URL は生きている。Traffic Policy でルーティング先を制御し、内部の agent や別の Cloud Endpoint に転送する
  • Agent Endpointagent プロセスが ngrok cloud service と接続している間だけオンライン。agent が authoritative(権限の所在地)であり、ダッシュボード / API からは read-only に見える。トラフィックは暗黙的に「作成元の agent」に転送される

手元で書いた mintunnel は 完全に Agent Endpoint 側のミニチュア だ。mintunnel-agent が落ちれば公開 URL も消える。Cloud Endpoint に相当するもの ── 「agent が居なくても生きている、policy だけで完結する公開エンドポイント」 ── は実装していない。これは省略した部分のうち最も大きいもののひとつだ。

このリネームは単なる言葉遊びではない。「tunnel」と呼ばれていたものが「常駐型(Cloud)」と「揮発型(Agent)」に分かれたことで、Traffic Policy を agent の存在から切り離して書けるようになった。ngrok のクラウド側だけで完結するロードバランサ的構成も、agent と組み合わせる Webhook 受信構成も、同じ抽象の上に乗る。

agent config v3 —— tunnels から endpoints

mintunnel の設定はミニマルな自前 YAML で、章ごとに必要なキーを足していった。本物の ngrok agent も設定ファイルを持っている ── そして 2024〜2025 年にかけて v2 から v3 へ大きく変わった。主な変更点は次の通りだ。

  • top-level だった agent 設定が agent: 配下にネストされた
  • tunnels キーが endpoints に置き換わった
  • server_addrconnect_url
  • root_casconnect_cas
  • labeled tunnels(タグでルーティングする旧機能)は廃止
  • 各 endpoint に直接 traffic_policy を書けるようになった

ここでも「tunnel」という言葉が死んでいることに気づく。tunnelsendpoints の名称変更は、前節の Cloud / Agent Endpoint 抽象と一貫している。

v2 構文も後方互換で残っているが、2025 年末で removal 予定だ。本記事の執筆時点である 2026 年 5 月では、すでに v3 のみが「現役の構文」と思っておいたほうがいい。読者の手元に古い tunnels: 構文の設定ファイルが残っていたら、棚卸しのきっかけになる節目だ。

HTTP ヘッダー書き換え —— 自作と本物が一致した場所

ここまで「省略したもの・別実装のもの」を並べてきたが、ポジティブな答え合わせも 1 つしておく。

第 5 章で mintunnel-edge に書き加えた x-forwarded-* ヘッダーの付与処理。あれは本物の ngrok と挙動がほぼ一致している。公式ドキュメントによる本物の ngrok の挙動は次の通りだ。

  • x-forwarded-for:オリジナルクライアントの IP を追加(既存があれば追記
  • x-forwarded-protohttp または https を追加(既存があれば追記
  • x-forwarded-host:client 側の Host を追加。無ければリクエストの Host の値(既存があれば追記

3 つとも追記方式なので、application 側は「最後の値」を読むのが定石だ。これは第 5 章の実装でも同じ判断をした。

Host ヘッダーの扱いだけは違う。Traffic Policy の add-headers アクションは、通常は追記方式だが Host ヘッダーは特例的に「置換」セマンティクスを採用している。これは HTTP の現実 ── Host は複数値を持つ概念ではない ── に合わせた合理的な選択で、mintunnel の第 5 章でも同様の挙動にしていた。

つまり「これは本物では Traffic Policy にこう書く」と並べたとき、第 5 章のコードと本物の add-headers アクションがやっていることはほぼ等価 だった。x-forwarded-for 系を追記、Host を置換 ── このセマンティクス自体は、エッジで動くプロキシとしてごく標準的な判断であることを、答え合わせとして確認しておきたい。

muxado は本物も使い続けている

そしてもう 1 つの大事な答え合わせ。muxado は本物も使い続けている。第 4 章でわざわざ muxado を選んだのは、「本物と同じものを使う体験」を読者に提供したかったからだが、これが 2026 年現在も成立していることを示す証拠を 2 つ挙げる。

  1. golang.ngrok.com/muxado/v2 v2.0.1 が 2024 年 10 月にリリースされている。リポジトリは github.com/ngrok/muxado-go、MIT ライセンスで ngrok 公式 namespace 配下で現役メンテされている
  2. golang.ngrok.com/ngrok/v2(v2.1.4、2026 年 4 月)に IsMuxadoDiagnoseFailure 関数が存在し、Diagnose メソッドは「addr に対して TCP, TLS, そして Muxado tunnel protocol をテストする」と公式ドキュメントに明記されている

ngrok-rust の公式ドキュメントも muxado モジュールについて次のように書いている。

“This is the stream multiplexing protocol that powers ngrok’s tunnels.”

ngrok のトンネルを支えているプロトコル ── これは 2016 年の Changelog Podcast で Alan Shreve 本人が同じ趣旨を語っていたものと地続きだ。創業当初から 13 年経っても、トランスポート層の選択は muxado のままで揺るがない

これは Web のトランスポート技術が「HTTP/2 ですべて足りる」とはならなかったことの 1 つの傍証でもある。第 4 章で muxado のフレームが 4 種類しかないことを見たが、あの徹底した必要最小主義が今でも生き残る理由になっている。

MCP / AI Gateway —— 2026 年のトンネリングの新しい顔

最後に、2026 年現在の ngrok が解こうとしている「新しい問題」にも触れておく。「Webhook 受信」だったユースケースが、ここ 1〜2 年で MCP(Model Context Protocol)GatewayAI Gateway という新しい用途に拡張されている。

MCP Gateway 用途 は公式ドキュメントに専用ガイドがあるほど明示的な領域だ。ローカル開発環境の MCP サーバーを Claude や OpenAI から安全に呼べるよう、internal Agent Endpoint + Cloud Endpoint + Traffic Policy(Authorization ヘッダーの検証、IP Intel フィルタリング、トークン単位のレート制限など)を組み合わせるパターンが公式パターンとして整理されている。プロローグで「LLM から MCP サーバー経由で自宅のスクリプトを叩かせたい」と書いたユースケースが、ここで具体的な構成として現れている。

AI Gateway はさらに新しい話で、2026 年 4 月の公式ブログにこんな記述がある。「2025 年の AI gateway が単純な prompt routing を扱っていたのに対し、2026 年版は session-aware orchestration を扱う」「1 つのユーザリクエストが 20〜50 の LLM call と tool 呼び出しを誘発する」。OpenAI / Anthropic / fine-tuned / local model 横断のルーティング、コストベースの選択、PII 削除、プロンプトインジェクション検知などを提供する。2026 年 2 月には Native Anthropic SDK 対応(prompt caching / extended thinking 対応)、4 月には AI Gateway key に provider key を attach できる機能も追加された。

「Webhook を受けるためのリバーストンネル」だったプロダクトが、「LLM の通り道を制御するゲートウェイ」に拡張されている。プロトコル層(muxado)は据え置きで、エッジで宣言的に書ける機能(Traffic Policy のアクション群)を増やすことで対応している ── という設計の方向性が見える。

自作 vs 本物の差分まとめ

ここまでの答え合わせを表にまとめる。

要素自作 mintunnel本物の ngrok
多重化方式muxado同じ:muxado
ストリーム型付けTypedStreamSession(第 6 章)同じ:TypedStreamSession + Heartbeat
TLS 終端位置edge で固定edge / agent / upstream を Traffic Policy で選択
TLS 証明書自前で配置公開 CA 由来の有効な証明書を自動提供
x-forwarded-* 付与手書きで追記同じ:自動追記Host のみ置換)
ルーティングGo で if r.Host == ...Traffic Policy の expressions
認証・認可authtoken を直接比較JWT / OAuth / mTLS / IP Intel など Traffic Policy で
レート制限なしTraffic Policy の rate-limit アクション
エッジの地理分散単一ホストGSLB で最寄り PoP へ、region pinning も
障害対策なしPoP 単位の failover
公開エンドポイントの寿命agent が生きている間だけAgent Endpoint(同じ)+ Cloud Endpoint(常駐)
設定ファイル自前 YAMLagent config v3(旧 v2 は 2025 年末で removal)
拡張ユースケースなしMCP Gateway / AI Gateway

「muxado とその上の型付け」までは自作と本物が一致している。「TCP の上に 1 本の TLS を張り、その中に複数の意味付きストリームを流す」というプロトコル層の判断は、自作で写経した時点で本物と同じになっていた。差分はそこから上 ── エッジで起きる宣言的な変換と、それを地理的に分散させる仕組み ── に集中している。

これは見方を変えると、「本物の ngrok を学ぶ」ということの大半は agent ↔ edge 間のプロトコルではなく、edge で何が起きているかを学ぶこと だと分かる。第 4 章〜第 6 章で muxado のレイヤーを写経しきった読者は、すでに最深部の半分には到達している。残りは Traffic Policy という設定言語を読み書きできるようになることで埋まる。

次章への接続

本物の ngrok の答え合わせは終わった。muxado というプロトコル選択も、x-forwarded-* のセマンティクスも、Cloud / Agent の 2 軸抽象も、Traffic Policy という宣言的な設定言語も、ここまでで輪郭が見えた。

では、同じ「リバーストンネルで NAT 越えを解く」という問題に対して、他のプロダクトはどう違う答えを出しているのか?

Cloudflare Tunnel は QUIC を transport の既定にしている。frp は yamux 系の独自 stream mux を選び、TCP / KCP / QUIC / WebSocket から選択できる。bore は暗号化さえ持たない代わりに 1,000 行未満の Rust で書かれる。Tailscale Funnel は WireGuard をベースに、TLS をエッジで終端せず agent 側まで届ける。

次章では、これらの競合プロダクトを「多重化方式 / TLS 終端位置 / OSS 境界」の 3 軸で並べ、設計判断の差を読み解いていく。本物の ngrok を 1 つの解として見たあと、別の解がどう同じ問題に向き合ったかを見ることで、トンネリングという技術領域の輪郭がより立体的に見えてくるはずだ。


章末まとめ

  • mintunnel は本物の Agent Endpoint 1 つ分のミニチュア。GSLB・PoP 冗長性・Traffic Policy・Cloud Endpoint は省略している
  • GSLB(2023-09 GA)は最寄り PoP への振り分けと PoP 単位の failover を担う。2026-03 リリースの region pinning でリージョン固定も可能
  • Traffic Policy は第 5 章の「Go で書いた粗いルーター」の宣言的版。add-headers / rate-limit / jwt-validation / terminate-tls を YAML で並べ、Variables / Macros / Secrets で構造化できる
  • Cloud Endpoint(常駐)と Agent Endpoint(agent が生きている間だけ)の 2 軸で旧来の「tunnel」概念が置き換えられた。agent config も v3 へ移行(tunnelsendpoints、v2 は 2025 年末で removal)
  • muxado と TypedStreamSession、x-forwarded-* の追記セマンティクスは自作と本物で一致している ── 写経していた読者は最深部の半分にすでに到達している
  • 2026 年のトンネリングは MCP Gateway と AI Gateway という新しい顔を持ち始めている。プロトコル層は据え置きで、エッジの宣言的機能で対応している
  • 次章では「同じ問題への別の解」として、Cloudflare Tunnel・frp・bore・Tailscale Funnel と設計判断を比較する