出过一次错,之后永远是同一个错
主线位置:握手主线和记录主线共用的
process_new_packets。连接的状态是Result<Box<dyn State>, Error>,出错时整个状态换成Err。这个设计和「出错后先write_tls发告警」的说明见 sans-IO,状态机本身见状态机。本页讲调用方以为「再试一次」能好时会看到什么。
stateDiagram-v2 [*] --> Running Running --> Running: 正常消息 Running --> Failed: 任何错误 Failed --> Failed: process_new_packets 同一错误 Failed --> Failed: writer write 同一错误 Failed --> Failed: reader read WouldBlock Failed --> [*]: write_tls 发告警后丢弃
实验 crate:exp-rustls/pita-lab,src/bin/p06_sticky.rs;对照用的 exp-rustls/pita-r45 是同一份代码,只把 rustls 换成 0.23.45。M2 Max、macOS 27、rustc 1.99.0,release,ring provider,TLS 1.3,内存握手。
现象
握手失败之后。 客户端信任的是另一个 CA,处理服务端第一轮时报 UnknownIssuer。接着照常调用各个方法:
== A: handshake fails on the client (wrong root)
process_new_packets #1: Err(InvalidCertificate(UnknownIssuer))
process_new_packets #2: Err(InvalidCertificate(UnknownIssuer))
process_new_packets #3: Err(InvalidCertificate(UnknownIssuer))
is_handshaking=true wants_write=true wants_read=false
write_tls after the error -> 30 bytes [14, 03, 03, 00, 01, 01, 17, 03]
writer().write(b"GET /") -> Err(Custom { kind: InvalidData, error: InvalidCertificate(UnknownIssuer) })
wants_write after that write: false
read_tls(6 more bytes) -> Ok(6)
process_new_packets #4: Err(InvalidCertificate(UnknownIssuer))
reader().read -> Err(Kind(WouldBlock))
process_new_packets无论调几次、中间喂不喂新字节,都是同一个错误。read_tls照收不误,收进来的字节不会再被处理。- 出错时已经有 30 字节排队:6 字节的 CCS 加 24 字节加密的告警。要
write_tls才会发出去。 writer().write返回包着同一个错误的InvalidData。reader().read不报这个错误,只报WouldBlock。
握手之后,一条坏记录。 服务端收到一条 37 字节的垃圾应用数据记录,之后客户端再发一条完全正常的:
== B: one bad record after the handshake (server side)
server after junk record: Err(DecryptError)
server after a good record: Err(DecryptError)
server write_tls after the error -> 24 bytes
server writer().write(b"200 OK") -> Err(Custom { kind: InvalidData, error: DecryptError }), then wants_write=false
client after the server's alert: Err(AlertReceived(BadRecordMac))
一条坏记录就让整个连接作废,后面的好记录也解不出来。客户端收到 bad_record_mac 告警,它这一侧也跟着粘住。
重试循环。 对这个服务端连接反复调 process_new_packets:
== C: retry loop on process_new_packets
still failing after 1000000 retries: Err(DecryptError)
一百万次都是同一个结果。在真实程序里,这样的循环就是一个吃满 CPU 的死循环。
原因
ConnectionCore::process_new_packets 的第一件事是把状态取出来;已经是 Err 就原样放回、原样返回:
let mut state = match mem::replace(&mut self.state, Err(Error::HandshakeNotComplete)) {
Ok(state) => state,
Err(e) => {
self.state = Err(e.clone());
return Err(e);
}
};
处理过程中任何一条消息出错,self.state 被换成 Err(e),旧的 Box<dyn State> 连同里面的密钥日程、转录哈希一起丢掉。没有可以「回到」的状态,所以也没有重试的余地:TLS 规定致命错误之后连接必须关闭,rustls 用类型把这一点写死了。
写的方向在 0.23.46 加了同样的检查,check_error:
fn check_error(&self) -> io::Result<()> {
match &self.core.state {
Ok(_) => Ok(()),
Err(err) => Err(io::Error::new(io::ErrorKind::InvalidData, err.clone())),
}
}
Writer 的 write、write_vectored、flush 都先调它。
读的方向没有。Reader 只看明文缓冲和两个标志:check_no_bytes_state 在「没收到 close_notify、也没见到 EOF」时返回 WouldBlock,不看 core.state。所以只靠 reader() 判断连接是否健康的程序,看到的永远是「再等等」。
write 在出错之后还会成功check_error 是 493d0893(2026-10-06,「conn: fuse writes for connections with errors」,没有 PR 编号,首次发布于 0.23.46)加的;紧接着的 7a69adee(「util: fuse flushes from connections with errors」)把 flush 也接了上去。pita-r45 用 rustls 0.23.45 跑同一个程序,差别只在写的那两行:
writer().write(b"GET /") -> Ok(5)
...
server writer().write(b"200 OK") -> Ok(6), then wants_write=true
server write_tls after that write -> 28 bytes, first byte Some(17)
0.23.45 的客户端在握手失败后收下了 5 字节明文,放进握手完成前的缓冲,永远不会发出去;服务端在 DecryptError 之后收下 6 字节,还加密排进了 sendable_tls:告警已经发出去之后,下一次 write_tls 又交出一条 28 字节、类型 0x17 的应用数据记录(实验在 pita-r45 里多打了这一行)。调用方从 write 的返回值上看不出任何问题。这个提交给 Writer::write 的文档加了一句:「If the connection previously encountered a fatal error ... this returns that error, even if buf is empty」。
修法
把 process_new_packets 的任何 Err 当作这个连接的终点。
- 再调一次
write_tls,把排队的告警发给对端(complete_io出错时会自己做这一步,叫「last-gasp」写); - 关掉套接字,丢掉连接对象;
- 需要的话,用新的
ClientConnection重新连。
不要对同一个连接对象重试 process_new_packets,也不要在它上面继续 write:0.23.46 会把错误还给你,更早的版本会静默吞掉数据。
连接池要把出错的连接剔除。 池里的连接出过一次错,以后每个借到它的请求都会失败;看到 rustls 的错误(或 complete_io / Stream / tokio-rustls 包出来的 InvalidData)就不要归还。
分清能不能重连。 证书错误(InvalidCertificate)、协议错误(PeerMisbehaved、InappropriateMessage)重连多半还是一样,应该报出去;DecryptError 这类可能来自线路上的损坏,重连有意义,但同样是新连接。
怎么检查
- 错误第二次出现时,看它是不是和第一次一模一样。 日志里同一个连接反复出现完全相同的 rustls 错误,就是代码在重试一个已经死掉的连接。
- 在读循环里同时检查写或处理的结果。 只调
reader().read的循环永远看不到这个错误;每次read_tls之后都要看process_new_packets的返回值。 - 升级到 0.23.46 之后,留意新出现的
InvalidData。 以前被静默吞掉的写入,现在会在write/flush上报出来;它说明代码在出错的连接上继续写,原因在更早的那次错误。
小结
process_new_packets一旦出错,连接状态变成Err,之后每次调用都返回同一个错误,新来的好数据也不会被处理。- 0.23.46 起
writer().write/flush也返回这个错误(493d0893、7a69adee);0.23.45 的write返回Ok(n),数据被吞掉或排在告警后面。 reader().read不看错误状态,只会WouldBlock。- 任何错误都是终点:
write_tls发告警,关掉连接,需要就新建连接;连接池剔除出错的连接。