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:
channel.read_to_end()
works differently than I think and just accidentally does the right thing for some kinds of requests?channel.read_to_end()
?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 :)
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 the ssh2 Session can't be used in hyper handlers due to thread safety restrictions (at least not in 2016 it couldn't).
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.