つながるSSHトンネルが俺の力だ!

ハハッ!

SSHトンネルがあればなんでもできる。SSHトンネルがあれば リモートワークしている同僚のターミナルを共有することもできる(しゃくれながら)

@pjxiao が SSH を使いこなしているのを見て悔しかったので自分もちゃんと理解しようと思い記事にします。

ローカルフォワーディング

開発者にとってはこれが最も一般的な使い方かなーと勝手に思ってます。
DBや内部システムで使われるサーバではアクセス元制限がかかっていることが多いんですが、動作確認のために接続したくなることはよくあります。
特に開発用のDBのレコードをローカル環境のプログラムで表示したくなるケースはとっても多いです。ね?

こんなときに使うのが俗にSSHポートフォワーディングと呼ばれるもので、ローカルに対するアクセスをリモートに受け流す方法です。

-Lオプション

具体的には-Lオプションを使います。

ssh 踏み台ホスト -L ローカルポート:リモートホスト:リモートポート

-L オプションは ローカルへの通信をリモートにバインドするためのオプションです。

実例を示します。Webサーバ以外からは接続できない MySQL サーバがあります。
Webサーバのホストは `www.example.com` で MySQL サーバのホストは `mysql.example.com` とします。
Webサーバでは22番ポートが全体に対し、MySQLサーバでは3306ポートがWebサーバのみに対し公開されている(危ない)設定だと思ってください。
(実際に試した環境はこれではないんですが、公開できないので書き換えてます)

最初にリモートホストに直接接続を試みます。

$ mysql -u username -p -h mysql.example.com
Enter password:
ERROR 2003 (HY000): Can't connect to MySQL server on 'mysql.example.com' (60)

つながりませんでした。これは期待通りです。

次に以下のようにトンネルを張ります。

$ ssh www.example.com:22 -L 3307:mysql.example.com:3306
Last login: XXX XXX HH:MM:SS YYYY from aa.bb.cc.dd

www.example.com に SSH 接続されました。
少しわかりにくいのですがこの時点で localhost:3307 ポートと mysql.example.com:3306 がトンネルでつながっています。
(ポート番号をずらしたのは読んでいる方が混同しないためなので、同じにしてもOK)

さて、ここで別のターミナルでローカルの 3307 番に接続してみましょう。トンネル元(localhost側)でやってください

$ mysql -u username -h localhost --protocol tcp --port 3307 -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 69664

今度はつながりましたね。
脱線ですが –protocol tcp がないと unix domain socket で接続を試みるようです(ループバックアドレスでも良いとのこと)
ローカルポートのmysqlに対してTCPで接続する

プログラムであれば、DBの接続先を上記のようにローカルに合わせれば動くようになるはずです。
この例は MySQL ですが PostgreSQL などのDBも同様です。さらに言えばDBに限った話ではないです。

-Nオプション

-L オプションでトンネルは掘ってくれましたが、接続先サーバでシェルが起動してしまいました。
トンネルするためなのでシェルは要らないという場合、-N オプションを指定します。

$ ssh -N www.example.com:22 -L 3307:mysql.example.com:3306

するとこの状態で止まります。プロセスを止めない限りフォアグラウンドに居続けます。
止まってくれたほうがわかりやすい感じがしますが、この状態で十分でしょうか?

-fオプション

固まり続けるターミナルなんて必要ありませんよね?私は要りません。
同時に -f オプションをつけてみましょう。
このオプションを指定するとプロセスがバックグラウンドで実行されるようになります。

以下のように連ねて記述できます。(本当はLオプションも繋げられるけどね)

$ ssh -fN www.example.com:22 -L 3307:mysql.example.com:3306
$ # 別のコマンドを入力できる。

ちなみに -f オプションだけ指定はできません。エラーです。

$ ssh -f www.example.com:22 -L 3307:mysql.example.com:3306
Cannot fork into background without a command to execute.

多段接続する

ここまで読んで、「いやいや、WebサーバのSSHポート全体に公開してないからw」という人も多いでしょう。
たしかに踏み台と呼ばれる中間のサーバからしか接続を許可していないことがよくあります。

このような場合、SSH接続を複数繋げて多段にします。
登場人物(gateway.example.com)を一人増やして接続例を見てみましょう。

