pingora icon indicating copy to clipboard operation
pingora copied to clipboard

Be able to get the SNI from the HTTP filter context

Open pszabop opened this issue 10 months ago • 2 comments

What is the problem your feature solves, or the need it fulfills?

I would like to be able to get the SNI when in the req_filter() callback, so that I can do some business logic on it with the HTTP HOST header.

Describe the solution you'd like

In the self or more likely the session, store the SNI (e.g. in the downstream session) so it can be retrieved. The SSL Digest is available in the req_filter and that would be a suitable place to put it. (session.digest();)

Describe alternatives you've considered

I tried to dive into the stream and get to the get_ssl() function, but was unable to figure out how (or if it is possible) to downcast stream to the SslStream.

if let Some(stream) = session.stream() {
    println!("session stream: {:?}", stream);
    println!("Actual type of stream: {}", std::any::type_name_of_val(&stream));
    // downcast it somehow
    // then
        if let Some(ssl) = ssl_stream.get_ssl() {
            if let Some(sni) = ssl.servername(pingora::tls::ssl::NameType::HOST_NAME) {

but the 5-6 methods I tried for downcasting didn't work. It would also be a lot of insider baseball even if it did work and likely fragile.

Additional context

From looking at the code, the SNI is not extracted anywhere. It is available in the TLS callback via a call to the underlying ssl instance but since there's no HTTP context there's no place to put it.

#[async_trait]
impl pingora::listeners::TlsAccept for DynamicCert {
    async fn certificate_callback(&self, ssl: &mut pingora::tls::ssl::SslRef) {
        // Inspect the SNI
        if let Some(sni) = ssl.servername(pingora::tls::ssl::NameType::HOST_NAME) {
            println!("SNI: {}", sni);
        }
        ext::ssl_use_certificate(ssl, &self.cert).unwrap();
        ext::ssl_use_private_key(ssl, &self.key).unwrap();
    }
}

It's possible I missed an ext field somewhere where the SNI could be stuffed.

pszabop avatar Mar 02 '25 05:03 pszabop

Patch to rc/protocols/tls/boringssl_openssl/stream.rs

+        use crate::protocols::tls::boringssl_openssl::stream::ssl::NameType;
+        let sni = ssl.servername(NameType::HOST_NAME);
+        let sni_string: Option<String> = sni.map(ToOwned::to_owned);

        SslDigest {
            cipher,
            version: ssl.version_str(),
            organization: org,
            serial_number: sn,
            cert_digest,
+            sni: sni_string,
        }

and src/protocols/tls/digest.rs

/// The TLS connection information
#[derive(Clone, Debug)]
pub struct SslDigest {
    /// The cipher used
    pub cipher: &'static str,
    /// The TLS version of this connection
    pub version: &'static str,
    /// The organization of the peer's certificate
    pub organization: Option<String>,
    /// The serial number of the peer's certificate
    pub serial_number: Option<String>,
    /// The digest of the peer's certificate
    pub cert_digest: Vec<u8>,
+    /// the SNI used in the negotiation
+    pub sni: Option<String>,
}

usage:

    async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<bool> {
        let digest:Option<&Digest> = session.digest();
        if let Some(digest) = digest {
            println!("session digest: {:?}", digest);
            if let Some(ssl_digest) = &digest.ssl_digest {
                println!("session digest: {:?}", ssl_digest);
            } else {
                println!("session ssl digest not found");
            }
        } else {
            println!("session digest not found");
        }

pszabop avatar Mar 02 '25 05:03 pszabop

We're quite open to this!

If you would like to open a PR, we'd be more than happy to review it!

Noah-Kennedy avatar Mar 07 '25 17:03 Noah-Kennedy