つながるSSHトンネルが俺の力だ!
2017-12-15

ハハッ! SSHトンネルがあればなんでもできる。

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

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

Local forwarding

開発者にとってはこれが最も一般的な使い方かなーと勝手に思ってます。

DBや内部システムで使われるサーバではアクセス元制限がかかっていることが多いんですが、動作確認のために接続したくなることはよくあります。

特に開発用のDBのレコードをローカル環境のプログラムで表示したくなるケースはとっても多いです。ね?

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

L option

具体的には -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:3307mysql.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に限った話ではないです。

このようにSSHトンネルを作った後のエンド間のやりとりは SSH を意識しなくてもよいのです。 今回の例で言うと MySQL クライアント は localhost:3307 にある MySQL サーバ に接続しているのと同じことで、これがSSH トンネルかどうかなんて知っている必要はありません。

N option

そういえば -L オプション でトンネルは掘ってくれましたが、接続先サーバでシェルが起動してしまいました。

トンネルするためなのでシェルは要らないという場合、 -Nオプション を指定します。

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

するとこの状態で止まります。プロセスを止めない限りフォアグラウンドに居続けます。

止まってくれたほうがわかりやすい感じがしますが、この状態で十分でしょうか?

f option

固まり続けるターミナルなんて私は要りません。今度は同時に -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.

多段接続する (Multi stage connection)

ここまで読んで、「いやいや、WebサーバのSSHポート全体に公開してないからw」という人も多いでしょう。

たしかに踏み台と呼ばれる中間のサーバからしか接続を許可していないことがよくあります。 このような場合、SSH接続を複数繋げて多段にします。

登場人物(gateway.example.com)を一人増やして接続例を見てみましょう。

t option

-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

更に接続を増やしたい場合、例えば

  1. gateway2
  2. gateway
  3. 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-Agent の説明はしません。

SOCKS で接続する

アクセス元が制限されたWebサイトへアクセスするにはどうしたらいいでしょう。

さっき説明したフォワーディングを使う? 答えは状況によるのですが概ね 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 option

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

使い方は -Lオプション と大して変わりません。

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

Web ブラウザの プロキシ設定に移動し、SOCKS(5) の

プロキシホスト localhost
プロキシポート 8888

となるように設定すればOKです。(HTTPプロキシではありません) FireFox だとこんな感じ。

firefox_proxy

これで設定は終わりです。

どうでしょうか。はてなブログは見えましたか?(キャプチャは撮ってないのでご自身でお確かめください)

SSH (-D) トンネル は SOCKS プロキシのように振る舞えるがイコールではないので注意してください。

(おまけ)圧縮を指示する -C オプションと同時に使われることが多いようです。

Remote forwarding

ローカルフォワーディングでは公開されているサーバに対して通信をトンネルしていましたが、リモートフォワーディングは逆向きの通信をトンネルします。つまりローカルに対して通信をトンネルします。

なぜそんなことが必要かというと、 ローカル環境のような非公開の端末に対してリモートから接続するのはネットワークの構成上難しいことが多いためです。

これを解決するために、ローカルからリモートにトンネルを張ってもらい、 それ以降の通信はリモートからローカルに行われるのでローカルフォワーディングとは逆になります。

ローカルフォワーディングでは対応できないのですね。

  ローカルフォワーディング リモートフォワーディング
接続時の方向 ローカル→リモート ローカル→リモート
接続移行の方向 ローカル→リモート リモート→ローカル

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

R option

これを実現するのが -Rオプション です。

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

ローカル環境の sshd に接続して Xさん(仮称)Fさん(仮称) の シェルを GNU Screen で共有するというシナリオで動かしてみましょう。 (Xさんありがとう)

Xさん と Fさん はお互い違う場所におり、二人をつなぐ中間サーバとして gateway.example.com という公開 SSH サーバ があります。

備考

  • ちなみに GNU Screen とはターミナル上で複数の仮想端末を管理するためのソフトウェアです。
  • 参考: GNU screen コマンド勉強録

screen プロセスは標準入出力を記憶しているため、これを利用しリアルタイムな画面共有を実現しようという試みです。

ローカル環境に SSH 接続したいので 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 接続
$ 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 と入力
スクリーン
# Xさんが接続するためのセッションを作る
root@e0015af4ec5c:~# screen -S shared_screen
# Fさんが作ったセッションにアタッチする
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

左右で同じ画面が見えていますね。共有できたのでこれにて一件落着です。

Unixdomain socket 同士で Screen だけ使って画面共有できたりしないかなーと思ったんですけど 接続が fail して共有できませんでした。(調べたけど時間切れ)

参考