连接选项与建立连接
主线位置:连接池主线的
connect那一步。池子没有空闲连接、又没到max_connections时,调PgConnectOptions::connect,也就是本页的PgConnection::establish;PgConnection::connect(url)直接走的也是它。查询主线上用到的每一条PgConnection,都是从这里造出来的。本页讲两件事:选项从哪来,以及从 TCP 连上到ReadyForQuery之间线上发生了什么。
sequenceDiagram participant C as PgConnection participant S as Postgres C->>S: SSLRequest(prefer 时) S-->>C: 一个字节 N C->>S: StartupMessage S-->>C: AuthenticationSASL C->>S: SASLInitialResponse S-->>C: SASLContinue 盐和次数 C->>S: SASLResponse 带证明 S-->>C: SASLFinal 服务端签名 S-->>C: AuthenticationOk S-->>C: ParameterStatus 若干 S-->>C: BackendKeyData S-->>C: ReadyForQuery
PgConnectOptions 的字段都在 options/mod.rs:主机、端口、Unix socket 目录、用户名、密码、库名、ssl_mode 和三个证书、statement_cache_capacity、application_name、日志设置、extra_float_digits、options(启动参数)。设置字段的 pub fn 都是按值收 self 再返回 Self 的 builder,另有八个 get_* 方法读回其中一部分(主机、端口、socket、用户、库、ssl_mode、application_name、options),没有读密码的。
选项从哪来
第一层:环境变量
new_without_pgpass 是一切的起点,它只读环境变量:
pub fn new_without_pgpass() -> Self {
let port = var("PGPORT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5432);
let host = var("PGHOSTADDR")
.ok()
.or_else(|| var("PGHOST").ok())
.unwrap_or_else(|| default_host(port));
let username = if let Ok(username) = var("PGUSER") {
username
} else if let Ok(username) = whoami::username() {
username
} else {
// keep the same fallback as previous version
"unknown".to_string()
};
let database = var("PGDATABASE").ok();
PgConnectOptions {
port,
host,
socket: None,
username,
password: var("PGPASSWORD").ok(),
// ...
ssl_mode: var("PGSSLMODE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or_default(),
statement_cache_capacity: 100,
application_name: var("PGAPPNAME").ok(),
extra_float_digits: Some("2".into()),
log_settings: Default::default(),
options: var("PGOPTIONS").ok(),
}
}
| 字段 | 环境变量 | 没有时 |
|---|---|---|
host | PGHOSTADDR,其次 PGHOST | default_host:依次看 /var/run/postgresql、/private/tmp、/tmp 下有没有 .s.PGSQL.<端口>,有就用那个目录,否则 localhost |
port | PGPORT | 5432 |
username | PGUSER | 当前操作系统用户名 |
password | PGPASSWORD | 无 |
database | PGDATABASE | 无(服务端按用户名选库) |
ssl_mode | PGSSLMODE | Prefer |
| 三个证书 | PGSSLROOTCERT、PGSSLCERT、PGSSLKEY | 无 |
application_name | PGAPPNAME | 无 |
options | PGOPTIONS | 无 |
statement_cache_capacity 和 extra_float_digits 没有环境变量。环境变量的值解析失败(PGPORT=abc、PGSSLMODE=xyz)时一声不吭地用默认值。PgConnectOptions::new() 和 Default 是 new_without_pgpass().apply_pgpass()。
第二层:URL
"postgres://...".parse() 走 parse_from_url,它的第一行是:
let mut options = Self::new_without_pgpass();
也就是说,URL 不是从空白开始,而是覆盖在环境变量之上:URL 里写了的字段用 URL 的,没写的仍然是环境变量给的。路径是库名,用户名和密码做百分号解码,主机名以 /(URL 里写成 %2F)开头时当作 Unix socket 目录。查询参数认 sslmode、sslrootcert、sslcert、sslkey、statement-cache-capacity、host、hostaddr、port、dbname、user、password、application_name、options,以及 options[键]=值 这种写法。其他参数只打一条 tracing::warn!:
_ => tracing::warn!(%key, %value, "ignoring unrecognized connect parameter"),
libpq 的 connect_timeout 就落在这里。sqlx 的连接超时归连接池管(acquire_timeout,见 PoolOptions 与默认值)。
options[search_path]=pg1 和 builder 方法 options 一样,拼成 -c search_path=pg1,空格和反斜杠按 libpq 的规则转义。它最后作为启动参数 options 发给服务端,服务端当作命令行参数处理,效果等于连接一建立就 SET。
第三层:passfile
parse_from_url 的最后一步是 apply_pgpass(),只在前两层都没给密码时才去读文件:
pub(crate) fn apply_pgpass(mut self) -> Self {
if self.password.is_none() {
self.password = pgpass::load_password(
&self.host,
self.port,
&self.username,
self.database.as_deref(),
);
}
self
}
load_password 先看 PGPASSFILE 指的文件,再看 ~/.pgpass(Windows 是 %APPDATA%\postgresql\pgpass.conf),逐行按 主机:端口:库:用户:密码 匹配,* 是通配。只有在 Linux 上才检查文件权限,组或其他人可读(mode & 0o77 != 0)就忽略整个文件并打一条警告;macOS 上不查。
所以密码的优先级是:URL(或 .password())> PGPASSWORD > passfile。
实验:同一个 URL,三种环境
URL 里不写密码,带一个 sqlx 不认识的 connect_timeout(src/bin/options_src.rs,M2 Max、macOS、rustc 1.99.0、Postgres 16.14):
use sqlx::postgres::PgConnectOptions;
use sqlx::{ConnectOptions, Connection, PgConnection};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.without_time()
.with_ansi(false)
.init();
// 1. what new() picks up from the environment
let o = PgConnectOptions::new();
println!(
"new(): host={} port={} user={} db={:?} ssl={:?}",
o.get_host(),
o.get_port(),
o.get_username(),
o.get_database(),
o.get_ssl_mode()
);
// 2. URL: host starting with '/' (percent-encoded) means a Unix socket directory
let o: PgConnectOptions = "postgres://%2Fvar%2Frun%2Fpostgresql/lab".parse()?;
println!("socket url: host={} socket={:?}", o.get_host(), o.get_socket());
// 3. query parameters, including one sqlx does not know
let o: PgConnectOptions =
"postgres://lab@192.168.64.106/lab?sslmode=disable&application_name=pg1&options[search_path]=pg1&connect_timeout=5"
.parse()?;
println!("options = {:?}", o.get_options());
println!("to_url_lossy = {}", o.to_url_lossy());
// 4. no password in the URL: the password comes from PGPASSWORD / the passfile, or nowhere
match PgConnection::connect_with(&o).await {
Ok(c) => {
println!("connected without a password in the URL");
c.close().await?;
}
Err(e) => println!("connect error: {e}"),
}
Ok(())
}
先清掉所有 PG* 变量、把 HOME 指到不存在的目录,再给一个 PGPASSFILE(内容是一行 192.168.64.106:5432:lab:lab:lab,权限 600):
== A:没有任何 PG* 变量
new(): host=localhost port=5432 user=alice db=None ssl=Prefer
socket url: host=localhost socket=Some("/var/run/postgresql")
WARN sqlx_postgres::options::parse: ignoring unrecognized connect parameter key=connect_timeout value=5
options = Some("-c search_path=pg1")
to_url_lossy = postgres://lab@192.168.64.106:5432/lab?sslmode=disable&statement-cache-capacity=100
connect error: error returned from database: password authentication failed for user "lab" at line 331
== B:只加 PGPASSFILE
to_url_lossy = postgres://lab:lab@192.168.64.106:5432/lab?sslmode=disable&statement-cache-capacity=100
connected without a password in the URL
- A 里
new()的主机是localhost:本机三个候选目录下都没有 socket 文件。用户名是操作系统用户hal,库名是None。 %2Fvar%2Frun%2Fpostgresql进了socket字段,host仍是环境给的默认值;连接时fetch_socket拼出<目录>/.s.PGSQL.<端口>,有它就走connect_uds,不再看host。- 没有密码时 sqlx 照样做 SCRAM(拿空串当密码),服务端回
28P01。错误后面的at line 331是服务端 C 源码的行号,见错误。 - B 里 passfile 补上了密码,
to_url_lossy里出现了lab:lab。
to_url_lossy 名副其实:build_url 只写回用户、密码、主机、端口、库、sslmode、三个证书和 statement-cache-capacity,application_name 和 options 都丢了;密码却是明文写回去的,别把它打进日志。
PGPASSWORD,passfile 里有对的密码同一个程序,环境同 B,再加上 PGPASSWORD=postgres(不是 lab 的密码)。to_url_lossy 那一行打印什么,能不能连上?
establish:一个循环收完握手
PgConnection::establish 先 PgStream::connect(建 TCP 或 Unix socket,按 sslmode 决定要不要升级成 TLS,最后包一层读写缓冲),再写 StartupMessage:
let mut params = vec![
// Sets the display format for date and time values,
// as well as the rules for interpreting ambiguous date input values.
("DateStyle", "ISO, MDY"),
// Sets the client-side encoding (character set).
// <https://www.postgresql.org/docs/devel/multibyte.html#MULTIBYTE-CHARSET-SUPPORTED>
("client_encoding", "UTF8"),
// Sets the time zone for displaying and interpreting time stamps.
("TimeZone", "UTC"),
];
if let Some(ref extra_float_digits) = options.extra_float_digits {
params.push(("extra_float_digits", extra_float_digits));
}
if let Some(ref application_name) = options.application_name {
params.push(("application_name", application_name));
}
if let Some(ref options) = options.options {
params.push(("options", options));
}
stream.write(Startup {
username: Some(&options.username),
database: options.database.as_deref(),
params: ¶ms,
})?;
stream.flush().await?;
前三个参数是写死的。sqlx 自己要解析服务端发来的文本:简单查询协议的结果全是文本格式,日期要按 ISO 的格式读,字符串要是 UTF-8,timestamptz 的文本要带固定时区。连接建好以后再 SET DateStyle,sqlx 并不跟着变,后果见消息与读写缓冲的实验。
extra_float_digits 默认是 "2",控制文本格式下浮点数输出多少位。它的 setter 的文档注释却写着「SQLx sets this value to 3 by default」。
extra_float_digits 从 3 改成了 2最早 sqlx 在启动参数里发的是 extra_float_digits=3。issue #801(「Issues with redshift」)报告「sqlx sends at the beginning of a pg session extra_float_digits = 3. This is not supported by redshift and will cause the connection to fail」;后来一个想加 URL 参数的 PR(#2708,未合并)贴出了 Redshift 的原话:「3 is outside the valid range for parameter "extra_float_digits" (-15 .. 2)」。PR #2832(「Update extra_float_digits default to 2 instead of 3」,首次发布于 0.7.3)只改了一行,就是上面 new_without_pgpass 里的 Some("2".into())。对 Postgres 本身 2 和 3 没有区别:在实验库上分别 set extra_float_digits 为 0、1、2、3 再查 (0.1::float8 + 0.2::float8)::text,0 得到 0.3,1、2、3 都是 0.30000000000000004。那段文档注释没跟着改,到 0.9.0 还写着 3。
写完 StartupMessage,establish 进入一个循环,每次 recv 一条后端消息:
loop {
let message = stream.recv().await?;
match message.format {
BackendMessageFormat::Authentication => match message.decode()? {
Authentication::Ok => {
// the authentication exchange is successfully completed
// do nothing; no more information is required to continue
}
Authentication::CleartextPassword => {
// ...
stream
.send(Password::Cleartext(
options.password.as_deref().unwrap_or_default(),
))
.await?;
}
Authentication::Md5Password(body) => {
// ...
stream
.send(Password::Md5 {
username: &options.username,
password: options.password.as_deref().unwrap_or_default(),
salt: body.salt,
})
.await?;
}
Authentication::Sasl(body) => {
sasl::authenticate(&mut stream, options, body).await?;
}
method => {
return Err(err_protocol!(
"unsupported authentication method: {:?}",
method
));
}
},
BackendMessageFormat::BackendKeyData => {
// ...
let data: BackendKeyData = message.decode()?;
process_id = data.process_id;
secret_key = data.secret_key;
}
BackendMessageFormat::ReadyForQuery => {
// start-up is completed. The frontend can now issue commands
transaction_status = message.decode::<ReadyForQuery>()?.transaction_status;
break;
}
// ...
}
}
认证方式只支持明文、MD5 和 SASL(GSSAPI、SSPI 等报 unsupported authentication method)。明文和 MD5 都是一条 PasswordMessage 了事,SASL 交给 sasl::authenticate,下一节讲。
循环里看不到 ParameterStatus:它和 NoticeResponse 一样在 recv 内部就被处理掉了,不会到这里(见消息与读写缓冲)。BackendKeyData 里的进程号和密钥存进了 PgConnectionInner,但这两个字段标着 #[allow(dead_code)],注释说「used to send cancel requests」,整个 sqlx-postgres 里却没有发送 CancelRequest 的代码。sqlx 不会主动取消服务端正在跑的查询,这件事在扩展查询协议与语句缓存里会再碰到。
收到 ReadyForQuery 循环结束,PgConnectionInner 才被构造出来:语句 id 从 sqlx_s_1 开始,语句缓存按 statement_cache_capacity 建好,pending_ready_for_query_count 是 0。PgStream 那边的注释解释了为什么要把握手放在 PgStream 上做:「to uphold the invariant where an instantiated [PgConnection] is a valid connection to postgres」。拿到 PgConnection 就意味着握手已经完成。
实验:抓一次握手
一次查询的全过程那页的代理在 StartupMessage 之后就只认「类型字节 + 长度」。这里复制一份,加了几样:SSLRequest 的单字节回复、认证消息的子类型、SASL 的载荷(长的 nonce 和证明只留前 8 个字符,括号里是原长度)、StartupMessage 和 ParameterStatus 的内容、ErrorResponse / NoticeResponse 的 S、C、M 三个字段。实验 crate 的 lib.rs 里有一个帮手函数,后面两页也用它:
pub const UPSTREAM: &str = "192.168.64.106:5432";
pub fn direct() -> anyhow::Result<PgConnectOptions> {
Ok(std::env::var("DATABASE_URL")?.parse::<PgConnectOptions>()?.options([("search_path", "pg1")]))
}
// Start a proxy and return options pointing at it (TLS off unless the caller changes it).
pub async fn via_proxy() -> anyhow::Result<(proxy::Proxy, PgConnectOptions)> {
let p = proxy::start(UPSTREAM).await?;
let opts = direct()?.host("127.0.0.1").port(p.port).ssl_mode(PgSslMode::Disable);
Ok((p, opts))
}
src/bin/startup.rs 先直连查一下服务端怎么存密码,再经代理用 Prefer 和 Require 各连一次:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// how is the password stored on the server?
let mut c = PgConnection::connect_with(&direct()?).await?;
let enc: String = sqlx::query_scalar("show password_encryption").fetch_one(&mut c).await?;
let stored: String = sqlx::query_scalar("select left(rolpassword, 14) from pg_authid where rolname = current_user")
.fetch_one(&mut c)
.await?;
println!("password_encryption = {enc}, stored = {stored}...");
c.close().await?;
let (_p, opts) = via_proxy().await?;
for mode in [PgSslMode::Prefer, PgSslMode::Require] {
println!("--- ssl_mode = {mode:?} ---");
let r = PgConnection::connect_with(&opts.clone().ssl_mode(mode).application_name("pg1-startup")).await;
settle().await;
match r {
Ok(conn) => {
println!("connected, server_version_num = {:?}", conn.server_version_num());
conn.close().await?;
settle().await;
}
Err(e) => println!("error: {e}"),
}
}
Ok(())
}
password_encryption = scram-sha-256, stored = SCRAM-SHA-256$...
--- ssl_mode = Prefer ---
[conn 1] C->S SSLRequest
[conn 1] S->C SSL 回复 'N'
[conn 1] C->S StartupMessage(v3.0 user=lab database=lab DateStyle=ISO, MDY client_encoding=UTF8 TimeZone=UTC extra_float_digits=2 application_name=pg1-startup options=-c search_path=pg1)
[conn 1] S->C AuthenticationSASL(SCRAM-SHA-256)
[conn 1] C->S SASLInitialResponse(SCRAM-SHA-256, "n,,n=lab,r=xX`TIl…(96)")
[conn 1] S->C AuthenticationSASLContinue("r=xX`TIl…(120),s=pmdfF7…(26),i=4096")
[conn 1] C->S SASLResponse("c=biws,r=xX`TIl…(120),p=3L6fHM…(46)")
[conn 1] S->C AuthenticationSASLFinal("v=bh2DwO…(46)"), AuthenticationOk, ParameterStatus(in_hot_standby=off), ParameterStatus(integer_datetimes=on), ParameterStatus(TimeZone=UTC), ParameterStatus(IntervalStyle=postgres), ParameterStatus(is_superuser=on), ParameterStatus(application_name=pg1-startup), ParameterStatus(default_transaction_read_only=off), ParameterStatus(scram_iterations=4096), ParameterStatus(DateStyle=ISO, MDY), ParameterStatus(standard_conforming_strings=on), ParameterStatus(session_authorization=lab), ParameterStatus(client_encoding=UTF8), ParameterStatus(server_version=16.14), ParameterStatus(server_encoding=UTF8), BackendKeyData, ReadyForQuery(I)
connected, server_version_num = Some(160014)
[conn 1] C->S Terminate
--- ssl_mode = Require ---
[conn 2] C->S SSLRequest
[conn 2] S->C SSL 回复 'N'
error: error occurred while attempting to establish a TLS connection: server does not support TLS
- 服务端的
password_encryption是scram-sha-256,lab的密码以SCRAM-SHA-256$开头存着,所以服务端提出的认证方式是 SASL,并且只列了SCRAM-SHA-256一种。 - 从
StartupMessage到ReadyForQuery是三个来回:启动、SASL 第一步、SASL 第二步。最后一次读里,服务端把签名、AuthenticationOk、14 条ParameterStatus、BackendKeyData、ReadyForQuery一口气发了过来。 server_version=16.14被recv截下来解析成server_version_num()的160014。Require遇到N直接报错,StartupMessage都没发。
SCRAM-SHA-256:两个来回
sasl::authenticate 照 RFC 5802 一步步来,变量名和 RFC 里的公式一一对应:
- 在服务端列出的机制里找
SCRAM-SHA-256或SCRAM-SHA-256-PLUS,都没有就报unsupported SASL authentication mechanisms。 - 发
SASLInitialResponse:n,,n=<用户名>,r=<客户端 nonce>。开头的n,,是 GS2 头,意思是「客户端不支持通道绑定」,所以即使服务端提供了-PLUS,sqlx 也用plus: false走普通的SCRAM-SHA-256。gen_nonce生成 64 到 127 个可打印 ASCII 字符(排除逗号);上面那次是 94 个,r=加起来 96。 - 服务端回
SASLContinue:r=是客户端 nonce 后面接上服务端的 24 个字符(120 = 96 + 24),s=是 base64 的盐,i=4096是迭代次数。 - 客户端算
SaltedPassword = Hi(密码, 盐, 4096),再由它派生ClientKey、StoredKey、ClientSignature,ClientProof = ClientKey XOR ClientSignature,发SASLResponse:c=biws,r=<完整 nonce>,p=<证明>。biws是n,,的 base64。 - 服务端回
SASLFinal,v=是服务端签名。客户端用ServerKey算出同一个签名比对:
// authentication is only considered valid if this verification passes
mac.verify_slice(&data.verifier).map_err(Error::protocol)?;
最后这步是 SCRAM 比 MD5 强的地方:不只服务端验证了客户端,客户端也验证了服务端确实知道这个密码,冒充的服务端过不了这一关。实验库里 lab 的 rolpassword 形如 SCRAM-SHA-256$4096:<盐>$<StoredKey>:<ServerKey>,服务端手里没有密码本身。
Hi 就是 PBKDF2-HMAC-SHA256,4096 次 HMAC 是纯 CPU 计算。hi 每算 5 轮让出一次:
for i in 1..iter_count {
mac.update(u.as_slice());
u = mac.finalize_reset().into_bytes();
hi = hi.iter().zip(u.iter()).map(|(&a, &b)| a ^ b).collect();
// For large iteration counts, this process can take a long time and block the event loop.
// It was measured as taking ~50ms for 4096 iterations (the default) on a developer machine.
// If we want to yield every 10-100us (as generally advised for tokio), then we can yield
// every 5 iterations which should be every ~50us.
if i % 5 == 0 {
rt::yield_now().await;
}
}
在 async 里做长时间计算会卡住整个 worker,见 tokio 卷的在 async 里阻塞。
0.9.0 之前,hi 是普通的同步函数,4096 轮一口气算完。issue #4005(「PostgreSQL SCRAM-SHA-256 auth causes event loop blocking」)用火焰图看到大量时间花在 sha2::sha256::compress256 上,而且是在 Future::poll 里,单独跑这段代码「can take ~50ms on my machine (MacBook Pro M1)」。修它的 PR #4006(首次发布于 0.9.0)起初是「a PoC of offloading the SHA256 HMAC generation into a blocking executor」,合并时的提交说明却是「Yield while salting password for PG SASL」:「we yield every few iterations of the hmac-sha256」,也就是上面的 i % 5 == 0。PR 后来改名为「add yield points to SHA256 calculation」,CHANGELOG 里却留着旧标题「run SHA256 in a blocking executor」。现在的做法不占用阻塞线程池,计算量也没变,只是被 yield_now 切成了八百多段(4096 / 5)。
TLS:SSLRequest 和 sslmode
TLS 是在 StartupMessage 之前、同一条 TCP 连接上协商的。maybe_upgrade:
match options.ssl_mode {
// FIXME: Implement ALLOW
PgSslMode::Allow | PgSslMode::Disable => return Ok(Box::new(socket)),
PgSslMode::Prefer => {
if !tls::available() {
return Ok(Box::new(socket));
}
// try upgrade, but its okay if we fail
if !request_upgrade(&mut socket, options).await? {
return Ok(Box::new(socket));
}
}
PgSslMode::Require | PgSslMode::VerifyFull | PgSslMode::VerifyCa => {
tls::error_if_unavailable()?;
if !request_upgrade(&mut socket, options).await? {
// upgrade failed, die
return Err(Error::Tls("server does not support TLS".into()));
}
}
}
let accept_invalid_certs = !matches!(
options.ssl_mode,
PgSslMode::VerifyCa | PgSslMode::VerifyFull
);
let accept_invalid_hostnames = !matches!(options.ssl_mode, PgSslMode::VerifyFull);
request_upgrade 往裸 socket 写 8 个字节的 SSLRequest(长度 8,请求码 80877103,高 16 位是 1234、低 16 位是 5679),读回一个字节:S 就在这条 socket 上做 TLS 握手,N 就返回 false。这个字节没有类型和长度,也不经过读写缓冲,这一步发生在 BufferedSocket 包上去之前。实验里 Prefer 收到 N 之后,StartupMessage 就在同一条 TCP 连接上明文发了出去,代理看到的是同一个 [conn 1]。
sslmode | 发 SSLRequest | 服务端回 N | 证书校验 |
|---|---|---|---|
disable | 不发 | — | — |
allow | 不发(FIXME: Implement ALLOW,等同 disable) | — | — |
prefer(默认) | 编译时开了 TLS 才发 | 明文继续 | 不校验 |
require | 发 | 报错 | 不校验 |
verify-ca | 发 | 报错 | 校验证书链,不校验主机名 |
verify-full | 发 | 报错 | 证书链和主机名都校验 |
「不校验」是真的什么都不查:rustls 后端在 accept_invalid_certs 为真时装的是一个 DummyTlsVerifier(tls_rustls.rs)。prefer 只在服务端回 N 时退回明文;回了 S 但握手失败,连接直接报错,不会再明文重试。这个实验环境的服务端没开 TLS,表里后三列的握手行为是读源码得到的,没有实测。
问: PgSslMode::Require 的文档写着「If a root CA file is present, verify the connection in the same way as if VerifyCa was specified」,那我配了 sslrootcert 再用 require,是不是就校验了?
答: 0.9.0 不会。accept_invalid_certs 只看 ssl_mode 是不是 VerifyCa / VerifyFull,不看有没有根证书;Require 加根证书照样走 DummyTlsVerifier,根证书根本不会被读。这句文档描述的是 libpq 的行为,sqlx 没有实现。要校验,就明确写 verify-ca 或 verify-full。
Unix socket 走的是同一个 MaybeUpgradeTls:PgStream::connect 只是在 connect_uds 和 connect_tcp 之间选一个,代码不区分两者,sslmode 是 prefer 时在 Unix socket 上同样先发 SSLRequest。TCP 那一侧在 tokio 下就是 tokio::net::TcpStream::connect((host, port)),按主机名解析出的地址逐个试;没有连接超时,超时由连接池的 acquire_timeout 兜底。
动脑筋答案
== C:B 的环境再加 PGPASSWORD=postgres
to_url_lossy = postgres://lab:postgres@192.168.64.106:5432/lab?sslmode=disable&statement-cache-capacity=100
connect error: error returned from database: password authentication failed for user "lab" at line 331
- URL 没写密码,
parse_from_url起点的new_without_pgpass已经从PGPASSWORD拿到了postgres,URL 没有覆盖它。 apply_pgpass只在password.is_none()时读文件,这时密码已经有了,passfile 里正确的lab根本没被读。- 于是用错的密码做 SCRAM,服务端拒绝。写这个实验的机器上,shell 里正好有一组给别的库用的
PGPASSWORD、PGUSER、PGHOST,第一次跑时就踩到了:URL 里没写的东西,都会被环境悄悄补上。
小结
- 选项分三层:
new_without_pgpass读PG*环境变量,URL 覆盖在它上面,最后只在还没有密码时读 passfile。密码优先级是 URL >PGPASSWORD> passfile;不认识的 URL 参数只打一条警告。 establish写一条StartupMessage(固定带DateStyle=ISO, MDY、client_encoding=UTF8、TimeZone=UTC、extra_float_digits=2),然后在一个循环里处理认证、BackendKeyData,到ReadyForQuery为止;ParameterStatus在recv里就被截走了。- SCRAM-SHA-256 是两个来回,不做通道绑定,最后校验服务端签名;4096 轮 HMAC 每 5 轮
yield_now一次(0.9.0 起)。 sslmode:allow等于disable;prefer收到N就在同一条连接上明文继续;prefer和require都不校验证书,配了根证书也一样,要校验得用verify-ca/verify-full。BackendKeyData存了但没用,sqlx 不发CancelRequest。