VPNモドキ over ssh for Subversion

古典的なネタですが,毎回,一度設定すると忘れるので(笑),きっちりメモっておこうと思います.
SSHのキー認証のみをもちいて,ポートフォワードを使ってVPNもどきを実現する方法です.

次のような状況を考えます.

                       |                         port=10000開放       port=20000(LAN内)開放
 +--------------+    +------+                    +--------------+      +-----------+
 |モバイルホスト|<-->+ルータ|<--インターネット-->|Firewallマシン|<---->|LAN内ホスト|
 +--------------+    +------+                    +--------------+      +-----------+
    userA              |                             userB                 userC
                      =====>                 <----------------->
            外へのアクセスのみ可                外へは自由,外からはポート100000sshのみ受け入れのみ可

さらに,sshの認証としては,モバイルホスト→Firewallマシン,Firewallマシン→LAN内ホストは,パスワードの有無を問わず,矢印前のマシンのキーの公開鍵が,矢印後のマシンのauthorozied_keysに登録されており,サクっとsshで入れるとします.
つまり,モバイルホストからFirewallマシンには,

[モバイルホスト] # ssh -p 10000 userB@Firewallマシン

でログイン可能,FirewallマシンとLAN内ホストは,互いに次のようにログイン可能だとします.

[Firewallマシン] # ssh -p 20000 userC@LAN内ホスト

[LAN内ホスト] # ssh -p 10000 userB@Firewallマシン

従って,2回,sshを使えば,「モバイルホスト」から「LAN内ホスト」にログインできることを前提とします.

[モバイルホスト] # ssh -p 10000 userB@Firewallマシン
[Firewallマシン] # ssh -p 20000 userC@LAN内ホスト

しかし,Firewallが無いかのように,一発でサクっとログインしたい!」ということを実現することとします.
それは,例えば外部のマシンから,subversionsvn+sshプロトコルでプログラムをLAN内のsubversionサーバとやりとりするようなことをやりたいわけです.

いくつか方法がありますが,今回はモバイルホストから,「ssh -p 15000 localhost」と打ち込むと,LAN内ホストにログインできる」ことを実現します.
localhostで全て解決すると,名前解決を考えなくていいんので,色々とメリットがあるんですね.途中のポートも自由に変更できますし.
これで,LAN内ホストにsubversionしたり,LAN内ホストからpopしたりと何でもできます.

モバイルホストから見て,マシンを1段踏み台にして2段目に入りたいので,当然,ポートフォワードが必要です.

まず,Firewallマシン上で,Firewallマシン→LAN内ホストを中継するポートフォワーダーが常に動いている必要があります.これは,モバイルホストがunix系統ならsshのデーモンで良いでしょうし,WindowsならportforwarderでもTeraTermでも構いません.
私は,NATのkeepaliveの関係で,常にpingでハートビートを打つようにしています.例えば,次のようなコマンドが,モバイルホスト上で常に動いているようにデーモン化します.

[モバイルホスト]# ssh -p 10000 -L 15000:LAN内ホスト:20000 userB@Firewallマシン 'ping -i 300 localhost'

この設定は,最初からセキュリティをチェックして公開されているFirewallマシンのポート10000を使ってログインしているだけなので,セキュリティ的には何の問題もありません.また,ポート転送をかけているFirewallマシンのポート15000も,ポート転送されているLAN内マシンのポート20000も,外部には公開されていないので問題ありません.

うまくいけば,モバイルホスト上で次のように動作確認ができます.

[モバイルホスト]# telnet localhost 15000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
SSH-2.0-OpenSSH

この場合,sshの設定がちゃんと出来ていれば,次のようにモバイルホストからLAN内ホストにログインできます.
(LAN内ホストのuserCのauthorized_keysに,モバイルホストのuserAの公開鍵の登録が必要です)

