Compare commits

..

3 Commits

Author SHA1 Message Date
Stefan Sundin
2ec415ecac Remove outdated information since hyper-trust-dns is no longer used in the example. 2024-05-16 21:18:41 -07:00
Stefan Sundin
598b99252e Update README.md a bit. 2024-05-16 21:16:47 -07:00
Stefan Sundin
88e08c98f1 Make examples/simple.rs compatible with hyper v1. 2024-05-16 21:07:55 -07:00
3 changed files with 94 additions and 171 deletions

View File

@ -31,23 +31,19 @@ tokio = { version = "1.17.0", features = ["io-util", "rt"] }
tracing = "0.1.34" tracing = "0.1.34"
[dev-dependencies] [dev-dependencies]
hyper = { version = "1.2.0", features = ["client", "http1"] } hyper = { version = "1.2.0", features = ["client", "http1", "server"] }
futures = "0.3.21" futures = "0.3.21"
async-trait = "0.1.53" async-trait = "0.1.53"
async-tungstenite = { version = "0.17", features = ["tokio-runtime"] } async-tungstenite = { version = "0.17", features = ["tokio-runtime"] }
tokio-test = "0.4.2" tokio-test = "0.4.2"
test-context = "0.1.3" test-context = "0.1.3"
tokiotest-httpserver = "0.2.1" tokiotest-httpserver = "0.2.1"
hyper-trust-dns = { version = "0.4.2", features = [
"rustls-http2",
"dnssec-ring",
"dns-over-https-rustls",
"rustls-webpki"
] }
rand = "0.8.5" rand = "0.8.5"
tungstenite = "0.17" tungstenite = "0.17"
url = "2.2" url = "2.2"
criterion = "0.3.5" criterion = "0.3.5"
hyper-rustls = "0.27.1"
rustls = "0.23.6"
[features] [features]

142
README.md
View File

