HTTP Proxy Server

This commit is contained in:
Sam Foxman 2022-06-25 23:34:18 -04:00
parent b78cab58ee
commit 7ea7b0e683
7 changed files with 238 additions and 37 deletions

92
Cargo.lock generated
View file

@ -146,6 +146,12 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.21" version = "0.3.21"
@ -267,6 +273,40 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 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]] [[package]]
name = "humantime" name = "humantime"
version = "1.3.0" version = "1.3.0"
@ -276,6 +316,29 @@ dependencies = [
"quick-error", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.1" version = "1.9.1"
@ -308,6 +371,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d"
[[package]]
name = "itoa"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
[[package]] [[package]]
name = "jni" name = "jni"
version = "0.19.0" version = "0.19.0"
@ -433,6 +502,7 @@ dependencies = [
"boringtun", "boringtun",
"clap", "clap",
"futures", "futures",
"hyper",
"log", "log",
"nom", "nom",
"pretty_env_logger", "pretty_env_logger",
@ -737,6 +807,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.35" version = "0.1.35"
@ -769,6 +845,12 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "try-lock"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.1" version = "1.0.1"
@ -804,6 +886,16 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View file

@ -21,6 +21,7 @@ nom = "7"
async-trait = "0.1.51" async-trait = "0.1.51"
priority-queue = "1.2.0" 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"] } 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 # bin-only dependencies
clap = { version = "2.33", default-features = false, features = ["suggestions"], optional = true } clap = { version = "2.33", default-features = false, features = ["suggestions"], optional = true }

View file

@ -15,6 +15,7 @@ pub struct Config {
pub port_forwards: Vec<PortForwardConfig>, pub port_forwards: Vec<PortForwardConfig>,
#[allow(dead_code)] #[allow(dead_code)]
pub remote_port_forwards: Vec<PortForwardConfig>, pub remote_port_forwards: Vec<PortForwardConfig>,
pub proxy_listen_addr: Option<SocketAddr>,
pub private_key: Arc<X25519SecretKey>, pub private_key: Arc<X25519SecretKey>,
pub endpoint_public_key: Arc<X25519PublicKey>, pub endpoint_public_key: Arc<X25519PublicKey>,
pub endpoint_addr: SocketAddr, pub endpoint_addr: SocketAddr,
@ -133,6 +134,13 @@ impl Config {
\t--remote 8080:[::1]:8081:TCP\n\ \t--remote 8080:[::1]:8081:TCP\n\
\t--remote 8080:google.com:80\ \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(); ]).get_matches();
// Combine `PORT_FORWARD` arg and `ONETUN_PORT_FORWARD_#` envs // Combine `PORT_FORWARD` arg and `ONETUN_PORT_FORWARD_#` envs
@ -203,8 +211,13 @@ impl Config {
port_forward.remote = true; port_forward.remote = true;
} }
if port_forwards.is_empty() && remote_port_forwards.is_empty() { let proxy_listen_addr = match matches.value_of("proxy-listen-addr") {
return Err(anyhow::anyhow!("No port forward configurations given.")); 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 // Read private key from file or CLI argument
@ -257,6 +270,7 @@ impl Config {
Ok(Self { Ok(Self {
port_forwards, port_forwards,
remote_port_forwards, remote_port_forwards,
proxy_listen_addr,
private_key: Arc::new( private_key: Arc::new(
parse_private_key(&private_key).with_context(|| "Invalid private key")?, parse_private_key(&private_key).with_context(|| "Invalid private key")?,
), ),

View file

@ -66,6 +66,7 @@ pub async fn start_tunnels(config: Config, bus: Bus) -> anyhow::Result<()> {
.port_forwards .port_forwards
.iter() .iter()
.any(|pf| pf.protocol == PortProtocol::Tcp) .any(|pf| pf.protocol == PortProtocol::Tcp)
|| config.proxy_listen_addr.is_some()
{ {
// TCP device // TCP device
let bus = bus.clone(); 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(()) Ok(())
} }

75
src/tunnel/http_proxy.rs Normal file
View file

@ -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<Body>, port_pool: TcpPortPool, bus: Bus) -> Result<Response<Body>, 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<String> {
uri.authority().and_then(|auth| Some(auth.to_string()))
}

View file

@ -1,4 +1,4 @@
use std::net::IpAddr; use std::net::{IpAddr, SocketAddr};
use std::sync::Arc; use std::sync::Arc;
use crate::config::{PortForwardConfig, PortProtocol}; use crate::config::{PortForwardConfig, PortProtocol};
@ -9,6 +9,7 @@ use crate::wg::WireGuardTunnel;
pub mod tcp; pub mod tcp;
pub mod udp; pub mod udp;
pub mod http_proxy;
pub async fn port_forward( pub async fn port_forward(
port_forward: PortForwardConfig, 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, 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<WireGuardTunnel>,
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
}

View file

@ -3,7 +3,7 @@ use crate::virtual_iface::VirtualPort;
use anyhow::Context; use anyhow::Context;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::Arc; use std::sync::Arc;
use tokio::net::{TcpListener, TcpStream}; use tokio::net::TcpListener;
use std::ops::Range; use std::ops::Range;
use std::time::Duration; use std::time::Duration;
@ -11,7 +11,7 @@ use std::time::Duration;
use crate::events::{Bus, Event}; use crate::events::{Bus, Event};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use rand::thread_rng; use rand::thread_rng;
use tokio::io::AsyncWriteExt; use tokio::io::{AsyncReadExt, AsyncWriteExt};
const MAX_PACKET: usize = 65536; const MAX_PACKET: usize = 65536;
const MIN_PORT: u16 = 1000; 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. /// Handles a new TCP connection with its assigned virtual port.
async fn handle_tcp_proxy_connection( pub async fn handle_tcp_proxy_connection<A>(
mut socket: TcpStream, mut socket: A,
virtual_port: VirtualPort, virtual_port: VirtualPort,
port_forward: PortForwardConfig, port_forward: PortForwardConfig,
bus: Bus, bus: Bus,
) -> anyhow::Result<()> { ) -> anyhow::Result<()>
where
A: AsyncReadExt + AsyncWriteExt + Unpin
{
let mut endpoint = bus.new_endpoint(); let mut endpoint = bus.new_endpoint();
endpoint.send(Event::ClientConnectionInitiated(port_forward, virtual_port)); endpoint.send(Event::ClientConnectionInitiated(port_forward, virtual_port));
let mut buffer = Vec::with_capacity(MAX_PACKET); let mut buffer = vec![0; MAX_PACKET];
loop { loop {
tokio::select! { tokio::select! {
readable_result = socket.readable() => { read_result = socket.read(&mut buffer) => {
match readable_result { match read_result {
Ok(_) => { Ok(size) if size > 0 => {
match socket.try_read_buf(&mut buffer) { let data = Vec::from(&buffer[..size]);
Ok(size) if size > 0 => { endpoint.send(Event::LocalData(port_forward, virtual_port, data));
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 => {
// Reset buffer continue;
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;
}
}
} }
Err(e) => { Err(e) => {
error!("[{}] Failed to check if readable: {:?}", virtual_port, e); error!(
"[{}] Failed to read from client TCP socket: {:?}",
virtual_port, e
);
break;
}
_ => {
break; break;
} }
} }
@ -123,9 +117,6 @@ async fn handle_tcp_proxy_connection(
} }
Event::RemoteData(e_vp, data) if e_vp == virtual_port => { Event::RemoteData(e_vp, data) if e_vp == virtual_port => {
// Have remote data to send to the local client // 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 expected = data.len();
let mut sent = 0; let mut sent = 0;
loop { loop {