Question

I have been trying to get port forwarding to work correctly with Net::SSH. From what I understand I need to fork out the Net::SSH session if I want to be able to use it from the same Ruby program so that the event handling loop can actually process packets being sent through the connection. However, this results in the ugliness you can see in the following:

#!/usr/bin/env ruby -w
require 'net/ssh'
require 'httparty'
require 'socket'
include Process

log = Logger.new(STDOUT)
log.level = Logger::DEBUG

local_port = 2006
child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
maxlen = 1000
hostname = "www.example.com"

pid = fork do
  parent_socket.close
  Net::SSH.start("hostname", "username") do |session|
    session.logger = log
    session.logger.sev_threshold=Logger::Severity::DEBUG
    session.forward.local(local_port, hostname, 80)
    child_socket.send("ready", 0)
    pidi = fork do
      msg = child_socket.recv(maxlen)
      puts "Message from parent was: #{msg}"
      exit
    end
    session.loop do
      status = waitpid(pidi, Process::WNOHANG)
      puts "Status: #{status.inspect}"
      status.nil?
    end
  end
end

child_socket.close

puts "Message from child: #{parent_socket.recv(maxlen)}"
resp = HTTParty.post("http://localhost:#{local_port}/", :headers => { "Host" => hostname } )
# the write cannot be the last statement, otherwise the child pid could end up
# not receiving it
parent_socket.write("done")
puts resp.inspect

Can anybody show me a more elegant/better working solution to this?

Was it helpful?

Solution

I spend a lot of time trying to figure out how to correctly implement port forwarding, then I took inspiration from net/ssh/gateway library. I needed a robust solution that works after various possible connection errors. This is what I'm using now, hope it helps:

require 'net/ssh'

ssh_options = ['host', 'login', :password => 'password']
tunnel_port = 2222
begin
  run_tunnel_thread = true
  tunnel_mutex = Mutex.new
  ssh = Net::SSH.start *ssh_options
  tunnel_thread = Thread.new do
    begin
      while run_tunnel_thread do
        tunnel_mutex.synchronize { ssh.process 0.01 }
        Thread.pass
      end
    rescue => exc
      puts "tunnel thread error: #{exc.message}"
    end
  end
  tunnel_mutex.synchronize do
    ssh.forward.local tunnel_port, 'tunnel_host', 22
  end

  begin
    ssh_tunnel = Net::SSH.start 'localhost', 'tunnel_login', :password => 'tunnel_password', :port => tunnel_port
    puts ssh_tunnel.exec! 'date'
  rescue => exc
    puts "tunnel connection error: #{exc.message}"
  ensure
    ssh_tunnel.close if ssh_tunnel
  end

  tunnel_mutex.synchronize do
    ssh.forward.cancel_local tunnel_port
  end
rescue => exc
  puts "tunnel error: #{exc.message}"
ensure
  run_tunnel_thread = false
  tunnel_thread.join if tunnel_thread
  ssh.close if ssh
end

OTHER TIPS

That's just how SSH in general is. If you're offended by how ugly it looks, you should probably wrap up that functionality into a port forwarding class of some sort so that the exposed part is a lot more succinct. An interface like this, perhaps:

forwarder = PortForwarder.new(8080, 'remote.host', 80)

So I have found a slightly better implementation. It only requires a single fork but still uses a socket for the communication. It uses IO#read_nonblock for checking if a message is ready. If there isn't one, the method throws an exception, in which case the block continues to return true and the SSH session keeps serving requests. Once the parent is done with the connection it sends a message, which causes child_socket.read_nonblock(maxlen).nil? to return false, making the loop exit and therefore shutting down the SSH connection.

I feel a little better about this, so between that and @tadman's suggestion to wrap it in a port forwarding class I think it's about as good as it'll get. However, any further suggestions for improving this are most welcome.

#!/usr/bin/env ruby -w
require 'net/ssh'
require 'httparty'
require 'socket'

log = Logger.new(STDOUT)
log.level = Logger::DEBUG

local_port = 2006
child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
maxlen = 1000
hostname = "www.example.com"

pid = fork do
  parent_socket.close
  Net::SSH.start("ssh-tunnel-hostname", "username") do |session|
    session.logger = log
    session.logger.sev_threshold=Logger::Severity::DEBUG
    session.forward.local(local_port, hostname, 80)
    child_socket.send("ready", 0)
    session.loop { child_socket.read_nonblock(maxlen).nil? rescue true }
  end
end

child_socket.close

puts "Message from child: #{parent_socket.recv(maxlen)}"
resp = HTTParty.post("http://localhost:#{local_port}/", :headers => { "Host" => hostname } )
# the write cannot be the last statement, otherwise the child pid could end up
# not receiving it
parent_socket.write("done")
puts resp.inspect
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top