TCP tunnel over SSH in Rust

2019-04-29 04:51发布

I'm trying to write a small program in Rust to accomplish basically what ssh -L 5000:localhost:8080 does: establish a tunnel between localhost:5000 on my machine and localhost:8080 on a remote machine, so that if an HTTP server is running on port 8080 on the remote, I can access it on my local via localhost:5000, bypassing the remote's firewall which might be blocking external access to 8080.

I realize ssh already does exactly this and reliably, this is a learning project, plus I might be adding some functionality if I get it to work :) This is a barebones (no threading, no error handling) version of what I've come up with so far (should compile on 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);
  };
}

As it turns out, this kind of works, but not quite. E.g. if the app running on the remote server is the Cloud9 IDE Core/SDK, the main HTML page gets loaded and some resources as well, but requests for other resources (.js, .css) systematically come back empty (whether requested by the main page or directly), i.e. nothing is read in the call to channel.read_to_end(). Other (simpler?) web apps or static sites seem to work fine. Crucially, when using ssh -L 5000:localhost:8080, even Cloud9 Core works fine.

I expect that other more complex apps will also be affected. I see various potential areas where the bug might be lurking in my code:

  1. Rust's stream reading/writing APIs: maybe the call to channel.read_to_end() works differently than I think and just accidentally does the right thing for some kinds of requests?
  2. HTTP: maybe I need to tinker with HTTP headers before forwarding the request to the remote server? or maybe I'm giving up to soon on the response stream by just calling channel.read_to_end()?
  3. Rust itself -- it's my first relatively earnest attempt at learning a systems programming language

I've already tried playing with some of the above, but I'll appreciate any suggestions of paths to explore, preferably along with an explanation as to why that might be the problem :)

1条回答
啃猪蹄的小仙女
2楼-- · 2019-04-29 05:30

tl;dr: use Go and its networking libraries for this particular task

Turns out my very rudimentary understanding of how HTTP works may be at fault here (I initially thought I could just shovel data back and forth over the ssh connection, but I haven't been able to achieve that -- if someone knows of a way to do this, I'm still curious!). See some of the suggestions in the comments, but basically it boils down to the intricacies of how HTTP connections are initiated, kept alive and closed. I tried using the hyper crate to abstract away these details, but it turns out that the ssh2 crate (like the underlying libssh2) is not threadsafe, which makes it impossible to use the ssh Session in hyper handlers.

At this point, I decided there's no simple, high-level way for a beginner to achieve this in Rust (I'd have to do some low-level plumbing first, and since I can't do that reliably and idiomatically, I figured it's not worth doing at all). So I ended up forking this 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.

查看更多
登录 后发表回答