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:
parent
7121474198
commit
048bbe8de5
5 changed files with 87 additions and 11 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
29
README.md
29
README.md
|
|
@ -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]]
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
62
src/proxy.rs
62
src/proxy.rs
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue