Search code examples
rubyportforwardportforwardingnet-ssh

How to properly implement Net::SSH port forwards


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?


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