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

SSH トンネル経由でリレーに接続する

リレーは自分のサーバーに SSH で接続し、そのサーバーにポートをリレーへ転送し返すよう依頼できます。そうすると、サーバーのアドレスでリレーに接続でき、自宅ではインターネットに向けて待ち受けるものが何もなくなります。ssh のプロセスは、リレー自身が起動し、監視し、再起動します。

公開アドレスと SSH アクセスのあるサーバーをすでに持っている場合に選んでください。サーバーがない場合は、組み込みの Cloudflare Tunnel ならサーバーは不要です。

必要なもの #

  • リレーのホストの PATH に ssh プログラムがあること(または、下記の command で別のプログラムを指定すること)。
  • サーバーへの SSH ログインが、鍵で、確認なしにできること。リレーは ssh をバッチモードで実行するため、パスワードや鍵のパスフレーズを入力できません。パスフレーズ付きの鍵は、SSH エージェントがそれを保持している場合にだけ使えます。まずリレーのホストから試してください。ssh relay@relay.example.com が何も尋ねずにログインできる必要があります。
  • トンネル用の、サーバー上の空いているポート(remotePort)。
  • サーバー上で、そのポートを HTTPS で提供するもの(Caddy や nginx などの Web サーバーまたはリバースプロキシ)。トンネルはただの HTTP を運ぶので、これがないと、ポートに到達できるようになった時点で、トークンと会話が暗号化されずにインターネットを通ります。リバースプロキシの背後でリレーを動かすを参照してください。

リレーには sshd の特別な設定は要りません。remoteBind が loopback の場合、リレーはバインドアドレスなしで転送を要求するので、ポートが開く場所はサーバー自身の GatewayPorts の設定で決まります。既定値の no では、それはサーバー自身だけになり、インターネットに面するのはリバースプロキシだけです。public の場合、リレーはすべてのインターフェース(*:)を要求し、その要求はサーバーの sshd が許可している場合(GatewayPorts が clientspecified または yes)にだけ有効になります。

設定する #

config.json に sshTunnel ブロックを追加し(ファイルの場所を参照)、oar restart でリレーを再起動します。

"sshTunnel": {
  "mode": "managed",
  "required": false,
  "user": "relay",
  "host": "relay.example.com",
  "remotePort": 4444,
  "remoteBind": "loopback",
  "identityFile": "~/.ssh/id_ed25519"
}

サーバーでは、HTTPS をトンネルのポートに送ります。Caddy の場合:

relay.example.com {
    reverse_proxy localhost:4444
}

ブラウザーで https://relay.example.com/ を開き、アクセストークンでサインインします。リレーの起動時のバナーには Remote: の行が、oar url には QR コード付きで Tunnel URL: の行が出力されます。どちらも https:// に host の値を付けたもので、ポートは含まれず、トンネルが動作しているかも確認しません。そのため、正しいのはプロキシが標準のポートで HTTPS を提供している場合だけです。

sshTunnel のキー #

すべてのキーは sshTunnel ブロックに入ります。この表は、同じ一覧がある config.json と一致しています。

キー 型 既定値 動作
mode "disabled" または "managed" "disabled" managed にすると、リレーがトンネルを起動し、再起動します。
user 文字列 なし サーバー上の SSH ユーザーです。管理対象の場合は必須です。
host 文字列 なし サーバーのホスト名またはアドレスです。管理対象の場合は必須です。
remotePort 数値 なし サーバー上で開くポートです(1 ~ 65535)。管理対象の場合は必須です。
remoteBind "loopback" または "public" "loopback" loopback はバインドアドレスなしでポートを要求します(サーバーの GatewayPorts が決めます。必要なものを参照)。public はすべてのインターフェースを要求します。それ以外の値は loopback とみなされます。localhostOnly とは独立しています。
identityFile 文字列 なし 秘密鍵のファイルです。~ は展開されます。指定しない場合、ssh はエージェントとその既定の鍵を使います。
command 文字列 ssh SSH のプログラムで、サーバー上のクリーンアップにも使われます。名前は PATH 上で探されます。スラッシュを含むパスはそのまま使われ、相対パスはリレーのプログラムのフォルダーを基準にします。
required 真偽値 false トンネルが接続されていない間、リレーはキューからメッセージを取り出しません。必須を参照してください。
autoReclaimPort 真偽値 true リモートのポートがすでに使われているとき、サーバー上でクリーンアップを実行して再試行します。オフになるのは false のときだけです。
reclaimStaleSshSessions 真偽値 false そのクリーンアップの一部として、サーバー上の自分のアイドル状態の SSH セッションも終了します。
remoteCleanupCommand 文字列 組み込み 組み込みのものの代わりにサーバー上で実行する、自分のクリーンアップコマンドです。

