From a3715cfda9054378664373bb896fd1fdfe66397f Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 31 Aug 2026 21:30:56 -0700 Subject: [PATCH] pay attention to sni info when resolving backend now that pingora supports getting sni info out of rustls, make use of that to avoid any weirdness around sni-vs-host-header shennaningans, checking that host == sni when sni is present. Fixes MM-26 Fixes #12 --- src/gateway.rs | 26 +++++++++++++++++++++++--- src/main.rs | 40 +++++++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/gateway.rs b/src/gateway.rs index 24a0928..b75e4a9 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -20,6 +20,12 @@ use crate::{config, cookies, httputil}; pub mod oidc; +/// tls sni information passed from the tls handshake to the backend processing logic +pub struct SniInfo { + /// the server name that the client specified in their tls request + pub server_name: String, +} + /// per-domain information about backends and such pub struct DomainInfo { /// the load balancer to use to select backends @@ -44,9 +50,11 @@ impl AuthGateway { /// fetch the domain info for this request fn domain_info<'s>(&'s self, session: &Session) -> Result<&'s DomainInfo> { let req = session.req_header(); - // TODO(potential-bug): afaict, right now, afaict, pingora a) does not check that SNI matches the `Host` - // header, b) does not support extracting the SNI info on rustls, so we'll have to switch - // to boringssl and implement that ourselves T_T + let sni_host = session + .digest() + .and_then(|d| d.ssl_digest.as_ref()) + .and_then(|d| d.extension.get::()) + .map(|info| &*info.server_name); let host = req .headers .get(http::header::HOST) @@ -63,6 +71,18 @@ impl AuthGateway { e, ) })?; + + if let Some(sni_host) = sni_host + && sni_host != host + { + return Err(status_error( + "host header does not match server name provided in tls sni handshake", + ErrorSource::Downstream, + // technically the standard says a proxy must not return this, but i'm p sure + // they're talking about a forward proxy, not a reverse proxy + StatusCode::MISDIRECTED_REQUEST, + )()); + } let info = self.domains.get(host).ok_or_else(status_error( "unknown host", ErrorSource::Downstream, diff --git a/src/main.rs b/src/main.rs index a50a693..27e8417 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,13 +3,15 @@ use std::sync::Arc; use color_eyre::eyre::Context as _; use pingora::lb::{self, Backend}; +use pingora::listeners::TlsAccept; use pingora::listeners::tls::TlsSettings; use pingora::prelude::*; +use pingora::protocols::tls::TlsRef; use pingora::utils::tls::CertKey; use rustls::sign::CertifiedKey; use self::backend_selection::SelectionChoice; -use self::gateway::{AuthGateway, BackendData, DomainInfo, oidc}; +use self::gateway::{AuthGateway, BackendData, DomainInfo, SniInfo, oidc}; mod backend_selection; mod config; @@ -163,22 +165,38 @@ fn main() -> color_eyre::Result<()> { "unable to load cert and key from file" )); }; - resolver.add( - &sni, - CertifiedKey::from_der( - cert, - key, - pingora::tls::CryptoProvider::get_default() - .expect("no default crypto provider"), + resolver + .add( + &sni, + CertifiedKey::from_der( + cert, + key, + pingora::tls::CryptoProvider::get_default() + .expect("no default crypto provider"), + ) + .context("parsing cert/key")?, ) - .context("parsing cert/key")?, - ).context("adding sni setup")?; + .context("adding sni setup")?; Ok(resolver) }, ) .context("setting up sni for tls")?; + + struct Callbacks; + #[async_trait::async_trait] + impl TlsAccept for Callbacks { + async fn handshake_complete_callback( + &self, + tls: &TlsRef, + ) -> Option> { + Some(Arc::new(SniInfo { + server_name: tls.server_name()?.to_string(), + })) + } + } let settings = { - let mut settings = TlsSettings::intermediate("", "").context("setting up tls")?; + let mut settings = + TlsSettings::with_callbacks(Box::new(Callbacks)).context("setting up tls")?; settings.set_cert_resolver(Arc::new(cert_resolver)); settings }; -- knotmirror 0.0.1