diff --git a/.bleep b/.bleep index d9ff5c9d..e0dc84ff 100644 --- a/.bleep +++ b/.bleep @@ -1 +1 @@ -9ec41e3e1817e36107195f6ca1a3168779857cc7 \ No newline at end of file +f03c6a3f42dddc28f95bc10022de285e10223866 diff --git a/Cargo.toml b/Cargo.toml index 2fdc570e..f50a2f53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "pingora-openssl", "pingora-boringssl", "pingora-runtime", + "pingora-rustls", "pingora-ketama", "pingora-load-balancing", "pingora-memory-cache", diff --git a/pingora-cache/Cargo.toml b/pingora-cache/Cargo.toml index 6ae6c123..235cd082 100644 --- a/pingora-cache/Cargo.toml +++ b/pingora-cache/Cargo.toml @@ -65,6 +65,7 @@ name = "lru_serde" harness = false [features] -default = ["openssl"] +default = [] openssl = ["pingora-core/openssl"] boringssl = ["pingora-core/boringssl"] +rustls = ["pingora-core/rustls"] diff --git a/pingora-core/Cargo.toml b/pingora-core/Cargo.toml index 5fa60ae9..713bb753 100644 --- a/pingora-core/Cargo.toml +++ b/pingora-core/Cargo.toml @@ -26,6 +26,7 @@ pingora-pool = { version = "0.3.0", path = "../pingora-pool" } pingora-error = { version = "0.3.0", path = "../pingora-error" } pingora-timeout = { version = "0.3.0", path = "../pingora-timeout" } pingora-http = { version = "0.3.0", path = "../pingora-http" } +pingora-rustls = { version = "0.3.0", path = "../pingora-rustls", optional = true } tokio = { workspace = true, features = ["net", "rt-multi-thread", "signal"] } futures = "0.3" async-trait = { workspace = true } @@ -66,6 +67,7 @@ openssl-probe = "0.1" tokio-test = "0.4" zstd = "0" httpdate = "1" +x509-parser = { version = "0.16.0", optional = true } [target.'cfg(unix)'.dependencies] daemonize = "0.5.0" @@ -87,9 +89,11 @@ hyperlocal = "0.8" jemallocator = "0.5" [features] -default = ["openssl"] -openssl = ["pingora-openssl", "some_tls"] -boringssl = ["pingora-boringssl", "some_tls"] +default = [] +openssl = ["pingora-openssl", "openssl_derived",] +boringssl = ["pingora-boringssl", "openssl_derived",] +rustls = ["pingora-rustls", "any_tls", "dep:x509-parser"] +patched_http1 = ["pingora-http/patched_http1"] +openssl_derived = ["any_tls"] +any_tls = [] sentry = ["dep:sentry"] -patched_http1 = [] -some_tls = [] diff --git a/pingora-core/src/connectors/http/mod.rs b/pingora-core/src/connectors/http/mod.rs index 33c5e015..01339909 100644 --- a/pingora-core/src/connectors/http/mod.rs +++ b/pingora-core/src/connectors/http/mod.rs @@ -95,7 +95,7 @@ impl Connector { } #[cfg(test)] -#[cfg(feature = "some_tls")] +#[cfg(feature = "any_tls")] mod tests { use super::*; use crate::protocols::http::v1::client::HttpSession as Http1Session; diff --git a/pingora-core/src/connectors/http/v1.rs b/pingora-core/src/connectors/http/v1.rs index fb5dfd0c..ffa6df20 100644 --- a/pingora-core/src/connectors/http/v1.rs +++ b/pingora-core/src/connectors/http/v1.rs @@ -103,7 +103,7 @@ mod tests { } #[tokio::test] - #[cfg(feature = "some_tls")] + #[cfg(feature = "any_tls")] async fn test_connect_tls() { let connector = Connector::new(None); let peer = HttpPeer::new(("1.1.1.1", 443), true, "one.one.one.one".into()); diff --git a/pingora-core/src/connectors/http/v2.rs b/pingora-core/src/connectors/http/v2.rs index 60e26fb6..a1f295af 100644 --- a/pingora-core/src/connectors/http/v2.rs +++ b/pingora-core/src/connectors/http/v2.rs @@ -460,7 +460,7 @@ mod tests { use crate::upstreams::peer::HttpPeer; #[tokio::test] - #[cfg(feature = "some_tls")] + #[cfg(feature = "any_tls")] async fn test_connect_h2() { let connector = Connector::new(None); let mut peer = HttpPeer::new(("1.1.1.1", 443), true, "one.one.one.one".into()); @@ -473,7 +473,7 @@ mod tests { } #[tokio::test] - #[cfg(feature = "some_tls")] + #[cfg(feature = "any_tls")] async fn test_connect_h1() { let connector = Connector::new(None); let mut peer = HttpPeer::new(("1.1.1.1", 443), true, "one.one.one.one".into()); @@ -499,7 +499,7 @@ mod tests { } #[tokio::test] - #[cfg(feature = "some_tls")] + #[cfg(feature = "any_tls")] async fn test_h2_single_stream() { let connector = Connector::new(None); let mut peer = HttpPeer::new(("1.1.1.1", 443), true, "one.one.one.one".into()); @@ -531,7 +531,7 @@ mod tests { } #[tokio::test] - #[cfg(feature = "some_tls")] + #[cfg(feature = "any_tls")] async fn test_h2_multiple_stream() { let connector = Connector::new(None); let mut peer = HttpPeer::new(("1.1.1.1", 443), true, "one.one.one.one".into()); diff --git a/pingora-core/src/connectors/mod.rs b/pingora-core/src/connectors/mod.rs index 57c16aa4..5a126cc7 100644 --- a/pingora-core/src/connectors/mod.rs +++ b/pingora-core/src/connectors/mod.rs @@ -17,11 +17,15 @@ pub mod http; pub mod l4; mod offload; + +#[cfg(feature = "any_tls")] mod tls; +#[cfg(not(feature = "any_tls"))] +use crate::tls::connectors as tls; + use crate::protocols::Stream; use crate::server::configuration::ServerConf; -use crate::tls::ssl::SslConnector; use crate::upstreams::peer::{Peer, ALPN}; pub use l4::Connect as L4Connect; @@ -34,6 +38,7 @@ use pingora_pool::{ConnectionMeta, ConnectionPool}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; +use tls::TlsConnector; use tokio::sync::Mutex; /// The options to configure a [TransportConnector] @@ -293,7 +298,7 @@ async fn do_connect( peer: &P, bind_to: Option, alpn_override: Option, - tls_ctx: &SslConnector, + tls_ctx: &TlsConnector, ) -> Result { // Create the future that does the connections, but don't evaluate it until // we decide if we need a timeout or not @@ -316,7 +321,7 @@ async fn do_connect_inner( peer: &P, bind_to: Option, alpn_override: Option, - tls_ctx: &SslConnector, + tls_ctx: &TlsConnector, ) -> Result { let stream = l4_connect(peer, bind_to).await?; if peer.tls() { @@ -383,12 +388,12 @@ fn test_reusable_stream(stream: &mut Stream) -> bool { } #[cfg(test)] -#[cfg(feature = "some_tls")] +#[cfg(feature = "any_tls")] mod tests { use pingora_error::ErrorType; + use tls::Connector; use super::*; - use crate::tls::ssl::SslMethod; use crate::upstreams::peer::BasicPeer; use tokio::io::AsyncWriteExt; #[cfg(unix)] @@ -498,8 +503,8 @@ mod tests { /// This assumes that the connection will fail to on the peer and returns /// the decomposed error type and message async fn get_do_connect_failure_with_peer(peer: &BasicPeer) -> (ErrorType, String) { - let ssl_connector = SslConnector::builder(SslMethod::tls()).unwrap().build(); - let stream = do_connect(peer, None, None, &ssl_connector).await; + let tls_connector = Connector::new(None); + let stream = do_connect(peer, None, None, &tls_connector.ctx).await; match stream { Ok(_) => panic!("should throw an error"), Err(e) => ( diff --git a/pingora-core/src/connectors/tls/boringssl_openssl/mod.rs b/pingora-core/src/connectors/tls/boringssl_openssl/mod.rs index f8568e71..a57668fb 100644 --- a/pingora-core/src/connectors/tls/boringssl_openssl/mod.rs +++ b/pingora-core/src/connectors/tls/boringssl_openssl/mod.rs @@ -16,6 +16,7 @@ use log::debug; use pingora_error::{Error, ErrorType::*, OrErr, Result}; use std::sync::{Arc, Once}; +use crate::connectors::tls::replace_leftmost_underscore; use crate::connectors::ConnectorOptions; use crate::protocols::tls::client::handshake; use crate::protocols::tls::SslStream; @@ -31,6 +32,8 @@ use crate::tls::ssl::{SslConnector, SslFiletype, SslMethod, SslVerifyMode, SslVe use crate::tls::x509::store::X509StoreBuilder; use crate::upstreams::peer::{Peer, ALPN}; +pub type TlsConnector = SslConnector; + const CIPHER_LIST: &str = "AES-128-GCM-SHA256\ :AES-256-GCM-SHA384\ :CHACHA20-POLY1305-SHA256\ @@ -147,33 +150,6 @@ impl Connector { } } -/* - OpenSSL considers underscores in hostnames non-compliant. - We replace the underscore in the leftmost label as we must support these - hostnames for wildcard matches and we have not patched OpenSSL. - - https://github.com/openssl/openssl/issues/12566 - - > The labels must follow the rules for ARPANET host names. They must - > start with a letter, end with a letter or digit, and have as interior - > characters only letters, digits, and hyphen. There are also some - > restrictions on the length. Labels must be 63 characters or less. - - https://datatracker.ietf.org/doc/html/rfc1034#section-3.5 -*/ -fn replace_leftmost_underscore(sni: &str) -> Option { - // wildcard is only leftmost label - if let Some((leftmost, rest)) = sni.split_once('.') { - // if not a subdomain or leftmost does not contain underscore return - if !rest.contains('.') || !leftmost.contains('_') { - return None; - } - // we have a subdomain, replace underscores - let leftmost = leftmost.replace('_', "-"); - return Some(format!("{leftmost}.{rest}")); - } - None -} - pub(crate) async fn connect( stream: T, peer: &P, @@ -283,41 +259,3 @@ where None => connect_future.await, } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_replace_leftmost_underscore() { - let none_cases = [ - "", - "some", - "some.com", - "1.1.1.1:5050", - "dog.dot.com", - "dog.d_t.com", - "dog.dot.c_m", - "d_g.com", - "_", - "dog.c_m", - ]; - - for case in none_cases { - assert!(replace_leftmost_underscore(case).is_none(), "{}", case); - } - - assert_eq!( - Some("bb-b.some.com".to_string()), - replace_leftmost_underscore("bb_b.some.com") - ); - assert_eq!( - Some("a-a-a.some.com".to_string()), - replace_leftmost_underscore("a_a_a.some.com") - ); - assert_eq!( - Some("-.some.com".to_string()), - replace_leftmost_underscore("_.some.com") - ); - } -} diff --git a/pingora-core/src/connectors/tls/mod.rs b/pingora-core/src/connectors/tls/mod.rs index 2a4d4695..e2504188 100644 --- a/pingora-core/src/connectors/tls/mod.rs +++ b/pingora-core/src/connectors/tls/mod.rs @@ -1,5 +1,84 @@ -#[cfg(feature = "some_tls")] +// Copyright 2024 Cloudflare, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(feature = "openssl_derived")] mod boringssl_openssl; -#[cfg(feature = "some_tls")] +#[cfg(feature = "openssl_derived")] pub use boringssl_openssl::*; + +/// OpenSSL considers underscores in hostnames non-compliant. +/// We replace the underscore in the leftmost label as we must support these +/// hostnames for wildcard matches and we have not patched OpenSSL. +/// +/// https://github.com/openssl/openssl/issues/12566 +/// +/// > The labels must follow the rules for ARPANET host names. They must +/// > start with a letter, end with a letter or digit, and have as interior +/// > characters only letters, digits, and hyphen. There are also some +/// > restrictions on the length. Labels must be 63 characters or less. +/// - https://datatracker.ietf.org/doc/html/rfc1034#section-3.5 +#[cfg(feature = "any_tls")] +pub fn replace_leftmost_underscore(sni: &str) -> Option { + // wildcard is only leftmost label + if let Some((leftmost, rest)) = sni.split_once('.') { + // if not a subdomain or leftmost does not contain underscore return + if !rest.contains('.') || !leftmost.contains('_') { + return None; + } + // we have a subdomain, replace underscores + let leftmost = leftmost.replace('_', "-"); + return Some(format!("{leftmost}.{rest}")); + } + None +} + +#[cfg(feature = "any_tls")] +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_replace_leftmost_underscore() { + let none_cases = [ + "", + "some", + "some.com", + "1.1.1.1:5050", + "dog.dot.com", + "dog.d_t.com", + "dog.dot.c_m", + "d_g.com", + "_", + "dog.c_m", + ]; + + for case in none_cases { + assert!(replace_leftmost_underscore(case).is_none(), "{}", case); + } + + assert_eq!( + Some("bb-b.some.com".to_string()), + replace_leftmost_underscore("bb_b.some.com") + ); + assert_eq!( + Some("a-a-a.some.com".to_string()), + replace_leftmost_underscore("a_a_a.some.com") + ); + assert_eq!( + Some("-.some.com".to_string()), + replace_leftmost_underscore("_.some.com") + ); + } +} diff --git a/pingora-core/src/lib.rs b/pingora-core/src/lib.rs index fbc33949..4f385808 100644 --- a/pingora-core/src/lib.rs +++ b/pingora-core/src/lib.rs @@ -37,6 +37,10 @@ //! # Optional features //! `boringssl`: Switch the internal TLS library from OpenSSL to BoringSSL. +// This enables the feature that labels modules that are only available with +// certain pingora features +#![cfg_attr(docsrs, feature(doc_cfg))] + pub mod apps; pub mod connectors; pub mod listeners; @@ -55,11 +59,14 @@ pub use pingora_error::{ErrorType::*, *}; #[cfg(feature = "boringssl")] pub use pingora_boringssl as tls; -#[cfg(all(not(feature = "boringssl"), feature = "openssl"))] +#[cfg(feature = "openssl")] pub use pingora_openssl as tls; -#[cfg(not(feature = "some_tls"))] -pub use protocols::tls::dummy_tls as tls; +#[cfg(feature = "rustls")] +pub use pingora_rustls as tls; + +#[cfg(not(feature = "any_tls"))] +pub use protocols::tls::noop_tls as tls; pub mod prelude { pub use crate::server::configuration::Opt; diff --git a/pingora-core/src/listeners/mod.rs b/pingora-core/src/listeners/mod.rs index 35e30d50..02a7b839 100644 --- a/pingora-core/src/listeners/mod.rs +++ b/pingora-core/src/listeners/mod.rs @@ -15,21 +15,41 @@ //! The listening endpoints (TCP and TLS) and their configurations. mod l4; -mod tls; -use crate::protocols::Stream; +#[cfg(feature = "any_tls")] +pub mod tls; + +#[cfg(not(feature = "any_tls"))] +pub use crate::tls::listeners as tls; + +use crate::protocols::{tls::TlsRef, Stream}; + #[cfg(unix)] use crate::server::ListenFds; +use async_trait::async_trait; use pingora_error::Result; use std::{fs::Permissions, sync::Arc}; use l4::{ListenerEndpoint, Stream as L4Stream}; -use tls::Acceptor; +use tls::{Acceptor, TlsSettings}; -pub use crate::protocols::tls::server::TlsAccept; +pub use crate::protocols::tls::ALPN; pub use l4::{ServerAddress, TcpSocketOptions}; -pub use tls::{TlsSettings, ALPN}; + +/// The APIs to customize things like certificate during TLS server side handshake +#[async_trait] +pub trait TlsAccept { + // TODO: return error? + /// This function is called in the middle of a TLS handshake. Structs who + /// implement this function should provide tls certificate and key to the + /// [TlsRef] via `ssl_use_certificate` and `ssl_use_private_key`. + async fn certificate_callback(&self, _ssl: &mut TlsRef) -> () { + // does nothing by default + } +} + +pub type TlsAcceptCallbacks = Box; struct TransportStackBuilder { l4: ServerAddress, @@ -200,6 +220,7 @@ impl Listeners { #[cfg(test)] mod test { use super::*; + #[cfg(feature = "any_tls")] use tokio::io::AsyncWriteExt; use tokio::net::TcpStream; use tokio::time::{sleep, Duration}; @@ -233,7 +254,7 @@ mod test { } #[tokio::test] - #[cfg(feature = "some_tls")] + #[cfg(feature = "any_tls")] async fn test_listen_tls() { use tokio::io::AsyncReadExt; diff --git a/pingora-core/src/listeners/tls/boringssl_openssl/mod.rs b/pingora-core/src/listeners/tls/boringssl_openssl/mod.rs index 3780a94d..500f9598 100644 --- a/pingora-core/src/listeners/tls/boringssl_openssl/mod.rs +++ b/pingora-core/src/listeners/tls/boringssl_openssl/mod.rs @@ -16,14 +16,16 @@ use log::debug; use pingora_error::{ErrorType, OrErr, Result}; use std::ops::{Deref, DerefMut}; -use crate::protocols::tls::{ - server::{handshake, handshake_with_callback, TlsAcceptCallbacks}, - SslStream, -}; +pub use crate::protocols::tls::ALPN; use crate::protocols::IO; use crate::tls::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod}; - -pub use crate::protocols::tls::ALPN; +use crate::{ + listeners::TlsAcceptCallbacks, + protocols::tls::{ + server::{handshake, handshake_with_callback}, + SslStream, + }, +}; pub const TLS_CONF_ERR: ErrorType = ErrorType::Custom("TLSConfigError"); diff --git a/pingora-core/src/listeners/tls/mod.rs b/pingora-core/src/listeners/tls/mod.rs index 2a4d4695..0bcaeac8 100644 --- a/pingora-core/src/listeners/tls/mod.rs +++ b/pingora-core/src/listeners/tls/mod.rs @@ -1,5 +1,19 @@ -#[cfg(feature = "some_tls")] +// Copyright 2024 Cloudflare, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(feature = "openssl_derived")] mod boringssl_openssl; -#[cfg(feature = "some_tls")] +#[cfg(feature = "openssl_derived")] pub use boringssl_openssl::*; diff --git a/pingora-core/src/modules/http/mod.rs b/pingora-core/src/modules/http/mod.rs index f4b17ad9..c04404b9 100644 --- a/pingora-core/src/modules/http/mod.rs +++ b/pingora-core/src/modules/http/mod.rs @@ -14,9 +14,11 @@ //! Modules for HTTP traffic. //! -//! [HttpModule]s define request and response filters to use while running an [HttpServer] +//! [HttpModule]s define request and response filters to use while running an +//! [HttpServer](crate::apps::http_app::HttpServer) //! application. -//! See the [ResponseCompression] module for an example of how to implement a basic module. +//! See the [ResponseCompression](crate::modules::http::compression::ResponseCompression) +//! module for an example of how to implement a basic module. pub mod compression; pub mod grpc_web; diff --git a/pingora-core/src/protocols/http/client.rs b/pingora-core/src/protocols/http/client.rs index 4d3044dc..6b1e00a8 100644 --- a/pingora-core/src/protocols/http/client.rs +++ b/pingora-core/src/protocols/http/client.rs @@ -161,7 +161,7 @@ impl HttpSession { } } - /// Return a mutable [Digest] reference for the connection, see [`digest`] for more details. + /// Return a mutable [Digest] reference for the connection. /// /// Will return `None` if this is an H2 session and multiple streams are open. pub fn digest_mut(&mut self) -> Option<&mut Digest> { diff --git a/pingora-core/src/protocols/http/v1/client.rs b/pingora-core/src/protocols/http/v1/client.rs index 2b2640bc..7125e49f 100644 --- a/pingora-core/src/protocols/http/v1/client.rs +++ b/pingora-core/src/protocols/http/v1/client.rs @@ -639,7 +639,7 @@ impl HttpSession { &self.digest } - /// Return a mutable [Digest] reference for the connection, see [`digest`] for more details. + /// Return a mutable [Digest] reference for the connection. pub fn digest_mut(&mut self) -> &mut Digest { &mut self.digest } diff --git a/pingora-core/src/protocols/http/v2/client.rs b/pingora-core/src/protocols/http/v2/client.rs index 1d89004a..4d5559a5 100644 --- a/pingora-core/src/protocols/http/v2/client.rs +++ b/pingora-core/src/protocols/http/v2/client.rs @@ -310,7 +310,7 @@ impl Http2Session { Some(self.conn.digest()) } - /// Return a mutable [Digest] reference for the connection, see [`digest`] for more details. + /// Return a mutable [Digest] reference for the connection /// /// Will return `None` if multiple H2 streams are open. pub fn digest_mut(&mut self) -> Option<&mut Digest> { diff --git a/pingora-core/src/protocols/mod.rs b/pingora-core/src/protocols/mod.rs index 62efb9cd..007675b3 100644 --- a/pingora-core/src/protocols/mod.rs +++ b/pingora-core/src/protocols/mod.rs @@ -55,19 +55,18 @@ pub trait UniqueID { /// Interface to get TLS info pub trait Ssl { /// Return the TLS info if the connection is over TLS - fn get_ssl(&self) -> Option<&crate::tls::ssl::SslRef> { + fn get_ssl(&self) -> Option<&TlsRef> { None } - /// Return the [`ssl::SslDigest`] for logging + /// Return the [`tls::SslDigest`] for logging fn get_ssl_digest(&self) -> Option> { None } /// Return selected ALPN if any fn selected_alpn_proto(&self) -> Option { - let ssl = self.get_ssl()?; - ALPN::from_wire_selected(ssl.selected_alpn_protocol()?) + None } } @@ -249,6 +248,8 @@ use std::os::unix::prelude::AsRawFd; use std::os::windows::io::AsRawSocket; use std::{net::SocketAddr as InetSocketAddr, path::Path}; +use crate::protocols::tls::TlsRef; + #[cfg(unix)] impl ConnFdReusable for SocketAddr { fn check_fd_match(&self, fd: V) -> bool { diff --git a/pingora-core/src/protocols/tls/boringssl_openssl/mod.rs b/pingora-core/src/protocols/tls/boringssl_openssl/mod.rs index c07f47e0..3c6584e5 100644 --- a/pingora-core/src/protocols/tls/boringssl_openssl/mod.rs +++ b/pingora-core/src/protocols/tls/boringssl_openssl/mod.rs @@ -1,2 +1,29 @@ +// Copyright 2024 Cloudflare, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + pub mod client; pub mod server; +mod stream; + +#[cfg(feature = "boringssl")] +use pingora_boringssl as ssl_lib; + +#[cfg(feature = "openssl")] +use pingora_openssl as ssl_lib; + +use ssl_lib::{ssl::SslRef, x509::X509}; +pub use stream::*; + +pub type TlsRef = SslRef; +pub type CaType = Box<[X509]>; diff --git a/pingora-core/src/protocols/tls/boringssl_openssl/server.rs b/pingora-core/src/protocols/tls/boringssl_openssl/server.rs index b7f2f6d2..024565a5 100644 --- a/pingora-core/src/protocols/tls/boringssl_openssl/server.rs +++ b/pingora-core/src/protocols/tls/boringssl_openssl/server.rs @@ -14,12 +14,13 @@ //! TLS server specific implementation +use crate::listeners::TlsAcceptCallbacks; use crate::protocols::tls::SslStream; use crate::protocols::{Shutdown, IO}; use crate::tls::ext; use crate::tls::ext::ssl_from_acceptor; use crate::tls::ssl; -use crate::tls::ssl::{SslAcceptor, SslRef}; +use crate::tls::ssl::SslAcceptor; use async_trait::async_trait; use log::warn; @@ -69,19 +70,6 @@ pub async fn handshake_with_callback( } } -/// The APIs to customize things like certificate during TLS server side handshake -#[async_trait] -pub trait TlsAccept { - // TODO: return error? - /// This function is called in the middle of a TLS handshake. Structs who implement this function - /// should provide tls certificate and key to the [SslRef] via [ext::ssl_use_certificate] and [ext::ssl_use_private_key]. - async fn certificate_callback(&self, _ssl: &mut SslRef) -> () { - // does nothing by default - } -} - -pub type TlsAcceptCallbacks = Box; - #[async_trait] impl Shutdown for SslStream where @@ -143,9 +131,12 @@ impl ResumableAccept for SslStream } #[tokio::test] -#[cfg(feature = "some_tls")] +#[cfg(feature = "any_tls")] async fn test_async_cert() { + use crate::protocols::tls::TlsRef; use tokio::io::AsyncReadExt; + + use crate::listeners::{TlsAccept, TlsAcceptCallbacks}; let acceptor = ssl::SslAcceptor::mozilla_intermediate_v5(ssl::SslMethod::tls()) .unwrap() .build(); @@ -153,7 +144,7 @@ async fn test_async_cert() { struct Callback; #[async_trait] impl TlsAccept for Callback { - async fn certificate_callback(&self, ssl: &mut SslRef) -> () { + async fn certificate_callback(&self, ssl: &mut TlsRef) -> () { assert_eq!( ssl.servername(ssl::NameType::HOST_NAME).unwrap(), "pingora.org" diff --git a/pingora-core/src/protocols/tls/boringssl_openssl/stream.rs b/pingora-core/src/protocols/tls/boringssl_openssl/stream.rs new file mode 100644 index 00000000..38c30271 --- /dev/null +++ b/pingora-core/src/protocols/tls/boringssl_openssl/stream.rs @@ -0,0 +1,217 @@ +// Copyright 2024 Cloudflare, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::protocols::digest::TimingDigest; +use crate::protocols::tls::{SslDigest, ALPN}; +use crate::protocols::{Peek, Ssl, UniqueID, UniqueIDType}; +use crate::tls::{self, ssl, tokio_ssl::SslStream as InnerSsl}; +use crate::utils::tls::{get_organization, get_serial}; +use log::warn; +use pingora_error::{ErrorType::*, OrErr, Result}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::SystemTime; +use tokio::io::{self, AsyncRead, AsyncWrite, ReadBuf}; + +#[cfg(feature = "boringssl")] +use pingora_boringssl as ssl_lib; + +#[cfg(feature = "openssl")] +use pingora_openssl as ssl_lib; + +use ssl_lib::{hash::MessageDigest, ssl::SslRef}; + +/// The TLS connection +#[derive(Debug)] +pub struct SslStream { + ssl: InnerSsl, + digest: Option>, + pub(super) timing: TimingDigest, +} + +impl SslStream +where + T: AsyncRead + AsyncWrite + std::marker::Unpin, +{ + /// Create a new TLS connection from the given `stream` + /// + /// The caller needs to perform [`Self::connect()`] or [`Self::accept()`] to perform TLS + /// handshake after. + pub fn new(ssl: ssl::Ssl, stream: T) -> Result { + let ssl = InnerSsl::new(ssl, stream) + .explain_err(TLSHandshakeFailure, |e| format!("ssl stream error: {e}"))?; + + Ok(SslStream { + ssl, + digest: None, + timing: Default::default(), + }) + } + + /// Connect to the remote TLS server as a client + pub async fn connect(&mut self) -> Result<(), ssl::Error> { + Self::clear_error(); + Pin::new(&mut self.ssl).connect().await?; + self.timing.established_ts = SystemTime::now(); + self.digest = Some(Arc::new(SslDigest::from_ssl(self.ssl()))); + Ok(()) + } + + /// Finish the TLS handshake from client as a server + pub async fn accept(&mut self) -> Result<(), ssl::Error> { + Self::clear_error(); + Pin::new(&mut self.ssl).accept().await?; + self.timing.established_ts = SystemTime::now(); + self.digest = Some(Arc::new(SslDigest::from_ssl(self.ssl()))); + Ok(()) + } + + #[inline] + fn clear_error() { + let errs = tls::error::ErrorStack::get(); + if !errs.errors().is_empty() { + warn!("Clearing dirty TLS error stack: {}", errs); + } + } +} + +impl SslStream { + pub fn ssl_digest(&self) -> Option> { + self.digest.clone() + } +} + +use std::ops::{Deref, DerefMut}; + +impl Deref for SslStream { + type Target = InnerSsl; + + fn deref(&self) -> &Self::Target { + &self.ssl + } +} + +impl DerefMut for SslStream { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ssl + } +} + +impl AsyncRead for SslStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Self::clear_error(); + Pin::new(&mut self.ssl).poll_read(cx, buf) + } +} + +impl AsyncWrite for SslStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + Self::clear_error(); + Pin::new(&mut self.ssl).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + Self::clear_error(); + Pin::new(&mut self.ssl).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + Self::clear_error(); + Pin::new(&mut self.ssl).poll_shutdown(cx) + } + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + Self::clear_error(); + Pin::new(&mut self.ssl).poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + true + } +} + +impl UniqueID for SslStream +where + T: UniqueID, +{ + fn id(&self) -> UniqueIDType { + self.ssl.get_ref().id() + } +} + +impl Ssl for SslStream { + fn get_ssl(&self) -> Option<&ssl::SslRef> { + Some(self.ssl()) + } + + fn get_ssl_digest(&self) -> Option> { + self.ssl_digest() + } + + /// Return selected ALPN if any + fn selected_alpn_proto(&self) -> Option { + let ssl = self.get_ssl()?; + ALPN::from_wire_selected(ssl.selected_alpn_protocol()?) + } +} + +impl SslDigest { + pub fn from_ssl(ssl: &SslRef) -> Self { + let cipher = match ssl.current_cipher() { + Some(c) => c.name(), + None => "", + }; + + let (cert_digest, org, sn) = match ssl.peer_certificate() { + Some(cert) => { + let cert_digest = match cert.digest(MessageDigest::sha256()) { + Ok(c) => c.as_ref().to_vec(), + Err(_) => Vec::new(), + }; + (cert_digest, get_organization(&cert), get_serial(&cert).ok()) + } + None => (Vec::new(), None, None), + }; + + SslDigest { + cipher, + version: ssl.version_str(), + organization: org, + serial_number: sn, + cert_digest, + } + } +} + +// TODO: implement Peek if needed +impl Peek for SslStream {} diff --git a/pingora-core/src/protocols/tls/digest.rs b/pingora-core/src/protocols/tls/digest.rs index 3cdb7aa0..72adae66 100644 --- a/pingora-core/src/protocols/tls/digest.rs +++ b/pingora-core/src/protocols/tls/digest.rs @@ -14,9 +14,6 @@ //! TLS information from the TLS connection -use crate::tls::{hash::MessageDigest, ssl::SslRef}; -use crate::utils; - /// The TLS connection information #[derive(Clone, Debug)] pub struct SslDigest { @@ -31,35 +28,3 @@ pub struct SslDigest { /// The digest of the peer's certificate pub cert_digest: Vec, } - -impl SslDigest { - pub fn from_ssl(ssl: &SslRef) -> Self { - let cipher = match ssl.current_cipher() { - Some(c) => c.name(), - None => "", - }; - - let (cert_digest, org, sn) = match ssl.peer_certificate() { - Some(cert) => { - let cert_digest = match cert.digest(MessageDigest::sha256()) { - Ok(c) => c.as_ref().to_vec(), - Err(_) => Vec::new(), - }; - ( - cert_digest, - utils::get_organization(&cert), - utils::get_serial(&cert).ok(), - ) - } - None => (Vec::new(), None, None), - }; - - SslDigest { - cipher, - version: ssl.version_str(), - organization: org, - serial_number: sn, - cert_digest, - } - } -} diff --git a/pingora-core/src/protocols/tls/dummy_tls/mod.rs b/pingora-core/src/protocols/tls/dummy_tls/mod.rs deleted file mode 100644 index 9a841d6f..00000000 --- a/pingora-core/src/protocols/tls/dummy_tls/mod.rs +++ /dev/null @@ -1,805 +0,0 @@ -// Copyright 2024 Cloudflare, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! This module contains a dummy TLS implementation for the scenarios where real TLS -//! implementations are unavailable. - -macro_rules! impl_display { - ($ty:ty) => { - impl std::fmt::Display for $ty { - fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - Ok(()) - } - } - }; -} - -macro_rules! impl_deref { - ($from:ty => $to:ty) => { - impl std::ops::Deref for $from { - type Target = $to; - fn deref(&self) -> &$to { - unimplemented!(); - } - } - impl std::ops::DerefMut for $from { - fn deref_mut(&mut self) -> &mut $to { - unimplemented!(); - } - } - }; -} - -pub mod ssl { - use super::error::ErrorStack; - use super::x509::verify::X509VerifyParamRef; - use super::x509::{X509VerifyResult, X509}; - - /// An error returned from an ALPN selection callback. - pub struct AlpnError; - impl AlpnError { - /// Terminate the handshake with a fatal alert. - pub const ALERT_FATAL: AlpnError = Self {}; - - /// Do not select a protocol, but continue the handshake. - pub const NOACK: AlpnError = Self {}; - } - - /// A type which allows for configuration of a client-side TLS session before connection. - pub struct ConnectConfiguration; - impl_deref! {ConnectConfiguration => SslRef} - impl ConnectConfiguration { - /// Configures the use of Server Name Indication (SNI) when connecting. - pub fn set_use_server_name_indication(&mut self, _use_sni: bool) { - unimplemented!(); - } - - /// Configures the use of hostname verification when connecting. - pub fn set_verify_hostname(&mut self, _verify_hostname: bool) { - unimplemented!(); - } - - /// Returns an `Ssl` configured to connect to the provided domain. - pub fn into_ssl(self, _domain: &str) -> Result { - unimplemented!(); - } - - /// Like `SslContextBuilder::set_verify`. - pub fn set_verify(&mut self, _mode: SslVerifyMode) { - unimplemented!(); - } - - /// Like `SslContextBuilder::set_alpn_protos`. - pub fn set_alpn_protos(&mut self, _protocols: &[u8]) -> Result<(), ErrorStack> { - unimplemented!(); - } - - /// Returns a mutable reference to the X509 verification configuration. - pub fn param_mut(&mut self) -> &mut X509VerifyParamRef { - unimplemented!(); - } - } - - /// An SSL error. - #[derive(Debug)] - pub struct Error; - impl_display!(Error); - impl Error { - pub fn code(&self) -> ErrorCode { - unimplemented!(); - } - } - - /// An error code returned from SSL functions. - #[derive(PartialEq)] - pub struct ErrorCode(i32); - impl ErrorCode { - /// An error occurred in the SSL library. - pub const SSL: ErrorCode = Self(0); - } - - /// An identifier of a session name type. - pub struct NameType; - impl NameType { - pub const HOST_NAME: NameType = Self {}; - } - - /// The state of an SSL/TLS session. - pub struct Ssl; - impl Ssl { - /// Creates a new `Ssl`. - pub fn new(_ctx: &SslContextRef) -> Result { - unimplemented!(); - } - } - impl_deref! {Ssl => SslRef} - - /// A type which wraps server-side streams in a TLS session. - pub struct SslAcceptor; - impl SslAcceptor { - /// Creates a new builder configured to connect to non-legacy clients. This should - /// generally be considered a reasonable default choice. - pub fn mozilla_intermediate_v5( - _method: SslMethod, - ) -> Result { - unimplemented!(); - } - } - - /// A builder for `SslAcceptor`s. - pub struct SslAcceptorBuilder; - impl SslAcceptorBuilder { - /// Consumes the builder, returning a `SslAcceptor`. - pub fn build(self) -> SslAcceptor { - unimplemented!(); - } - - /// Sets the callback used by a server to select a protocol for Application Layer Protocol - /// Negotiation (ALPN). - pub fn set_alpn_select_callback(&mut self, _callback: F) - where - F: for<'a> Fn(&mut SslRef, &'a [u8]) -> Result<&'a [u8], AlpnError> - + 'static - + Sync - + Send, - { - unimplemented!(); - } - - /// Loads a certificate chain from a file. - pub fn set_certificate_chain_file>( - &mut self, - _file: P, - ) -> Result<(), ErrorStack> { - unimplemented!(); - } - - /// Loads the private key from a file. - pub fn set_private_key_file>( - &mut self, - _file: P, - _file_type: SslFiletype, - ) -> Result<(), ErrorStack> { - unimplemented!(); - } - - /// Sets the maximum supported protocol version. - pub fn set_max_proto_version( - &mut self, - _version: Option, - ) -> Result<(), ErrorStack> { - unimplemented!(); - } - } - - /// Reference to an [`SslCipher`]. - pub struct SslCipherRef; - impl SslCipherRef { - /// Returns the name of the cipher. - pub fn name(&self) -> &'static str { - unimplemented!(); - } - } - - /// A type which wraps client-side streams in a TLS session. - pub struct SslConnector; - impl SslConnector { - /// Creates a new builder for TLS connections. - pub fn builder(_method: SslMethod) -> Result { - Ok(SslConnectorBuilder) - } - - /// Returns a structure allowing for configuration of a single TLS session before connection. - pub fn configure(&self) -> Result { - Ok(ConnectConfiguration) - } - - /// Returns a shared reference to the inner raw `SslContext`. - pub fn context(&self) -> &SslContextRef { - &SslContextRef - } - } - - /// A builder for `SslConnector`s. - pub struct SslConnectorBuilder; - impl SslConnectorBuilder { - /// Consumes the builder, returning an `SslConnector`. - pub fn build(self) -> SslConnector { - SslConnector - } - - /// Sets the list of supported ciphers for protocols before TLSv1.3. - pub fn set_cipher_list(&mut self, _cipher_list: &str) -> Result<(), ErrorStack> { - Ok(()) - } - - /// Sets the context’s supported signature algorithms. - pub fn set_sigalgs_list(&mut self, _sigalgs: &str) -> Result<(), ErrorStack> { - Ok(()) - } - - /// Sets the minimum supported protocol version. - pub fn set_min_proto_version( - &mut self, - _version: Option, - ) -> Result<(), ErrorStack> { - Ok(()) - } - - /// Sets the maximum supported protocol version. - pub fn set_max_proto_version( - &mut self, - _version: Option, - ) -> Result<(), ErrorStack> { - Ok(()) - } - - /// Use the default locations of trusted certificates for verification. - pub fn set_default_verify_paths(&mut self) -> Result<(), ErrorStack> { - Ok(()) - } - - /// Loads trusted root certificates from a file. - pub fn set_ca_file>( - &mut self, - _file: P, - ) -> Result<(), ErrorStack> { - Ok(()) - } - - /// Loads a leaf certificate from a file. - pub fn set_certificate_file>( - &mut self, - _file: P, - _file_type: SslFiletype, - ) -> Result<(), ErrorStack> { - Ok(()) - } - - /// Loads the private key from a file. - pub fn set_private_key_file>( - &mut self, - _file: P, - _file_type: SslFiletype, - ) -> Result<(), ErrorStack> { - Ok(()) - } - - /// Sets the TLS key logging callback. - pub fn set_keylog_callback(&mut self, _callback: F) - where - F: Fn(&SslRef, &str) + 'static + Sync + Send, - { - } - } - - /// A context object for TLS streams. - pub struct SslContext; - impl SslContext { - /// Creates a new builder object for an `SslContext`. - pub fn builder(_method: SslMethod) -> Result { - unimplemented!(); - } - } - impl_deref! {SslContext => SslContextRef} - - /// A builder for `SslContext`s. - pub struct SslContextBuilder; - impl SslContextBuilder { - /// Consumes the builder, returning a new `SslContext`. - pub fn build(self) -> SslContext { - unimplemented!(); - } - } - - /// Reference to [`SslContext`] - pub struct SslContextRef; - - /// An identifier of the format of a certificate or key file. - pub struct SslFiletype; - impl SslFiletype { - /// The PEM format. - pub const PEM: SslFiletype = Self {}; - } - - /// A type specifying the kind of protocol an `SslContext`` will speak. - pub struct SslMethod; - impl SslMethod { - /// Support all versions of the TLS protocol. - pub fn tls() -> SslMethod { - Self - } - } - - /// Reference to an [`Ssl`]. - pub struct SslRef; - impl SslRef { - /// Like [`SslContextBuilder::set_verify`]. - pub fn set_verify(&mut self, _mode: SslVerifyMode) { - unimplemented!(); - } - - /// Returns the current cipher if the session is active. - pub fn current_cipher(&self) -> Option<&SslCipherRef> { - unimplemented!(); - } - - /// Sets the host name to be sent to the server for Server Name Indication (SNI). - pub fn set_hostname(&mut self, _hostname: &str) -> Result<(), ErrorStack> { - unimplemented!(); - } - - /// Returns the peer’s certificate, if present. - pub fn peer_certificate(&self) -> Option { - unimplemented!(); - } - - /// Returns the certificate verification result. - pub fn verify_result(&self) -> X509VerifyResult { - unimplemented!(); - } - - /// Returns a string describing the protocol version of the session. - pub fn version_str(&self) -> &'static str { - unimplemented!(); - } - - /// Returns the protocol selected via Application Layer Protocol Negotiation (ALPN). - pub fn selected_alpn_protocol(&self) -> Option<&[u8]> { - unimplemented!(); - } - - /// Returns the servername sent by the client via Server Name Indication (SNI). - pub fn servername(&self, _type_: NameType) -> Option<&str> { - unimplemented!(); - } - } - - /// Options controlling the behavior of certificate verification. - pub struct SslVerifyMode; - impl SslVerifyMode { - /// Verifies that the peer’s certificate is trusted. - pub const PEER: Self = Self {}; - - /// Disables verification of the peer’s certificate. - pub const NONE: Self = Self {}; - } - - /// An SSL/TLS protocol version. - pub struct SslVersion; - impl SslVersion { - /// TLSv1.0 - pub const TLS1: SslVersion = Self {}; - - /// TLSv1.2 - pub const TLS1_2: SslVersion = Self {}; - - /// TLSv1.3 - pub const TLS1_3: SslVersion = Self {}; - } - - /// A standard implementation of protocol selection for Application Layer Protocol Negotiation - /// (ALPN). - pub fn select_next_proto<'a>(_server: &[u8], _client: &'a [u8]) -> Option<&'a [u8]> { - unimplemented!(); - } -} - -pub mod ssl_sys { - pub const X509_V_OK: i32 = 0; - pub const X509_V_ERR_INVALID_CALL: i32 = 69; -} - -pub mod error { - use super::ssl::Error; - - /// Collection of [`Errors`] from OpenSSL. - #[derive(Debug)] - pub struct ErrorStack; - impl_display!(ErrorStack); - impl std::error::Error for ErrorStack {} - impl ErrorStack { - /// Returns the contents of the OpenSSL error stack. - pub fn get() -> ErrorStack { - unimplemented!(); - } - - /// Returns the errors in the stack. - pub fn errors(&self) -> &[Error] { - unimplemented!(); - } - } -} - -pub mod x509 { - use super::asn1::{Asn1IntegerRef, Asn1StringRef, Asn1TimeRef}; - use super::error::ErrorStack; - use super::hash::{DigestBytes, MessageDigest}; - use super::nid::Nid; - - /// An `X509` public key certificate. - #[derive(Debug, Clone)] - pub struct X509; - impl_deref! {X509 => X509Ref} - impl X509 { - /// Deserializes a PEM-encoded X509 structure. - pub fn from_pem(_pem: &[u8]) -> Result { - unimplemented!(); - } - } - - /// A type to destructure and examine an `X509Name`. - pub struct X509NameEntries<'a> { - marker: std::marker::PhantomData<&'a ()>, - } - impl<'a> Iterator for X509NameEntries<'a> { - type Item = &'a X509NameEntryRef; - fn next(&mut self) -> Option<&'a X509NameEntryRef> { - unimplemented!(); - } - } - - /// Reference to `X509NameEntry`. - pub struct X509NameEntryRef; - impl X509NameEntryRef { - pub fn data(&self) -> &Asn1StringRef { - unimplemented!(); - } - } - - /// Reference to `X509Name`. - pub struct X509NameRef; - impl X509NameRef { - /// Returns the name entries by the nid. - pub fn entries_by_nid(&self, _nid: Nid) -> X509NameEntries<'_> { - unimplemented!(); - } - } - - /// Reference to `X509`. - pub struct X509Ref; - impl X509Ref { - /// Returns this certificate’s subject name. - pub fn subject_name(&self) -> &X509NameRef { - unimplemented!(); - } - - /// Returns a digest of the DER representation of the certificate. - pub fn digest(&self, _hash_type: MessageDigest) -> Result { - unimplemented!(); - } - - /// Returns the certificate’s Not After validity period. - pub fn not_after(&self) -> &Asn1TimeRef { - unimplemented!(); - } - - /// Returns this certificate’s serial number. - pub fn serial_number(&self) -> &Asn1IntegerRef { - unimplemented!(); - } - } - - /// The result of peer certificate verification. - pub struct X509VerifyResult; - impl X509VerifyResult { - /// Return the integer representation of an `X509VerifyResult`. - pub fn as_raw(&self) -> i32 { - unimplemented!(); - } - } - - pub mod store { - use super::super::error::ErrorStack; - use super::X509; - - /// A builder type used to construct an `X509Store`. - pub struct X509StoreBuilder; - impl X509StoreBuilder { - /// Returns a builder for a certificate store.. - pub fn new() -> Result { - unimplemented!(); - } - - /// Constructs the `X509Store`. - pub fn build(self) -> X509Store { - unimplemented!(); - } - - /// Adds a certificate to the certificate store. - pub fn add_cert(&mut self, _cert: X509) -> Result<(), ErrorStack> { - unimplemented!(); - } - } - - /// A certificate store to hold trusted X509 certificates. - pub struct X509Store; - impl_deref! {X509Store => X509StoreRef} - - /// Reference to an `X509Store`. - pub struct X509StoreRef; - } - - pub mod verify { - /// Reference to `X509VerifyParam`. - pub struct X509VerifyParamRef; - } -} - -pub mod nid { - /// A numerical identifier for an OpenSSL object. - pub struct Nid; - impl Nid { - pub const COMMONNAME: Nid = Self {}; - pub const ORGANIZATIONNAME: Nid = Self {}; - pub const ORGANIZATIONALUNITNAME: Nid = Self {}; - } -} - -pub mod pkey { - use super::error::ErrorStack; - - /// A public or private key. - #[derive(Clone)] - pub struct PKey { - marker: std::marker::PhantomData, - } - impl std::ops::Deref for PKey { - type Target = PKeyRef; - fn deref(&self) -> &PKeyRef { - unimplemented!(); - } - } - impl std::ops::DerefMut for PKey { - fn deref_mut(&mut self) -> &mut PKeyRef { - unimplemented!(); - } - } - impl PKey { - pub fn private_key_from_pem(_pem: &[u8]) -> Result, ErrorStack> { - unimplemented!(); - } - } - - /// Reference to `PKey`. - pub struct PKeyRef { - marker: std::marker::PhantomData, - } - - /// A tag type indicating that a key has private components. - #[derive(Clone)] - pub enum Private {} - unsafe impl HasPrivate for Private {} - - /// A trait indicating that a key has private components. - pub unsafe trait HasPrivate {} -} - -pub mod hash { - /// A message digest algorithm. - pub struct MessageDigest; - impl MessageDigest { - pub fn sha256() -> MessageDigest { - unimplemented!(); - } - } - - /// The resulting bytes of a digest. - pub struct DigestBytes; - impl AsRef<[u8]> for DigestBytes { - fn as_ref(&self) -> &[u8] { - unimplemented!(); - } - } -} - -pub mod asn1 { - use super::bn::BigNum; - use super::error::ErrorStack; - - /// A reference to an `Asn1Integer`. - pub struct Asn1IntegerRef; - impl Asn1IntegerRef { - /// Converts the integer to a `BigNum`. - pub fn to_bn(&self) -> Result { - unimplemented!(); - } - } - - /// A reference to an `Asn1String`. - pub struct Asn1StringRef; - impl Asn1StringRef { - pub fn as_utf8(&self) -> Result<&str, ErrorStack> { - unimplemented!(); - } - } - - /// Reference to an `Asn1Time` - pub struct Asn1TimeRef; - impl_display! {Asn1TimeRef} -} - -pub mod bn { - use super::error::ErrorStack; - - /// Dynamically sized large number implementation - pub struct BigNum; - impl BigNum { - /// Returns a hexadecimal string representation of `self`. - pub fn to_hex_str(&self) -> Result<&str, ErrorStack> { - unimplemented!(); - } - } -} - -pub mod ext { - use super::error::ErrorStack; - use super::pkey::{HasPrivate, PKeyRef}; - use super::ssl::{Ssl, SslAcceptor, SslRef}; - use super::x509::store::X509StoreRef; - use super::x509::verify::X509VerifyParamRef; - use super::x509::X509Ref; - - /// Add name as an additional reference identifier that can match the peer's certificate - pub fn add_host(_verify_param: &mut X509VerifyParamRef, _host: &str) -> Result<(), ErrorStack> { - unimplemented!(); - } - - /// Set the verify cert store of `_ssl` - pub fn ssl_set_verify_cert_store( - _ssl: &mut SslRef, - _cert_store: &X509StoreRef, - ) -> Result<(), ErrorStack> { - unimplemented!(); - } - - /// Load the certificate into `_ssl` - pub fn ssl_use_certificate(_ssl: &mut SslRef, _cert: &X509Ref) -> Result<(), ErrorStack> { - unimplemented!(); - } - - /// Load the private key into `_ssl` - pub fn ssl_use_private_key(_ssl: &mut SslRef, _key: &PKeyRef) -> Result<(), ErrorStack> - where - T: HasPrivate, - { - unimplemented!(); - } - - /// Clear the error stack - pub fn clear_error_stack() {} - - /// Create a new [Ssl] from &[SslAcceptor] - pub fn ssl_from_acceptor(_acceptor: &SslAcceptor) -> Result { - unimplemented!(); - } - - /// Suspend the TLS handshake when a certificate is needed. - pub fn suspend_when_need_ssl_cert(_ssl: &mut SslRef) { - unimplemented!(); - } - - /// Unblock a TLS handshake after the certificate is set. - pub fn unblock_ssl_cert(_ssl: &mut SslRef) { - unimplemented!(); - } - - /// Whether the TLS error is SSL_ERROR_WANT_X509_LOOKUP - pub fn is_suspended_for_cert(_error: &super::ssl::Error) -> bool { - unimplemented!(); - } - - /// Add the certificate into the cert chain of `_ssl` - pub fn ssl_add_chain_cert(_ssl: &mut SslRef, _cert: &X509Ref) -> Result<(), ErrorStack> { - unimplemented!(); - } - - /// Set renegotiation - pub fn ssl_set_renegotiate_mode_freely(_ssl: &mut SslRef) {} - - /// Set the curves/groups of `_ssl` - pub fn ssl_set_groups_list(_ssl: &mut SslRef, _groups: &str) -> Result<(), ErrorStack> { - unimplemented!(); - } - - /// Sets whether a second keyshare to be sent in client hello when PQ is used. - pub fn ssl_use_second_key_share(_ssl: &mut SslRef, _enabled: bool) {} - - /// Get a mutable SslRef ouf of SslRef, which is a missing functionality even when holding &mut SslStream - /// # Safety - pub unsafe fn ssl_mut(_ssl: &SslRef) -> &mut SslRef { - unimplemented!(); - } -} - -pub mod tokio_ssl { - use std::pin::Pin; - use std::task::{Context, Poll}; - use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - - use super::error::ErrorStack; - use super::ssl::{Error, Ssl, SslRef}; - - /// A TLS session over a stream. - #[derive(Debug)] - pub struct SslStream { - marker: std::marker::PhantomData, - } - impl SslStream { - /// Creates a new `SslStream`. - pub fn new(_ssl: Ssl, _stream: S) -> Result { - unimplemented!(); - } - - /// Initiates a client-side TLS handshake. - pub async fn connect(self: Pin<&mut Self>) -> Result<(), Error> { - unimplemented!(); - } - - /// Initiates a server-side TLS handshake. - pub async fn accept(self: Pin<&mut Self>) -> Result<(), Error> { - unimplemented!(); - } - - /// Returns a shared reference to the `Ssl` object associated with this stream. - pub fn ssl(&self) -> &SslRef { - unimplemented!(); - } - - /// Returns a shared reference to the underlying stream. - pub fn get_ref(&self) -> &S { - unimplemented!(); - } - - /// Returns a mutable reference to the underlying stream. - pub fn get_mut(&mut self) -> &mut S { - unimplemented!(); - } - } - impl AsyncRead for SslStream - where - S: AsyncRead + AsyncWrite, - { - fn poll_read( - self: Pin<&mut Self>, - _ctx: &mut Context<'_>, - _buf: &mut ReadBuf<'_>, - ) -> Poll> { - unimplemented!(); - } - } - impl AsyncWrite for SslStream - where - S: AsyncRead + AsyncWrite, - { - fn poll_write( - self: Pin<&mut Self>, - _ctx: &mut Context<'_>, - _buf: &[u8], - ) -> Poll> { - unimplemented!(); - } - - fn poll_flush(self: Pin<&mut Self>, _ctx: &mut Context<'_>) -> Poll> { - unimplemented!(); - } - - fn poll_shutdown( - self: Pin<&mut Self>, - _ctx: &mut Context<'_>, - ) -> Poll> { - unimplemented!(); - } - } -} diff --git a/pingora-core/src/protocols/tls/mod.rs b/pingora-core/src/protocols/tls/mod.rs index fceb0a5c..c479a2d9 100644 --- a/pingora-core/src/protocols/tls/mod.rs +++ b/pingora-core/src/protocols/tls/mod.rs @@ -15,177 +15,19 @@ //! The TLS layer implementations pub mod digest; +pub use digest::*; -#[cfg(feature = "some_tls")] +#[cfg(feature = "openssl_derived")] mod boringssl_openssl; -#[cfg(feature = "some_tls")] +#[cfg(feature = "openssl_derived")] pub use boringssl_openssl::*; -#[cfg(not(feature = "some_tls"))] -pub mod dummy_tls; +#[cfg(not(feature = "any_tls"))] +pub mod noop_tls; -use crate::protocols::digest::TimingDigest; -use crate::protocols::{Peek, Ssl, UniqueID, UniqueIDType}; -use crate::tls::{self, ssl, tokio_ssl::SslStream as InnerSsl}; -use log::warn; -use pingora_error::{ErrorType::*, OrErr, Result}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::SystemTime; -use tokio::io::{self, AsyncRead, AsyncWrite, ReadBuf}; - -pub use digest::SslDigest; - -/// The TLS connection -#[derive(Debug)] -pub struct SslStream { - ssl: InnerSsl, - digest: Option>, - timing: TimingDigest, -} - -impl SslStream -where - T: AsyncRead + AsyncWrite + std::marker::Unpin, -{ - /// Create a new TLS connection from the given `stream` - /// - /// The caller needs to perform [`Self::connect()`] or [`Self::accept()`] to perform TLS - /// handshake after. - pub fn new(ssl: ssl::Ssl, stream: T) -> Result { - let ssl = InnerSsl::new(ssl, stream) - .explain_err(TLSHandshakeFailure, |e| format!("ssl stream error: {e}"))?; - - Ok(SslStream { - ssl, - digest: None, - timing: Default::default(), - }) - } - - /// Connect to the remote TLS server as a client - pub async fn connect(&mut self) -> Result<(), ssl::Error> { - Self::clear_error(); - Pin::new(&mut self.ssl).connect().await?; - self.timing.established_ts = SystemTime::now(); - self.digest = Some(Arc::new(SslDigest::from_ssl(self.ssl()))); - Ok(()) - } - - /// Finish the TLS handshake from client as a server - pub async fn accept(&mut self) -> Result<(), ssl::Error> { - Self::clear_error(); - Pin::new(&mut self.ssl).accept().await?; - self.timing.established_ts = SystemTime::now(); - self.digest = Some(Arc::new(SslDigest::from_ssl(self.ssl()))); - Ok(()) - } - - #[inline] - fn clear_error() { - let errs = tls::error::ErrorStack::get(); - if !errs.errors().is_empty() { - warn!("Clearing dirty TLS error stack: {}", errs); - } - } -} - -impl SslStream { - pub fn ssl_digest(&self) -> Option> { - self.digest.clone() - } -} - -use std::ops::{Deref, DerefMut}; - -impl Deref for SslStream { - type Target = InnerSsl; - - fn deref(&self) -> &Self::Target { - &self.ssl - } -} - -impl DerefMut for SslStream { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.ssl - } -} - -impl AsyncRead for SslStream -where - T: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - Self::clear_error(); - Pin::new(&mut self.ssl).poll_read(cx, buf) - } -} - -impl AsyncWrite for SslStream -where - T: AsyncRead + AsyncWrite + Unpin, -{ - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context, - buf: &[u8], - ) -> Poll> { - Self::clear_error(); - Pin::new(&mut self.ssl).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - Self::clear_error(); - Pin::new(&mut self.ssl).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - Self::clear_error(); - Pin::new(&mut self.ssl).poll_shutdown(cx) - } - - fn poll_write_vectored( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - bufs: &[std::io::IoSlice<'_>], - ) -> Poll> { - Self::clear_error(); - Pin::new(&mut self.ssl).poll_write_vectored(cx, bufs) - } - - fn is_write_vectored(&self) -> bool { - true - } -} - -impl UniqueID for SslStream -where - T: UniqueID, -{ - fn id(&self) -> UniqueIDType { - self.ssl.get_ref().id() - } -} - -impl Ssl for SslStream { - fn get_ssl(&self) -> Option<&ssl::SslRef> { - Some(self.ssl()) - } - - fn get_ssl_digest(&self) -> Option> { - self.ssl_digest() - } -} - -// TODO: implement Peek if needed -impl Peek for SslStream {} +#[cfg(not(feature = "any_tls"))] +pub use noop_tls::*; /// The protocol for Application-Layer Protocol Negotiation #[derive(Hash, Clone, Debug)] @@ -236,6 +78,7 @@ impl ALPN { } } + #[cfg(feature = "openssl_derived")] pub(crate) fn to_wire_preference(&self) -> &[u8] { // https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_alpn_select_cb.html // "vector of nonempty, 8-bit length-prefixed, byte strings" @@ -246,6 +89,7 @@ impl ALPN { } } + #[cfg(feature = "any_tls")] pub(crate) fn from_wire_selected(raw: &[u8]) -> Option { match raw { b"http/1.1" => Some(Self::H1), @@ -253,4 +97,13 @@ impl ALPN { _ => None, } } + + #[cfg(feature = "rustls")] + pub(crate) fn to_wire_protocols(&self) -> Vec> { + match self { + ALPN::H1 => vec![b"http/1.1".to_vec()], + ALPN::H2 => vec![b"h2".to_vec()], + ALPN::H2H1 => vec![b"h2".to_vec(), b"http/1.1".to_vec()], + } + } } diff --git a/pingora-core/src/protocols/tls/noop_tls/mod.rs b/pingora-core/src/protocols/tls/noop_tls/mod.rs new file mode 100644 index 00000000..ee34a0c6 --- /dev/null +++ b/pingora-core/src/protocols/tls/noop_tls/mod.rs @@ -0,0 +1,218 @@ +// Copyright 2024 Cloudflare, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This is a set of stubs that provides the minimum types to let pingora work +//! without any tls providers configured + +pub struct TlsRef; + +pub type CaType = [CertWrapper]; + +#[derive(Debug)] +pub struct CertWrapper; + +impl CertWrapper { + pub fn not_after(&self) -> &str { + "" + } +} + +pub mod connectors { + use pingora_error::Result; + + use crate::{ + connectors::ConnectorOptions, + protocols::{ALPN, IO}, + upstreams::peer::Peer, + }; + + use super::stream::SslStream; + + #[derive(Clone)] + pub struct Connector { + pub ctx: TlsConnector, + } + + #[derive(Clone)] + pub struct TlsConnector; + + pub struct TlsSettings; + + impl Connector { + pub fn new(_: Option) -> Self { + Self { ctx: TlsConnector } + } + } + + pub async fn connect( + _: T, + _: &P, + _: Option, + _: &TlsConnector, + ) -> Result> + where + T: IO, + P: Peer + Send + Sync, + { + Ok(SslStream::default()) + } +} + +pub mod listeners { + use pingora_error::Result; + use tokio::io::{AsyncRead, AsyncWrite}; + + use super::stream::SslStream; + + pub struct Acceptor; + + pub struct TlsSettings; + + impl TlsSettings { + pub fn build(&self) -> Acceptor { + Acceptor + } + + pub fn intermediate(_: &str, _: &str) -> Result { + Ok(Self) + } + + pub fn enable_h2(&mut self) {} + } + + impl Acceptor { + pub async fn tls_handshake(&self, _: S) -> Result> { + unimplemented!("No tls feature was specified") + } + } +} + +pub mod stream { + use std::{ + pin::Pin, + task::{Context, Poll}, + }; + + use async_trait::async_trait; + use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + + use crate::protocols::{ + GetProxyDigest, GetSocketDigest, GetTimingDigest, Peek, Shutdown, Ssl, UniqueID, + }; + + /// A TLS session over a stream. + #[derive(Debug)] + pub struct SslStream { + marker: std::marker::PhantomData, + } + + impl Default for SslStream { + fn default() -> Self { + Self { + marker: Default::default(), + } + } + } + + impl AsyncRead for SslStream + where + S: AsyncRead + AsyncWrite, + { + fn poll_read( + self: Pin<&mut Self>, + _ctx: &mut Context<'_>, + _buf: &mut ReadBuf<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + } + + impl AsyncWrite for SslStream + where + S: AsyncRead + AsyncWrite, + { + fn poll_write( + self: Pin<&mut Self>, + _ctx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush(self: Pin<&mut Self>, _ctx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown( + self: Pin<&mut Self>, + _ctx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + } + + #[async_trait] + impl Shutdown for SslStream { + async fn shutdown(&mut self) {} + } + + impl UniqueID for SslStream { + fn id(&self) -> crate::protocols::UniqueIDType { + 0 + } + } + + impl Ssl for SslStream {} + + impl GetTimingDigest for SslStream { + fn get_timing_digest(&self) -> Vec> { + vec![] + } + } + + impl GetProxyDigest for SslStream { + fn get_proxy_digest( + &self, + ) -> Option> { + None + } + } + + impl GetSocketDigest for SslStream { + fn get_socket_digest(&self) -> Option> { + None + } + } + + impl Peek for SslStream {} +} + +pub mod utils { + use std::fmt::Display; + + use super::CertWrapper; + + #[derive(Debug, Clone, Hash)] + pub struct CertKey; + + impl Display for CertKey { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } + } + + pub fn get_organization_unit(_: &CertWrapper) -> Option { + None + } +} diff --git a/pingora-core/src/server/configuration/mod.rs b/pingora-core/src/server/configuration/mod.rs index 21428aa6..db034554 100644 --- a/pingora-core/src/server/configuration/mod.rs +++ b/pingora-core/src/server/configuration/mod.rs @@ -67,21 +67,24 @@ pub struct ServerConf { /// Timeout in seconds of the final step for the graceful shutdown. pub graceful_shutdown_timeout_seconds: Option, // These options don't belong here as they are specific to certain services - /// IPv4 addresses for a client connector to bind to. See [`ConnectorOptions`]. + /// IPv4 addresses for a client connector to bind to. See + /// [`ConnectorOptions`](crate::connectors::ConnectorOptions). /// Note: this is an _unstable_ field that may be renamed or removed in the future. pub client_bind_to_ipv4: Vec, - /// IPv6 addresses for a client connector to bind to. See [`ConnectorOptions`]. + /// IPv6 addresses for a client connector to bind to. See + /// [`ConnectorOptions`](crate::connectors::ConnectorOptions). /// Note: this is an _unstable_ field that may be renamed or removed in the future. pub client_bind_to_ipv6: Vec, - /// Keepalive pool size for client connections to upstream. See [`ConnectorOptions`]. + /// Keepalive pool size for client connections to upstream. See + /// [`ConnectorOptions`](crate::connectors::ConnectorOptions). /// Note: this is an _unstable_ field that may be renamed or removed in the future. pub upstream_keepalive_pool_size: usize, /// Number of dedicated thread pools to use for upstream connection establishment. - /// See [`ConnectorOptions`]. + /// See [`ConnectorOptions`](crate::connectors::ConnectorOptions). /// Note: this is an _unstable_ field that may be renamed or removed in the future. pub upstream_connect_offload_threadpools: Option, /// Number of threads per dedicated upstream connection establishment pool. - /// See [`ConnectorOptions`]. + /// See [`ConnectorOptions`](crate::connectors::ConnectorOptions). /// Note: this is an _unstable_ field that may be renamed or removed in the future. pub upstream_connect_offload_thread_per_pool: Option, /// When enabled allows TLS keys to be written to a file specified by the SSLKEYLOG diff --git a/pingora-core/src/services/listening.rs b/pingora-core/src/services/listening.rs index b1b2800f..3cdb3b87 100644 --- a/pingora-core/src/services/listening.rs +++ b/pingora-core/src/services/listening.rs @@ -19,7 +19,8 @@ //! more endpoints to listen to. use crate::apps::ServerApp; -use crate::listeners::{Listeners, ServerAddress, TcpSocketOptions, TlsSettings, TransportStack}; +use crate::listeners::tls::TlsSettings; +use crate::listeners::{Listeners, ServerAddress, TcpSocketOptions, TransportStack}; use crate::protocols::Stream; #[cfg(unix)] use crate::server::ListenFds; diff --git a/pingora-core/src/upstreams/peer.rs b/pingora-core/src/upstreams/peer.rs index 4030667d..87bfab63 100644 --- a/pingora-core/src/upstreams/peer.rs +++ b/pingora-core/src/upstreams/peer.rs @@ -33,11 +33,11 @@ use std::time::Duration; use crate::connectors::{l4::BindTo, L4Connect}; use crate::protocols::l4::socket::SocketAddr; +use crate::protocols::tls::CaType; #[cfg(unix)] use crate::protocols::ConnFdReusable; use crate::protocols::TcpKeepalive; -use crate::tls::x509::X509; -use crate::utils::{get_organization_unit, CertKey}; +use crate::utils::tls::{get_organization_unit, CertKey}; pub use crate::protocols::tls::ALPN; @@ -148,7 +148,7 @@ pub trait Peer: Display + Clone { /// Get the CA cert to use to validate the server cert. /// /// If not set, the default CAs will be used. - fn get_ca(&self) -> Option<&Arc>> { + fn get_ca(&self) -> Option<&Arc> { match self.get_peer_options() { Some(opt) => opt.ca.as_ref(), None => None, @@ -316,7 +316,7 @@ pub struct PeerOptions { /* accept the cert if it's CN matches the SNI or this name */ pub alternative_cn: Option, pub alpn: ALPN, - pub ca: Option>>, + pub ca: Option>, pub tcp_keepalive: Option, pub tcp_recv_buf: Option, pub dscp: Option, diff --git a/pingora-core/src/utils/mod.rs b/pingora-core/src/utils/mod.rs index c36f7c82..9826cf90 100644 --- a/pingora-core/src/utils/mod.rs +++ b/pingora-core/src/utils/mod.rs @@ -15,12 +15,13 @@ //! This module contains various types that make it easier to work with bytes and X509 //! certificates. -// TODO: move below to its own mod -use crate::tls::{nid::Nid, pkey::PKey, pkey::Private, x509::X509}; -use crate::Result; +#[cfg(feature = "any_tls")] +pub mod tls; + +#[cfg(not(feature = "any_tls"))] +pub use crate::tls::utils as tls; + use bytes::Bytes; -use pingora_error::{ErrorType::*, OrErr}; -use std::hash::{Hash, Hasher}; /// A `BufRef` is a reference to a buffer of bytes. It removes the need for self-referential data /// structures. It is safe to use as long as the underlying buffer does not get mutated. @@ -108,125 +109,3 @@ pub const EMPTY_KV_REF: KVRef = KVRef { name: BufRef(0, 0), value: BufRef(0, 0), }; - -fn get_subject_name(cert: &X509, name_type: Nid) -> Option { - cert.subject_name() - .entries_by_nid(name_type) - .next() - .map(|name| { - name.data() - .as_utf8() - .map(|s| s.to_string()) - .unwrap_or_default() - }) -} - -/// Return the organization associated with the X509 certificate. -pub fn get_organization(cert: &X509) -> Option { - get_subject_name(cert, Nid::ORGANIZATIONNAME) -} - -/// Return the common name associated with the X509 certificate. -pub fn get_common_name(cert: &X509) -> Option { - get_subject_name(cert, Nid::COMMONNAME) -} - -/// Return the common name associated with the X509 certificate. -pub fn get_organization_unit(cert: &X509) -> Option { - get_subject_name(cert, Nid::ORGANIZATIONALUNITNAME) -} - -/// Return the serial number associated with the X509 certificate as a hexadecimal value. -pub fn get_serial(cert: &X509) -> Result { - let bn = cert - .serial_number() - .to_bn() - .or_err(InvalidCert, "Invalid serial")?; - let hex = bn.to_hex_str().or_err(InvalidCert, "Invalid serial")?; - - let hex_str: &str = hex.as_ref(); - Ok(hex_str.to_owned()) -} - -/// This type contains a list of one or more certificates and an associated private key. The leaf -/// certificate should always be first. -#[derive(Clone)] -pub struct CertKey { - certificates: Vec, - key: PKey, -} - -impl CertKey { - /// Create a new `CertKey` given a list of certificates and a private key. - pub fn new(certificates: Vec, key: PKey) -> CertKey { - assert!( - !certificates.is_empty(), - "expected a non-empty vector of certificates in CertKey::new" - ); - - CertKey { certificates, key } - } - - /// Peek at the leaf certificate. - pub fn leaf(&self) -> &X509 { - // This is safe due to the assertion above. - &self.certificates[0] - } - - /// Return the key. - pub fn key(&self) -> &PKey { - &self.key - } - - /// Return a slice of intermediate certificates. An empty slice means there are none. - pub fn intermediates(&self) -> &[X509] { - if self.certificates.len() <= 1 { - return &[]; - } - &self.certificates[1..] - } - - /// Return the organization from the leaf certificate. - pub fn organization(&self) -> Option { - get_organization(self.leaf()) - } - - /// Return the serial from the leaf certificate. - pub fn serial(&self) -> Result { - get_serial(self.leaf()) - } -} - -impl Hash for CertKey { - fn hash(&self, state: &mut H) { - for certificate in &self.certificates { - if let Ok(serial) = get_serial(certificate) { - serial.hash(state) - } - } - } -} - -// hide private key -impl std::fmt::Debug for CertKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CertKey") - .field("X509", &self.leaf()) - .finish() - } -} - -impl std::fmt::Display for CertKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let leaf = self.leaf(); - if let Some(cn) = get_common_name(leaf) { - // Write CN if it exists - write!(f, "CN: {cn},")?; - } else if let Some(org_unit) = get_organization_unit(leaf) { - // CA cert might not have CN, so print its unit name instead - write!(f, "Org Unit: {org_unit},")?; - } - write!(f, ", expire: {}", leaf.not_after()) - // ignore the details of the private key - } -} diff --git a/pingora-core/src/utils/tls/boringssl_openssl.rs b/pingora-core/src/utils/tls/boringssl_openssl.rs new file mode 100644 index 00000000..8842c3c6 --- /dev/null +++ b/pingora-core/src/utils/tls/boringssl_openssl.rs @@ -0,0 +1,140 @@ +// Copyright 2024 Cloudflare, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::tls::{nid::Nid, pkey::PKey, pkey::Private, x509::X509}; +use crate::Result; +use pingora_error::{ErrorType::*, OrErr}; +use std::hash::{Hash, Hasher}; + +fn get_subject_name(cert: &X509, name_type: Nid) -> Option { + cert.subject_name() + .entries_by_nid(name_type) + .next() + .map(|name| { + name.data() + .as_utf8() + .map(|s| s.to_string()) + .unwrap_or_default() + }) +} + +/// Return the organization associated with the X509 certificate. +pub fn get_organization(cert: &X509) -> Option { + get_subject_name(cert, Nid::ORGANIZATIONNAME) +} + +/// Return the common name associated with the X509 certificate. +pub fn get_common_name(cert: &X509) -> Option { + get_subject_name(cert, Nid::COMMONNAME) +} + +/// Return the common name associated with the X509 certificate. +pub fn get_organization_unit(cert: &X509) -> Option { + get_subject_name(cert, Nid::ORGANIZATIONALUNITNAME) +} + +/// Return the serial number associated with the X509 certificate as a hexadecimal value. +pub fn get_serial(cert: &X509) -> Result { + let bn = cert + .serial_number() + .to_bn() + .or_err(InvalidCert, "Invalid serial")?; + let hex = bn.to_hex_str().or_err(InvalidCert, "Invalid serial")?; + + let hex_str: &str = hex.as_ref(); + Ok(hex_str.to_owned()) +} + +/// This type contains a list of one or more certificates and an associated private key. The leaf +/// certificate should always be first. +#[derive(Clone)] +pub struct CertKey { + certificates: Vec, + key: PKey, +} + +impl CertKey { + /// Create a new `CertKey` given a list of certificates and a private key. + pub fn new(certificates: Vec, key: PKey) -> CertKey { + assert!( + !certificates.is_empty(), + "expected a non-empty vector of certificates in CertKey::new" + ); + + CertKey { certificates, key } + } + + /// Peek at the leaf certificate. + pub fn leaf(&self) -> &X509 { + // This is safe due to the assertion above. + &self.certificates[0] + } + + /// Return the key. + pub fn key(&self) -> &PKey { + &self.key + } + + /// Return a slice of intermediate certificates. An empty slice means there are none. + pub fn intermediates(&self) -> &[X509] { + if self.certificates.len() <= 1 { + return &[]; + } + &self.certificates[1..] + } + + /// Return the organization from the leaf certificate. + pub fn organization(&self) -> Option { + get_organization(self.leaf()) + } + + /// Return the serial from the leaf certificate. + pub fn serial(&self) -> Result { + get_serial(self.leaf()) + } +} + +impl Hash for CertKey { + fn hash(&self, state: &mut H) { + for certificate in &self.certificates { + if let Ok(serial) = get_serial(certificate) { + serial.hash(state) + } + } + } +} + +// hide private key +impl std::fmt::Debug for CertKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CertKey") + .field("X509", &self.leaf()) + .finish() + } +} + +impl std::fmt::Display for CertKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let leaf = self.leaf(); + if let Some(cn) = get_common_name(leaf) { + // Write CN if it exists + write!(f, "CN: {cn},")?; + } else if let Some(org_unit) = get_organization_unit(leaf) { + // CA cert might not have CN, so print its unit name instead + write!(f, "Org Unit: {org_unit},")?; + } + write!(f, ", expire: {}", leaf.not_after()) + // ignore the details of the private key + } +} diff --git a/pingora-core/src/utils/tls/mod.rs b/pingora-core/src/utils/tls/mod.rs new file mode 100644 index 00000000..0bcaeac8 --- /dev/null +++ b/pingora-core/src/utils/tls/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 Cloudflare, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(feature = "openssl_derived")] +mod boringssl_openssl; + +#[cfg(feature = "openssl_derived")] +pub use boringssl_openssl::*; diff --git a/pingora-core/tests/test_basic.rs b/pingora-core/tests/test_basic.rs index ace35cbb..32954de9 100644 --- a/pingora-core/tests/test_basic.rs +++ b/pingora-core/tests/test_basic.rs @@ -14,23 +14,20 @@ mod utils; -use hyper::Client; -#[cfg(unix)] +#[cfg(all(unix, feature = "any_tls"))] use hyperlocal::{UnixClientExt, Uri}; -use utils::init; -#[cfg(feature = "some_tls")] #[tokio::test] async fn test_http() { - init(); + utils::init(); let res = reqwest::get("http://127.0.0.1:6145").await.unwrap(); assert_eq!(res.status(), reqwest::StatusCode::OK); } -#[cfg(feature = "some_tls")] +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_https_http2() { - init(); + utils::init(); let client = reqwest::Client::builder() .danger_accept_invalid_certs(true) @@ -53,12 +50,12 @@ async fn test_https_http2() { } #[cfg(unix)] -#[cfg(feature = "some_tls")] +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_uds() { - init(); + utils::init(); let url = Uri::new("/tmp/echo.sock", "/").into(); - let client = Client::unix(); + let client = hyper::Client::unix(); let res = client.get(url).await.unwrap(); assert_eq!(res.status(), reqwest::StatusCode::OK); diff --git a/pingora-core/tests/utils/mod.rs b/pingora-core/tests/utils/mod.rs index 46f050f2..a832c4a0 100644 --- a/pingora-core/tests/utils/mod.rs +++ b/pingora-core/tests/utils/mod.rs @@ -82,7 +82,7 @@ fn entry_point(opt: Option) { listeners.add_uds("/tmp/echo.sock", None); let mut tls_settings = - pingora_core::listeners::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); + pingora_core::listeners::tls::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); tls_settings.enable_h2(); listeners.add_tls_with_settings("0.0.0.0:6146", None, tls_settings); diff --git a/pingora-error/src/lib.rs b/pingora-error/src/lib.rs index d8567633..935b2520 100644 --- a/pingora-error/src/lib.rs +++ b/pingora-error/src/lib.rs @@ -105,6 +105,7 @@ pub enum ErrorType { ConnectTimedout, ConnectRefused, ConnectNoRoute, + TLSWantX509Lookup, TLSHandshakeFailure, TLSHandshakeTimedout, InvalidCert, @@ -164,6 +165,7 @@ impl ErrorType { ErrorType::ConnectRefused => "ConnectRefused", ErrorType::ConnectNoRoute => "ConnectNoRoute", ErrorType::ConnectProxyFailure => "ConnectProxyFailure", + ErrorType::TLSWantX509Lookup => "TLSWantX509Lookup", ErrorType::TLSHandshakeFailure => "TLSHandshakeFailure", ErrorType::TLSHandshakeTimedout => "TLSHandshakeTimedout", ErrorType::InvalidCert => "InvalidCert", diff --git a/pingora-load-balancing/Cargo.toml b/pingora-load-balancing/Cargo.toml index 523e2f41..e87c598b 100644 --- a/pingora-load-balancing/Cargo.toml +++ b/pingora-load-balancing/Cargo.toml @@ -35,6 +35,9 @@ derivative = "2.2.0" [dev-dependencies] [features] -default = ["openssl"] -openssl = ["pingora-core/openssl"] -boringssl = ["pingora-core/boringssl"] +default = [] +openssl = ["pingora-core/openssl", "openssl_derived"] +boringssl = ["pingora-core/boringssl", "openssl_derived"] +rustls = ["pingora-core/rustls", "any_tls"] +openssl_derived = ["any_tls"] +any_tls = [] diff --git a/pingora-load-balancing/src/health_check.rs b/pingora-load-balancing/src/health_check.rs index ac63579b..4e658bb1 100644 --- a/pingora-load-balancing/src/health_check.rs +++ b/pingora-load-balancing/src/health_check.rs @@ -375,6 +375,7 @@ mod test { assert!(tcp_check.check(&backend).await.is_err()); } + #[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_check() { let tls_check = TcpHealthCheck::new_tls("one.one.one.one"); @@ -387,6 +388,7 @@ mod test { assert!(tls_check.check(&backend).await.is_ok()); } + #[cfg(feature = "any_tls")] #[tokio::test] async fn test_https_check() { let https_check = HttpHealthCheck::new("one.one.one.one", true); diff --git a/pingora-proxy/Cargo.toml b/pingora-proxy/Cargo.toml index 13d48048..b65e0d81 100644 --- a/pingora-proxy/Cargo.toml +++ b/pingora-proxy/Cargo.toml @@ -44,7 +44,7 @@ env_logger = "0.9" hyper = "0.14" tokio-tungstenite = "0.20.1" pingora-limits = { version = "0.3.0", path = "../pingora-limits" } -pingora-load-balancing = { version = "0.3.0", path = "../pingora-load-balancing" } +pingora-load-balancing = { version = "0.3.0", path = "../pingora-load-balancing", default-features=false } prometheus = "0" futures-util = "0.3" serde = { version = "1.0", features = ["derive"] } @@ -55,9 +55,12 @@ serde_yaml = "0.8" hyperlocal = "0.8" [features] -default = ["openssl"] -openssl = ["pingora-core/openssl", "pingora-cache/openssl"] -boringssl = ["pingora-core/boringssl", "pingora-cache/boringssl"] +default = [] +openssl = ["pingora-core/openssl", "pingora-cache/openssl", "openssl_derived"] +boringssl = ["pingora-core/boringssl", "pingora-cache/boringssl", "openssl_derived"] +rustls = ["pingora-core/rustls", "pingora-cache/rustls", "any_tls"] +openssl_derived = ["any_tls"] +any_tls = [] sentry = ["pingora-core/sentry"] # or locally cargo doc --config "build.rustdocflags='--cfg doc_async_trait'" diff --git a/pingora-proxy/examples/load_balancer.rs b/pingora-proxy/examples/load_balancer.rs index 614981d6..a428b9b3 100644 --- a/pingora-proxy/examples/load_balancer.rs +++ b/pingora-proxy/examples/load_balancer.rs @@ -86,7 +86,7 @@ fn main() { let key_path = format!("{}/tests/keys/key.pem", env!("CARGO_MANIFEST_DIR")); let mut tls_settings = - pingora_core::listeners::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); + pingora_core::listeners::tls::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); tls_settings.enable_h2(); lb.add_tls_with_settings("0.0.0.0:6189", None, tls_settings); diff --git a/pingora-proxy/tests/test_basic.rs b/pingora-proxy/tests/test_basic.rs index 569e51a8..f33a2474 100644 --- a/pingora-proxy/tests/test_basic.rs +++ b/pingora-proxy/tests/test_basic.rs @@ -67,6 +67,7 @@ async fn test_simple_proxy() { } #[tokio::test] +#[cfg(feature = "any_tls")] async fn test_h2_to_h1() { init(); let client = reqwest::Client::builder() @@ -74,7 +75,12 @@ async fn test_h2_to_h1() { .build() .unwrap(); - let res = client.get("https://127.0.0.1:6150").send().await.unwrap(); + let res = client + .get("https://127.0.0.1:6150") + .header("sni", "openrusty.org") + .send() + .await + .unwrap(); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!(res.version(), reqwest::Version::HTTP_2); @@ -104,6 +110,7 @@ async fn test_h2_to_h1() { } #[tokio::test] +#[cfg(feature = "any_tls")] async fn test_h2_to_h2() { init(); let client = reqwest::Client::builder() @@ -113,6 +120,7 @@ async fn test_h2_to_h2() { let res = client .get("https://127.0.0.1:6150") + .header("sni", "openrusty.org") .header("x-h2", "true") .send() .await @@ -189,6 +197,7 @@ async fn test_h1_on_h2c_port() { } #[tokio::test] +#[cfg(feature = "openssl_derived")] async fn test_h2_to_h2_host_override() { init(); let client = reqwest::Client::builder() @@ -212,6 +221,7 @@ async fn test_h2_to_h2_host_override() { } #[tokio::test] +#[cfg(feature = "any_tls")] async fn test_h2_to_h2_upload() { init(); let client = reqwest::Client::builder() @@ -223,6 +233,7 @@ async fn test_h2_to_h2_upload() { let res = client .get("https://127.0.0.1:6150/echo") + .header("sni", "openrusty.org") .header("x-h2", "true") .body(payload) .send() @@ -235,6 +246,7 @@ async fn test_h2_to_h2_upload() { } #[tokio::test] +#[cfg(feature = "any_tls")] async fn test_h2_to_h1_upload() { init(); let client = reqwest::Client::builder() @@ -246,6 +258,7 @@ async fn test_h2_to_h1_upload() { let res = client .get("https://127.0.0.1:6150/echo") + .header("sni", "openrusty.org") .body(payload) .send() .await @@ -313,7 +326,10 @@ async fn test_simple_proxy_uds_peer() { assert!(is_specified_port(sockaddr.port())); assert_eq!(headers["x-upstream-client-addr"], "unset"); // unnamed UDS - assert_eq!(headers["x-upstream-server-addr"], "/tmp/nginx-test.sock"); + assert_eq!( + headers["x-upstream-server-addr"], + "/tmp/pingora_nginx_test.sock" + ); let body = res.text().await.unwrap(); assert_eq!(body, "Hello World!\n"); @@ -444,6 +460,8 @@ async fn test_dropped_conn() { test_dropped_conn_post_body_over().await; } +// currently not supported with Rustls implementation +#[cfg(feature = "openssl_derived")] #[tokio::test] async fn test_tls_no_verify() { init(); @@ -457,6 +475,7 @@ async fn test_tls_no_verify() { assert_eq!(res.status(), StatusCode::OK); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_verify_sni_not_host() { init(); @@ -473,6 +492,8 @@ async fn test_tls_verify_sni_not_host() { assert_eq!(res.status(), StatusCode::OK); } +// currently not supported with Rustls implementation +#[cfg(feature = "openssl_derived")] #[tokio::test] async fn test_tls_none_verify_host() { init(); @@ -489,6 +510,7 @@ async fn test_tls_none_verify_host() { assert_eq!(res.status(), StatusCode::OK); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_verify_sni_host() { init(); @@ -506,6 +528,7 @@ async fn test_tls_verify_sni_host() { assert_eq!(res.status(), StatusCode::OK); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_underscore_sub_sni_verify_host() { init(); @@ -523,6 +546,7 @@ async fn test_tls_underscore_sub_sni_verify_host() { assert_eq!(res.status(), StatusCode::OK); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_underscore_non_sub_sni_verify_host() { init(); @@ -542,6 +566,7 @@ async fn test_tls_underscore_non_sub_sni_verify_host() { assert_eq!(headers[header::CONNECTION], "close"); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_alt_verify_host() { init(); @@ -560,6 +585,7 @@ async fn test_tls_alt_verify_host() { assert_eq!(res.status(), StatusCode::OK); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_underscore_sub_alt_verify_host() { init(); @@ -578,6 +604,7 @@ async fn test_tls_underscore_sub_alt_verify_host() { assert_eq!(res.status(), StatusCode::OK); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_underscore_non_sub_alt_verify_host() { init(); @@ -691,6 +718,7 @@ async fn test_connect_close() { } #[tokio::test] +#[cfg(feature = "any_tls")] async fn test_mtls_no_client_cert() { init(); let client = reqwest::Client::new(); @@ -709,6 +737,7 @@ async fn test_mtls_no_client_cert() { assert_eq!(res.status(), StatusCode::BAD_REQUEST); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_mtls_no_intermediate_cert() { init(); @@ -730,6 +759,7 @@ async fn test_mtls_no_intermediate_cert() { } #[tokio::test] +#[cfg(feature = "any_tls")] async fn test_mtls() { init(); let client = reqwest::Client::new(); @@ -748,6 +778,7 @@ async fn test_mtls() { assert_eq!(res.status(), StatusCode::OK); } +#[cfg(feature = "any_tls")] async fn assert_reuse(req: reqwest::RequestBuilder) { req.try_clone().unwrap().send().await.unwrap(); let res = req.send().await.unwrap(); @@ -755,6 +786,7 @@ async fn assert_reuse(req: reqwest::RequestBuilder) { assert!(headers.get("x-conn-reuse").is_some()); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_mtls_diff_cert_no_reuse() { init(); @@ -789,6 +821,7 @@ async fn test_mtls_diff_cert_no_reuse() { assert!(headers.get("x-conn-reuse").is_none()); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_diff_verify_no_reuse() { init(); @@ -815,6 +848,7 @@ async fn test_tls_diff_verify_no_reuse() { assert!(headers.get("x-conn-reuse").is_none()); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_diff_verify_host_no_reuse() { init(); @@ -843,6 +877,7 @@ async fn test_tls_diff_verify_host_no_reuse() { assert!(headers.get("x-conn-reuse").is_none()); } +#[cfg(feature = "any_tls")] #[tokio::test] async fn test_tls_diff_alt_cnt_no_reuse() { init(); diff --git a/pingora-proxy/tests/utils/cert.rs b/pingora-proxy/tests/utils/cert.rs index 674a3ac7..fb6f54c9 100644 --- a/pingora-proxy/tests/utils/cert.rs +++ b/pingora-proxy/tests/utils/cert.rs @@ -13,35 +13,64 @@ // limitations under the License. use once_cell::sync::Lazy; -use pingora_core::tls::pkey::{PKey, Private}; -use pingora_core::tls::x509::X509; +#[cfg(feature = "rustls")] +use pingora_core::tls::{load_pem_file_ca, load_pem_file_private_key}; +#[cfg(feature = "openssl_derived")] +use pingora_core::tls::{ + pkey::{PKey, Private}, + x509::X509, +}; use std::fs; -pub static ROOT_CERT: Lazy = Lazy::new(|| load_cert("keys/root.crt")); -pub static ROOT_KEY: Lazy> = Lazy::new(|| load_key("keys/root.key")); -pub static INTERMEDIATE_CERT: Lazy = Lazy::new(|| load_cert("keys/intermediate.crt")); -pub static INTERMEDIATE_KEY: Lazy> = Lazy::new(|| load_key("keys/intermediate.key")); -pub static LEAF_CERT: Lazy = Lazy::new(|| load_cert("keys/leaf.crt")); -pub static LEAF2_CERT: Lazy = Lazy::new(|| load_cert("keys/leaf2.crt")); -pub static LEAF_KEY: Lazy> = Lazy::new(|| load_key("keys/leaf.key")); -pub static LEAF2_KEY: Lazy> = Lazy::new(|| load_key("keys/leaf2.key")); -pub static SERVER_CERT: Lazy = Lazy::new(|| load_cert("keys/server.crt")); -pub static SERVER_KEY: Lazy> = Lazy::new(|| load_key("keys/key.pem")); -pub static CURVE_521_TEST_KEY: Lazy> = +#[cfg(feature = "openssl_derived")] +mod key_types { + use super::*; + pub type PrivateKeyType = PKey; + pub type CertType = X509; +} + +#[cfg(feature = "rustls")] +mod key_types { + use super::*; + pub type PrivateKeyType = Vec; + pub type CertType = Vec; +} + +use key_types::*; + +pub static INTERMEDIATE_CERT: Lazy = Lazy::new(|| load_cert("keys/intermediate.crt")); +pub static LEAF_CERT: Lazy = Lazy::new(|| load_cert("keys/leaf.crt")); +pub static LEAF2_CERT: Lazy = Lazy::new(|| load_cert("keys/leaf2.crt")); +pub static LEAF_KEY: Lazy = Lazy::new(|| load_key("keys/leaf.key")); +pub static LEAF2_KEY: Lazy = Lazy::new(|| load_key("keys/leaf2.key")); +pub static CURVE_521_TEST_KEY: Lazy = Lazy::new(|| load_key("keys/curve_test.521.key.pem")); -pub static CURVE_521_TEST_CERT: Lazy = Lazy::new(|| load_cert("keys/curve_test.521.crt")); -pub static CURVE_384_TEST_KEY: Lazy> = +pub static CURVE_521_TEST_CERT: Lazy = Lazy::new(|| load_cert("keys/curve_test.521.crt")); +pub static CURVE_384_TEST_KEY: Lazy = Lazy::new(|| load_key("keys/curve_test.384.key.pem")); -pub static CURVE_384_TEST_CERT: Lazy = Lazy::new(|| load_cert("keys/curve_test.384.crt")); +pub static CURVE_384_TEST_CERT: Lazy = Lazy::new(|| load_cert("keys/curve_test.384.crt")); +#[cfg(feature = "openssl_derived")] fn load_cert(path: &str) -> X509 { let path = format!("{}/{path}", super::conf_dir()); let cert_bytes = fs::read(path).unwrap(); X509::from_pem(&cert_bytes).unwrap() } - +#[cfg(feature = "openssl_derived")] fn load_key(path: &str) -> PKey { let path = format!("{}/{path}", super::conf_dir()); let key_bytes = fs::read(path).unwrap(); PKey::private_key_from_pem(&key_bytes).unwrap() } + +#[cfg(feature = "rustls")] +fn load_cert(path: &str) -> Vec { + let path = format!("{}/{path}", super::conf_dir()); + load_pem_file_ca(&path) +} + +#[cfg(feature = "rustls")] +fn load_key(path: &str) -> Vec { + let path = format!("{}/{path}", super::conf_dir()); + load_pem_file_private_key(&path) +} diff --git a/pingora-proxy/tests/utils/conf/keys/README.md b/pingora-proxy/tests/utils/conf/keys/README.md index 13965cd6..44944ab4 100644 --- a/pingora-proxy/tests/utils/conf/keys/README.md +++ b/pingora-proxy/tests/utils/conf/keys/README.md @@ -16,3 +16,12 @@ openssl ecparam -genkey -name secp256r1 -noout -out test_key.pem openssl req -new -key test_key.pem -out test.csr openssl x509 -req -in test.csr -CA server.crt -CAkey key.pem -CAcreateserial -CAserial test.srl -out test.crt -days 3650 -sha256 ``` + +``` +openssl version +# OpenSSL 3.1.1 +echo '[v3_req]' > openssl.cnf +openssl req -config openssl.cnf -new -x509 -key key.pem -out server_rustls.crt -days 3650 -sha256 \ + -subj '/C=US/ST=CA/L=San Francisco/O=Cloudflare, Inc/CN=openrusty.org' \ + -addext "subjectAltName=DNS:*.openrusty.org,DNS:openrusty.org,DNS:cat.com,DNS:dog.com" +``` \ No newline at end of file diff --git a/pingora-proxy/tests/utils/conf/keys/server_boringssl_openssl.crt b/pingora-proxy/tests/utils/conf/keys/server_boringssl_openssl.crt new file mode 100644 index 00000000..afb2d1e0 --- /dev/null +++ b/pingora-proxy/tests/utils/conf/keys/server_boringssl_openssl.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB9zCCAZ2gAwIBAgIUMI7aLvTxyRFCHhw57hGt4U6yupcwCgYIKoZIzj0EAwIw +ZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMRgwFgYDVQQKDA9DbG91ZGZsYXJlLCBJbmMxFjAUBgNVBAMMDW9wZW5ydXN0 +eS5vcmcwHhcNMjIwNDExMjExMzEzWhcNMzIwNDA4MjExMzEzWjBkMQswCQYDVQQG +EwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xGDAWBgNV +BAoMD0Nsb3VkZmxhcmUsIEluYzEWMBQGA1UEAwwNb3BlbnJ1c3R5Lm9yZzBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABNn/9RZtR48knaJD6tk9BdccaJfZ0hGEPn6B +SDXmlmJPhcTBqa4iUwW/ABpGvO3FpJcNWasrX2k+qZLq3g205MKjLTArMCkGA1Ud +EQQiMCCCDyoub3BlbnJ1c3R5Lm9yZ4INb3BlbnJ1c3R5Lm9yZzAKBggqhkjOPQQD +AgNIADBFAiAjISZ9aEKmobKGlT76idO740J6jPaX/hOrm41MLeg69AIhAJqKrSyz +wD/AAF5fR6tXmBqlnpQOmtxfdy13wDr4MT3h +-----END CERTIFICATE----- diff --git a/pingora-proxy/tests/utils/conf/keys/server.csr b/pingora-proxy/tests/utils/conf/keys/server_boringssl_openssl.csr similarity index 100% rename from pingora-proxy/tests/utils/conf/keys/server.csr rename to pingora-proxy/tests/utils/conf/keys/server_boringssl_openssl.csr diff --git a/pingora-proxy/tests/utils/conf/keys/server_rustls.crt b/pingora-proxy/tests/utils/conf/keys/server_rustls.crt new file mode 100644 index 00000000..28cdadff --- /dev/null +++ b/pingora-proxy/tests/utils/conf/keys/server_rustls.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICJzCCAc6gAwIBAgIUU+G0acG/uiMu1ZDSjlcoY4gH53QwCgYIKoZIzj0EAwIw +ZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNp +c2NvMRgwFgYDVQQKDA9DbG91ZGZsYXJlLCBJbmMxFjAUBgNVBAMMDW9wZW5ydXN0 +eS5vcmcwHhcNMjQwNzI0MTMzOTQ4WhcNMzQwNzIyMTMzOTQ4WjBkMQswCQYDVQQG +EwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xGDAWBgNV +BAoMD0Nsb3VkZmxhcmUsIEluYzEWMBQGA1UEAwwNb3BlbnJ1c3R5Lm9yZzBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABNn/9RZtR48knaJD6tk9BdccaJfZ0hGEPn6B +SDXmlmJPhcTBqa4iUwW/ABpGvO3FpJcNWasrX2k+qZLq3g205MKjXjBcMDsGA1Ud +EQQ0MDKCDyoub3BlbnJ1c3R5Lm9yZ4INb3BlbnJ1c3R5Lm9yZ4IHY2F0LmNvbYIH +ZG9nLmNvbTAdBgNVHQ4EFgQUnfYAFWyQnSN57IGokj7jcz8ChJQwCgYIKoZIzj0E +AwIDRwAwRAIgQr+Ly2cH04CncbnbhUf4hBl5frTp1pXgGnn8dYjd+UcCICuunEtp +H/a42/sVGBFvjS6FOFe6ZDs4oWBNEqQSw0S2 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/pingora-proxy/tests/utils/conf/origin/conf/nginx.conf b/pingora-proxy/tests/utils/conf/origin/conf/nginx.conf index 6d5abd73..2718f881 100644 --- a/pingora-proxy/tests/utils/conf/origin/conf/nginx.conf +++ b/pingora-proxy/tests/utils/conf/origin/conf/nginx.conf @@ -6,7 +6,7 @@ error_log /dev/stdout; #error_log logs/error.log notice; #error_log logs/error.log info; -pid /tmp/mock_origin.pid; +pid /tmp/pingora_mock_origin.pid; master_process off; daemon off; @@ -85,7 +85,7 @@ http { listen 8001; listen [::]:8000; #listen 8443 ssl; - listen unix:/tmp/nginx-test.sock; + listen unix:/tmp/pingora_nginx_test.sock; listen 8443 ssl http2; server_name localhost; @@ -97,6 +97,9 @@ http { # for benchmark http2_max_requests 999999; + # increase max body size for /upload/ test + client_max_body_size 128m; + #charset koi8-r; #access_log logs/host.access.log main; diff --git a/pingora-proxy/tests/utils/mock_origin.rs b/pingora-proxy/tests/utils/mock_origin.rs index db84f8df..ec59e51d 100644 --- a/pingora-proxy/tests/utils/mock_origin.rs +++ b/pingora-proxy/tests/utils/mock_origin.rs @@ -13,15 +13,38 @@ // limitations under the License. use once_cell::sync::Lazy; +use std::path::Path; use std::process; use std::{thread, time}; pub static MOCK_ORIGIN: Lazy = Lazy::new(init); fn init() -> bool { + #[cfg(feature = "rustls")] + let src_cert_path = format!( + "{}/tests/utils/conf/keys/server_rustls.crt", + env!("CARGO_MANIFEST_DIR") + ); + #[cfg(feature = "openssl_derived")] + let src_cert_path = format!( + "{}/tests/utils/conf/keys/server_boringssl_openssl.crt", + env!("CARGO_MANIFEST_DIR") + ); + + #[cfg(feature = "any_tls")] + { + let mut dst_cert_path = format!("{}/tests/keys/server.crt", env!("CARGO_MANIFEST_DIR")); + std::fs::copy(Path::new(&src_cert_path), Path::new(&dst_cert_path)); + dst_cert_path = format!( + "{}/tests/utils/conf/keys/server.crt", + env!("CARGO_MANIFEST_DIR") + ); + std::fs::copy(Path::new(&src_cert_path), Path::new(&dst_cert_path)); + } + // TODO: figure out a way to kill openresty when exiting process::Command::new("pkill") - .args(["-F", "/tmp/mock_origin.pid"]) + .args(["-F", "/tmp/pingora_mock_origin.pid"]) .spawn() .unwrap(); let _origin = thread::spawn(|| { diff --git a/pingora-proxy/tests/utils/mod.rs b/pingora-proxy/tests/utils/mod.rs index 6a5a1c99..df769e53 100644 --- a/pingora-proxy/tests/utils/mod.rs +++ b/pingora-proxy/tests/utils/mod.rs @@ -14,7 +14,9 @@ #![allow(unused)] +#[cfg(feature = "any_tls")] pub mod cert; + pub mod mock_origin; pub mod server_utils; pub mod websocket; diff --git a/pingora-proxy/tests/utils/server_utils.rs b/pingora-proxy/tests/utils/server_utils.rs index 885fcb1d..62b5882c 100644 --- a/pingora-proxy/tests/utils/server_utils.rs +++ b/pingora-proxy/tests/utils/server_utils.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(feature = "any_tls")] use super::cert; use async_trait::async_trait; use clap::Parser; @@ -32,7 +33,7 @@ use pingora_core::protocols::{l4::socket::SocketAddr, Digest}; use pingora_core::server::configuration::Opt; use pingora_core::services::Service; use pingora_core::upstreams::peer::HttpPeer; -use pingora_core::utils::CertKey; +use pingora_core::utils::tls::CertKey; use pingora_error::{Error, ErrorSource, Result}; use pingora_http::{RequestHeader, ResponseHeader}; use pingora_proxy::{ProxyHttp, Session}; @@ -106,6 +107,7 @@ fn response_filter_common( } #[async_trait] +#[cfg(feature = "any_tls")] impl ProxyHttp for ExampleProxyHttps { type CTX = CTX; fn new_ctx(&self) -> Self::CTX { @@ -283,7 +285,7 @@ impl ProxyHttp for ExampleProxyHttp { #[cfg(unix)] if req.headers.contains_key("x-uds-peer") { return Ok(Box::new(HttpPeer::new_uds( - "/tmp/nginx-test.sock", + "/tmp/pingora_nginx_test.sock", false, "".to_string(), )?)); @@ -558,27 +560,36 @@ fn test_main() { http_logic.server_options = Some(http_server_options); proxy_service_h2c.add_tcp("0.0.0.0:6146"); - let mut proxy_service_https = - pingora_proxy::http_proxy_service(&my_server.configuration, ExampleProxyHttps {}); - proxy_service_https.add_tcp("0.0.0.0:6149"); - let cert_path = format!("{}/tests/keys/server.crt", env!("CARGO_MANIFEST_DIR")); - let key_path = format!("{}/tests/keys/key.pem", env!("CARGO_MANIFEST_DIR")); - let mut tls_settings = - pingora_core::listeners::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); - tls_settings.enable_h2(); - proxy_service_https.add_tls_with_settings("0.0.0.0:6150", None, tls_settings); + let mut proxy_service_https_opt: Option> = None; + + #[cfg(feature = "any_tls")] + { + let mut proxy_service_https = + pingora_proxy::http_proxy_service(&my_server.configuration, ExampleProxyHttps {}); + proxy_service_https.add_tcp("0.0.0.0:6149"); + let cert_path = format!("{}/tests/keys/server.crt", env!("CARGO_MANIFEST_DIR")); + let key_path = format!("{}/tests/keys/key.pem", env!("CARGO_MANIFEST_DIR")); + let mut tls_settings = + pingora_core::listeners::tls::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); + tls_settings.enable_h2(); + proxy_service_https.add_tls_with_settings("0.0.0.0:6150", None, tls_settings); + proxy_service_https_opt = Some(Box::new(proxy_service_https)) + } let mut proxy_service_cache = pingora_proxy::http_proxy_service(&my_server.configuration, ExampleProxyCache {}); proxy_service_cache.add_tcp("0.0.0.0:6148"); - let services: Vec> = vec![ + let mut services: Vec> = vec![ Box::new(proxy_service_h2c), Box::new(proxy_service_http), - Box::new(proxy_service_https), Box::new(proxy_service_cache), ]; + if let Some(proxy_service_https) = proxy_service_https_opt { + services.push(proxy_service_https) + } + set_compression_dict_path("tests/headers.dict"); my_server.add_services(services); my_server.run_forever(); diff --git a/pingora-rustls/Cargo.toml b/pingora-rustls/Cargo.toml new file mode 100644 index 00000000..677cf18c --- /dev/null +++ b/pingora-rustls/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "pingora-rustls" +version = "0.3.0" +license = "Apache-2.0" +edition = "2021" +repository = "https://github.com/cloudflare/pingora" +categories = ["asynchronous", "network-programming"] +keywords = ["async", "tls", "ssl", "pingora"] +description = """ +RusTLS async APIs for Pingora. +""" + +[lib] +name = "pingora_rustls" +path = "src/lib.rs" diff --git a/pingora-rustls/LICENSE b/pingora-rustls/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/pingora-rustls/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pingora-rustls/src/lib.rs b/pingora-rustls/src/lib.rs new file mode 100644 index 00000000..814bcb90 --- /dev/null +++ b/pingora-rustls/src/lib.rs @@ -0,0 +1,17 @@ +// Copyright 2024 Cloudflare, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub fn rustls() { + todo!() +} diff --git a/pingora/Cargo.toml b/pingora/Cargo.toml index 236c2d38..4213c652 100644 --- a/pingora/Cargo.toml +++ b/pingora/Cargo.toml @@ -18,7 +18,7 @@ name = "pingora" path = "src/lib.rs" [package.metadata.docs.rs] -all-features = true +features = ["document-features"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] @@ -29,6 +29,10 @@ pingora-load-balancing = { version = "0.3.0", path = "../pingora-load-balancing" pingora-proxy = { version = "0.3.0", path = "../pingora-proxy", optional = true, default-features = false } pingora-cache = { version = "0.3.0", path = "../pingora-cache", optional = true, default-features = false } +# Only used for documenting features, but doesn't work in any other dependency +# group :( +document-features = { version = "0.2.10", optional = true } + [dev-dependencies] clap = { version = "3.2.25", features = ["derive"] } tokio = { workspace = true, features = ["rt-multi-thread", "signal"] } @@ -49,21 +53,72 @@ hyperlocal = "0.8" jemallocator = "0.5" [features] -default = ["openssl"] +default = [] + +#! ### Tls +#! Tls is provided by adding one of these features. If no tls-providing feature +#! is added, only unencrypted http. Only one tls-providing feature can be +#! selected at a time + +## Use [OpenSSL](https://crates.io/crates/openssl) for tls +## +## Requires native openssl libraries and build tooling openssl = [ "pingora-core/openssl", "pingora-proxy?/openssl", "pingora-cache?/openssl", "pingora-load-balancing?/openssl", + "openssl_derived", ] + +## Use [BoringSSL](https://crates.io/crates/boring) for tls +## +## Requires native boring libraries and build tooling boringssl = [ "pingora-core/boringssl", "pingora-proxy?/boringssl", "pingora-cache?/boringssl", "pingora-load-balancing?/boringssl", + "openssl_derived", ] + +# Coming soon +# ## Use [rustls](https://crates.io/crates/rustls) for tls +# ## +# ## ⚠️ _Highly Experimental_! ⚠️ Try it, but don't rely on it (yet) +rustls = [ + "pingora-core/rustls", + "pingora-proxy?/rustls", + "pingora-cache?/rustls", + "pingora-load-balancing?/rustls", + "any_tls", +] + +#! ### Pingora extensions + +## Include the [proxy](crate::proxy) module +## +## This feature will include and export `pingora_proxy::prelude::*` proxy = ["pingora-proxy"] + +## Include the [lb](crate::lb) (load-balancing) module +## +## This feature will include and export `pingora_load_balancing::prelude::*` lb = ["pingora-load-balancing", "proxy"] + +## Include the [cache](crate::cache) module +## +## This feature will include and export `pingora_cache::prelude::*` cache = ["pingora-cache"] + +## Enable time/scheduling functionality time = [] + +## Enable sentry for error notifications sentry = ["pingora-core/sentry"] + +# These features are intentionally not documented +openssl_derived = ["any_tls"] +any_tls = [] +patched_http1 = ["pingora-core/patched_http1"] +document-features = ["dep:document-features", "proxy", "lb", "cache", "time", "sentry"] diff --git a/pingora/examples/server.rs b/pingora/examples/server.rs index a2a092e9..ff25151d 100644 --- a/pingora/examples/server.rs +++ b/pingora/examples/server.rs @@ -15,6 +15,7 @@ #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; +use pingora::listeners::tls::TlsSettings; use pingora::protocols::TcpKeepalive; use pingora::server::configuration::Opt; use pingora::server::{Server, ShutdownWatch}; @@ -49,31 +50,35 @@ impl BackgroundService for ExampleBackgroundService { } } } +#[cfg(feature = "openssl_derived")] +mod boringssl_openssl { + use super::*; + use pingora::tls::pkey::{PKey, Private}; + use pingora::tls::x509::X509; + + pub(super) struct DynamicCert { + cert: X509, + key: PKey, + } -use pingora::tls::pkey::{PKey, Private}; -use pingora::tls::x509::X509; -struct DynamicCert { - cert: X509, - key: PKey, -} - -impl DynamicCert { - fn new(cert: &str, key: &str) -> Box { - let cert_bytes = std::fs::read(cert).unwrap(); - let cert = X509::from_pem(&cert_bytes).unwrap(); + impl DynamicCert { + pub(super) fn new(cert: &str, key: &str) -> Box { + let cert_bytes = std::fs::read(cert).unwrap(); + let cert = X509::from_pem(&cert_bytes).unwrap(); - let key_bytes = std::fs::read(key).unwrap(); - let key = PKey::private_key_from_pem(&key_bytes).unwrap(); - Box::new(DynamicCert { cert, key }) + let key_bytes = std::fs::read(key).unwrap(); + let key = PKey::private_key_from_pem(&key_bytes).unwrap(); + Box::new(DynamicCert { cert, key }) + } } -} -#[async_trait] -impl pingora::listeners::TlsAccept for DynamicCert { - async fn certificate_callback(&self, ssl: &mut pingora::tls::ssl::SslRef) { - use pingora::tls::ext; - ext::ssl_use_certificate(ssl, &self.cert).unwrap(); - ext::ssl_use_private_key(ssl, &self.key).unwrap(); + #[async_trait] + impl pingora::listeners::TlsAccept for DynamicCert { + async fn certificate_callback(&self, ssl: &mut pingora::tls::ssl::SslRef) { + use pingora::tls::ext; + ext::ssl_use_certificate(ssl, &self.cert).unwrap(); + ext::ssl_use_private_key(ssl, &self.key).unwrap(); + } } } @@ -132,12 +137,32 @@ pub fn main() { echo_service_http.add_tcp_with_settings("0.0.0.0:6145", options); echo_service_http.add_uds("/tmp/echo.sock", None); - let dynamic_cert = DynamicCert::new(&cert_path, &key_path); - let mut tls_settings = pingora::listeners::TlsSettings::with_callbacks(dynamic_cert).unwrap(); - // by default intermediate supports both TLS 1.2 and 1.3. We force to tls 1.2 just for the demo - tls_settings - .set_max_proto_version(Some(pingora::tls::ssl::SslVersion::TLS1_2)) - .unwrap(); + let mut tls_settings; + + // NOTE: dynamic certificate callback is only supported with BoringSSL/OpenSSL + #[cfg(feature = "openssl_derived")] + { + use std::ops::DerefMut; + + let dynamic_cert = boringssl_openssl::DynamicCert::new(&cert_path, &key_path); + tls_settings = TlsSettings::with_callbacks(dynamic_cert).unwrap(); + // by default intermediate supports both TLS 1.2 and 1.3. We force to tls 1.2 just for the demo + + tls_settings + .deref_mut() + .deref_mut() + .set_max_proto_version(Some(pingora::tls::ssl::SslVersion::TLS1_2)) + .unwrap(); + } + #[cfg(feature = "rustls")] + { + tls_settings = TlsSettings::intermediate(&cert_path, &key_path).unwrap(); + } + #[cfg(not(feature = "any_tls"))] + { + tls_settings = TlsSettings; + } + tls_settings.enable_h2(); echo_service_http.add_tls_with_settings("0.0.0.0:6148", None, tls_settings); diff --git a/pingora/src/lib.rs b/pingora/src/lib.rs index 9362818f..ae2516e3 100644 --- a/pingora/src/lib.rs +++ b/pingora/src/lib.rs @@ -36,12 +36,11 @@ //! //! If looking to build a (reverse) proxy, see [`pingora-proxy`](https://docs.rs/pingora-proxy) crate. //! -//! # features -//! * `openssl`: Using OpenSSL as the internal TLS backend. This feature is default on. -//! * `boringssl`: Switch the internal TLS library from OpenSSL to BoringSSL. This feature will disable `openssl`. -//! * `proxy`: This feature will include and export `pingora_proxy::prelude::*`. -//! * `lb`: This feature will include and export `pingora_load_balancing::prelude::*`. -//! * `cache`: This feature will include and export `pingora_cache::prelude::*`. +//! # Feature flags +#![cfg_attr( + feature = "document-features", + cfg_attr(doc, doc = ::document_features::document_features!()) +)] pub use pingora_core::*;