Rust 中基于 SSH 的 TCP 隧道

TCP tunnel over SSH in Rust

我正在尝试用 Rust 编写一个小程序来基本上完成 ssh -L 5000:localhost:8080 所做的事情:在我机器上的 localhost:5000 和远程机器上的 localhost:8080 之间建立隧道,因此,如果 HTTP 服务器 运行 在远程端口 8080 上,我可以通过 localhost:5000 在本地访问它,绕过可能阻止外部访问 8080 的远程防火墙。

我意识到 ssh 已经做到了这一点并且可靠,这是一个学习项目,而且如果我让它工作,我可能会添加一些功能 :) 这是一个准系统(没有线程,没有错误handling) 到目前为止我想出的版本(应该在 Rust 1.8 上编译):

extern crate ssh2; // see http://alexcrichton.com/ssh2-rs/

use std::io::Read;
use std::io::Write;
use std::str;
use std::net;

fn main() {
  // establish SSH session with remote host
  println!("Connecting to host...");
  // substitute appropriate value for IPv4
  let tcp = net::TcpStream::connect("<IPv4>:22").unwrap();
  let mut session = ssh2::Session::new().unwrap();
  session.handshake(&tcp).unwrap();
  // substitute appropriate values for username and password
  // session.userauth_password("<username>", "<password>").unwrap();
  assert!(session.authenticated());
  println!("SSH session authenticated.");

  // start listening for TCP connections
  let listener = net::TcpListener::bind("localhost:5000").unwrap();
  println!("Started listening, ready to accept");
  for stream in listener.incoming() {
    println!("===============================================================================");

    // read the incoming request
    let mut stream = stream.unwrap();
    let mut request = vec![0; 8192];
    let read_bytes = stream.read(&mut request).unwrap();
    println!("REQUEST ({} BYTES):\n{}", read_bytes, str::from_utf8(&request).unwrap());

    // send the incoming request over ssh on to the remote localhost and port
    // where an HTTP server is listening
    let mut channel = session.channel_direct_tcpip("localhost", 8080, None).unwrap();
    channel.write(&request).unwrap();

    // read the remote server's response (all of it, for simplicity's sake)
    // and forward it to the local TCP connection's stream
    let mut response = Vec::new();
    let read_bytes = channel.read_to_end(&mut response).unwrap();
    stream.write(&response).unwrap();
    println!("SENT {} BYTES AS RESPONSE", read_bytes);
  };
}

事实证明,这种方法有效,但不完全有效。例如。如果远程服务器上的应用 运行 是 Cloud9 IDE Core/SDK,则会加载主 HTML 页面和一些资源,但 请求其他资源 (.js, .css) 系统地返回空(无论是主页请求还是直接请求),即没有读取任何内容致电 channel.read_to_end()。其他(更简单?)网络应用程序或静态网站似乎工作正常。至关重要的是,当使用 ssh -L 5000:localhost:8080 时,即使是 Cloud9 Core 也能正常工作。

我预计其他更复杂的应用程序也会受到影响。我发现我的代码中可能潜伏着 bug 的各种潜在区域:

  1. Rust 的流 reading/writing API:也许对 channel.read_to_end() 的调用与我想象的不同,只是不小心为某些类型的请求做了正确的事情?
  2. HTTP:也许我需要在将请求转发到远程服务器之前修改 HTTP headers?或者我可能很快就会通过调用 channel.read_to_end()?
  3. 放弃响应流
  4. Rust 本身——这是我第一次相对认真地尝试学习系统编程语言

我已经尝试过上面的一些方法,但我将不胜感激任何关于探索路径的建议,最好连同对为什么这可能是问题的解释:)

tl;dr:使用 Go 及其网络库完成此特定任务

原来我对 HTTP 工作原理的非常基本的理解可能在这里有问题(我最初认为我可以通过 ssh 连接来回铲数据,但我还没有能够实现这一点——如果有人知道这样做的方法,我仍然很好奇!)。查看评论中的一些建议,但基本上归结为 HTTP 连接如何启动、保持活动和关闭的复杂性。我尝试使用 hyper 板条箱来抽象出这些细节,但由于线程安全限制,ssh2 会话不能用于超级处理程序(至少在 2016 年不能)。

在这一点上,我决定没有简单高级 初学者[=23]的方法=] 以在 Rust 中实现这一点(我必须首先做一些低级管道,并且由于我不能可靠地和惯用地做到这一点,我认为它根本不值得做)。所以我最终分叉了这个 SSHTunnel repository written in Go, where library support for this particular task is readily available, and my solution to the Cloud9 setup described in the OP can be found here.