6. Port Forwarding

6.1. Introduction

Port forwarding is a feature of the SSH protocol that allows you to specify a port on one of the hosts, and have network connections on that port forwarded to a port on a different host, using the SSH connection as a proxy. There are basically two ways to use this forwarding:

  1. A port on the local host is forwarded via the remote host to another machine. Any connection to the specified port will cause all subsequent data to be sent over the connection to the remote host, where it will then be forwarded to the requested destination host.
  2. A port on the remote host is forwarded over the connection to the local host, and from there to (potentially) some other remote destination. Any connection to the specified port on the remote host is forwarded over the connection to the local host, which then makes a connection to the specified remote destination and sends the data there.

All port forwarding in the Net::SSH library is managed by the #forward service. Just invoke methods on that service to set up any of various port forwarding configurations.

Accessing the #forward service [ruby]
1
2
3
4
5
Net::SSH.start( 'host' ) do |session|
  forward = session.forward
  ...
  session.loop
end

You can define any number of forwards before invoking the main loop, in which case all of those forwards will be handled transparently (and silently) in parallel, over the same connection. (Isn’t SSH lovely?)

Naturally, you can also have remote processes, SFTP sessions, and more all working at the same time on the connection.

6.2. Local-to-Remote

Forwarding a local connection to a remote destination is simply a matter of invoking the #local method of the #forward service. The simplest version of the method just takes three parameters: the local port to listen on, and the remote host and port to forward the connection to:

Forwarding a local port [ruby]
1
2
3
4
Net::SSH.start( 'host' ) do |session|
  session.forward.local( 1234, 'www.google.com', 80 )
  session.loop
end

In the above example, then, any connection received on port 1234 will be forwarded to port 80 on “www.google.com”. This means that if you were to point a browser at “http://localhost:1234”, it would pull up Google.

By default, only connections from the local host are accepted. This is because the default bind address is 127.0.0.1. You can specify any bind address you want (including 0.0.0.0 to allow connections from anywhere) by specifying that address as the first parameter to #local, with the local port number immediately following.

Specifying the bind address when forwarding a local port [ruby]
session.forward.local( '0.0.0.0', 1234, 'www.google.com', 80 )

In this configuration, anyone from anywhere can connect to your machine on port 1234 and be forwarded to Google.

6.3. Remote-to-Local

Forwarding remote connections to the local host is also straightforward; simply call the #remote_to method of the #forward service. This takes three (or four) parameters: the local port and host to be forwarded to (in that order), and the remote port to listen on. The fourth parameter is optional, and is the bind address on the remote machine; this defaults to “127.0.0.1”.

Forwarding a remote port [ruby]
1
2
3
4
Net::SSH.start( 'host' ) do |session|
  session.forward.remote_to( 80, 'www.google.com', 1234 )
  session.loop
end

The above example causes any connection on port 1234 of the remote machine (from the remote machine) to be forwarded via the local host to port 80 at www.google.com. To make things a bit more open, you could specify a bind address of 0.0.0.0:

Specifying the bind address when forwarding a remote port [ruby]
session.forward.remote_to( 80, 'www.google.com', 1234, '0.0.0.0' )

6.4. Direct Channels

Sometimes it might be nice to programmatically simulate a network connection on a local port and have it forwarded to the remote host. You can do this by means of the #direct_channel method.

The #direct_channel method looks similar to #local: the first three parameters are the local port to simulate the connection from, and the remote host and port that the connection should be forwarded to. The fourth parameter, however, is a handler, an object that is used as a callback for a variety of different events.

The handler for the #direct_channel method may implement any of the following callbacks (all are optional, though you probably want to implement at least one or two of them):

Callback Description
confirm This is invoked when the channel has been opened and the remote host has confirmed it. This accepts four parameters: the channel itself, the local port, remote host, and remote port. (In this way, the same handler may be used for multiple forward requests.)
process After the channel has been confirmed, this is invoked, to process the channel. This callback will be invoked in a new Thread, so that if your handler needs to listen to a socket and then send data received from it over the channel, it can do so without blocking the main loop. The callback accepts a single parameter, the channel handle itself.
on_close This is called when the channel over which this forwarded connection is being processed has been closed. The callback accepts a single parameter, the channel itself.
on_eof When the remote machine indicates it will send no more data, this callback will be invoked. It accepts a single parameter, the channel itself.
on_receive This is invoked when data is received from the remote machine. It accepts two parameters: the channel handle, and the data that was received.

For example, the following example pretends to be a client that has connected to the local host on a forwarded port:

Using a handler object to mimic a forwarded port [ruby]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Handler
  def on_receive( channel, data )
    puts "got data: #{data.inspect}"
    channel.send_data "subsequent request"
  end

  def process( channel )
    channel.send_data "initial request"
  end
end

Net::SSH.start( 'host' ) do |session|
  session.forward.direct_channel( 1234, 'somewhere.else.net',
    4321, Handler.new )

  session.loop
end

The local port number for #direct_channel has no real purpose, other than to report to the SSH server that the “virtual” connection occurred on that port.

6.5. Remote-to-Local Handlers

You can use handlers going in the other direction, too. If you want to programmatically process forwarded data from a remote host, you can use the #remote method. This takes two parameters, with an optional third parameter. The two required parameters are the handler to use, and the remote port that should be listened to. The optional parameter is the remote bind address, which defaults to ‘127.0.0.1’.

(Incidentally, if the port is 0, a new port will be allocated for you automatically by the server.)

Whenever connections are received on the remote port, they will be forwarded to the handler, which may implement the following callbacks:

Callback Description
error This is invoked if the forward could not be initiated. It accepts a single parameter, which is the error message.
on_close This is invoked when the channel that was assigned to process this forwarded connection has been closed. The callback takes one parameter: the channel itself.
on_eof This is invoked when the remote end of the connection has promised not to send any more data. The local end of the channel may continue to send data, however. This callback takes on parameter: the channel itself.
on_open This is invoked when a new connection is received over the forwarded channel. It accepts five parameters: the channel object, the connected address, the connected port, the originator address, and the originator port.
on_receive This is invoked when data is received over the channel from the remote connection. It accepts two parameters: the channel object, and the data that was received.
setup This is invoked immediately after the forward request has been acknowledged as successful. It accepts a single parameter, which is the port that was assigned to this forward. If the port parameter to #remote was not 0, then that same value will be passed to the callback. Otherwise, the newly allocated port number will be passed to the callback.

Note that the on_receive handler is required—all other callbacks may remain unimplemented by the handler.