[モバイルホスト]# ssh -p 15000 userC@localhost
Last login: Tue Nov 20 00:00:04 2007 from モバイルホスト
[LAN内ホスト]#

あと,ポートフォワードのsshですが,そのままではpingの結果が画面に出力されてウザいと思います.なのでデーモン化なのですが,(1) crontabに書き込んで,定期的に落ちていないかチェックしつつ,cronで実行してもらう,(2) nohupコマンドを使う,等があります.
(1)が良いと思いますが,とりあえずということでしたら,

[モバイルホスト]# nohup ssh -p 10000 -L 15000:LAN内ホスト:20000 userB@Firewallマシン 'ping -i 300 localhost' &

等と打ち込んで,一度端末を抜けます.これでnotty状態になります.
(1)のようにcrontabを使う場合,スクリプトを書くと良いと思います.例えばモバイルホスト上での場合...
FreeBSDなら...

#!/bin/tcsh
ex=`ps ax|egrep "[0-9] ssh -p 10000 -L 15000:LAN内ホスト:20000"|wc -l`
if ($ex == 0) then
nohup  sudo -u userA ssh -p 10000 -L 15000:LAN内ホスト:20000 userB@Firewallマシン 'ping -i 300 localhost' &
endif

*/3 * * * * /usr/local/sbin/vpn_to_lan.sh >& /dev/null

Linuxなら...

#!/bin/bash
ex=`ps ax|egrep "[0-9] ssh -p 10000 -L 15000:LAN内ホスト:20000"|wc -l`
if [ $ex -eq 0 ];
then
nohup  sudo -u userA ssh -p 10000 -L 15000:LAN内ホスト:20000 userB@Firewallマシン 'ping -i 300 localhost' &
fi

*/3 * * * * /usr/local/sbin/vpn_to_lan.sh >/dev/null 2>&1

とかですか...あんまり検証していないので,いろいろお試し下さい.

さて,これでlocalhostにuserCでsshすることで,LAN内ホストに入れるようになったのですが...なぜか,subversionがうまく動きません.
svnserveの-t(トンネリングモード)が,sshのポート番号や認証鍵やユーザ名を認識してくれないのです.
最初,

[モバイルホスト]# svn ls svn+ssh://localhost/svn-repo
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
(中略)
svn: Connection closed unexpectedly

となったので,まぁ,localhostを使いまわす限りこれは仕方が無い,無理矢理消そうと思って

FreeBSD:

[モバイルホスト]# setenv SVN_SSH "ssh -o StrictHostKeyChecking=no"

Linux:

[モバイルホスト]# export SVN_SSH="ssh -o StrictHostKeyChecking=no"

とやったのですが,

[モバイルホスト]# svn ls svn+ssh://localhost/svn-repo
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
(中略)
Keyboard-interactive authentication is disabled to avoid man-in-the-middle attacks.
svn: No repository found in 'svn+ssh://localhost/svn-repo'

ログメッセージを見ると,ポート22にsshログインしている模様.なので,ポートもアカウントも指定しました.

FreeBSD:

setenv SVN_SSH "ssh -o StrictHostKeyChecking=no -p 15000 -l userC"

Linux:

export SVN_SSH="ssh -o StrictHostKeyChecking=no -p 15000 -l userC"

これで,いけました.

[モバイルホスト]# svn ls svn+ssh://localhost/svn-repo
Makefile.in
[モバイルホスト]# svn log svn+ssh://localhost/svn-repo -r head

                                                                                                                                              • -

r01 | userC | 2007-11-27 17:00:0 +0900 (Tue, 27 Nov 2007) | 3 lines

project start

                                                                                                                                              • -

なお,今回はsubversionサーバは,同僚がOpenVZ内で立ち上げてくれました.従って本設定は,同一の物理マシンの中にFirewallマシンとLAN内ホストがあっても使えますし,何かあったら仮想マシンであるsubversionサーバを落とせば被害が最小で良い(笑)という利点があります.