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

推荐订阅源

GbyAI
GbyAI
N
News and Events Feed by Topic
D
DataBreaches.Net
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Engineering at Meta
Engineering at Meta
T
Tailwind CSS Blog
博客园_首页
Microsoft Azure Blog
Microsoft Azure Blog
Y
Y Combinator Blog
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
月光博客
月光博客
A
About on SuperTechFans
I
InfoQ
S
Securelist
Last Week in AI
Last Week in AI
S
Schneier on Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
Hacker News: Ask HN
Hacker News: Ask HN
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
腾讯CDC
大猫的无限游戏
大猫的无限游戏
S
Security @ Cisco Blogs
博客园 - 三生石上(FineUI控件)
Simon Willison's Weblog
Simon Willison's Weblog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
美团技术团队
aimingoo的专栏
aimingoo的专栏
G
Google Developers Blog
罗磊的独立博客
Vercel News
Vercel News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Cloudflare Blog
S
Secure Thoughts
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Latest news
Latest news
Recent Announcements
Recent Announcements
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
LINUX DO - 热门话题
Security Latest
Security Latest
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队

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 Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog NAT 类型初探 | 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);

}

}

}

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