すべてのページ
このページの内容

リバースプロキシの背後でリレーを動かす

リバースプロキシは、ブラウザーからの接続を受けてリレーに渡す、自分で用意する Web サーバーです。LAN やサーバー上のリレーで HTTPS を使いたいとき、すでに運用しているサイトのサブパス(https://example.com/oar/)の下にリレーを置きたいとき、またはトンネルのエッジ(例:Cloudflare)がブラウザーとリレーの間に入るときに必要になります。リレー自身のマシン上のプロキシは、設定なしで動きます。別のマシン上のプロキシには 2 つか 3 つの設定が必要で、後述します。config.json の 5 つのキーがリレーにプロキシのことを伝え、それぞれが特定の問題を解決します。それらはその後で説明します。

プロキシがすべきこと #

リレーのページ、API、ライブ接続は、すべて 1 つのポートを使います(port を変更していなければ 3333)。プロキシは次のことをする必要があります。

  • WebSocket のアップグレードを通す。 アプリは、パス /socket.io/(プレフィックスがある場合は <prefix>/socket.io/)でリレーへの Socket.IO 接続を開いたままにします。アップグレードが通らないと、ページは読み込まれますが、ライブ更新が表示されません。
  • 長時間の接続をバッファリングしたり、タイムアウトさせたりしない。 このソケットはページが開いている間ずっと開いたままで、ターンは進行に合わせてそこにストリーミングされます。読み取りとアイドルのタイムアウトを長くし、レスポンスをバッファリングしないでください。
  • Host ヘッダーを通すか、X-Forwarded-Host を送る。リレーはそこから共有リンクのアドレスを組み立て、0.9.11 で追加 そのアドレス上のページからのライブ接続だけを受け付けます(allowedOrigins を参照)。X-Forwarded-Host と X-Forwarded-Proto が有効になるのは、リレーが信頼するプロキシから来た場合だけです。
  • プロキシで TLS を終端する場合は、ブラウザーが HTTPS を使ったことをリレーが分かるように X-Forwarded-Proto を送り(Cookie と HTTPS を参照)、クライアントのアドレスが分かるように X-Forwarded-For を送る。
  • パスをそのままにする。 書き換えないでください。プレフィックスの下でリレーを提供する場合は、リレーが自分でプレフィックスを取り除きます(remotePath を参照)。

キャッシュのルールは不要です。リレーは、ページ、Service Worker、マニフェスト、API に Cache-Control: no-store を付け、さらに CDN-Cache-Control: no-store と Cloudflare-CDN-Cache-Control: no-store も付けます。/api/shared/* をキャッシュするルールは追加しないでください。共有ビューは新しい内容をポーリングしており、キャッシュされた応答では閲覧者に古いスナップショットが表示されてしまいます。

同じマシン上のリレー向けの、最小限の Caddy の例です。Caddy は WebSocket のアップグレードを自動で通し、X-Forwarded-Proto を追加します。

relay.example.com {
    reverse_proxy 127.0.0.1:3333
}

nginx などのプロキシでは、アップグレードを通す方法とタイムアウトの設定方法を、それぞれのドキュメントで上の一覧と照らし合わせて確認してください。トンネルについては SSH トンネルと Cloudflare Tunnel を、アクセストークンについてはアクセスを参照してください。

別のマシン上のプロキシ 0.9.11 で追加 #

組み込みのトンネルとリレー自身のマシン上のプロキシは 127.0.0.1 経由でリレーに接続し、既定の trustProxy はそれらの転送ヘッダーを信頼します。別のホスト、あるいは別のコンテナーや VM 上のプロキシは、別のピアです。信頼するまで、リレーはその X-Forwarded-For、X-Forwarded-Host、X-Forwarded-Proto を無視するので、次のようになります。

  • すべての訪問者がプロキシのアドレスになります。全員で 1 つのサインインのロックアウトを共有し、アクセスログのどのエントリも同じアドレスになります。
  • 新しい共有リンクは http:// と、プロキシが送ったホストで始まります。
  • サインイン用 Cookie に Secure が付きません。

設定ファイルで 3 つのキーを設定し、リレーを再起動します。

{
  "localhostOnly": false,
  "trustProxy": "192.0.2.10",
  "allowedOrigins": ["https://relay.example.com"]
}
  • localhostOnly: false で、リレーはループバック以外でも待ち受けます。ポートはプロキシ以外のすべてに対して閉じておいてください。
  • trustProxy は、プロキシのアドレス、そのサブネット、またはリレーの前にあるプロキシの数(1 つなら 1)です。後述を参照してください。
  • allowedOrigins が必要なのは、プロキシが Host ヘッダーを書き換え、X-Forwarded-Host を送らない場合だけです。後述を参照してください。

リレー自身のマシン上のプロキシには、trustProxy も localhostOnly: false も不要で、allowedOrigins は Host を書き換える場合にだけ必要です。URL プレフィックスの下では、remotePath も設定してください。

リレーにプロキシのことを伝えるキー #

5 つすべてが config.json の表にあります。変更した後はリレーを再起動してください。

trustProxy #

既定値は "loopback" です。リレーが誰の X-Forwarded-* ヘッダーを信頼するかを決めます。既定値では、同じマシンから接続するプロキシだけが信頼されます。これは、リレーと並んで動く Caddy、nginx、cloudflared に適しています。

信頼するプロキシのヘッダーから、リレーが読み取るもの:

  • リクエストが HTTPS で来たかどうか。これにより、サインイン用 Cookie に Secure フラグが付くかどうかと、新しい共有リンクのスキームが決まります。
  • ブラウザーがアクセスしたホスト(X-Forwarded-Host)。新しい共有リンクと、ライブ接続のオリジンの確認に使います。
  • クライアントのアドレス。サインインのロックアウト、共有ビューの presence の制限、共有リンクのアクセスログに使います。そのログの国(cf-ipcountry)も、信頼するプロキシからだけ読み取ります。

プロキシが別のマシンで動いている場合、リレーにはローカルでないピアに見え、これらのヘッダーを無視します。trustProxy には、Express の trust proxy が受け付ける値を指定します。ホップ数(1)、アドレスやサブネット("10.0.0.0/8")、それらのリスト、または "loopback" です。

前段にプロキシがある場合だけ

trustProxy を "loopback" より広くするのは、リレーへのすべての接続が本当にプロキシを経由する場合だけにしてください。そうでないと、ポートに到達できる人は誰でも偽の X-Forwarded-Proto を送れます。

0.9.11 で追加 転送ヘッダーの利用はすべて trustProxy に従います。0.9.10 では、共有ビューの presence の制限と新しい共有リンクのアドレスは、どのクライアントからでもこれらのヘッダーを読み取っていました。

allowedOrigins 0.9.11 で追加 #

既定値は [] です。ライブ接続(Socket.IO)は、リレー自身のオリジンからのブラウザーのハンドシェイクだけを受け付けます。ブラウザーが送る Origin は、ブラウザーがアクセスしたホストと一致する必要があります。このホストは、信頼するプロキシが X-Forwarded-Host を送る場合はその値、それ以外は Host です。別のサイト上のページが、サインイン用 Cookie を使ってライブ接続を開くことはできません。スクリプトのように Origin を送らないクライアントは影響を受けません。

Host ヘッダーをリレーの内部アドレスに書き換え、X-Forwarded-Host を送らないプロキシでは、この比較が成り立ちません。その場合は、公開しているオリジンを、ブラウザーに表示されるとおりに、スキーム、ホスト、既定以外のポートを含めて列挙します。

"allowedOrigins": ["https://relay.example.com"]

これがないと、ページは読み込まれサインインもできますが、何もライブで更新されず、リレーはオリジンごとに 1 回ログを出します。

[socket] Refused a handshake from origin https://relay.example.com (host 127.0.0.1:3333); list it in allowedOrigins if a proxy rewrites the Host header

remotePath #

既定値は "" です。プロキシがサブパスの下でリレーを提供する場合に、そのプレフィックス(例:"/oar")を設定します。先頭のスラッシュは補われ、末尾のスラッシュは取り除かれます。

これを設定すると、リレーは次のようにします。

  • 受信したすべてのリクエストからプレフィックスを取り除きます。そのため、プロキシはパスをプレフィックス付きのまま渡し、取り除きません。
  • サインイン用 Cookie のパス、ソケットのパス(/oar/socket.io/)、ファイルや共有リンクのアドレスにプレフィックスを入れます。

これがないと、プレフィックスの下のリレーは自分のファイルやソケットを見つけられません。ページが /socket.io/ や /api/... をサイトのルートに要求するからです。リレーは末尾にスラッシュを付けて開いてください(https://example.com/oar/)。

インストールしたアプリはインストール元のアドレスに属するので、プレフィックスの下のリレーはそれ自体で別のアプリになります。アプリとしてインストールを参照してください。

publicHostnames #

既定値は [] です。リレー自体に到達するホスト名を列挙します(例:["relay.example.com"])。リレーがこれを使うのは 1 か所だけです。プレビューレーンは、これらの名前(または localhost か 127.0.0.1)上の previews.publicBaseUrl を拒否します。これにより、プレビューしているアプリがリレーとログイン Cookie を共有することはありません。新しい名前でプロキシの背後にリレーを置き、プレビューを使う場合は、その名前をここに追加してください。

tunnelMarkerHeaders #

既定値は [] です。プロキシやトンネルのエッジが、転送するすべてのリクエストに追加するヘッダーの名前です。リレーはこれを、下記のセッションワーカーのパスガードに使います。Cloudflare が追加する cf-ray は、常にこの一覧に含まれます。値は配列でも、カンマ区切りの文字列でもかまいません。環境変数 COPILOT_TUNNEL_MARKER_HEADERS(カンマ区切り)で名前を追加できます。0.9.11 で追加 2 つの一覧はマージされるので、tunnelMarkerHeaders: [] で環境変数が無効になることはなくなりました。環境変数を参照してください。

ステータスドット

tunnelMarkerHeaders によってステータスドットが琥珀色になることはありません。ドットが琥珀色になるのは、リレー自身が管理する Cloudflare トンネルが接続している間です(会話メニューを参照)。自分で運用するプロキシでは、琥珀色にはなりません。

セッションワーカーのパスガード #

各エージェントセッションはリレーのマシン上のワーカーで動き、ワーカーは /api/session-worker/ws(プレフィックスがある場合は <prefix>/api/session-worker/ws)の WebSocket でリレーと通信します。プロキシやトンネルは、そのホスト名のすべてのパスをリレーに転送するので、ガードがなければこのパスはインターネットから到達できてしまいます。これを使うべきなのはローカルのワーカーだけです。

0.9.11 で追加 ガードは、このパスへのリクエストを、リレー自身のマシンから来ていて何にも転送されていない場合にだけ通します。

  • 接続しているピアがループバック(127.0.0.1 または ::1)である。
  • マーカーヘッダー(cf-ray、または tunnelMarkerHeaders にあるもの)を含まない。
  • X-Forwarded-For、Forwarded、X-Real-IP を含まない。

それ以外は、サインインを確認する前に拒否されます。

  • 通常のリクエストには、{"error":"Session worker endpoints are reachable from this machine only"} とともに 403 が返ります。
  • WebSocket のアップグレードには 403 Forbidden が返り、接続が閉じられます。

拒否のたびに [tunnel-guard] Blocked session-worker path …: not local (…) というログが出て、リクエストの何が決め手になったかが示されます。ローカルのワーカーはこれらのヘッダーなしで 127.0.0.1 に直接接続するので、影響を受けません。共有リンクはトンネル経由でも動作し続けます。ガードが見るのはワーカーのパスだけで、リクエストが匿名かどうかではありません。

Cloudflare は cf-ray を、Caddy は X-Forwarded-For を自動で追加するので、これらでは設定なしでガードが機能します。nginx では、proxy_set_header で X-Forwarded-For を送ってください。SSH のリバーストンネルや素の TCP プロキシのように何も追加しない転送手段は、ローカルのプロセスのように見えます。その場合、パスを守るのはアクセストークンだけです。そうした転送手段では、空でない固定値のヘッダー(例:X-Oar-Edge: 1)を追加させ、その名前を "tunnelMarkerHeaders": ["x-oar-edge"] のように列挙してください。またはリレーの前段でこのパスを拒否してください。

リレーのマシンだけが使えるエンドポイント #

0.9.11 で追加 次の 2 つは、パスガードと同じ規則で、プロキシが転送していない、リレー自身のマシンからのリクエストにだけ応答します。

  • 上記のセッションワーカーのソケット。
  • 画像操作の実行(POST /api/image-operations/…/execute)。リレー自身のワーカーが呼び出します。それ以外には 403 Image operation execution is reachable from this machine only(画像操作の実行はこのマシンからのみ利用できます)が返ります。

それ以外はすべて、どこから来たかに関係なく、アクセストークンだけが必要です。これには 🌄 Restart web relay(Web リレーを再起動)、キューに入れた再起動をそのバナーの Cancel(キャンセル)ボタンで取り消すこと、🚮 Empty queue(キューを空にする)が含まれます。サインイン済みのブラウザーなら、LAN 経由でも、どのプロキシやトンネル経由でも使えます。

0.9.10 までは、これら 3 つはリレーのマシン専用のはずでしたが、確認の仕組みがトンネルや同じマシン上のプロキシ経由のものをすべて通し、LAN からの直接接続は拒否しながらページには成功と表示していました。リモートのユーザーにリレーの再起動やキューを空にすることをさせたくない場合は、プロキシで POST /api/relay/shutdown、POST /api/relay/shutdown/cancel、POST /api/queue/empty を拒否してください。リレーのマシン上の oar restart は常に使えます。

Cookie と HTTPS #

サインイン用 Cookie は HttpOnly かつ SameSite=Lax で、30 日間有効で、パスは /(または remotePath のプレフィックス)です。リレーがリクエストを安全とみなした場合は Secure が付きます。

  • 同じマシン上のプロキシが TLS を終端する場合。 プロキシが X-Forwarded-Proto: https を送り、既定の trustProxy がそれを信頼するので、Cookie は Secure になります。
  • 別のマシン上のプロキシが TLS を終端する場合。 上記のとおり trustProxy を設定します。設定するまで Cookie に Secure フラグは付きませんが、Cookie 自体は機能します。
  • 混在した構成。 LAN 上で平文の http でもリレーを直接開く場合、そのリクエストは安全ではないので、その Cookie に Secure フラグは付きません。プロキシ経由の HTTPS のリクエストにはフラグが付きます。ブラウザーは Cookie をアドレスごとに保持します。

設定を確認する #

  1. プロキシ経由のアドレスをブラウザーで開きます。サインインか会話リストが表示されます。
  2. ステータスドットを見ます。緑色になり、ターンを開始するとページにライブ更新が表示されます。再接続中の状態が終わらない場合は、WebSocket が通っていません。
  3. プレフィックスの下では、末尾にスラッシュを付けてアドレスを開きます。ページ、そのスタイル、ライブ接続が読み込まれ、Cookie のパスがプレフィックスになっています(ブラウザーの開発者ツールで確認します)。
  4. 共有リンクを作成します(共有)。そのアドレスは公開ホスト名と https を使い、プレフィックスを設定していればそれも含みます。
  5. 外部からガードをテストします。https://relay.example.com/api/session-worker/ws にリクエストを送ります。403 が返り、リレーのログに [tunnel-guard] の行が出るはずです。別の応答が返る場合、プロキシはマーカーヘッダーも X-Forwarded-For も追加していません。
  6. 別のマシン上のプロキシの背後では、2 台のデバイスからサインインし、Settings → Sharing(設定 → 共有)で、共有リンクのアクセスログにプロキシではなく閲覧者のアドレスが表示されることを確認します。プロキシのアドレスが表示される場合、trustProxy がプロキシと一致していません。

うまくいかないとき #

表示される内容 考えられる原因
ページは読み込まれるが再接続を繰り返す、または更新されない プロキシが /socket.io/ で WebSocket のアップグレードを通していないか、アイドル状態の接続を閉じています。
ページは読み込まれるが何もライブで更新されず、ログに Refused a handshake from origin と出る プロキシが Host ヘッダーを書き換えています。公開しているオリジンを allowedOrigins に列挙するか、プロキシで Host をそのまま渡してください。
サインインで、全員に同時に Too many failed sign-in attempts(サインインの失敗が多すぎます)と表示される リレーが別のマシン上のプロキシを信頼していないため、すべての訪問者がそのアドレスとそのロックアウトを共有しています。trustProxy を設定してください。
プレフィックスの下で、ページが空白になる、またはファイルが見つからない remotePath が設定されていないか、プロキシがプレフィックスを取り除いています。プロキシはプレフィックスを付けたまま渡す必要があります。
プレフィックスの下で、何度もサインインを求められる Cookie のパスは remotePath のプレフィックスなので、ブラウザーはそのプレフィックスの下でだけ Cookie を送ります。リレーはそのプレフィックスの下で、末尾にスラッシュを付けて開いてください。
共有リンクやファイルのリンクのアドレスが誤っている、またはプレフィックスがない remotePath が設定されていないか、プロキシが Host も X-Forwarded-Host / X-Forwarded-Proto も送っていないか、リレーがプロキシを信頼していません。
別のマシン上のプロキシの背後で、共有リンクが http:// で始まる、または Cookie に Secure フラグがない trustProxy がまだ "loopback" のままです。
プロキシ経由で、ワーカーのパスが 403 以外を返す プロキシがマーカーヘッダーも X-Forwarded-For も追加していません。パスガードを参照してください。
ドットが琥珀色にならない 想定どおりです。そうなるのは、リレー自身が管理する Cloudflare トンネルだけです。
新しい名前でプレビューが起動を拒否する リレーのホスト名を publicHostnames に追加してください。

最初のサインインとアクセストークンについては、初回起動を参照してください。