2012-12-21 47 views
2

我一直试图让端口转发与Net :: SSH正常工作。根据我的理解,如果我希望能够从同一个Ruby程序使用它,就需要分离出Net :: SSH会话,以便事件处理循环可以真正处理通过连接发送的数据包。然而,这会导致你可以在下面看到的丑陋:如何正确执行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

我花了很多时间想弄清楚如何正确实现端口转发,然后我从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 
+0

这很不错。我首先想到的是,这个互斥量同步0.01秒的短暂等待时间会引入很多CPU开销,但并不像我想象的那么糟糕(我的MBP的持续背景只有不到2%)。当然,这也可以通过增加ssh进程等待时间来稍微调整,以避免响应中可预测性的轻微损失。 – HerbCSO

+0

PS:注意[SSH :: Net的文档]中的警告(http://net-ssh.github.io/ssh/v2/api/classes/Net/SSH/Connection/Session.html#M000088):don' t设置进程轮询时间为0,那么你会吃掉大量的CPU。 ] – HerbCSO

0

这就是一般的SSH。如果你看起来很丑,你应该把这个功能包装到某种端口转发类中,这样暴露的部分就更简洁了。一个这样的接口,或许:

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

不够公平,但有实现这个,而不是使用两个单独的叉子和插座(并在梨树上的鹧鸪更好的办法.. 。;])?它看起来非常混乱,我忍不住想它可以被优化多一点...... – HerbCSO

+1

双叉是完全脱离壳体的过程。一个fork会留下一个* zombie *进程,这是一个没有被正确收获的子进程,而这个进程相当不爽。没有人喜欢僵尸。 – tadman

0

所以我找到了一个稍微好一点的实现。它只需要一个分支,但仍然使用套接字进行通信。它使用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