惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IT之家
IT之家
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The GitHub Blog
The GitHub Blog
MyScale Blog
MyScale Blog
爱范儿
爱范儿
GbyAI
GbyAI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
美团技术团队
Y
Y Combinator Blog
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
Martin Fowler
Martin Fowler
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
M
MIT News - Artificial intelligence
博客园 - Franky
V
Visual Studio Blog
I
InfoQ
V
V2EX
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
博客园 - 司徒正美
L
LangChain Blog

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog
Learning Pingora 05 - Connect with TLS | Yesterday17's Blog
Yesterday17 · 2024-10-11 · via Yesterday17's Blog

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

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

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

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

ToC

  • connect
  • handshake
  • SslStream

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

}

}

}

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