-tオプション

-tオプションの後ろには SSH 接続先のサーバで実行するためのコマンドを記述できます。
これを利用して更に SSH コマンドを書けば多段接続となります。

$ ssh gateway.example.com -t ssh www.example.com

とっても簡単ですね。更に接続を増やしたい場合、-t オプションを増やしていけばOKです。

先程のトンネルと組み合わせると以下のようになります。

$ ssh gateway.example.com -fNL 3307:mysql.example.com:3306 -t ssh www.example.com
$

ProxyCommand

上記のように毎回複数のホストを書くのは結構な手間です。
~/.ssh/config を設定することで最終ホストの指定だけで接続できるようになります。

Host www.example.com
    HostName www.example.com
    User username
    ProxyCommand ssh -W %h:%p gateway.example.com

更に接続を増やしたい場合、例えば gateway2 -> gateway -> www のようにホップしたいときは以下のように記述します。

Host www.example.com
    HostName www.example.com
    User username
    ProxyCommand ssh -W %h:%p gateway.example.com
    IdentityFile ~/.ssh/id_rsa

Host www.gateway.com
    HostName www.gateway.com
    User username
    ProxyCommand ssh -W %h:%p gateway2.example.com
    IdentityFile ~/.ssh/id_rsa2

Host gateway2.example.com
    HostName gateway2.example.com
    User username
    IdentityFile ~/.ssh/id_rsa3

SSH-Agentが有効な場合か中間サーバ側に接続用の設定がある場合は IdentityFile の設定は不要です。
多段sshの方法

この設定をしておけばトンネル時に最終ホストだけを指定すれば良いので、長期的に利用する場合はこちらのほうがおすすめです。
今回 SSH-Agent の説明はしません。

SOCKSで接続する

SSH トンネルを SOCKS プロキシとみなす方法です。

プロキシはアクセス元やアクセス先の制限に利用されたりしますが、今回はアクセス元制限されているというシナリオを考えてみましょう。
さっきのローカルフォワーディングで良いでしょうか。答えは状況によるのですが概ね No です。
試しに hatenablog にローカルフォワーディングで繋いでみましょう。

$ ssh gateway.example.com -NL 8888:hatenablog.com:80

次にWebブラウザのアドレスバーに localhost:8888 にアクセス。

hatena_404

なんとかページは表示されましたが何故か404。
Webサーバにとっても、Webブラウザにとっても、あるいはWebアプリにとってもアドレス(ドメイン)というのは大事な意味を持ちます。サーバ側とブラウザ側でこの解釈が異なると表示が崩れてしまうのは仕方のないことなのです。
特に SSL 対応しているサイトは SSL証明書が ドメインに対して発行されているため、 localhost というドメインと一致せず弾かれることは目に見えています。

フォワーディングが適さない理由がわかりました。
こういう場合はプロキシ接続によって繋いであげる必要があります。(先程の「ProxyCommand」は関係ありませんよ)
じゃあ普通にHTTPプロキシたてればいいじゃんてことになりますが、ただ経由するだけならHTTPプロキシを建てるまでのことはないし、何よりめんどくさいです。

そこで SOCKS を使うことで SSH サーバだけで プロキシ接続できてしまいます。
Wikipedia によると

SOCKS は、ネットワーク・ファイアウォール越えやアクセス制御等を目的として、クライアントサーバ型のプロトコルが、透過的に使用できるよう設計されたプロキシ(proxy)のプロトコル、及びシステム(の一つ)である。”SOCKetS” [1] の略。

ということで、簡単に言うと HTTP にかぎらず大体どんなものでも通すプロキシです。

実際に SSH で SOCKS を使ってみましょう。

-Dオプション

-D オプションはアプリケーションレベルの動的なポート転送を指定します。
何を言ってるのかよくわかりませんが、SOCKS プロキシのためのオプションだと思えばOKです。

使い方は -L と大して変わりません。

$ ssh gateway.example.com -fND localhost:8888

Web ブラウザの プロキシ設定に移動し、SOCKS(5) の プロキシサーバ: localhost, プロキシポート:8888 となるように設定すればOKです。
HTTP プロキシではありません。はてなブログは見えましたか?

参考: https://www.kmc.gr.jp/advent-calendar/ssh/2013/12/14/tsocks.html