古い形式の "enabled": true も引き続き使え、"mode": "managed" を意味します。ただし、mode が disabled または managed に設定されていない場合に限ります。

リレーは、サーバーの remotePort を 127.0.0.1 上の自分のポート(port を変更していなければ 3333)に転送します。public の場合、転送は *:remotePort として要求され、それ以外の場合は remotePort として要求されます。

次の設定は固定で、キーはありません。リレーは ssh -N を ServerAliveInterval=30、ServerAliveCountMax=3、BatchMode=yes、StrictHostKeyChecking=accept-new、ExitOnForwardFailure=yes で実行します。ssh は、30 秒間隔の確認に 3 回続けて応答がないと、その接続をあきらめます。

必須の値(管理対象モードでの user、host、または有効な remotePort)が欠けている場合、リレーは通常どおり起動し、どれが欠けているかをログに出し、トンネルはオフのままにします。

動作 #

起動。 リレーは、待ち受けを始めた後にトンネルを起動します。ssh が 1.2 秒間動作し、サーバーが転送を拒否していなければ、トンネルが接続されたとみなします。その後、ログに Tunnel up to user@host remote port N と出ます。

再接続。 ssh が終了すると、リレーは待ち時間を延ばしながら再び起動します。5、10、20、40、そして 60 秒で、以降は 60 秒のままです。30 秒より長く続いた接続が切れると、カウントは最初からやり直しになります。リレーを停止すると、トンネルと再試行も止まります。

ホスト鍵。 リレーのホストが初めて接続するサーバーは、最初の接続で受け入れられ、記憶されます。その後ホスト鍵が変わると、known_hosts から古いエントリを削除するまで、ssh は接続を拒否します。

必須 #

required: true の場合、キューは起動時からトンネルが接続されるまで一時停止し、接続が切れるたびに再び一時停止します。送ったメッセージはキューで待ち、トンネルが動作すると実行されます。トンネルなしではリレーが役に立たない場合に使ってください。値が欠けていて起動できないトンネルでも、キューは一時停止したままになります。

リモートのポートが使われているとき #

サーバーがポートをバインドできないと言う場合、よくある原因は、以前のトンネルのセッションが残っていて、それが切れていることにサーバーが気付いていないことです。autoReclaimPort が false でない限り、リレーはサーバーに 2 回目の接続をして、そこでクリーンアップコマンドを実行します。

これらのオプションはサーバー上でコマンドを実行します

クリーンアップは自分の SSH ログインで実行され、プロセスを終了させます。そのユーザーのプロセスでリモートのポートを待ち受けているものは、残ったトンネルであるかどうかにかかわらず終了します。reclaimStaleSshSessions を使うと、そのユーザーのアイドル状態の SSH セッションもすべて終了します。これには、同じログインでほかのポートに張っている ssh -N -R のトンネルも含まれます。remoteCleanupCommand は、書いたものを何でも実行します。オンのままにする前に、各手順が何をするかを読んでください。

組み込みのクリーンアップは、次の順で処理します。

  1. ポートが使われているかを確認します(ss を使い、なければカーネルの待ち受けソケットの一覧を読みます)。使われていなければ、そこで止まり、成功を報告します。
  2. ポートで待ち受けているプロセスを探し(lsof、なければ fuser のうち、存在するほう)、終了シグナル(kill)を送って 1 秒待ちます。自分のログインがシグナルを送れないプロセスは残ります。この時点でポートが空いていれば、成功を報告します。
  3. reclaimStaleSshSessions: true の場合に限り、かつポートがまだ使われている場合に限り、子プロセスを持たない自分のユーザーのすべての sshd セッションに終了シグナルを送り、1 秒待ちます。これらは ssh -N -R が残すセッションです。シェル、エディターのリモートサーバー、そのほかのプログラムを実行しているセッションは子プロセスを持つため、対象外です。クリーンアップを実行しているセッション自体も対象外です。
  4. ポートがまだ使われている場合、クリーンアップはコード 3 で終了し、リレーはそれを is still held とログに出します。

remoteCleanupCommand を設定すると、これらすべての代わりに自分のコマンドが実行され、reclaimStaleSshSessions は効果がなくなります。リレーはそれを、同じ鍵による 2 回目の ssh ログインで、バッチモードで sh -lc '<command>' として実行します。コマンドは、ポートが空いていれば 0 で、まだ使われていればそれ以外のコードで終了する必要があります。

ポートを空けられたクリーンアップの後、リレーは 1 秒後に再試行し、続けて最大 3 回まで繰り返します。クリーンアップが失敗した後、または autoReclaimPort がオフの場合は、上記の通常の待ち時間を使います。

トンネルの状態を見る #

