The dRuby Book

12.2 Accessing Remote Services via SSH Port Forwarding

Now that we’ve tightened our security level, let’s find out how to extend dRuby’s services across a network. We’ll see how to connect across multiple networks via Secure Shell (SSH) port forwarding, as well as by using the dRuby gateway.

Experimenting with Port Forwarding with dRuby

SSH (OpenSSH) has a functionality called port forwarding, which transfers all communication to a specific TCP port on one machine to another using an encrypted channel. With port forwarding, you can use the services of other networks in a secure way.

You can use dRuby securely using this SSH port forwarding. Let’s try it (see Figure 47, Accessing objects on a different network using SSH port forwarding).

images/d2sshport.png

Figure 47. Accessing objects on a different network using SSH port forwarding

In this example, we connect to an iBook (10.0.1.2) and a Linux box (10.0.1.202) with SSH. A connection to port 12345 on the iBook is transferred to port 12345 on the Linux box and from port 23456 on the Linux box to the same port on the iBook. The iBook provides dRuby service on port 23456, and the Linux box does the same on port 12345.

In terminal 1, login to the Linux box via ssh with the -L and -R options specified. Once logged in, start the dRuby server. (I wanted to use localhost rather than the IP address, but it didn’t work in my Mac OS X environment.)

  ​# [Terminal 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 --prompt simple -r drb​
  ​>> DRb.start_service('druby://localhost:12345', {})​
  ​>> DRb.front.keys​
  ​=> []​

In terminal 2, start the dRuby server (assuming you are on the iBook). It’s important to specify localhost as the dRuby URI in terminals 1 and 2. Don’t specify an actual hostname such as druby://hostname:12345. dRuby tries to connect hostname:12345 directly rather than using SSH when a remote procedure call refers to the local object, such as when using yield.

  ​# [Terminal 2]​
  ​linux% irb --prompt simple -r drb​
  ​>> DRb.start_service('druby://localhost:23456', {})​
  ​>> DRb.front.keys​
  ​=> []​

Next, let’s try to assign some values to each other’s front object (Hash).

  ​# [Terminal 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"}​
  ​# [Terminal 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"}​
  ​# [Terminal 1]​
  ​>> DRb.front​
  ​=> {:msg=>"osx to linux box"}​

You can verify that the content of the front object changed. You can call methods of other machines since their ports are forwarded. You can use yield, too.

In this exercise, we connected services located in two different networks via SSH port forwarding. You can use this to provide simple services via SSH.

dRuby Gateway

In the previous examples, you could only access objects that are exposed via SSH port forwarding (see Figure 48, Only the gateway object is accessible). You couldn’t access other processes of the external network.

images/d2gw0.png

Figure 48. Only the gateway object is accessible.

drb/gw.rb acts as a gateway so that you can access objects among other networks. GWIdConv replaces a reference object of the internal network with reference information for the gateway object. This replacement allows you to access objects that aren’t accessible otherwise.

DRbObject consists of two components: a URI (@uri) for identifying the DRbServer location and reference information (@ref) that DRbServer uses to identify the object. drb/gw.rb wraps the original referenced objects. Once wrapped, the URI of the wrapping object contains the URI of the gateway object, and its reference points to the original DRbObject that contains both the original URI and the references (see Figure 49, Wrapping the DRbObject).

images/d2gw2.png

Figure 49. Wrapping the DRbObject
  ​@uri = Gateway URI​
  ​@ref = Source DRbObject​

After this replacement, any following method invocation goes through the gateway.

DRbObject is referenced when Marshal#dump and Marshal#load are called. DRbObject is dumped when sending DRbObject to other processes, and it is loaded when DRbServer receives DRbObject from other processes.

The replacement action happens in the following order: it wraps when sending objects and unwraps when receiving objects.

When dumping
  • When sending an object within the same process as the gateway, (= when DRb.uri and @uri are the same), it unwraps and sends the object. (It takes out the content of @ref and dump.)

  • When sending an object to other processes, it wraps the object and sends the wrapped object. @ref stores the @uri and @ref of the original and then gets loaded.

When loading
  • When receiving an object within the same process, it uses @ref to regain the original object as usual. If @ref is DRbObject, it returns DRbObject itself.

  • When receiving an object from different processes, it returns the reference by wrapping the reference to its own reference.

DRbObjects are wrapped into Array. Otherwise, DRbObjects will be recursively dumped forever (see Figure 50, Wrapping the DRbObject and storing it into Array).

images/d2gw1.png

Figure 50. Wrapping the DRbObject and storing it into Array
DRbObject

Modified DRbObject with _load and _dump methods that are redefined to replace reference information.

GWIdConv

Same as DRbIdConv, but this version handles the preceding modified DRbObject. You need to use this when using GW.

GW

Specified as front objects of DRbServer on both sides. This is a Hash protected with MonitorMixin to work under a multithreading environment. GW is used as a utility class and is optional.

Now let’s look at the actual gateway.

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​

First, let’s install GWIdConv. Then, start two DRbServers. In a normal situation, you need to start only one DRbServer in DRb.start_service, but we need two DRbServers for gateway use.

You can invoke gw_s.rb as follows. Make sure to assign two different URIs.

  ​% ruby gw_s.rb druby://localhost:12321 drbunix:/tmp/gw_s​

In this example, we assign a TCP URI for other machines to be able to access and a Unix domain URI for different processes within the same machine to access.

Let’s start a new terminal on the same machine where the gateway runs and start irb.

  ​# [Terminal 1]​
  ​% irb --prompt simple -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')​

In this example, we specify the Unix domain to the URI for the same machine so that it can be accessed only within the same machine.

ro is a reference that’s created by specifying the gateway Unix domain as the URI.

Let’s generate a Queue object and assign it to ro[:unix]. We’ll explicitly pass by reference, because a Queue object is passed by value by default.

Next, let’s start irb from a different machine.

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

ro is a reference generated by specifying the URI of the gateway TCP. Notice that ro[:unix] is wrapped twice. You can see that ro[:unix].pop is actually calling an object on terminal 1.

What would happen if you registered an object on terminal 1 by using the TCP of the gateway? Both the Unix domain and TCP are the same DRb::GW object, but do they behave differently?

  ​# [Terminal 1]​
  ​>> ro = DRbObject.new_with_uri('druby://localhost:12321')​
  ​>> ro[:tcp] = DRbObject.new($q)​
  ​>> ro[:tcp].push('test')​
  ​# [Terminal 2]​
  ​>> ro[:tcp]​
  ​=> #<DRb::DRbObject:0x40307c30 @ref=2010008, @uri="drbunix:/tmp/gw_c">​

This does not look good.

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

It raises an error, because it looked into the DRbServer on terminal 2, whose URI is drbunix:/tmp/gw_c, but it didn’t find the URI.

The point of this gateway is to wrap the reference when a reference from one server goes to another. If you give a reference to DRbServer on the same side, it won’t work as expected. As long as you’re aware of this difference, you can publish objects across networks via the gateway easily.