SSH (-D) トンネル は SOCKS プロキシのように振る舞えるがイコールではないので注意してください。
(おまけ)圧縮を指示する -C オプションと同時に使われることが多いようです。

リモートフォワーディング

ローカルフォワーディングでは公開されているサーバに対して通信をトンネルしていましたが、リモートフォワーディングは逆向きの通信をトンネルします。つまりローカルに対して通信をトンネルします。
なぜそんなことが必要かというと、ローカル環境のような非公開の端末に接続するのはネットワークの構成上難しいことが多く、リモートからローカルに対してトンネルを張るということはせず、ローカルからリモートにトンネルを張ってもらいます。それ以降の通信はリモートからローカルに行われるのでローカルフォワーディングとは逆になりますね。

ローカルフォワーディング リモートフォワーディング
接続時の通信方向 ローカル→リモート ローカル→リモート
接続以降の通信方向 ローカル→リモート リモート→ローカル

概念的には FTP のパッシブモードにちょっと似てるかも。ちょっとだけね。

Rオプション

これを実現するのが-Rオプションです。逆向き(リモートからローカルへ)のトンネルを貼ります。先ほどの -L オプションと対をなすイメージですね。

ローカル環境の sshd に接続して Xさん(仮称) と Fさん(仮称) の シェルを GNU Screen で共有するというシナリオで動かしてみましょう。(Xさんありがとう)
Xさん と Fさん はお互い違う場所におり、二人をつなぐ中間サーバとして gateway.example.com という公開 SSH サーバ があります。

ちなみにGNU Screen とはターミナル上で複数の仮想端末を管理するためのソフトウェアです。screen プロセスは標準入出力を記憶しているため、これを利用しリアルタイムな画面共有を実現しようという試みです。
使い方は GNU screen コマンド勉強録 でも解説しています。

ローカル環境に SSH 接続したいので https://hub.docker.com/r/rastasheep/ubuntu-sshd/ のイメージを使って用意します。vagrant とか使ってる人はデフォルトでSSHが動作してるので楽ですね。

今回は Fさん 側のシェルを共有するので、Fさん側で色々準備します。

$ docker pull rastasheep/ubuntu-sshd
$ docker run -d -p 22 rastasheep/ubuntu-sshd /usr/sbin/sshd -D
e0015af4ec5cea58f39a12e22942d3389c6e7ca3bb82496be785a89eab812742
$ docker ps
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS                   NAMES
e0015af4ec5c        rastasheep/ubuntu-sshd   "/usr/sbin/sshd -D"      10 seconds ago      Up 6 seconds        0.0.0.0:32769->22/tcp   elated_easley

$ ssh root@localhost -p 32769
The authenticity of host '[localhost]:32769 ([::1]:32769)' can't be established.
ECDSA key fingerprint is SHA256:9U5v1a2QycyWSEGFL3GnU6OAaTmWjKaScIDGKlbH5so.
ECDSA key fingerprint is MD5:0e:fc:94:f4:8e:f6:bd:2a:c8:42:7c:4c:86:51:05:b2.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[localhost]:32769' (ECDSA) to the list of known hosts.
root@localhost's password: # root で入れる
root@e0015af4ec5c:~# apt-get install screen
Reading package lists... Done
(...後略)

準備が完了したので接続してみましょう。

流れ Fさん Xさん
トンネル
# docker ps で表示されてるポートに合わせてトンネルを張る
$ ssh gateway.example.com -R 22222:localhost:32769
# Fさんの作ったトンネルに接続する
$ ssh gateway.example.com -t ssh root@localhost -p 22222
root@localhost's password: # root と入力
スクリーン
root@e0015af4ec5c:~# screen -S shared_screen
root@e0015af4ec5c:~# screen -x shared_screen # アタッチする
共有中
root@e0015af4ec5c:~# date
Thu Dec 14 14:37:15 UTC 2017
root@e0015af4ec5c:~# date
Thu Dec 14 14:37:15 UTC 2017

同じ画面が見えていますね。共有できたのでこれにて一件落着です。
Unix domain socket 同士で Screen だけ使って画面共有できたりしないかなーと思ったんですけど 接続が fail して共有できませんでした。(調べたけど時間切れ)

その他参考にしたリンク: