在最基本的 L4 连接建立完成之后,如果有需要,Pingora 就可以在此基础之开始建立 TLS 连接了。TLS 建立有两种选择:opensslboringsslPingora 分别为二者包装了 pingora-opensslpingora-boringssl 两个库。

在本文中,我们以 OpenSSL 为核心去描绘基本的连接建立流程,因此BoringSSL 相关的代码将会折叠

事先声明,这篇文章整体会比较水,因为 TLS 客户端的实现其实也没什么可说的,而且基本都是一看就懂。所以本文也不会讲解太多。

此外,对 Rustls 的支持在 #29 中讨论。

ToC

connect

TLS 部分的 connect 基本就是根据 peer 的情况做一些参数上的调整,包括但不限于 cacert、可用的 curvesecond keyshare 相关的检查与设置。在这些都配置完之后,就进入了 handshake 的流程。根据 connection_timeout 的配置,handshake 会有一个可选的超时时间。

pub(crate) async fn connect<T, P>(

stream: T,

peer: &P,

alpn_override: Option<ALPN>,

tls_ctx: &SslConnector,

) -> Result<SslStream<T>>

where

T: IO,

P: Peer + Send + Sync,

{

let mut ssl_conf = tls_ctx.configure().unwrap();

3 collapsed lines

ssl_set_renegotiate_mode_freely(&mut ssl_conf);

// Set up CA/verify cert store

// TODO: store X509Store in the peer directly

if let Some(ca_list) = peer.get_ca() {

let mut store_builder = X509StoreBuilder::new().unwrap();

for ca in &***ca_list {

store_builder.add_cert(ca.clone()).unwrap();

}

ssl_set_verify_cert_store(&mut ssl_conf, &store_builder.build())

.or_err(InternalError, "failed to load cert store")?;

}

// Set up client cert/key

if let Some(key_pair) = peer.get_client_cert_key() {

debug!("setting client cert and key");

ssl_use_certificate(&mut ssl_conf, key_pair.leaf())

.or_err(InternalError, "invalid client cert")?;

ssl_use_private_key(&mut ssl_conf, key_pair.key())

.or_err(InternalError, "invalid client key")?;

let intermediates = key_pair.intermediates();

if !intermediates.is_empty() {

debug!("adding intermediate certificates for mTLS chain");

for int in intermediates {

ssl_add_chain_cert(&mut ssl_conf, int)

.or_err(InternalError, "invalid intermediate client cert")?;

}

}

}

if let Some(curve) = peer.get_peer_options().and_then(|o| o.curves) {

ssl_set_groups_list(&mut ssl_conf, curve).or_err(InternalError, "invalid curves")?;

}

6 collapsed lines

// second_keyshare is default true

if !peer.get_peer_options().map_or(true, |o| o.second_keyshare) {

ssl_use_second_key_share(&mut ssl_conf, false);

}

// disable verification if sni does not exist

// XXX: verify on empty string cause null string seg fault

if peer.sni().is_empty() {

ssl_conf.set_use_server_name_indication(false);

/* NOTE: technically we can still verify who signs the cert but turn it off to be

consistent with nginx's behavior */

ssl_conf.set_verify(SslVerifyMode::NONE);

} else if peer.verify_cert() {

if peer.verify_hostname() {

let verify_param = ssl_conf.param_mut();

add_host(verify_param, peer.sni()).or_err(InternalError, "failed to add host")?;

// if sni had underscores in leftmost label replace and add

if let Some(sni_s) = replace_leftmost_underscore(peer.sni()) {

add_host(verify_param, sni_s.as_ref()).unwrap();

}

if let Some(alt_cn) = peer.alternative_cn() {

if !alt_cn.is_empty() {

add_host(verify_param, alt_cn).unwrap();

// if alt_cn had underscores in leftmost label replace and add

if let Some(alt_cn_s) = replace_leftmost_underscore(alt_cn) {

add_host(verify_param, alt_cn_s.as_ref()).unwrap();

}

}

}

}

ssl_conf.set_verify(SslVerifyMode::PEER);

} else {

ssl_conf.set_verify(SslVerifyMode::NONE);

}

/*

We always set set_verify_hostname(false) here because:

- verify case.) otherwise ssl.connect calls X509_VERIFY_PARAM_set1_host

which overrides the names added by add_host. Verify is

essentially on as long as the names are added.

- off case.) the non verify hostname case should have it disabled

*/

ssl_conf.set_verify_hostname(false);

if let Some(alpn) = alpn_override.as_ref().or(peer.get_alpn()) {

ssl_conf.set_alpn_protos(alpn.to_wire_preference()).unwrap();

}

clear_error_stack();

let connect_future = handshake(ssl_conf, peer.sni(), stream);

match peer.connection_timeout() {

Some(t) => match pingora_timeout::timeout(t, connect_future).await {

Ok(res) => res,

Err(_) => Error::e_explain(

ConnectTimedout,

format!("connecting to server {}, timeout {:?}", peer, t),

),

},

None => connect_future.await,

}

}

