Rust in Everything
基于 rustls 0.23.46

本站的 rustls

主线位置:握手主线的第一步,ClientHello 发出之前。本站服务端里有三个 HTTP / 数据库 / 邮件客户端,本页看每个客户端在构建 ClientConfig 时拿的是哪个 TLS 库、哪个 CryptoProvider、哪套根证书,以及它们发出的 ClientHello 长什么样。

flowchart TB
  ws["工作区 Cargo.toml"] --> rq["reqwest 0.12.28"]
  rq --> pick["TlsBackend default"]
  pick --> nt["native-tls"]
  nt --> ossl["Linux OpenSSL"]
  nt --> sf["macOS SecFramework"]
  rq -.-> rr["rustls ring"]
  sea["sea-orm"] --> sq["sqlx 0.8.6"]
  sq --> rr
  modr["module-moderation"] --> le["lettre 0.11.23"]
  le --> rr
  rr --> roots["webpki-roots"]

本页说的「本站」是框架仓库 main 分支的提交 363dd8f。发布镜像由 release.yml 用 APP_FEATURES=--no-default-features --features accounts 构建,dx 再给服务端加上 server。

实验有两个目录,都在 exp-rustls/ 下(M2 Max、macOS 27、rustc 1.99.0):

  • site-mirror:用 git archive 把 main 的 Cargo.toml、Cargo.lock 和 crates/ 原样导出,只在这份拷贝上跑 cargo tree --offline --locked -p app --no-default-features --features server,accounts --target x86_64-unknown-linux-gnu。--locked 通过,说明锁文件没被改动;--target 让 cargo 按生产的 Linux 平台算依赖。
  • site-lab:一个镜像 crate,按站点的写法声明 reqwest(json 加 rustls-tls,默认 feature 不关)、lettre(和 module-moderation 一字不差)、sqlx(runtime-tokio 加 tls-rustls-ring-webpki),锁文件从站点拷来,解析出的 reqwest 0.12.28、hyper-rustls 0.27.9、lettre 0.11.23、sqlx-core 0.8.6、rustls 0.23.46、ring 0.17.14 都和站点一致。服务端是 tokio-rustls 加 ring,证书用 rcgen 现造(一个私有 CA 和一张 localhost 叶子证书),全部在 127.0.0.1 上。服务端在把字节交给 rustls 之前先读出第一条记录,按 RFC 8446 的格式解析 ClientHello,所以不管客户端用的是哪个 TLS 库,看到的都是线上的原始字节。

谁把 rustls 带进来

cargo tree -e normal -i rustls 列出的直接依赖方:

rustls v0.23.46
|-- hyper-rustls v0.27.9
|   `-- reqwest v0.12.28
|-- lettre v0.11.23
|   `-- module-moderation v0.1.0 (.../crates/modules/moderation)
|-- reqwest v0.12.28 (*)
|-- sqlx-core v0.8.6
|   |-- sqlx v0.8.6
|   `-- sqlx-postgres v0.8.6
`-- tokio-rustls v0.26.4
    |-- hyper-rustls v0.27.9 (*)
    |-- lettre v0.11.23 (*)
    `-- reqwest v0.12.28 (*)

打开了哪些 feature、是谁打开的(-e features -i rustls --depth 3,截取):

|-- rustls feature "ring"
|   |-- reqwest feature "__rustls-ring" (*)
|   |-- hyper-rustls feature "ring" (*)
|   |-- tokio-rustls feature "ring"
|   |   `-- reqwest feature "__rustls-ring" (*)
|   |-- sqlx-core feature "_tls-rustls-ring-webpki"
|   |   `-- sqlx feature "tls-rustls-ring-webpki"
|   `-- lettre feature "ring"
|       `-- lettre feature "rustls-tls"
|-- rustls feature "std"
...
`-- rustls feature "tls12"

一共五个 feature:ring、std、tls12、logging、log。没有 aws_lc_rs,也没有 prefer-post-quantum:sqlx-core、lettre、reqwest、hyper-rustls、tokio-rustls 依赖 rustls 时都写了 default-features = false,provider 由各自的 feature 打开,三条路最后都落到 ring。cargo tree -i aws-lc-rs 报「did not match any packages」,锁文件里也没有 aws-lc-rs 和 rustls-native-certs。

