diff --git a/Cargo.lock b/Cargo.lock index d8ea4ca..cdfa2d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "futures" version = "0.3.21" @@ -267,6 +273,40 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + [[package]] name = "humantime" version = "1.3.0" @@ -276,6 +316,29 @@ dependencies = [ "quick-error", ] +[[package]] +name = "hyper" +version = "0.14.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "indexmap" version = "1.9.1" @@ -308,6 +371,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + [[package]] name = "jni" version = "0.19.0" @@ -433,6 +502,7 @@ dependencies = [ "boringtun", "clap", "futures", + "hyper", "log", "nom", "pretty_env_logger", @@ -737,6 +807,12 @@ dependencies = [ "syn", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.35" @@ -769,6 +845,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + [[package]] name = "unicode-ident" version = "1.0.1" @@ -804,6 +886,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 59ce18f..68bac99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ nom = "7" async-trait = "0.1.51" priority-queue = "1.2.0" smoltcp = { version = "0.8.0", default-features = false, features = ["std", "log", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-udp", "socket-tcp"] } +hyper = { version = "0.14", features = ["server", "http1", "tcp"] } # bin-only dependencies clap = { version = "2.33", default-features = false, features = ["suggestions"], optional = true } diff --git a/src/config.rs b/src/config.rs index 9d9732d..792185f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ pub struct Config { pub port_forwards: Vec, #[allow(dead_code)] pub remote_port_forwards: Vec, + pub proxy_listen_addr: Option, pub private_key: Arc, pub endpoint_public_key: Arc, pub endpoint_addr: SocketAddr, @@ -133,6 +134,13 @@ impl Config { \t--remote 8080:[::1]:8081:TCP\n\ \t--remote 8080:google.com:80\ "), + Arg::with_name("proxy-listen-addr") + .required(false) + .takes_value(true) + .long("http-proxy") + .env("ONETUN_PROXY_LISTEN_ADDR") + .help("Create an HTTP proxy server that listens on this address. All connections through the proxy will be forwarded through the WireGuard tunnel. \ + Currently, only the CONNECT method is supported, and only IP addresses are supported (the server will not do any DNS lookups)."), ]).get_matches(); // Combine `PORT_FORWARD` arg and `ONETUN_PORT_FORWARD_#` envs @@ -203,8 +211,13 @@ impl Config { port_forward.remote = true; } - if port_forwards.is_empty() && remote_port_forwards.is_empty() { - return Err(anyhow::anyhow!("No port forward configurations given.")); + let proxy_listen_addr = match matches.value_of("proxy-listen-addr") { + Some(proxy_listen_addr) => Some(parse_addr(Some(proxy_listen_addr)).with_context(|| "Invalid proxy listen address")?), + None => None + }; + + if port_forwards.is_empty() && remote_port_forwards.is_empty() && proxy_listen_addr.is_none() { + return Err(anyhow::anyhow!("No port forward or http proxy configurations given.")); } // Read private key from file or CLI argument @@ -257,6 +270,7 @@ impl Config { Ok(Self { port_forwards, remote_port_forwards, + proxy_listen_addr, private_key: Arc::new( parse_private_key(&private_key).with_context(|| "Invalid private key")?, ), diff --git a/src/lib.rs b/src/lib.rs index 57a3fc4..609b055 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,7 @@ pub async fn start_tunnels(config: Config, bus: Bus) -> anyhow::Result<()> { .port_forwards .iter() .any(|pf| pf.protocol == PortProtocol::Tcp) + || config.proxy_listen_addr.is_some() { // TCP device let bus = bus.clone(); @@ -118,5 +119,14 @@ pub async fn start_tunnels(config: Config, bus: Bus) -> anyhow::Result<()> { }); } + if let Some(proxy_listen_addr) = config.proxy_listen_addr { + let source_peer_ip = config.source_peer_ip; + tokio::spawn(async move { + tunnel::http_proxy_listen(proxy_listen_addr, source_peer_ip, tcp_port_pool.clone(), wg.clone(), bus.clone()) + .await + .unwrap_or_else(|e| error!("HTTP Proxy Listen failed: {}", e)); + }); + } + Ok(()) } diff --git a/src/tunnel/http_proxy.rs b/src/tunnel/http_proxy.rs new file mode 100644 index 0000000..efe8d9f --- /dev/null +++ b/src/tunnel/http_proxy.rs @@ -0,0 +1,75 @@ +use std::convert::Infallible; +use std::net::SocketAddr; +use crate::{PortProtocol, TcpPortPool}; +use crate::Bus; + +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Server, Method, Request, Body, Response, http}; +use hyper::server::conn::AddrStream; +use crate::config::PortForwardConfig; +use crate::tunnel::tcp::handle_tcp_proxy_connection; + +pub async fn http_proxy_server( + listen_addr: SocketAddr, + port_pool: TcpPortPool, + bus: Bus, +) -> anyhow::Result<()> { + + let make_service = make_service_fn(move |conn: &AddrStream| { + let bus = bus.clone(); + let port_pool = port_pool.clone(); + let addr = conn.remote_addr(); + async move { + let addr = addr.clone(); + Ok::<_, Infallible>(service_fn(move |req| proxy(addr, req, port_pool.clone(), bus.clone()))) + } + }); + + let server = Server::bind(&listen_addr).serve(make_service); + + Ok(server.await?) + +} + +async fn proxy(remote_addr: SocketAddr, req: Request, port_pool: TcpPortPool, bus: Bus) -> Result, hyper::Error> { + debug!("http request from {}: {:?}", remote_addr, req); + + if Method::CONNECT == req.method() { + if let Some(addr) = host_addr(req.uri()) { + tokio::task::spawn(async move { + match hyper::upgrade::on(req).await { + Ok(upgraded) => { + let config = PortForwardConfig { + destination: addr.parse().expect("failed to parse destination SocketAddr"), + source: remote_addr, // doesn't matter, won't be used + protocol: PortProtocol::Tcp, + remote: false + }; + info!("proxy {} -> {}", remote_addr, config.destination); + if let Err(e) = handle_tcp_proxy_connection(upgraded, port_pool.next().await.unwrap(), config, bus.clone()).await { + eprintln!("server io error: {}", e); + }; + } + Err(e) => eprintln!("upgrade error: {}", e), + } + }); + + Ok(Response::new(Body::empty())) + } else { + eprintln!("CONNECT host is not socket addr: {:?}", req.uri()); + let mut resp = Response::new(Body::from("CONNECT must be to a socket address")); + *resp.status_mut() = http::StatusCode::BAD_REQUEST; + + Ok(resp) + } + } else { + let mut resp = Response::new(Body::from("Only CONNECT method supported")); + *resp.status_mut() = http::StatusCode::METHOD_NOT_ALLOWED; + + Ok(resp) + } +} + +fn host_addr(uri: &http::Uri) -> Option { + uri.authority().and_then(|auth| Some(auth.to_string())) +} \ No newline at end of file diff --git a/src/tunnel/mod.rs b/src/tunnel/mod.rs index eadf8b0..4f64b8e 100644 --- a/src/tunnel/mod.rs +++ b/src/tunnel/mod.rs @@ -1,4 +1,4 @@ -use std::net::IpAddr; +use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use crate::config::{PortForwardConfig, PortProtocol}; @@ -9,6 +9,7 @@ use crate::wg::WireGuardTunnel; pub mod tcp; pub mod udp; +pub mod http_proxy; pub async fn port_forward( port_forward: PortForwardConfig, @@ -32,3 +33,20 @@ pub async fn port_forward( PortProtocol::Udp => udp::udp_proxy_server(port_forward, udp_port_pool, bus).await, } } + +pub async fn http_proxy_listen( + listen_addr: SocketAddr, + source_peer_ip: IpAddr, + tcp_port_pool: TcpPortPool, + wg: Arc, + bus: Bus, +) -> anyhow::Result<()> { + info!( + "HTTP proxy server listening on {} (forwarding through {} as peer {})", + listen_addr, + &wg.endpoint, + source_peer_ip + ); + + http_proxy::http_proxy_server(listen_addr, tcp_port_pool, bus).await +} \ No newline at end of file diff --git a/src/tunnel/tcp.rs b/src/tunnel/tcp.rs index e9644b9..9c7bfad 100644 --- a/src/tunnel/tcp.rs +++ b/src/tunnel/tcp.rs @@ -3,7 +3,7 @@ use crate::virtual_iface::VirtualPort; use anyhow::Context; use std::collections::VecDeque; use std::sync::Arc; -use tokio::net::{TcpListener, TcpStream}; +use tokio::net::TcpListener; use std::ops::Range; use std::time::Duration; @@ -11,7 +11,7 @@ use std::time::Duration; use crate::events::{Bus, Event}; use rand::seq::SliceRandom; use rand::thread_rng; -use tokio::io::AsyncWriteExt; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; const MAX_PACKET: usize = 65536; const MIN_PORT: u16 = 1000; @@ -72,45 +72,39 @@ pub async fn tcp_proxy_server( } /// Handles a new TCP connection with its assigned virtual port. -async fn handle_tcp_proxy_connection( - mut socket: TcpStream, +pub async fn handle_tcp_proxy_connection( + mut socket: A, virtual_port: VirtualPort, port_forward: PortForwardConfig, bus: Bus, -) -> anyhow::Result<()> { +) -> anyhow::Result<()> +where + A: AsyncReadExt + AsyncWriteExt + Unpin +{ let mut endpoint = bus.new_endpoint(); endpoint.send(Event::ClientConnectionInitiated(port_forward, virtual_port)); - let mut buffer = Vec::with_capacity(MAX_PACKET); + let mut buffer = vec![0; MAX_PACKET]; + loop { tokio::select! { - readable_result = socket.readable() => { - match readable_result { - Ok(_) => { - match socket.try_read_buf(&mut buffer) { - Ok(size) if size > 0 => { - let data = Vec::from(&buffer[..size]); - endpoint.send(Event::LocalData(port_forward, virtual_port, data)); - // Reset buffer - buffer.clear(); - } - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { - continue; - } - Err(e) => { - error!( - "[{}] Failed to read from client TCP socket: {:?}", - virtual_port, e - ); - break; - } - _ => { - break; - } - } + read_result = socket.read(&mut buffer) => { + match read_result { + Ok(size) if size > 0 => { + let data = Vec::from(&buffer[..size]); + endpoint.send(Event::LocalData(port_forward, virtual_port, data)); + } + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { + continue; } Err(e) => { - error!("[{}] Failed to check if readable: {:?}", virtual_port, e); + error!( + "[{}] Failed to read from client TCP socket: {:?}", + virtual_port, e + ); + break; + } + _ => { break; } } @@ -123,9 +117,6 @@ async fn handle_tcp_proxy_connection( } Event::RemoteData(e_vp, data) if e_vp == virtual_port => { // Have remote data to send to the local client - if let Err(e) = socket.writable().await { - error!("[{}] Failed to check if writable: {:?}", virtual_port, e); - } let expected = data.len(); let mut sent = 0; loop {