handshake

connect 相比,handshake 能讲的其实更少了。主要的内容就是两个:36 行的 SslSteram::new38 行的 connect

其中,SslStreampingora 对不同 SSL 实现的抽象这个我们接下来马上会讲到;而 connect 则是直接调用了各种 SSL 实现的 connect 方法,比如 openssl 就是调用了 tokio-opensslSslStream::connect

其他本身大部分都是在做错误处理,这里就不多赘述了。

/// Perform the TLS handshake for the given connection with the given configuration

pub async fn handshake<S: IO>(

conn_config: ConnectConfiguration,

domain: &str,

io: S,

) -> Result<SslStream<S>> {

let ssl = conn_config

.into_ssl(domain)

.explain_err(TLSHandshakeFailure, |e| format!("ssl config error: {e}"))?;

let mut stream = SslStream::new(ssl, io)

.explain_err(TLSHandshakeFailure, |e| format!("ssl stream error: {e}"))?;

let handshake_result = stream.connect().await;

match handshake_result {

33 collapsed lines

Ok(()) => Ok(stream),

Err(e) => {

let context = format!("TLS connect() failed: {e}, SNI: {domain}");

match e.code() {

ssl::ErrorCode::SSL => {

// Unify the return type of `verify_result` for openssl

#[cfg(not(feature = "boringssl"))]

fn verify_result<S>(stream: SslStream<S>) -> Result<(), i32> {

match stream.ssl().verify_result().as_raw() {

crate::tls::ssl_sys::X509_V_OK => Ok(()),

e => Err(e),

}

}

// Unify the return type of `verify_result` for boringssl

#[cfg(feature = "boringssl")]

fn verify_result<S>(stream: SslStream<S>) -> Result<(), i32> {

stream.ssl().verify_result().map_err(|e| e.as_raw())

}

match verify_result(stream) {

Ok(()) => Error::e_explain(TLSHandshakeFailure, context),

// X509_V_ERR_INVALID_CALL in case verify result was never set

Err(X509_V_ERR_INVALID_CALL) => {

Error::e_explain(TLSHandshakeFailure, context)

}

_ => Error::e_explain(InvalidCert, context),

}

}

/* likely network error, but still mark as TLS error */

_ => Error::e_explain(TLSHandshakeFailure, context),

}

}

}

}

SslStream

先来看定义吧。这个 struct 里定义了:

  • ssl: 实际的工作部分。这里的 InnerSsl 实际就是 tokio_ssl::SslStream
  • digest: TLS 连接的相关信息,包括 cipher、版本、证书等信息。
  • timing: 用于记录一些时间,当前版本只记录了连接的建立时间。

/// The TLS connection

#[derive(Debug)]

pub struct SslStream<T> {

ssl: InnerSsl<T>,

digest: Option<Arc<SslDigest>>,

timing: TimingDigest,

}

来看主要的部分。可以看到,SslStreamT 的要求是 AsyncRead + AsyncWrite + Unpin。由于 SslStream 本身需要 Async Readable/Writable,所以自然其内部承载的流也需要具备该特性。初次之外就都是对 self.ssl 的一般包装,看看就好(

impl<T> SslStream<T>

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<Self> {

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);

}

}

}

嘛,看到这里基本都看完了,很神奇吧)