diff --git a/Cargo.lock b/Cargo.lock index cf9a55c2..5c9fbe54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4254,6 +4254,7 @@ dependencies = [ "mime_guess", "minify-html", "minify-js", + "native-tls", "notify", "notify-debouncer-full", "nu-ansi-term", @@ -4266,6 +4267,7 @@ dependencies = [ "remove_dir_all", "reqwest", "rstest", + "rustls", "schemars", "seahash", "semver", diff --git a/Cargo.toml b/Cargo.toml index 0969ec71..0d913747 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ lol_html = "1.2.1" mime_guess = "2.0.4" minify-html = "0.15.0" minify-js = "0.5.6" # stick with 0.5.x as 0.6 seems to create broken JS +native-tls = { version = "0.2", default-features = false, optional = true } notify = "8" notify-debouncer-full = "0.5" once_cell = "1" @@ -54,6 +55,7 @@ rand = "0.9.0" regex = "1" remove_dir_all = "1" reqwest = { version = "0.12", default-features = false, features = ["stream", "trust-dns"] } +rustls = { version = "0.23", default-features = false, optional = true } schemars = { version = "0.8", features = ["derive"] } seahash = { version = "4", features = ["use_std"] } semver = "1" @@ -103,6 +105,7 @@ rustls = [ "reqwest/rustls-tls-native-roots", "tokio-tungstenite/rustls", "tokio-tungstenite/rustls-tls-native-roots", + "dep:rustls", ] rustls-aws-lc = [ @@ -120,10 +123,11 @@ native-tls = [ "axum-server/tls-openssl", "reqwest/native-tls", "tokio-tungstenite/native-tls", + "dep:native-tls", ] # enable the update check on startup update_check = ["crates_io_api"] # enable vendoring on crates supporting that -vendored = ["openssl?/vendored"] +vendored = ["openssl?/vendored", "native-tls?/vendored"] diff --git a/src/proxy.rs b/src/proxy.rs index 44c1950a..d8c712a0 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -14,9 +14,11 @@ use bytes::BytesMut; use futures_util::{sink::SinkExt, stream::StreamExt, TryStreamExt}; use http::{header::HOST, HeaderMap}; use std::sync::Arc; +use tokio::net::TcpStream; use tokio_tungstenite::{ connect_async, - tungstenite::{protocol::CloseFrame, Message as MsgTng}, + tungstenite::{self, protocol::CloseFrame, Message as MsgTng}, + MaybeTlsStream, WebSocketStream, }; use tower_http::trace::TraceLayer; @@ -144,6 +146,121 @@ fn make_outbound_request( Ok(request) } +/// Attempts to create a websocket connection. +/// If `insecure` is true, it will allow websocket connections over insecure TLS. +/// Will fail if `insecure` is true and none of the `native-tls` or `rustls` features are enabled. +async fn connect_websocket( + outbound_request: http::Request<()>, + insecure: bool, +) -> anyhow::Result<( + WebSocketStream>, + tungstenite::handshake::client::Response, +)> { + if insecure { + #[cfg(any(feature = "native-tls", feature = "rustls"))] + { + use tokio_tungstenite::connect_async_tls_with_config; + connect_async_tls_with_config(outbound_request, None, false, make_insecure_connector()) + .await + .map_err(|err| { + anyhow::anyhow!("error establishing insecure WebSocket connection: {err}") + }) + } + #[cfg(not(any(feature = "native-tls", feature = "rustls")))] + { + Err(anyhow::anyhow!( + "Insecure WebSockets requires the `native-tls` or `rustls` to be feature enabled." + )) + } + } else { + connect_async(outbound_request) + .await + .map_err(|err| anyhow::anyhow!("error establishing secure WebSocket connection: {err}")) + } +} + +/// Create a connector which does not verify TLS certificates. +/// Defaults to a `rustls` connector if both the `rustls` and `native-tls` features are enabled. +#[cfg(any(feature = "native-tls", feature = "rustls"))] +fn make_insecure_connector() -> Option { + #[cfg(feature = "rustls")] + { + use rustls::{ + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + pki_types::{CertificateDer, ServerName, UnixTime}, + ClientConfig, DigitallySignedStruct, SignatureScheme, + }; + + /// A `rustls` certificate verifier that allows insecure certificates. + #[derive(Debug)] + struct NoCertVerification; + + impl ServerCertVerifier for NoCertVerification { + fn verify_server_cert( + &self, + _: &CertificateDer<'_>, + _: &[CertificateDer<'_>], + _: &ServerName<'_>, + _: &[u8], + _: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _: &[u8], + _: &CertificateDer<'_>, + _: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _: &[u8], + _: &CertificateDer<'_>, + _: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::ED25519, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED448, + ] + } + } + + Some(tokio_tungstenite::Connector::Rustls(std::sync::Arc::new( + ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoCertVerification {})) + .with_no_client_auth(), + ))) + } + #[cfg(all(feature = "native-tls", not(feature = "rustls")))] + { + match native_tls::TlsConnector::builder() + .danger_accept_invalid_certs(true) + .build() + { + Ok(connector) => Some(tokio_tungstenite::Connector::NativeTls(connector)), + Err(err) => { + tracing::error!(error = ?err, "error building native TLS connector"); + None + } + } + } +} + impl ProxyHandlerHttp { /// Construct a new instance. pub fn new( @@ -243,6 +360,8 @@ pub struct ProxyHandlerWebSocket { rewrite: Option, /// The headers to inject with the request request_headers: HeaderMap, + /// Allow insecure TLS websocket connections. + insecure: bool, } impl ProxyHandlerWebSocket { @@ -252,12 +371,14 @@ impl ProxyHandlerWebSocket { backend: Uri, headers: HeaderMap, rewrite: Option, + insecure: bool, ) -> Arc { Arc::new(Self { proto, backend, rewrite, request_headers: headers, + insecure, }) } @@ -337,14 +458,15 @@ impl ProxyHandlerWebSocket { } }; - // Establish WS connection to backend. - let (backend, _res) = match connect_async(outbound_request).await { + // Try to astablish a websocket connection to the backend and handle potential errors + let (backend, _res) = match connect_websocket(outbound_request, self.insecure).await { Ok(backend) => backend, Err(err) => { tracing::error!(error = ?err, "error establishing WebSocket connection to backend {:?} for proxy", &outbound_uri); return; } }; + let (mut backend_sink, mut backend_stream) = backend.split(); let (mut frontend_sink, mut frontend_stream) = ws.split(); diff --git a/src/serve/proxy.rs b/src/serve/proxy.rs index 1610f879..bfb31840 100644 --- a/src/serve/proxy.rs +++ b/src/serve/proxy.rs @@ -45,17 +45,24 @@ impl ProxyBuilder { .to_string(); if ws { + let insecure = opts.insecure; let handler = ProxyHandlerWebSocket::new( proto, backend.clone(), request_headers.clone(), rewrite, + insecure, ); tracing::info!( - "{}proxying websocket {} -> {}", + "{}proxying websocket {} -> {} {}", SERVER, handler.path(), - &backend + &backend, + if insecure { + format!("; {DANGER}️ insecure TLS") + } else { + Default::default() + } ); self.router = handler.register(self.router); Ok(self)