I am trying to write a small program in Rust to accomplish basically what ssh -L 5000:localhost:8080 : set up a tunnel between localhost:5000 on my computer and localhost:8080 on the remote machine so that when starting the HTTP server on port 8080 on the remote control, I can access it through the local network through localhost:5000 , bypassing the remote firewall, which can block external access to 8080.
I understand that ssh already does just that and is reliable, this is a training project, plus I can add some functionality if I earn it :) This is an empty game (without streaming processing, without errors) that I came 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 response (all of it, for simplicity sake) // and forward it to the local TCP connection 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 turned out, this kind of work, but not quite. For instance. if the application running on the remote server is Cloud9 IDE Core / SDK , the main HTML page and some resources are loaded, but requests for other resources ( .js , .css ) are systematically returned empty (regardless of whether it is requested on the main page or directly), i.e. nothing is read in the call until channel.read_to_end() . Other (simpler?) Web apps or static sites seem to work fine. Actually, when using ssh -L 5000:localhost:8080 even Cloud9 Core works fine.
I expect other more complex applications to be affected as well. I see various potential areas where the error may be hiding in my code:
- Rust stream read / write APIs: maybe the call to
channel.read_to_end() works differently than I think, and just accidentally does the right thing for some kind of request? - HTTP: maybe I need to bother with HTTP headers before sending a request to a remote server? or maybe I will give the response flow soon, just by calling
channel.read_to_end() ? - Rust itself is my first relatively serious attempt to learn a system programming language.
I already tried playing with some of the above, but I will be grateful for any suggestions on ways to explore, preferably along with an explanation of why this might be a problem :)