如何正确实现Net :: SSH端口转发

时间:2012-12-21 22:58:49

标签: ruby port forward portforwarding net-ssh

我一直在尝试使用Net :: SSH使端口转发正常工作。根据我的理解,如果我希望能够从同一个Ruby程序中使用它,那么我需要分叉Net :: SSH会话,以便事件处理循环可以实际处理通过连接发送的数据包。但是,这会导致您在下面看到的丑陋:

#!/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

有人能告诉我一个更优雅/更好的解决方案吗?

3 个答案:

答案 0 :(得分:4)

我花了很多时间试图弄清楚如何正确实现端口转发,然后我从net / ssh / gateway库中获取灵感。我需要一个在各种可能的连接错误之后工作的强大解决方案这就是我现在正在使用的,希望它有所帮助:

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

答案 1 :(得分:0)

这就是SSH的一般方式。如果你对它看起来多么丑陋感到冒犯,你应该把这个功能包装到某种类型的端口转发类中,这样暴露的部分就会更加简洁。像这样的界面,可能是:

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

答案 2 :(得分:0)

所以我发现了一个更好的实现。它只需要一个fork,但仍然使用套接字进行通信。它使用IO#read_nonblock来检查消息是否准备就绪。如果没有,则该方法抛出异常,在这种情况下,块继续返回true并且SSH会话继续提供请求。一旦父完成连接,它就会发送一条消息,导致child_socket.read_nonblock(maxlen).nil?返回false,使循环退出,从而关闭SSH连接。

我对此感觉好一些,所以在@ tadman的建议中将它包装在一个端口转发类中我觉得它和它一样好。但是,任何进一步改善这一点的建议都是受欢迎的。

#!/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