1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
use anyhow::{Context, bail};
use bytes::Bytes;
use http_body_util::{BodyExt, Full, combinators::BoxBody};
use hyper::body::Incoming;
use hyper::header::HeaderValue;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use rcgen::{BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::collections::HashMap;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::{Arc, RwLock};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::task::JoinHandle;
use tokio_rustls::TlsAcceptor;
type Body = BoxBody<Bytes, hyper::Error>;
pub struct Task(pub JoinHandle<()>);
impl Drop for Task {
fn drop(&mut self) {
self.0.abort();
}
}
#[derive(Clone, Default)]
pub struct Routes(Arc<RwLock<HashMap<String, SocketAddr>>>);
impl Routes {
pub fn set(&self, host: &str, backend: SocketAddr) {
self.0.write().unwrap().insert(host.to_ascii_lowercase(), backend);
}
fn get(&self, host: &str) -> Option<SocketAddr> {
let host = host.to_ascii_lowercase();
let routes = self.0.read().unwrap();
routes
.get(&host)
.or_else(|| host.split_once('.').and_then(|(_, parent)| routes.get(&format!("*.{parent}"))))
.copied()
}
}
pub struct Certs {
pub ca_pem: String,
leaf_der: Vec<u8>,
leaf_key: Vec<u8>,
}
impl Certs {
pub fn generate(hosts: &[&str]) -> anyhow::Result<Certs> {
let ca_key = KeyPair::generate()?;
let mut ca_params = CertificateParams::default();
ca_params.distinguished_name.push(DnType::CommonName, "zkwid devnet CA");
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
ca_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
let ca_cert = ca_params.self_signed(&ca_key)?;
let issuer = Issuer::new(ca_params, ca_key);
let leaf_key = KeyPair::generate()?;
let mut leaf_params = CertificateParams::new(hosts.iter().map(|host| host.to_string()).collect::<Vec<_>>())?;
leaf_params.distinguished_name.push(DnType::CommonName, hosts[0]);
leaf_params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
leaf_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
let leaf = leaf_params.signed_by(&leaf_key, &issuer)?;
Ok(Certs { ca_pem: ca_cert.pem(), leaf_der: leaf.der().to_vec(), leaf_key: leaf_key.serialize_der() })
}
pub fn acceptor(&self) -> anyhow::Result<TlsAcceptor> {
let config = rustls::ServerConfig::builder_with_provider(Arc::new(rustls::crypto::aws_lc_rs::default_provider()))
.with_safe_default_protocol_versions()?
.with_no_client_auth()
.with_single_cert(
vec![CertificateDer::from(self.leaf_der.clone())],
PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(self.leaf_key.clone())),
)?;
Ok(TlsAcceptor::from(Arc::new(config)))
}
}
pub async fn start(routes: Routes, tls: TlsAcceptor) -> anyhow::Result<(SocketAddr, Task)> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let task = tokio::spawn(async move {
loop {
let Ok((stream, peer)) = listener.accept().await else { continue };
let (routes, tls) = (routes.clone(), tls.clone());
tokio::spawn(async move {
tunnel(stream, peer, routes, tls).await.ok();
});
}
});
Ok((addr, Task(task)))
}
async fn tunnel(mut stream: TcpStream, peer: SocketAddr, routes: Routes, tls: TlsAcceptor) -> anyhow::Result<()> {
let head = read_head(&mut stream).await?;
let target = head.lines().next().and_then(|line| line.strip_prefix("CONNECT ")).and_then(|rest| rest.split(' ').next());
let host = match target.and_then(|target| target.strip_suffix(":443")) {
Some(host) if routes.get(host).is_some() => host.to_ascii_lowercase(),
_ => {
stream.write_all(b"HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\nconnection: close\r\n\r\n").await?;
return Ok(());
}
};
stream.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n").await?;
let stream = tls.accept(stream).await?;
let service = service_fn(move |request| forward(routes.clone(), host.clone(), peer, request));
hyper::server::conn::http1::Builder::new().serve_connection(TokioIo::new(stream), service).await?;
Ok(())
}
async fn read_head(stream: &mut TcpStream) -> anyhow::Result<String> {
let mut head = Vec::new();
while !head.ends_with(b"\r\n\r\n") {
if head.len() > 8192 {
bail!("CONNECT head too large");
}
head.push(stream.read_u8().await.context("reading CONNECT head")?);
}
Ok(String::from_utf8(head)?)
}
async fn forward(routes: Routes, host: String, peer: SocketAddr, request: Request<Incoming>) -> Result<Response<Body>, Infallible> {
let Some(backend) = routes.get(&host) else {
return Ok(bad_gateway(format!("no route for {host}")));
};
Ok(match send(backend, &host, peer, request).await {
Ok(response) => response.map(BodyExt::boxed),
Err(error) => bad_gateway(format!("{host} -> {backend}: {error:#}")),
})
}
async fn send(backend: SocketAddr, host: &str, peer: SocketAddr, mut request: Request<Incoming>) -> anyhow::Result<Response<Incoming>> {
let headers = request.headers_mut();
headers.insert("x-forwarded-proto", HeaderValue::from_static("https"));
headers.insert("x-forwarded-host", HeaderValue::from_str(host)?);
headers.insert("x-forwarded-for", HeaderValue::from_str(&peer.ip().to_string())?);
let stream = TcpStream::connect(backend).await?;
let (mut sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
tokio::spawn(connection);
Ok(sender.send_request(request).await?)
}
fn bad_gateway(message: String) -> Response<Body> {
let mut response = Response::new(Full::new(Bytes::from(message)).map_err(|never| match never {}).boxed());
*response.status_mut() = StatusCode::BAD_GATEWAY;
response
}