@ -1,6 +1,6 @@
# This is a fork # This is a fork
This repo contains a fork of the[original hyper-reverse-proxy This repo contains a fork of the [original hyper-reverse-proxy
codebase][upstream], adding to it a few improvements: codebase][upstream], adding to it a few improvements:
- Fix to a bug where the `Host` header was getting overwritten on the upstream - Fix to a bug where the `Host` header was getting overwritten on the upstream
@ -17,13 +17,10 @@ Plus more as time goes on.
# hyper-reverse-proxy # hyper-reverse-proxy
[![License][license-img]](LICENSE) [![License][license-img]](LICENSE)
[![CI][ci-img]][ci-url]
[![docs][docs-img]][docs-url] [![docs][docs-img]][docs-url]
[![version][version-img]][version-url] [![version][version-img]][version-url]
[license-img]: https://img.shields.io/crates/l/hyper-reverse-proxy.svg [license-img]: https://img.shields.io/crates/l/hyper-reverse-proxy.svg
[ci-img]: https://github.com/felipenoris/hyper-reverse-proxy/workflows/CI/badge.svg
[ci-url]: https://github.com/felipenoris/hyper-reverse-proxy/actions/workflows/main.yml
[docs-img]: https://docs.rs/hyper-reverse-proxy/badge.svg [docs-img]: https://docs.rs/hyper-reverse-proxy/badge.svg
[docs-url]: https://docs.rs/hyper-reverse-proxy [docs-url]: https://docs.rs/hyper-reverse-proxy
[version-img]: https://img.shields.io/crates/v/hyper-reverse-proxy.svg [version-img]: https://img.shields.io/crates/v/hyper-reverse-proxy.svg
@ -43,139 +40,16 @@ The implementation is based on Go's [`httputil.ReverseProxy`].
# Example # Example
Add these dependencies to your `Cargo.toml` file. Run the example by cloning this repository and running:
```toml ```shell
[dependencies] cargo run --example simple
hyper-reverse-proxy = "?"
hyper = { version = "?", features = ["full"] }
tokio = { version = "?", features = ["full"] }
lazy_static = "?"
hyper-trust-dns = { version = "?", features = [
"rustls-http2",
"dnssec-ring",
"dns-over-https-rustls",
"rustls-webpki",
"https-only"
] }
``` ```
The following example will set up a reverse proxy listening on `127.0.0.1:13900`, The example will set up a reverse proxy listening on `127.0.0.1:8000`, and will proxy these calls:
and will proxy these calls:
* `"/target/first"` will be proxied to `http://127.0.0.1:13901` * `http://service1.localhost:8000` will be proxied to `http://127.0.0.1:13901`
* `"/target/second"` will be proxied to `http://127.0.0.1:13902` * `http://service2.localhost:8000` will be proxied to `http://127.0.0.1:13902`
* All other URLs will be handled by `debug_request` function, that will display request information. * All other URLs will display request information.
```rust
use hyper::server::conn::AddrStream;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server, StatusCode};
use hyper_reverse_proxy::ReverseProxy;
use hyper_trust_dns::{RustlsHttpsConnector, TrustDnsResolver};
use std::net::IpAddr;
use std::{convert::Infallible, net::SocketAddr};
lazy_static::lazy_static! {
static ref PROXY_CLIENT: ReverseProxy<RustlsHttpsConnector> = {
ReverseProxy::new(
hyper::Client::builder().build::<_, hyper::Body>(TrustDnsResolver::default().into_rustls_webpki_https_connector()),
)
};
}
fn debug_request(req: &Request<Body>) -> Result<Response<Body>, Infallible> {
let body_str = format!("{:?}", req);
Ok(Response::new(Body::from(body_str)))
}
async fn handle(client_ip: IpAddr, req: Request<Body>) -> Result<Response<Body>, Infallible> {
if req.uri().path().starts_with("/target/first") {
match PROXY_CLIENT.call(client_ip, "http://127.0.0.1:13901", req)
.await
{
Ok(response) => {
Ok(response)
},
Err(_error) => {
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::empty())
.unwrap())},
}
} else if req.uri().path().starts_with("/target/second") {
match PROXY_CLIENT.call(client_ip, "http://127.0.0.1:13902", req)
.await
{
Ok(response) => Ok(response),
Err(_error) => Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::empty())
.unwrap()),
}
} else {
debug_request(&req)
}
}
#[tokio::main]
async fn main() {
let bind_addr = "127.0.0.1:8000";
let addr: SocketAddr = bind_addr.parse().expect("Could not parse ip:port.");
let make_svc = make_service_fn(|conn: &AddrStream| {
let remote_addr = conn.remote_addr().ip();
async move { Ok::<_, Infallible>(service_fn(move |req| handle(remote_addr, req))) }
});
let server = Server::bind(&addr).serve(make_svc);
println!("Running server on {:?}", addr);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
```
### A word about Security
Handling outgoing requests can be a security nightmare. This crate does not control the client for the outgoing requests, as it needs to be supplied to the proxy call. The following chapters may give you an overview on how you can secure your client using the `hyper-trust-dns` crate.
> You can see them being used in the example.
#### HTTPS
You should use a secure transport in order to know who you are talking to and so you can trust the connection. By default `hyper-trust-dns` enables the feature flag `https-only` which will panic if you supply a transport scheme which isn't `https`. It is a healthy default as it's not only you needing to trust the source but also everyone else seeing the content on unsecure connections.
> ATTENTION: if you are running on a host with added certificates in your cert store, make sure to audit them in a interval, so neither old certificates nor malicious certificates are considered as valid by your client.
#### TLS 1.2
By default `tls 1.2` is disabled in favor of `tls 1.3`, because many parts of `tls 1.2` can be considered as attach friendly. As not yet all services support it `tls 1.2` can be enabled via the `rustls-tls-12` feature.
> ATTENTION: make sure to audit the services you connect to on an interval
#### DNSSEC
As dns queries and entries aren't "trustworthy" by default from a security standpoint. `DNSSEC` adds a new cryptographic layer for verification. To enable it use the `dnssec-ring` feature.
#### HTTP/2
By default only rustlss `http1` feature is enabled for dns queries. While `http/3` might be just around the corner. `http/2` support can be enabled using the `rustls-http2` feature.
#### DoT & DoH
DoT and DoH provide you with a secure transport between you and your dns.
By default none of them are enabled. If you would like to enabled them, you can do so using the features `doh` and `dot`.
Recommendations:
- If you need to monitor network activities in relation to accessed ports, use dot with the `dns-over-rustls` feature flag
- If you are out in the wild and have no need to monitor based on ports, doh with the `dns-over-https-rustls` feature flag as it will blend in with other `https` traffic
It is highly recommended to use one of them.
> Currently only includes dns queries as `esni` or `ech` is still in draft by the `ietf`

View File

