本站的 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 feature | provider | 根证书 |
|---|---|---|---|---|
| sqlx 0.8.6 | 工作区 sea-orm 的 runtime-tokio-rustls | tls-rustls-ring-webpki | 写死 ring | webpki-roots 0.26.11(转发到 1.0.8) |
| lettre 0.11.23 | module-moderation 的 tokio1-rustls-tls | rustls-tls → ring + webpki-roots | 进程默认值,没有就 ring | webpki-roots 1.0.8 |
| reqwest 0.12.28 | 工作区 reqwest(默认 feature)、crates/llm 的 rustls-tls | default-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():
- 登录时换 token 的
crates/core/src/auth/mod.rs; crates/llm的 Anthropic 和 OpenAI 客户端;- 支付网关请求
crates/modules/payment/src/host.rs(发布镜像的accounts档不含它)。
实测:同一个 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。
问: 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/ringfeature 选一个,不看 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。
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 无关。