依赖站点里的声明打开的 TLS featureprovider根证书
sqlx 0.8.6工作区 sea-orm 的 runtime-tokio-rustlstls-rustls-ring-webpki写死 ringwebpki-roots 0.26.11(转发到 1.0.8)
lettre 0.11.23module-moderation 的 tokio1-rustls-tlsrustls-tls → ring + webpki-roots进程默认值,没有就 ringwebpki-roots 1.0.8
reqwest 0.12.28工作区 reqwest(默认 feature)、crates/llm 的 rustls-tlsdefault-tls 和 rustls-tls 同时开走 rustls 时:进程默认值,没有就 ring走 rustls 时 webpki-roots 1.0.8

Cargo.lock 本身记的是所有平台、工作区所有 feature 组合的并集,里面 quinn、tungstenite 0.28 也挂着 rustls;在 Linux 服务端这次构建里它们要么不出现,要么没开 TLS,所以看「实际编进来什么」要用带 --target 和 feature 的 cargo tree,不能直接读锁文件。

reqwest:两种 TLS 都编进来,请求走 native-tls

reqwest 的 feature 是谁打开的(-e features -i reqwest,截取):

|-- reqwest feature "__tls"
|   |-- reqwest feature "__rustls" (*)
|   `-- reqwest feature "default-tls"
|       `-- reqwest feature "default"
|           |-- app-core v0.1.0 (.../crates/core) (*)
|           `-- llm v0.1.0 (.../crates/llm)
...
|-- reqwest feature "rustls-tls"
|   |-- dioxus-fullstack v0.7.9 (*)
|   `-- llm v0.1.0 (.../crates/llm) (*)
|   `-- oauth2 feature "rustls-tls" (*)

default-tls 来自工作区那行 reqwest = { version = "0.12", features = ["json"] }:没写 default-features = false,reqwest 0.12 的 default 里就有 default-tls,也就是 native-tls。rustls-tls 有三处来源:crates/llm 显式加的、dioxus-fullstack 0.7.9 自己声明的(它关了 reqwest 的默认 feature,TLS 只要 rustls-tls)、oauth2 5.0.0 的默认 feature rustls-tls。cargo 的 feature 是并集,所以最后两种后端都编进同一个 reqwest。

两个都在时用哪个,由 TlsBackend 的 Default 决定:

impl Default for TlsBackend {
    fn default() -> TlsBackend {
        #[cfg(all(feature = "default-tls", not(feature = "http3")))]
        {
            TlsBackend::Default
        }

        #[cfg(any(
            all(feature = "__rustls", not(feature = "default-tls")),
            feature = "http3"
        ))]
        {
            TlsBackend::Rustls
        }
    }
}

只要 default-tls 开着(且没开 http3),默认就是 TlsBackend::Default,build() 在 TlsBackend::Default => 分支里建 native_tls::TlsConnector。rustls 只在调了 use_rustls_tls() 时才用。站点建 reqwest 客户端的四处都是 Client::builder() 加超时再 build(),没有一处调 use_rustls_tls():

实测:同一个 reqwest,两种 ClientHello

site-lab 的客户端和站点一样只调 Client::builder(),额外加 add_root_certificate(私有 CA)(两个后端都认这个 API)和 no_proxy();对照组多调一次 use_rustls_tls()。服务端是 rustls,RUST_LOG=rustls::client=debug 只打印客户端一侧 rustls 的日志:

let ca = reqwest::Certificate::from_pem(pki.ca_pem.as_bytes()).unwrap();
let mut b = reqwest::Client::builder().no_proxy().add_root_certificate(ca).tls_info(true);
if mode == "rustls" {
  b = b.use_rustls_tls();
}
let r = b.build().unwrap().get(format!("https://localhost:{port}/")).send().await;
######## reqwest default
[client] reqwest builder mode=default
[server] ClientHello record 169 B, 22 cipher suites, 7 extensions
  sni=localhost alpn=[]
  supported_versions=[]
  supported_groups=["secp256r1", "secp384r1", "secp521r1"]
  key_share=[]
  first suites=[00ff, c02c, c02b, c024, c023, c00a]
[server] server negotiated: Some(TLSv1_2), Some(TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384), kx=Some(secp256r1), alpn=None
[client] status=200 OK version=HTTP/1.1 tls_info peer_cert_len=Some(Some(362))
[client] body="hello from rustls server\n"
######## reqwest rustls
[client] reqwest builder mode=rustls
[2026-10-29T17:34:31Z DEBUG rustls::client::hs] No cached session for DnsName("localhost")
[2026-10-29T17:34:31Z DEBUG rustls::client::hs] Not resuming any session
[server] ClientHello record 254 B, 10 cipher suites, 11 extensions
  sni=localhost alpn=["h2", "http/1.1"]
  supported_versions=["0x0304", "0x0303"]
  supported_groups=["X25519", "secp256r1", "secp384r1"]
  key_share=["X25519(32B)"]
  first suites=[1302, 1301, 1303, c02c, c02b, cca9]
[2026-10-29T17:34:31Z DEBUG rustls::client::hs] Using ciphersuite TLS13_AES_256_GCM_SHA384
[2026-10-29T17:34:31Z DEBUG rustls::client::tls13] Not resuming
...
[server] server negotiated: Some(TLSv1_3), Some(TLS13_AES_256_GCM_SHA384), kx=Some(X25519), alpn=Some("http/1.1")
[client] status=200 OK version=HTTP/1.1 tls_info peer_cert_len=Some(Some(362))
[client] body="hello from rustls server\n"

默认那一组没有一行 rustls::client 日志,ClientHello 也不是 rustls 能发出来的:没有 supported_versions 扩展(只会 TLS 1.2),22 个套件里有 CBC 套件(c024、c023、c00a),没有 X25519,没有 ALPN。这是 macOS 上 native-tls 的后端 Security.framework。native-tls 0.2.18 在 Apple 平台上用的 Secure Transport 接口只到 TLS 1.2,它的 imp/security_framework.rs 在调用方把最高版本设成 TLS 1.3 时也改写成 1.2,注释是「use the latest that is actually supported - 1.2」。rustls 那一组是 一次握手 里那个 236 字节的 ring ClientHello,多了 18 字节的 ALPN 扩展(h2、http/1.1),所以是 254 字节。

第一次跑时叶子证书没有扩展密钥用途(EKU),默认那一组直接失败,错误码也只有 Security.framework 会给:

[client] error: reqwest::Error { kind: Request, url: "https://localhost:59164/", source: hyper_util::client::legacy::Error(Connect, Error { code: -67609, message: "The extended key usage is not valid." }) }

同一张证书,rustls 那一组握手成功:rustls-webpki 的 KeyUsage::server_auth 是 required_if_present,证书没有 EKU 扩展就不查,注释引的是 RFC 5280 第 4.2.1.12 节。证书里加上 serverAuth 之后才有上面两组都成功的输出。

生产服务器是 Linux,native-tls 在非 Apple、非 Windows 平台上编译的是 imp/openssl.rs,即 OpenSSL;锁文件里没有 openssl-src,openssl-sys 只开了 default,所以是动态链接系统的 libssl。根证书走 SslConnector::builder 里的 set_default_verify_paths(),也就是系统证书目录,镜像的 Dockerfile 装的 ca-certificates 就是给它用的。Linux 上的 ClientHello 没有实测(本卷只有 macOS),按 OpenSSL 的默认配置会提议 TLS 1.3。两个平台相同的一点是没有 ALPN:reqwest 只在开了 native-tls-alpn 时才给 native-tls 设 ALPN,站点没开,所以这四处客户端即使对端支持 h2,也只会说 HTTP/1.1。

📝NOTE
没有蠢问题

问: crates/llm 的 Cargo.toml 专门写了 features = ["json", "rustls-tls"],为什么它的请求还是 native-tls?想强制走 native-tls 又该怎么写?

答: feature 只决定「编进来什么」,不决定「用哪个」。reqwest 在 build() 时按 TlsBackend::default() 选,default-tls 一编进来就优先,rustls-tls 写在哪个 crate 里都一样。反过来,想显式选 native-tls 的 use_native_tls() 要求的是 native-tls feature,不是 default-tls;站点的组合下这个方法根本不存在:

error[E0599]: no method named `use_native_tls` found for struct `ClientBuilder` in the current scope
 --> src/bin/native.rs:3:39
  |
3 |   let _c = reqwest::Client::builder().use_native_tls().build();
  |                                       ^^^^^^^^^^^^^^ method not found in `ClientBuilder`

要走 rustls,只能在每个客户端上调 use_rustls_tls(),或者在所有声明 reqwest 的地方去掉默认 feature。

进程级默认 provider:没人装,也没人要

进程级默认值 只在两种情况下被读:有人调 install_default,或者有人走了不带 provider 的入口(ClientConfig::builder()、ServerConfig::builder()、WebPkiServerVerifier::builder() 等),由 get_default_or_install_from_crate_features 按 feature 推断一个装上。站点服务端里这三个库都没走这两条路,它们各自显式拿 provider 再调 builder_with_provider:

  • reqwest 在 rustls 分支里 CryptoProvider::get_default(),没有就 rustls::crypto::ring::default_provider()(因为 __rustls-ring 开着;没开时这里是 panic!("No provider set"))。
  • lettre 的 src/rustls_crypto.rs 是同一个写法:get_default(),没有就按 lettre 自己的 aws-lc-rs / ring feature 选一个,不看 rustls 的 feature。
  • sqlx-core 的 handshake 不看默认值,按自己的 feature 写死 Arc::new(rustls::crypto::ring::default_provider())。

注意 get_default 只是读 OnceLock,不会推断也不会安装。hyper-rustls 里倒是有 ClientConfig::builder(),但都在 with_webpki_roots()、with_native_roots() 这类便捷构造器和测试里,reqwest 是把自己建好的 ClientConfig 直接交给 HttpsConnector::from,不经过它们。站点代码里 git grep 不到 CryptoProvider、install_default 或 rustls::。

site-lab 的 provider 子命令依次建 reqwest(rustls 和默认两种)、lettre 的 relay(),每步之后看一眼进程默认值,最后才调一次 ClientConfig::builder():

start:                         get_default = None
after reqwest use_rustls_tls:  get_default = None
after reqwest default backend: get_default = None
after lettre relay():          get_default = None
calling rustls::ClientConfig::builder() ...
after ClientConfig::builder(): get_default = Some(kx=["X25519", "secp256r1", "secp384r1"])

三个库用完,进程默认值还是空的。第一次 builder() 才按 feature 推断出 ring(ring provider 的三个组 X25519、P-256、P-384)并装上。站点今天不 panic,是因为只开了一个 provider;没人调 builder() 是另一层保险。

如果哪天某个依赖打开了 rustls 的默认 feature

rustls 的默认 feature 含 aws_lc_rs 和 prefer-post-quantum。只要依赖图里有一个 crate 写了 rustls = "0.23" 而没关默认 feature,并集就变成 ring 加 aws-lc-rs 两个 provider。site-lab 用一个 rustls-defaults = ["rustls/default"] 的 feature 模拟这件事,cargo tree 里 rustls 多出 aws-lc-rs、aws_lc_rs、default、prefer-post-quantum 四项。同一个 provider 子命令:

start:                         get_default = None
after reqwest use_rustls_tls:  get_default = None
after reqwest default backend: get_default = None
after lettre relay():          get_default = None
calling rustls::ClientConfig::builder() ...

thread 'main' (7580126) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-0.23.46/src/crypto/mod.rs:249:14:

Could not automatically determine the process-level CryptoProvider from Rustls crate features.
Call CryptoProvider::install_default() before this point to select a provider manually, or make sure exactly one of the 'aws-lc-rs' and 'ring' features is enabled.
See the documentation of the CryptoProvider type for more information.
            
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101

三个库照常工作,同一个二进制再跑一遍各自的握手:

######## --features rustls-defaults: reqwest rustls
[server] ClientHello record 254 B, 10 cipher suites, 11 extensions
  key_share=["X25519(32B)"]
[client] status=200 OK version=HTTP/1.1 tls_info peer_cert_len=Some(Some(361))
######## --features rustls-defaults: sqlx prefer
[server] ClientHello record 236 B, 10 cipher suites, 10 extensions
  key_share=["X25519(32B)"]
[client] error: error returned from database: site-lab: stopping after TLS
######## --features rustls-defaults: lettre
[server] ClientHello record 236 B, 10 cipher suites, 10 extensions
  key_share=["X25519(32B)"]

(输出按行截取,sqlx 那一行错误是假 Postgres 故意回的,见下一节。)所以在站点今天的依赖组合里,「多编进一个 provider」本身不会让任何请求失败;会 panic 的只有走进程默认值的入口。将来引入一个在内部调 ClientConfig::builder() 的依赖,或者站点自己写了这样的代码,panic 会出现在第一次建配置的时候,信息就是上面那三行,修法是 main 开头 install_default 一个 provider。

❗IMPORTANT
动脑筋:在 main 开头装上 aws-lc-rs,三个 ClientHello 各多长?

还是 --features rustls-defaults 的 site-lab,在 main 的第一行加上:

rustls::crypto::aws_lc_rs::default_provider().install_default().unwrap();

然后分别跑 reqwest(use_rustls_tls)、sqlx(sslmode=prefer)、lettre(relay)。不改各库的任何 feature,服务端还是只开 ring。三个 ClientHello 记录各多少字节?答案在文末。

sqlx 和 lettre 发出的 ClientHello

这两个才是站点里真正跑 rustls 握手的客户端。实验服务端各模仿一种协议:

  • 假 Postgres:读 8 字节,确认是 SSLRequest(00000008 04d2162f),回一个 S,接着做 TLS;握手后读 StartupMessage,回一个 ErrorResponse 让客户端停下。
  • 假 SMTPS:TCP 一接上就做 TLS(对应站点 notify.rs 在端口 465 时用的 relay(),即隐式 TLS;其他端口用 starttls_relay(),先明文再 STARTTLS,TLS 配置相同)。
######## sqlx prefer
[client] sqlx PgConnection::connect sslmode=prefer
[pg] first 8 bytes: [00, 00, 00, 08, 04, d2, 16, 2f] (SSLRequest = 00000008 04d2162f)
[2026-10-29T17:35:01Z DEBUG rustls::client::hs] No cached session for DnsName("localhost")
[2026-10-29T17:35:01Z DEBUG rustls::client::hs] Not resuming any session
[server] ClientHello record 236 B, 10 cipher suites, 10 extensions
  sni=localhost alpn=[]
  supported_versions=["0x0304", "0x0303"]
  supported_groups=["X25519", "secp256r1", "secp384r1"]
  key_share=["X25519(32B)"]
  first suites=[1302, 1301, 1303, c02c, c02b, cca9]
[2026-10-29T17:35:01Z DEBUG rustls::client::hs] Using ciphersuite TLS13_AES_256_GCM_SHA384
...
[server] server negotiated: Some(TLSv1_3), Some(TLS13_AES_256_GCM_SHA384), kx=Some(X25519), alpn=None
[pg] StartupMessage over TLS: ["user", "rie", "database", "rie", "DateStyle", "ISO, MDY", "client_encoding", "UTF8", "TimeZone", "UTC", "extra_float_digits", "2"]
[client] error: error returned from database: site-lab: stopping after TLS
...
######## sqlx verify-full
...
[client] error: error communicating with database: invalid peer certificate: UnknownIssuer
[server] handshake failed: tls handshake eof
######## lettre
[client] lettre relay(localhost) port=59290
...
[server] ClientHello record 236 B, 10 cipher suites, 10 extensions
  sni=localhost alpn=[]
  supported_versions=["0x0304", "0x0303"]
  supported_groups=["X25519", "secp256r1", "secp384r1"]
  key_share=["X25519(32B)"]
  first suites=[1302, 1301, 1303, c02c, c02b, cca9]
...
[client] error: Connection error: Connection error: invalid peer certificate: UnknownIssuer / lettre::transport::smtp::Error { kind: Connection, source: lettre::transport::smtp::Error { kind: Connection, source: Custom { kind: InvalidData, error: InvalidCertificate(UnknownIssuer) } } }
[server] handshake failed: received fatal alert: UnknownCA

两个 ClientHello 一模一样,都是 客户端 TLS 1.3 逐字节拆过的那个默认 ring ClientHello:236 字节,9 个套件加 TLS_EMPTY_RENEGOTIATION_INFO_SCSV,提议 TLS 1.3 和 1.2,三个组但只给第一个 X25519 算了 32 字节的密钥共享,不带 ALPN。236 是 SNI 为 localhost(9 个字符)时的长度,SNI 每多一个字符 ClientHello 多一个字节;生产里 sqlx 的 SNI 是 compose 里的主机名 postgres,lettre 的是 SMTP 服务器的域名。服务端不支持 X25519 时会多一轮 HelloRetryRequest,见套件与密钥交换。

两边都是 UnknownIssuer,因为私有 CA 不在它们的根证书集合里。这个集合是编进二进制的 webpki-roots:lettre 在 CertificateStore::Default 下,没开 rustls-native-certs 时调 load_webpki_roots;sqlx 的 certs_from_webpki 用的 webpki-roots 0.26.11 只是转发到 1.0.8 的兼容壳。webpki-roots 1.0.8 的 TLS_SERVER_ROOTS 是从 Mozilla 的 CCADB 报告生成的 118 个信任锚。镜像里 ca-certificates 装的系统证书,这两个库都不读;要换根证书集合,得升级依赖重新构建。收到的证书链怎么对着这 118 个锚建路径,见服务器证书校验。

sqlx 的 prefer 那一组握手成功了,用的还是同一张私有 CA 签的证书:prefer 和 require 下 sqlx-postgres 把 accept_invalid_certs 设成 true,装的是不做校验的 DummyTlsVerifier,只有 verify-ca / verify-full 才用上面的根证书。这一点 sqlx 卷的 sslmode=require 不校验服务端证书 有完整的实验。站点 compose 里的 DATABASE_URL 没写 sslmode,是默认的 prefer;Postgres 回 N 时 sqlx 在同一条连接上明文继续,rustls 根本不出场:

[client] sqlx PgConnection::connect sslmode=prefer
[pg] first 8 bytes: [00, 00, 00, 08, 04, d2, 16, 2f] (SSLRequest = 00000008 04d2162f)
[pg] answered 'N'; next 64 bytes in cleartext: "\0\0\0i\0\u{3}\0\0user\0rie\0database\0rie\0DateStyle\0ISO, MDY\0client_encoding"
[client] error: error communicating with database: Connection reset by peer (os error 54)

生产的 Postgres 开没开 ssl,本页没有核实(sqlx 卷的本站页记录了同款 postgres:16-alpine 实验库上是 off)。lettre 则不同:relay() 和 starttls_relay() 都是 Tls::Wrapper / Tls::Required,握手失败就不发信,站点配置了 SMTP 时,给管理员的待审通知邮件一定经过 rustls。

网关:OpenSSL,不在本卷

公网 443 端口不是 rustls 终止的。网关 crates/gateway 是单独的 workspace,Cargo.toml 写的是 pingora = { version = "0.8", features = ["lb", "openssl"] },锁文件里没有 rustls。打开 openssl 的提交 f3011f7 说明了原因:不选 TLS 后端时 pingora 编译的是 noop 模块,「every handshake on :443 panics with "No tls feature was specified"」,而 Dockerfile 早就装了 libssl-dev,「so openssl is the intended backend」。pingora 的 openssl 后端经 pingora-openssl 用 vendored 把 OpenSSL 3.6.3 的源码编进二进制,不用系统 libssl,这和 app 里 native-tls 动态链接系统 OpenSSL 正好相反。pingora 怎么用一组同名模块在 openssl 和 rustls 之间切换,见 pingora 卷的 TLS;网关的配置和实测见本站的网关。

动脑筋答案

######## --features rustls-defaults, INSTALL_AWS=1: reqwest rustls
installed aws-lc-rs as process default: true
[client] reqwest builder mode=rustls
[server] ClientHello record 1484 B, 10 cipher suites, 11 extensions
  key_share=["X25519MLKEM768(1216B)", "X25519(32B)"]
[server] server negotiated: Some(TLSv1_3), Some(TLS13_AES_256_GCM_SHA384), kx=Some(X25519), alpn=Some("http/1.1")
[client] status=200 OK version=HTTP/1.1 tls_info peer_cert_len=Some(Some(363))
[client] body="hello from rustls server\n"
######## --features rustls-defaults, INSTALL_AWS=1: sqlx prefer
installed aws-lc-rs as process default: true
[client] sqlx PgConnection::connect sslmode=prefer
[server] ClientHello record 236 B, 10 cipher suites, 10 extensions
  key_share=["X25519(32B)"]
[server] server negotiated: Some(TLSv1_3), Some(TLS13_AES_256_GCM_SHA384), kx=Some(X25519), alpn=None
[client] error: error returned from database: site-lab: stopping after TLS
######## --features rustls-defaults, INSTALL_AWS=1: lettre
installed aws-lc-rs as process default: true
[client] lettre relay(localhost) port=59481
[server] ClientHello record 1466 B, 10 cipher suites, 10 extensions
  key_share=["X25519MLKEM768(1216B)", "X25519(32B)"]
[client] error: Connection error: Connection error: invalid peer certificate: UnknownIssuer / lettre::transport::smtp::Error { kind: Connection, source: lettre::transport::smtp::Error { kind: Connection, source: Custom { kind: InvalidData, error: InvalidCertificate(UnknownIssuer) } } }

reqwest 1484 字节,lettre 1466 字节,sqlx 236 字节不变。reqwest 和 lettre 都先问 get_default(),装了 aws-lc-rs 就用它,于是带上 prefer-post-quantum 排在第一位的 X25519MLKEM768(1216 字节的共享)和顺带的 X25519 共享,长度就是 客户端 TLS 1.3 里 aws-lc-rs 默认 ClientHello 的 1466 字节;reqwest 再加 18 字节 ALPN。sqlx 不看进程默认值,仍是 ring 的 236 字节。服务端只有 ring,不认识 X25519MLKEM768,直接选了客户端顺带发的 X25519,没有多一轮;这一次 lettre 和 reqwest 发出的 1216 字节混合共享都没用上。

小结

  • 站点服务端编进来的 rustls 只有 ring、std、tls12、logging;锁文件里没有 aws-lc-rs 和 rustls-native-certs。
  • reqwest 0.12.28 同时编进 native-tls(工作区声明没关默认 feature)和 rustls(llm、dioxus-fullstack、oauth2 打开);TlsBackend::default() 在 default-tls 存在时选 native-tls,站点四处 Client::builder().build() 都走它:本机是 Security.framework(只到 TLS 1.2),Linux 是动态链接的系统 OpenSSL,根证书来自系统目录,没有 ALPN。
  • 真正跑 rustls 握手的是 sqlx 和 lettre:ClientHello 236 字节(SNI 为 localhost 时),只有一个 X25519 共享;根证书是编进去的 webpki-roots 1.0.8(118 个锚)。sqlx 在 prefer / require 下不校验证书,服务端回 N 时干脆不用 TLS。
  • 进程里没有装默认 provider,三个库都不需要它:reqwest、lettre 读默认值、没有就用 ring,sqlx 写死 ring。打开 rustls 默认 feature 后它们照常工作,只有 ClientConfig::builder() 这类入口会 panic。
  • 网关用 pingora 的 openssl 后端,OpenSSL 3.6.3 静态编进二进制,和 rustls 无关。

💬 关联讨论

发起讨论
加载中...