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 :)
vec![0; 8192]is suspicious. This creates a vector with many zeroes. if you read one byte from the socket, there will still be 8191 zeroes following it in the buffer. When you write that back out, you are writing all those zeroes, which the remote side has to deal with. You should only be sending the data you read.\r\n\r\n, IIRC), then send that upstream.vec![0; 8192]: That sounds like a trail! I mistakenly thought they just went away (or rather, thatstream.read()truncated the pre-allocated vector if it turned out to be too long) -- simply because when I log the request to STDOUT, they're gone. But now you mention it, that's probably because the string gets truncated at the first null by the terminal emulator? I'll look into it tonight and report back.\r\n\r\n: Right again, this was just a quick and dirty way to pre-allocate more than enough for a typical HTTP request, since I thought (again, mistakenly) that the extra nulls were getting thrown away. The requests for the resources that turn up blank aren't getting truncated though (according to the log), so it sounds more like a case of the extra nulls messing things up rather than headers missing.