1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-10-01 08:44:09 +02:00

feat(proxy): Support direct TLS connection

This allows runing haze proxy without a reverse proxy while still having
the possiblity to use HTTPS.

Signed-off-by: Louis Chmn <louis@chmn.me>
This commit is contained in:
Louis Chmn 2026-09-16 22:31:16 +02:00
commit 048bbe8de5
5 changed files with 87 additions and 11 deletions

View file

@ -201,6 +201,10 @@ pub struct ProxyConfig {
pub address: String,
#[serde(default)]
pub https: bool,
#[serde(default)]
pub cert: Option<String>,
#[serde(default)]
pub key: Option<String>,
}
impl ProxyConfig {

View file

@ -32,6 +32,10 @@ use tokio::net::UnixListener;
use tokio::signal::ctrl_c;
use tokio::spawn;
use tokio::time::sleep;
use tokio_rustls::TlsAcceptor;
use tokio_rustls::rustls::ServerConfig;
use tokio_rustls::rustls::pki_types::pem::PemObject;
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
use tokio_stream::wrappers::{TcpListenerStream, UnixListenerStream};
use tracing::{debug, error, info};
@ -147,9 +151,34 @@ pub async fn proxy(docker: Docker, config: HazeConfig) -> Result<()> {
}
let listen = config.proxy.listen.clone();
let acceptor = match (&config.proxy.cert, &config.proxy.key) {
(None, None) => None,
(Some(_), None) => return Err(miette!("`cert` is set without `key`")),
(None, Some(_)) => return Err(miette!("`key` is set without `cert`")),
(Some(cert), Some(key)) => Some(tls_acceptor(cert, key)?),
};
let base_address = config.proxy.address.clone();
let instances = ActiveInstances::new(docker, config);
serve(instances, listen, base_address).await
serve(instances, listen, base_address, acceptor).await
}
/// Build a TLS acceptor from a PEM encoded certificate chain and private key on disk
fn tls_acceptor(cert: &str, key: &str) -> Result<TlsAcceptor> {
let certs = CertificateDer::pem_file_iter(cert)
.map_err(|e| miette!("failed to load certificate from {cert}: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| miette!("failed to load certificate from {cert}: {e}"))?;
let key = PrivateKeyDer::from_pem_file(key)
.map_err(|e| miette!("failed to load private key from {key}: {e}"))?;
let mut server_config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.into_diagnostic()?;
server_config.alpn_protocols = vec![b"http/1.1".to_vec()];
Ok(TlsAcceptor::from(Arc::new(server_config)))
}
#[derive(Clone)]
@ -159,7 +188,12 @@ struct AppState {
proxy_client: Arc<Client>,
}
async fn serve(instances: ActiveInstances, listen: String, base_address: String) -> Result<()> {
async fn serve(
instances: ActiveInstances,
listen: String,
base_address: String,
acceptor: Option<TlsAcceptor>,
) -> Result<()> {
let instances = Arc::new(instances);
let base_address = Arc::new(base_address);
let last_instances = instances.clone();
@ -188,12 +222,13 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String)
if !listen.starts_with('/') {
let addr: SocketAddr = listen.parse().into_diagnostic()?;
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
let scheme = if acceptor.is_some() { "https" } else { "http" };
println!("Listening on {scheme}://{}", listener.local_addr().unwrap());
let mut connections = pin!(TcpListenerStream::new(listener).take_until(cancel));
while let Some(stream) = connections.next().await {
match stream {
Ok(stream) => handle_connection(state.clone(), stream),
Ok(stream) => handle_connection(state.clone(), stream, acceptor.clone()).await,
Err(error) => {
error!(%error, "connection failed");
}
@ -216,7 +251,7 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String)
while let Some(stream) = connections.next().await {
match stream {
Ok(stream) => handle_connection(state.clone(), stream),
Ok(stream) => handle_connection(state.clone(), stream, None).await,
Err(error) => {
error!(%error, "connection failed");
}
@ -227,7 +262,22 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String)
Ok(())
}
fn handle_connection<I: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
async fn handle_connection<I: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
state: AppState,
stream: I,
acceptor: Option<TlsAcceptor>,
) {
// Spawn a tokio task to serve multiple connections concurrently
match acceptor {
Some(acceptor) => match acceptor.accept(stream).await {
Ok(stream) => serve_connection(state, stream).await,
Err(error) => error!(%error, "tls handshake failed"),
},
None => serve_connection(state, stream).await,
}
}
async fn serve_connection<I: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
state: AppState,
stream: I,
) {