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

Merge pull request 'feat(proxy): Support direct TLS connection' (#50) from artonge/haze:artonge/feat/support_tls into main

Reviewed-on: https://codeberg.org/icewind/haze/pulls/50
This commit is contained in:
Robin Appelman 2026-09-16 22:45:11 +02:00
commit 9dfaf4b27d
5 changed files with 87 additions and 11 deletions

2
Cargo.lock generated
View file

@ -933,6 +933,7 @@ dependencies = [
"tar", "tar",
"termion", "termion",
"tokio", "tokio",
"tokio-rustls",
"tokio-stream", "tokio-stream",
"toml", "toml",
"tracing", "tracing",
@ -2016,6 +2017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"log",
"once_cell", "once_cell",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki", "rustls-webpki",

View file

@ -44,6 +44,7 @@ tokio = { version = "1.53.1", features = [
"rt-multi-thread", "rt-multi-thread",
"signal" "signal"
] } ] }
tokio-rustls = "0.26"
tokio-stream = { version = "0.1.19", features = ["net"] } tokio-stream = { version = "0.1.19", features = ["net"] }
toml = "1.1.4" toml = "1.1.4"
tracing = "0.1.44" tracing = "0.1.44"

View file

@ -299,14 +299,16 @@ By default, instances can be accessed by their IP. In order to get more
memorable URLs and allow supporting https, haze comes with a builtin reverse memorable URLs and allow supporting https, haze comes with a builtin reverse
proxy to allow using a wildcard domain. proxy to allow using a wildcard domain.
### Requirements ### DNS Setup
#### Requirements
- A domain name you can set wildcard DNS records for - A domain name you can set wildcard DNS records for
- A reverse proxy like Nginx or Apache - A reverse proxy like Nginx or Apache
- (optionally) a wildcard ssl certificate (can be acquiring using letsencrypt - (optionally) a wildcard ssl certificate (can be acquiring using letsencrypt
and dns verification) and dns verification)
### DNS Setup #### Steps
- Set a DNS record for `*.haze.example.com` and `haze.example.com` pointing to - Set a DNS record for `*.haze.example.com` and `haze.example.com` pointing to
your development machine. your development machine.
@ -334,8 +336,23 @@ mkcert -cert-file <path-to-your-certificats>haze.test.crt -key-file <path-to-you
- Set up a service to run `haze proxy` in the background as your own user. A - Set up a service to run `haze proxy` in the background as your own user. A
systemd user service is recommended (see [haze.service](./haze.service) for an systemd user service is recommended (see [haze.service](./haze.service) for an
example). example).
- Setup a reverse proxy to proxy `*.haze.test` and `haze.test` to the - Either point haze at the certificate directly:
`haze proxy`'s socket. Example for Nginx:
```toml
[proxy]
address = "haze.test"
https = true
listen = "0.0.0.0:443"
cert = "<path-to-your-certificats>/haze.test.crt"
key = "<path-to-your-certificats>/haze.test.key"
```
Binding to port 443 as a regular user requires either
`sudo setcap cap_net_bind_service=+ep $(which haze)` or
`sysctl net.ipv4.ip_unprivileged_port_start=443`.
- Or, if you already have another web server, setup it up to proxy `*.haze.test`
and `haze.test` to the `haze proxy`'s socket. Example for Nginx:
```nginx ```nginx
upstream haze-handler { upstream haze-handler {
@ -467,9 +484,11 @@ read_only = true
[proxy] # optional [proxy] # optional
address = "haze.example.com" # base domain address = "haze.example.com" # base domain
https = true # Is the proxy behind a https terminating proxy https = true # Whether the instances are reachable over https
listen = "/run/haze/haze.sock" # either a unix socket path listen = "/run/haze/haze.sock" # either a unix socket path
#listen = "127.0.0.1:8080" # or a socket address #listen = "127.0.0.1:8080" # or a socket address
cert = "/path/to/haze.test.crt" # optional - PEM encoded certificate chain
key = "/path/to/haze.test.key" # optional - PEM encoded private key
# presets allow for easy usage of commonly used setups # presets allow for easy usage of commonly used setups
[[preset]] [[preset]]

View file

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

View file

@ -32,6 +32,10 @@ use tokio::net::UnixListener;
use tokio::signal::ctrl_c; use tokio::signal::ctrl_c;
use tokio::spawn; use tokio::spawn;
use tokio::time::sleep; 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 tokio_stream::wrappers::{TcpListenerStream, UnixListenerStream};
use tracing::{debug, error, info}; 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 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 base_address = config.proxy.address.clone();
let instances = ActiveInstances::new(docker, config); 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)] #[derive(Clone)]
@ -159,7 +188,12 @@ struct AppState {
proxy_client: Arc<Client>, 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 instances = Arc::new(instances);
let base_address = Arc::new(base_address); let base_address = Arc::new(base_address);
let last_instances = instances.clone(); let last_instances = instances.clone();
@ -188,12 +222,13 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String)
if !listen.starts_with('/') { if !listen.starts_with('/') {
let addr: SocketAddr = listen.parse().into_diagnostic()?; let addr: SocketAddr = listen.parse().into_diagnostic()?;
let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 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)); let mut connections = pin!(TcpListenerStream::new(listener).take_until(cancel));
while let Some(stream) = connections.next().await { while let Some(stream) = connections.next().await {
match stream { match stream {
Ok(stream) => handle_connection(state.clone(), stream), Ok(stream) => handle_connection(state.clone(), stream, acceptor.clone()).await,
Err(error) => { Err(error) => {
error!(%error, "connection failed"); 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 { while let Some(stream) = connections.next().await {
match stream { match stream {
Ok(stream) => handle_connection(state.clone(), stream), Ok(stream) => handle_connection(state.clone(), stream, None).await,
Err(error) => { Err(error) => {
error!(%error, "connection failed"); error!(%error, "connection failed");
} }
@ -227,7 +262,22 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String)
Ok(()) 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, state: AppState,
stream: I, stream: I,
) { ) {