@ -1,26 +1,51 @@
use hyper::server::conn::AddrStream; use std::convert::Infallible;
use hyper::service::{make_service_fn, service_fn}; use std::io;
use hyper::{Body, Request, Response, Server, StatusCode}; use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use http_body_util::combinators::UnsyncBoxBody;
use http_body_util::{BodyExt, Empty, Full};
use hyper::body::{Bytes, Incoming};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
use tokio::net::TcpListener;
use hyper_reverse_proxy::ReverseProxy; use hyper_reverse_proxy::ReverseProxy;
use hyper_trust_dns::{RustlsHttpsConnector, TrustDnsResolver}; use hyper_rustls::{ConfigBuilderExt, HttpsConnector};
use std::net::IpAddr; use hyper_util::client::legacy::connect::HttpConnector;
use std::{convert::Infallible, net::SocketAddr};
type Connector = HttpsConnector<HttpConnector>;
type ResponseBody = UnsyncBoxBody<Bytes, std::io::Error>;
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref PROXY_CLIENT: ReverseProxy<RustlsHttpsConnector> = { static ref PROXY_CLIENT: ReverseProxy<Connector> = {
let connector: Connector = Connector::builder()
.with_tls_config(
rustls::ClientConfig::builder()
.with_native_roots()
.expect("with_native_roots")
.with_no_client_auth(),
)
.https_or_http()
.enable_http1()
.build();
ReverseProxy::new( ReverseProxy::new(
hyper::Client::builder().build::<_, hyper::Body>(TrustDnsResolver::default().into_rustls_webpki_https_connector()), hyper_util::client::legacy::Builder::new(TokioExecutor::new())
.pool_idle_timeout(Duration::from_secs(3))
.pool_timer(TokioTimer::new())
.build::<_, Incoming>(connector),
) )
}; };
} }
fn debug_request(req: &Request<Body>) -> Result<Response<Body>, Infallible> { async fn handle(
let body_str = format!("{:?}", req); client_ip: IpAddr,
Ok(Response::new(Body::from(body_str))) req: Request<Incoming>,
} ) -> Result<Response<ResponseBody>, Infallible> {
let host = req.headers().get("host").and_then(|v| v.to_str().ok());
async fn handle(client_ip: IpAddr, req: Request<Body>) -> Result<Response<Body>, Infallible> { if host.is_some_and(|host| host.starts_with("service1.localhost")) {
if req.uri().path().starts_with("/target/first") {
match PROXY_CLIENT match PROXY_CLIENT
.call(client_ip, "http://127.0.0.1:13901", req) .call(client_ip, "http://127.0.0.1:13901", req)
.await .await
@ -28,10 +53,12 @@ async fn handle(client_ip: IpAddr, req: Request<Body>) -> Result<Response<Body>,
Ok(response) => Ok(response), Ok(response) => Ok(response),
Err(_error) => Ok(Response::builder() Err(_error) => Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) .status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::empty()) .body(UnsyncBoxBody::new(
Empty::<Bytes>::new().map_err(io::Error::other),
))
.unwrap()), .unwrap()),
} }
} else if req.uri().path().starts_with("/target/second") { } else if host.is_some_and(|host| host.starts_with("service2.localhost")) {
match PROXY_CLIENT match PROXY_CLIENT
.call(client_ip, "http://127.0.0.1:13902", req) .call(client_ip, "http://127.0.0.1:13902", req)
.await .await
@ -39,29 +66,55 @@ async fn handle(client_ip: IpAddr, req: Request<Body>) -> Result<Response<Body>,
Ok(response) => Ok(response), Ok(response) => Ok(response),
Err(_error) => Ok(Response::builder() Err(_error) => Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) .status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::empty()) .body(UnsyncBoxBody::new(
Empty::<Bytes>::new().map_err(io::Error::other),
))
.unwrap()), .unwrap()),
} }
} else { } else {
debug_request(&req) let body_str = format!("{:?}", req);
Ok(Response::new(UnsyncBoxBody::new(
Full::new(Bytes::from(body_str)).map_err(io::Error::other),
)))
} }
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let bind_addr = "127.0.0.1:8000"; let bind_addr = "127.0.0.1:8000";
let addr: SocketAddr = bind_addr.parse().expect("Could not parse ip:port."); let addr: SocketAddr = bind_addr.parse().expect("Could not parse ip:port.");
let make_svc = make_service_fn(|conn: &AddrStream| { // We create a TcpListener and bind it to the address
let remote_addr = conn.remote_addr().ip(); let listener = TcpListener::bind(addr).await?;
async move { Ok::<_, Infallible>(service_fn(move |req| handle(remote_addr, req))) }
println!(
"Access service1 on http://service1.localhost:{}",
addr.port()
);
println!(
"Access service2 on http://service2.localhost:{}",
addr.port()
);
// We start a loop to continuously accept incoming connections
loop {
let (stream, remote_addr) = listener.accept().await?;
let client_ip = remote_addr.ip();
// Use an adapter to access something implementing `tokio::io` traits as if they implement
// `hyper::rt` IO traits.
let io = TokioIo::new(stream);
// Spawn a tokio task to serve multiple connections concurrently
tokio::task::spawn(async move {
// Finally, we bind the incoming connection to our `hello` service
if let Err(err) = http1::Builder::new()
// `service_fn` converts our function in a `Service`
.serve_connection(io, service_fn(move |req| handle(client_ip, req)))
.await
{
eprintln!("Error serving connection: {:?}", err);
}
}); });
let server = Server::bind(&addr).serve(make_svc);
println!("Running server on {:?}", addr);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
} }
} }