SSH トンネルは、Cloudflare トンネルが表示される場所には表示されません。oar status と oar doctor が報告するのは Cloudflare トンネルだけで、ステータスドットも SSH トンネルでは琥珀色になりません(ステータスドットを参照)。代わりに次の場所を見てください。

  • リレーのログ。 すべての行が [ssh-tunnel] で始まります。実行する ssh のコマンドライン、ssh が出力する内容、Tunnel up to ...、Reconnecting in Ns (attempt N)...、ポートの回収の手順が表示されます。ログの場所はリレーの実行方法によって異なります。ファイルとフォルダーを参照してください。
  • 起動時のバナー。 管理対象のトンネルが設定されているとき、Remote: の行と Tunnel: mode=loopback(または public)の行があります。これらが示すのは設定であり、トンネルが動作しているかどうかではありません。oar url は同じアドレスを Tunnel URL: として出力します。
  • /api/status。 connected、connectedSince、reconnectAttempts、lastError、blocking、required、mode、valid、host、remotePort、remoteBindMode を持つ sshTunnel オブジェクトがあります。トークンを付けて取得してください。

lastError は、ssh が終了した後は exit:<code>、サーバーがポートを拒否した後は remote-forward-bind-failed:<code>、ブロックが不完全なときは invalid-config または値が欠けていることを示すメッセージになり、トンネルが動作している間は空です。

セキュリティ #

トンネルが動作すると、リレーにインターネットから接続できるようになり、見知らぬ人とあなたのエージェント(あなたのマシンでコマンドを実行します)との間にあるのはアクセストークンだけになります。そのため、次のようにしてください。

  • remoteBind は loopback のままにし、サーバー上で前段に HTTPS を置いてください。public の場合、サーバーのトンネルのポートに到達できる人は誰でも、ただの HTTP でリレーに到達します。
  • アクセストークンが唯一の鍵です。アクセストークンとリレーに接続できる範囲と OAR が公開するものを参照してください。
  • リレーは、内部のセッションワーカーのパスへのリクエストが Cloudflare のマーカーヘッダー cf-ray か、tunnelMarkerHeaders に記載したヘッダーを含む場合、そのリクエストを拒否します。通常のリバースプロキシはこれらを何も付け加えないので、プロキシが付け加えるヘッダーのうちブロックしたいものがあれば、それを記載してください。config.json を参照してください。

トラブルシューティング #

まずログの [ssh-tunnel] の行を見てください。stderr: で始まる行は、ssh 自身が出力したものです。

表示 意味 対処
sshTunnel.user is required when sshTunnel.mode is "managed"(host の場合も同様) 必須の値が欠けています。トンネルはオフのままです。 値を設定し、oar restart を実行します。
sshTunnel.remotePort must be a positive integer when sshTunnel.mode is "managed" ポートがないか、1 ~ 65535 の数値ではありません。 修正して再起動します。
Tunnel mode disabled; running direct relay only. mode が managed ではありません。 "mode": "managed" を設定します。
stderr: ... Permission denied (publickey) サーバーが鍵を受け入れませんでした。バッチモードのため、パスワードの入力は求められません。 identityFile を設定し、リレーのホストで同じ ssh user@host を手で試します。
Process exited (code=N). Scheduling reconnect... が繰り返される ssh が接続またはログインできませんでした。 ログに出た ssh コマンドを手で実行し、エラーを読みます。
stderr: ... remote port forwarding failed for listen port N の後に Attempting remote reclaim ... サーバー上のポートが使われています。 回収を待ちます。is still held で終わった場合は、自分でポートを空けるか、別の remotePort を選びます。
Remote reclaim finished: listen port N is still held (code=3) クリーンアップでポートを空けられませんでした。 サーバー上でポートを保持しているものを探すか、reclaimStaleSshSessions: true を試すか、remoteCleanupCommand を設定します。
stderr: ... Host key verification failed または REMOTE HOST IDENTIFICATION HAS CHANGED サーバーの鍵が、保存されているものと異なります。 変更が想定どおりなら、known_hosts から古いエントリを削除します。そうでなければ、接続しないでください。
Reconnecting in 60s (attempt N) が何度も出て、トンネルが Tunnel up にならない 接続が毎回失敗しています。 それぞれの前にある stderr: の行を読みます。
トンネルは Tunnel up と出ているのに、アドレスが応答しない リレー側は問題ありません。サーバー側に問題があります。 リバースプロキシが動作して remotePort を指していること、remoteBind がプロキシからの到達方法と合っていることを確認します。
トンネルが接続、切断、再接続を繰り返す サーバーかネットワーク上の何かが接続を終了させています。 ログに終了コードと ssh のメッセージが出ています。サーバーの sshd のログを確認します。
メッセージがキューに入ったままになる required が true で、トンネルが接続されていません。 トンネルを直すか、required を false に設定します。