10. sshポートフォワーディングの利用

このコラムではsshのポートフォワーディングを用いて、 二つのネットワーク間を行き来するアプリケーションの作成について考えます。

10.1 実験

ssh(Secure SHell)には、暗号化された通信路を用いて 一方のマシンのTCPのポートをもう一方へ転送する機能、 ポートフォワーディングがあります。 ポートフォワーディングを用いるとネットワーク間の暗号化された 経路を通じて相互のサービスを利用することが可能となります。

sshのポートフォワーディングを用いて、暗号化された通信経路を通じて dRubyを利用することができます。実験してみましょう。

http://www2a.biglobe.ne.jp/~seki/ruby/d2sshport.jpg

図10.1 sshポートフォワーディングによって異なるネットワークのオブジェクトを利用できる。

iBook(10.0.1.2)とLinux Box(10.0.1.202)との間をsshを使って接続します。 iBookの12345への接続をLinux Boxの12345へ転送し、 Linux Boxの23456への接続をiBookの23456へ転送します。 iBookではポート番号23456でdRubyのサービスを行い、 Linux Boxではポート番号12345でdRubyのサービスを行います。

ターミナル1はsshの-Lと-Rオプションを指定して、Linux Boxへログインします。 Linux Box上でdRubyのサービスを準備します。 *1

[ターミナル1]
osx% ssh -L 12345:localhost:12345 -R 23456:127.0.0.1:23456 10.0.1.202
Enter passphrase for key '/Users/mas/.ssh/id_rsa': 
Last login: ....
linux% irb --simple-prompt -r drb 
>> DRb.start_service('druby://localhost:12345', {})
>> DRb.front.keys
=> []

ターミナル2はiBook側のdRubyのサービスを準備します。 ターミナル1,2ともにdRubyのURIのホスト名にlocalhostを指定するのが重要です。 もしもdruby://hostname:12345などとすると、yieldなどリモートから ローカルを呼び返す際に、転送中のポートではなくhostname:12345へ 接続しようとしてしまい、失敗します。

[ターミナル2]
linux% irb --simple-prompt -r drb
>> DRb.start_service('druby://localhost:23456', {})
>> DRb.front.keys
=> []

次に、お互いのfrontオブジェクト(ハッシュ)になにか代入してみましょう。

[ターミナル1]
>> osx = DRbObject.new_with_uri('druby://localhost:23456')
>> >osx[:msg] = 'linux box to osx'
>> osx.keys
=> [:msg]
>> osx.each { |kv| p kv }
[:msg, "linux box to osx"]
=> {:msg=>"linux box to osx"}


[ターミナル2]
>> DRb.front
=> {:msg=>"linux box to osx"}
>> linux = DRbObject.new_with_uri('druby://localhost:12345')
>> linux[:msg] = 'osx to linux box'
>> linux.keys
=> [:msg]
=> linux.each { |kv| p kv }
[:msg, "osx to linux box"]
=> {:msg=>"osx to linux box"}

[ターミナル1]
>> DRb.front
=> {:msg=>"osx to linux box"}

frontオブジェクトの内容がたしかに変更されました。 それぞれのマシンのポートを転送しているので、 相互にメソッドを呼び合うことができます。 もちろん、yieldも動作します。

二つのネットワークに配置されたサービス間をsshのポートフォワーディングを 利用してつなぐことができることを確かめました。 簡単なサービスであればssh越しに利用することができることがわかりました。

10.2 dRubyゲートウェイ

上の実験では、メソッド呼び出し可能なオブジェクトでは それぞれのネットワークの端点にある二つの出島のようなプロセスの オブジェクトだけです。 ネットワークの内側のプロセスのオブジェクトのメソッドを呼び出すことは できません。

http://www2a.biglobe.ne.jp/~seki/ruby/d2gw0.jpg

図10.2 ポートが転送される出島のオブジェクトのメソッドだけが起動できる。

drb/gw.rbはこのような状況で、それぞれのネットワークの内側のオブジェクトに 対しても操作が可能なようにするしかけです。 GWIdConvはネットワークの内側のオブジェクトの参照情報を、 出島となるプロセスの参照情報に書き換えます。 DRbObjectの参照情報を書き換えることで、そのままではアクセスできない 内側のオブジェクトのメソッドにアクセスできるようにします。

DRbObjectは、DRbServerを特定するURI(@uri)とDRbServerがオブジェクトを 特定するための参照情報(@ref)で構成されます。 drb/gw.rbではDRbObjectのURIをゲートウェイとなるDRbServerのものに、 本来あったURIと参照情報をまとめて参照情報にします。 これで本来のDRbObjectをゲートウェイの管理下に包み込んだ参照が出来上がります。

@uri = ゲートウェイのURI
@ref = 本来のDRbObject

http://www2a.biglobe.ne.jp/~seki/ruby/d2gw2.jpg

図10.3 DRbObjectをラップする。

この書き換え処理によって、メソッド呼び出しは一度ゲートウェイを 経由するようになります。

DRbObjectの参照情報はMarshalでdump、loadされるときに行なわれます。 DRbObjectをdumpする場合とは、他のプロセスにDRbObjectを送るとき、 またloadする場合は他のプロセスからDRbObjectを受け取るときと言えます。 つまりゲートウェイとなるプロセスからDRbObjectが送られる、あるいは DRbObjectを受け取る際に参照情報が調整されるのです。

