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:
- 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? - 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()
? - 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 :)