diff --git a/.gitignore b/.gitignore index 65f1105..a919aef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,6 @@ # will have compiled files and executables /target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d5b9f9f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,266 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "blake2b_simd" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "rust-argon2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rust_util" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c152cad8a04f2de2840155adcc473dac011ffbe8ff33f8dfcfab489afa92d134" +dependencies = [ + "lazy_static", + "libc", + "term", + "term_size", +] + +[[package]] +name = "simple-rust-udp-proxy" +version = "0.1.0" +dependencies = [ + "getopts", + "rand 0.3.23", + "rust_util", +] + +[[package]] +name = "term" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" +dependencies = [ + "byteorder", + "dirs", + "winapi", +] + +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4a65c6d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "simple-rust-udp-proxy" +version = "0.1.0" +authors = ["Hatter Jiang "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +getopts = "0.2" +rand = "0.3" +rust_util = "0.6" diff --git a/README.md b/README.md index af54ebd..1873320 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ # simple-rust-udp-proxy -Simple Rust UDP Proxy \ No newline at end of file +Simple Rust UDP Proxy + + + +from: https://github.com/neosmart/udpproxy + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1686cca --- /dev/null +++ b/src/main.rs @@ -0,0 +1,190 @@ +extern crate getopts; +extern crate rand; +#[macro_use] +extern crate rust_util; + +use getopts::Options; +use std::collections::HashMap; +use std::env; +use std::net::UdpSocket; +use std::sync::Arc; +use std::sync::atomic::{ AtomicBool, Ordering }; +use std::sync::mpsc::channel; +use std::thread; +use std::time::Duration; + +const TIMEOUT: u64 = 3 * 60 * 100; //3 minutes +static mut DEBUG: bool = false; + +fn print_usage(program: &str, opts: Options) { + let program_path = std::path::PathBuf::from(program); + let program_name = program_path.file_stem().unwrap().to_str().unwrap(); + let brief = format!("Usage: {} [-b BIND_ADDR] -l LOCAL_PORT -h REMOTE_ADDR -r REMOTE_PORT", program_name); + information!("{}", opts.usage(&brief)); +} + +fn main() { + let args: Vec = env::args().collect(); + let program = args[0].clone(); + + let mut opts = Options::new(); + opts.reqopt("l", + "local-port", + "The local port to which udpproxy should bind to", + "LOCAL_PORT"); + opts.reqopt("r", + "remote-port", + "The remote port to which UDP packets should be forwarded", + "REMOTE_PORT"); + opts.reqopt("h", + "host", + "The remote address to which packets will be forwarded", + "REMOTE_ADDR"); + opts.optopt("b", + "bind", + "The address on which to listen for incoming requests", + "BIND_ADDR"); + opts.optflag("d", "debug", "Enable debug mode"); + + let matches = opts.parse(&args[1..]) + .unwrap_or_else(|_| { + print_usage(&program, opts); + std::process::exit(-1); + }); + + unsafe { + DEBUG = matches.opt_present("d"); + } + let local_port: i32 = matches.opt_str("l").unwrap().parse().unwrap(); + let remote_port: i32 = matches.opt_str("r").unwrap().parse().unwrap(); + let remote_host = matches.opt_str("h").unwrap(); + let bind_addr = match matches.opt_str("b") { + Some(addr) => addr, + None => "127.0.0.1".to_owned(), + }; + + forward(&bind_addr, local_port, &remote_host, remote_port); +} + +fn forward(bind_addr: &str, local_port: i32, remote_host: &str, remote_port: i32) { + let local_addr = format!("{}:{}", bind_addr, local_port); + let local = UdpSocket::bind(&local_addr).unwrap_or_else(|_| panic!("Unable to bind to {}", &local_addr)); + information!("Listening on {}", local.local_addr().unwrap()); + + let remote_addr = format!("{}:{}", remote_host, remote_port); + + let responder = local + .try_clone() + .unwrap_or_else(|_| panic!("Failed to clone primary listening address socket {}", local.local_addr().unwrap())); + let (main_sender, main_receiver) = channel::<(_, Vec)>(); + thread::spawn(move || { + debugging!("Started new thread to deal out responses to clients"); + loop { + let (dest, buf) = main_receiver.recv().unwrap(); + let to_send = buf.as_slice(); + responder + .send_to(to_send, dest) + .unwrap_or_else(|_| panic!("Failed to forward response from upstream server to client {}", dest)); + } + }); + + let mut client_map = HashMap::new(); + let mut buf = [0; 64 * 1024]; + loop { + let (num_bytes, src_addr) = local.recv_from(&mut buf).expect("Didn't receive data"); + + //we create a new thread for each unique client + let mut remove_existing = false; + loop { + debugging!("Received packet from client {}", src_addr); + + let mut ignore_failure = true; + let client_id = format!("{}", src_addr); + + if remove_existing { + debugging!("Removing existing forwarder from map."); + client_map.remove(&client_id); + } + + let sender = client_map.entry(client_id.clone()).or_insert_with(|| { + //we are creating a new listener now, so a failure to send shoud be treated as an error + ignore_failure = false; + + let local_send_queue = main_sender.clone(); + let (sender, receiver) = channel::>(); + let remote_addr_copy = remote_addr.clone(); + thread::spawn(move|| { + //regardless of which port we are listening to, we don't know which interface or IP + //address the remote server is reachable via, so we bind the outgoing + //connection to 0.0.0.0 in all cases. + let temp_outgoing_addr = format!("0.0.0.0:{}", 1024 + rand::random::()); + debugging!("Establishing new forwarder for client {} on {}", src_addr, &temp_outgoing_addr); + let upstream_send = UdpSocket::bind(&temp_outgoing_addr) + .unwrap_or_else(|_| panic!("Failed to bind to transient address {}", &temp_outgoing_addr)); + let upstream_recv = upstream_send.try_clone() + .unwrap_or_else(|_| panic!("Failed to clone client-specific connection to upstream!")); + + let mut timeouts : u64 = 0; + let timed_out = Arc::new(AtomicBool::new(false)); + + let local_timed_out = timed_out.clone(); + thread::spawn(move|| { + let mut from_upstream = [0; 64 * 1024]; + upstream_recv.set_read_timeout(Some(Duration::from_millis(TIMEOUT + 100))).unwrap(); + loop { + match upstream_recv.recv_from(&mut from_upstream) { + Ok((bytes_rcvd, _)) => { + let to_send = from_upstream[..bytes_rcvd].to_vec(); + local_send_queue.send((src_addr, to_send)) + .expect("Failed to queue response from upstream server for forwarding!"); + }, + Err(_) => { + if local_timed_out.load(Ordering::Relaxed) { + debugging!("Terminating forwarder thread for client {} due to timeout", src_addr); + break; + } + } + }; + } + }); + + loop { + match receiver.recv_timeout(Duration::from_millis(TIMEOUT)) { + Ok(from_client) => { + upstream_send.send_to(from_client.as_slice(), &remote_addr_copy) + .unwrap_or_else(|_| panic!("Failed to forward packet from client {} to upstream server!", src_addr)); + timeouts = 0; //reset timeout count + }, + Err(_) => { + timeouts += 1; + if timeouts >= 10 { + debugging!("Disconnecting forwarder for client {} due to timeout", src_addr); + timed_out.store(true, Ordering::Relaxed); + break; + } + } + }; + } + }); + sender + }); + + let to_send = buf[..num_bytes].to_vec(); + match sender.send(to_send) { + Ok(_) => { + break; + } + Err(_) => { + if !ignore_failure { + panic!("Failed to send message to datagram forwarder for client {}", client_id); + } + //client previously timed out + debugging!("New connection received from previously timed-out client {}", client_id); + remove_existing = true; + continue; + } + } + } + } +} +