次のような規則で調整します。 基本的には、送るときにラップし受け取るときにラップをはがす、といった 動作をします。

  • dumpするとき‥
    • 自局の参照(DRb.uriと@uriが同じ)の場合、ラップされた具を取り出して 送信します。具体的には@refの内容を取り出してdumpします。
    • 他局の参照の場合、自局の参照となるようにラップして送信します。 @refに本来の@uriと@refをしまってからdumpします。
  • loadするとき‥
    • 自局の参照の場合、通常と同様に@refを使って本来のオブジェクトに 変換したものを返します。 このとき、@refがDRbObjectの場合にはDRbObjectを返します。
    • 他局の参照の場合、自局の参照となるようにラップした参照を返します。

ラップする場合、本当にDRbObjectを入れてしまうと、再起的にdumpが 発生するのでArrayで同様な情報をしまっています。

http://www2a.biglobe.ne.jp/~seki/ruby/d2gw1.jpg

図10.4 DRbObjectをラップし、Arrayで表現する。

drb/gw.rbには三つのクラスが定義されています。

  • DRbObject − 参照情報の付け替えを行なうように_loadと_dumpを再定義した DRbObject。
  • GWIdConv − 参照情報の付け替えに対応したDRbIdConv。 明示的に利用しなくてはならないクラスです。
  • GW − 二つのDRbServerのフロントに指定する、マルチスレッド下でも 安全に動作するようにMonitorMixinで保護したHash。 GWのユーティリティ的なクラスで、その使用は必須ではありません。

実際のゲートウェイを見てみましょう。

List 10.1 gw_s.rb

# gw_s.rb
require 'drb/drb'
require 'drb/gw'

DRb.install_id_conv(DRb::GWIdConv.new)
gw = DRb::GW.new
s1 = DRb::DRbServer.new(ARGV.shift, gw)
s2 = DRb::DRbServer.new(ARGV.shift, gw)
s1.thread.join
s2.thread.join

はじめにGWIdConvをインストールします。 そして二つのDRbServerを起動します。 通常はDRb.start_serviceでDRbServerを一つ起動するのですが、 ゲートウェイではその性質上二つのDRbServerが必要となります。

gw_s.rbは次のように起動します。二つの異なるURIを与えてください。

% ruby gw_s.rb druby://yourhost:12321 drbunix:/tmp/gw_s

この例では一方は他のマシン用にTCPのURIを与え、 もう一方は同じマシンの別プロセス用にUNIXドメインのURIを与えました。

ゲートウェイと同じマシンで新しいターミナルを準備してirbで実験してみます。

[ターミナル1]
% irb --simple-prompt -r drb/drb
>> DRb.start_service('drbunix:/tmp/gw_c')
>> ro = DRbObject.new_with_uri('drbunix:/tmp/gw_s')
>> require 'thread'
>> $q = Queue.new
>> ro[:unix] = DRbObject.new($q)
>> ro[:unix].push('test')

この実験では自局のURIはUNIXドメインを指定します。 このプロセスのオブジェクトは同じマシンからのみ利用できることになります。

roはゲートウェイのUNIXドメイン側のURIを指定して生成した参照です。

Queueを生成して、ro[:unix]へ与えます。 QueueはそのままではMarshal可能なため値が渡されてしまうので、 明示的にDRbObject(参照)に変換してからセットしています。

次に、違うマシンのターミナルからirbで実験してみます。

[ターミナル2]
% irb --simple-prompt -r drb/drb
>> DRb.start_service
>> ro = DRbObject.new_with_uri('druby://yourhost:12321')
>> ro[:unix]
=> #<DRb::DRbObject:0x40300d18 @ref=[:DRbObject, "drbunix:/tmp/gw_s", [:DRbObject, "drbunix:/tmp/gw_c", 2010008]], @uri="druby://yourhost:12321">
>> ro[:unix].pop
=> 'test'

roはゲートウェイのTCP側のURIを指定して生成した参照です。 ro[:unix]が二重にラップされているのがわかりますか? ro[:unix].popも正しくターミナル1のオブジェクトを呼び出せています。

もしもターミナル1がゲートウェイのTCP側からオブジェクトを登録したら どうなるでしょう。UNIXドメインもTCP側も同じDRb::GWのオブジェクトですが 動作に違いはあるでしょうか?

[ターミナル1]
>> ro = DRbObject.new_with_uri('druby://yourhost:12321')
>> ro[:tcp] = DRbObject.new($q)
>> ro[:tcp].push('test')

[ターミナル2]
>> ro[:tcp]
=> #<DRb::DRbObject:0x40307c30 @ref=2010008, @uri="drbunix:/tmp/gw_c">

むむ。嫌な予感がしますね。

>> ro[:tcp].pop
DRb::DRbConnError: drbunix:/tmp/gw_c - #<Errno::ENOENT: No such file or directory - /tmp/gw_c>

   ....

やはり予感は当たりました。ターミナル2のマシンの中"drbunix:/tmp/gw_c" というURIのDRbServerを探しにいって、見つからず上記のエラーとなったのです。

このゲートウェイのミソは、一方のDRbServerから入ってきた参照が もう一方に出て行くときに参照のラッピングが行われるところです。 ですから、上記の二つ目のテストのように同じ側のDRbServerに参照を 与えても素通ししてしまい、意図した動作となりません。 この点に注意すれば、簡単にローカルネットワーク(あるいはホスト)の オブジェクトをゲートウェイを通じて公開することが可能になります。


*1ssh -L 12345:localhost:12345 -R 23456:localhost:23456 10.0.1.202の ように、それぞれlocalhostと指定したいが、OSX10.3ではうまく動作しなかった