ノート PC で何かが動いていて、インターネットの反対側にある何かがそこへ到達する必要がある。たとえば開発サーバー、API、あるいは自宅の NAT の内側にあるマシンです。グローバル IP はなく、ルーターにも手を出せず、VPN を立てる時間もありません。そんなときに使うのが SSH リバーストンネル ssh -R です。到達できるサーバー側にポートを 1 つ出現させ、そこに届いた通信をすべて静かにあなたのマシンへ送り返します。
このフラグは多くの人が向きを取り違えるので、まず頭の中のモデルを整えてから、実際のケースを一つずつ見ていきましょう。
考え方:-L の鏡写し
どの SSH トンネルも、片方のマシンでリッスンするポートを開き、そこに届いたものを、もう一方のマシンから到達できるどこかへ送り届けます。ローカル転送(-L)では、ポートはあなたのマシンに開き、通信はサーバー側から出ていきます。リバーストンネルは、その正確な鏡写しです。
-R では、リッスンするポートがリモートサーバーに開き、そこへの接続があなたの側から出てきます。
仕掛けはこれがすべてです。受信接続を受け付けられないマシンが、代わりにアウトバウンドの SSH 接続を行い、その接続に乗せて逆向きの接続を受け取るのです。それでも 2 つが混ざって見えるなら、一家総出の全体像はSSH ポート転送の解説にあります。
構文を左から右に読む
ssh -R [bind_address:]remote_port:localhost:local_port user@server
次のように読みます:サーバー上で remote_port をリッスンし、そこに届いたものを(自分のマシンから見た)localhost:local_port へ転送する。 つまり:
ssh -R 8080:localhost:3000 deploy@vps.example.com
は VPS 上でポート 8080 を開き、そこへの接続をすべてノート PC のポート 3000 に着地させます。宛先はノート PC 自身である必要はありません。-R 8080:192.168.1.50:80 は、ノート PC を出口として自分の LAN 上の別のマシンへ転送します。
覚えておく価値のある使い方が 1 つあります。-R 0:localhost:3000 はサーバーに空きポートを 1 つ選ばせ、Allocated port 43022 for remote forward と表示するので、割り当てられたポートが分かります。
実際に作られる 3 つのもの
ローカルの開発サーバーをクライアントに見せる。 作りかけのものが localhost:3000 で動いています。デモのためだけにどこかへデプロイする代わりに:
ssh -N -R 8080:localhost:3000 deploy@demo.example.com
demo.example.com:8080 に到達できる人は誰でも、いまやノート PC 上のアプリをそのままライブで見ています。(-N はリモートシェルを省きます。欲しいのはトンネルだけです。誰でも接続できるのか、それともサーバー自身だけなのかは GatewayPorts 次第で、これは次のセクションで扱います。)
NAT の内側の自宅マシンに到達する。 自宅のマシンは接続を受け付けられませんが、こちらから接続することはできます。小さな VPS へ向けてトンネルを張らせておきます:
# on the home machine
ssh -N -R 2222:localhost:22 tunnel@vps.example.com
これで VPS のポート 2222 が自宅マシンの SSH ポートそのものになり、どこからでも:
ssh -J vps.example.com -p 2222 you@localhost
で VPS を経由して NAT の内側のマシンへ直接飛び込めます。これは自宅サーバーに SSH する方法に対する、昔ながらのセルフホストでの答えです。
Webhook をノート PC に届ける。 決済プロバイダーや Git ホストが、開発中のハンドラーへ POST する必要があります。到達可能なエンドポイントを手元へトンネルで戻します:
ssh -N -R 9000:localhost:9000 deploy@vps.example.com
Webhook の宛先を VPS に向けます。トンネルをサーバーのループバックにバインドしたまま(既定)にしておくなら、VPS 上の 127.0.0.1:9000 の前に nginx などのリバースプロキシを置きます。TLS 終端が無料で手に入り、生のトンネルポートを公開せずに済みます。
GatewayPorts:なぜ外部からトンネルに到達できないのか
一番の驚きどころです。サーバー上から試すとトンネルは動く(curl localhost:8080 が成功する)のに、外からは誰も接続できない、という現象です。これは仕様です。既定では、どんなバインドアドレスを指定しても、sshd はリモート転送のポートを 127.0.0.1 だけにバインドします。
トンネルを世界に向けて開くには、サーバー側の /etc/ssh/sshd_config に次の設定が必要です:
GatewayPorts clientspecified
そのうえで sshd を再起動し、バインドを明示的に要求します:ssh -R 0.0.0.0:8080:localhost:3000 ...。(GatewayPorts yes はすべてのリモート転送を全インターフェースにバインドするので、たいていの場合は広すぎます。)これはサーバー側の判断で、クライアントからは上書きできません。意図的な安全側の既定です。リバーストンネルは内側から開けた穴であり、そこで公開されるものは事実上インターネットに面することになるからです。そのつもりで扱ってください。
関連するサーバー側のスイッチが 2 つ、思わぬ落とし穴になります。AllowTcpForwarding no はトンネルを完全に無効化し、PermitListen は -R が確保できるポートを制限できます。
接続を維持する
リバーストンネルは 1 本の TCP 接続であり、アイドル状態の TCP 接続はいずれ切れます。デモより長く使うものには、すべて:
ssh -N -R 2222:localhost:22 \
-o ServerAliveInterval=30 \
-o ExitOnForwardFailure=yes \
tunnel@vps.example.com
ServerAliveInterval 30は通信を流し続け、NAT のエントリやアイドルタイムアウトで接続が刈り取られないようにします。ExitOnForwardFailure yesは、リモートのポートをバインドできないときに ssh を明示的に終了させます。接続だけして何も転送しない、という状態を防ぎます。autosshはコマンドをラップし、トンネルが切れるたびに再起動します。再起動や不安定なネットワークをまたいで生き残るべきトンネルのための定番ツールで、通常は systemd ユニットの下でautossh -M 0 -N -R ...として実行します。
そして、トンネルが恒久的な居場所を得たら、RemoteForward の行として ~/.ssh/config に移し、ホストのエントリと一緒に持ち運べるようにします。構成については ssh_config ガイド(英語)で扱っています:
Host homelink
HostName vps.example.com
User tunnel
RemoteForward 2222 localhost:22
ServerAliveInterval 30
ExitOnForwardFailure yes
-L・-R・-D を一目で
| フラグ | ポートが開く側 | 通信が出る側 | 典型的な用途 |
|---|---|---|---|
-L | 自分のマシン | サーバー側 | リモートの DB/ダッシュボードを localhost に引き込む |
-R | サーバー | 自分の側 | ローカルのサービスを公開する、NAT の内側のマシンに到達する |
-D | 自分のマシン | サーバー側(任意の宛先) | ネットワークへの SOCKS プロキシ |
リバーストンネルが適さない場面
リバーストンネルは、一時的な答えとしては抜群です。デモ、Webhook のデバッグ、NAT の内側のマシンの救出などです。一方、恒久的な答えとしては不向きです。公開したいものが何か月も稼働し続け、実ユーザーに応え、ノート PC のスリープをまたいで生き残る必要があるなら、それはイングレス(受信)であり、イングレスには本物のインフラがふさわしいのです。サーバー上のリバースプロキシ、VPN やオーバーレイネットワーク、きちんとしたデプロイです。トンネルの魅力は、常駐する痕跡を残さず 1 つのコマンドで済むことにあります。そのトンネルの監視を書き始めた時点で、もうトンネルでは手に負えなくなっています。
よくある失敗のトラブルシューティング
Warning: remote port forwarding failed for listen port 8080(リッスンポート 8080 のリモートポート転送に失敗)——サーバー側でそのポートがすでに使われています。犯人はたいてい自分の前回のトンネルです。古い sshd プロセスが、切れた接続がタイムアウトするまでポートを握り続けます。キープアライブでゾンビを防ぎ、-R 0:...で衝突を回避し、ExitOnForwardFailureで見逃せないエラーに変えられます。- サーバーからは動くのに外からは拒否される——リスナーがサーバーのループバックにいます。これは
GatewayPortsの既定動作が正しく働いている証拠で、設定するか、リバースプロキシをポートの前に置いてください。 - トンネルはつながるのにサービスが応答しない——ローカル側を確認します。指定したローカルポートで本当に何かがリッスンしていますか。
-R 8080:localhost:3000で:3000に何もなければ、訪問者全員が connection refused になります。 - SSH 接続そのものが確立しない——これは別の問題で、独自のチェックリストがあります:connection refused と timed out を読み解く。
トンネルが日々の設備になるとき
一度きり実行するリバーストンネルなら、手で打てばよいでしょう。ですが、自宅のラボや、デモ用のマシン、Webhook のエンドポイントを運ぶトンネルは、ノート PC がスリープするたびに毎朝組み直す状態であり、木曜にはどれがどれだか分からなくなります。Termalin の保存できるトンネルマネージャーは、ローカル・リモート・ダイナミックの各転送を、それらが属するホストにひも付けて保持します。クリックで起動でき、どれが稼働中か一目で分かり、接続と一緒に再確立されるので、シェルの履歴から打ち直す必要はありません。ホスト一覧(鍵、踏み台ホスト、キープアライブ、トンネルを含む)は、複数のマシン間でエンドツーエンド暗号化で同期します。そして、あるマシンがずっと NAT の内側にある場合は、アウトバウンド専用の Connector が、手作りのリバーストンネルをまるごと置き換えます。受信ポートは不要で、世話をする autossh ユニットもありません。コマンドラインは -R の意味を教えてくれます。クライアントは、頼りにしているトンネルをあなたの記憶に依存させずに済ませてくれます。
Termalin は、トンネル・SFTP・鍵マネージャーを内蔵した無料のクロスプラットフォーム SSH クライアントです。ダウンロードするか、鍵を安全に扱う仕組みをご覧ください。