Inside Capistrano: the Gateway implementation
Most Capistrano users have probably never needed to use its gateway feature. I find that vaguely ironic, since it was one of the features in Capistrano that were on the original list of requirements when I sat down to code it all up.
Basically, what the gateway lets you do is tunnel your connections through a single computer. This lets you connect to computers that are behind a firewall, or on a VPN. We use this feature all the time at 37signals, since the bulk of our cluster is not directly accessible via the Internet.
1 2 3 4 5 6 7 8 |
# specify the gateway server set :gateway, "gateway.server.com" # the following servers are behind a firewall and # cannot be accessed directly role :app, "app.server.com" role :web, "web.server.com" role :db, "db.server.com", :primary => true |
The gateway code is a bare 100 lines long, including comments. Basically, all it does is establish a connection to the gateway machine, and then for every connection established via the gateway, it forwards a port from the local host to the requested server. Then, it establishes a connection to the requested server via that forwarded port. It makes heavy use of threads to accomplish this, and is one of the places that helped iron out several synchronicity issues in Net::SSH. In fact, the code is a good showcase of what you can do with forwarded ports in Net::SSH.
So, let’s take it all apart and walk through the code, beginning with the initialize
method. (For those of you that want to follow along, the file in question is capistrano/gateway.rb.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def initialize(server, config) @config = config @next_port = MAX_PORT @terminate_thread = false @port_guard = Mutex.new mutex = Mutex.new waiter = ConditionVariable.new @thread = Thread.new do SSH.connect(server, @config) do |@session| mutex.synchronize { waiter.signal } @session.loop { !@terminate_thread } end end mutex.synchronize { waiter.wait(mutex) } end |
(In the interest of keeping things compact, I’ve removed the comments and the lines related to logging.)
The meat of this is the Thread.new
statement there in the middle. All it does is establish the gateway’s SSH connection. (The config
instance variable is a Capistrano::Configuration instance, from which various SSH options are pulled, including the user, password, port, etc.) Once the connection is live, the block will be called, and we signal the “waiter” (the condition variable). This wakes up the calling thread (which is blocked in the wait
call following the thread). Once the connection is live, we enter the session loop, which goes until asked to terminate (the terminate_thread
instance variable).
Note that SSH.connect
is another Capistrano abstraction that basically wraps the lower-level Net::SSH.start
. There’s not much to it; you can read the entire thing in capistrano/ssh.rb.
Once the gateway connection is live, other connections may be established through it by calling the connect_to
method, passing in a string that names the target server.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
def connect_to(server) connection = nil port = next_port thread = Thread.new do begin @session.forward.local(port, server, 22) connection = SSH.connect('127.0.0.1', @config, port) rescue Errno::EADDRINUSE port = next_port retry rescue Exception => e puts e.class.name puts e.backtrace.join("\n") end end thread.join connection or raise "Could not establish connection to #{server}" end def next_port @port_guard.synchronize do port = @next_port @next_port -= 1 @next_port = MAX_PORT if @next_port < MIN_PORT port end end |
For this bit, we first get the next (possibly) available port on the local host. Then, in a thread, we start a forwarded port from the local host to the remote host, and try to establish an SSH connection through it. If the port turns out to be in use, we grab the next port and try again.
And that’s it, really.
There isn’t that much to the gateway implementation, but we like it that way. It is one of the most critical parts of Capistrano for us at 37signals, and the current implementation is both simpler than before (compare it to the version in Capistrano 1.1) and more robust. You could even conceivably use the gateway code directly in your own scripts, if you ever needed to connect to one or more hosts through a forwarded port. Something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
require 'capistrano/gateway' require 'capistrano/logger' # First, we create a config object that quacks, # mostly, like a Capistrano::Configuration object. config = Struct.new(:user, :password, :ssh_options, :logger).new config.user = "username" config.password = "password" config.ssh_options = {} config.logger = Capistrano::Logger.new( :output => "/dev/null") # Connect to the Gateway... gateway = Capistrano::Gateway.new("gateway", config) # Establish a connection to an internal machine via # the gateway host = gateway.connect_to("internal") # "host" is now an SSH session object. We can # manipulate it using the Net::SSH API. host.open_channel do |ch| ch.on_data do |ch, data| puts(data) end ch.exec "hostname" end host.loop |
For more information on Net::SSH, you can tackle the Net::SSH documentation. It tries to be fairly comprehensive.
Reader Comments
26 Sep 2006
26 Sep 2006
26 Sep 2006
27 Sep 2006
27 Sep 2006