sans-IO
主线位置:记录主线的两端。
writer()收下明文、write_tls交出密文,是记录主线的出口;read_tls收下密文、process_new_packets处理、reader()交出明文,是入口。握手主线的每一轮也靠同一组调用推着走:rustls 不会自己去读写任何东西,调用方不调,它就停在原地。
flowchart TB net[调用方的网络] -->|read_tls| inbuf["DeframerVecBuffer"] inbuf -->|process_new_packets| core["ConnectionCore 当前状态"] core --> plain["received_plaintext"] plain -->|reader| app[应用] app -->|writer| enc["加密 进 sendable_tls"] core --> enc enc -->|write_tls| net
一段加密管道
lib.rs 的文档用一张图说明连接对象是什么:左边是 TLS 字节,经 read_tls() / write_tls() 进出;右边是明文,经 reader()(io::Read)/ writer()(io::Write)进出。中间的 ClientConnection 或 ServerConnection 不认识套接字。
两者都包着同一个 ConnectionCommon:
pub struct ConnectionCommon<Data> {
pub(crate) core: ConnectionCore<Data>,
deframer_buffer: DeframerVecBuffer,
sendable_plaintext: ChunkVecBuffer,
}
core 里是握手状态(Result<Box<dyn State<Data>>, Error>)和 CommonState:两个方向的记录层、待发的 TLS 字节 sendable_tls、已解密待读的明文 received_plaintext。deframer_buffer 存还没处理的输入字节,sendable_plaintext 存握手完成前就写进来的明文。Connection 是一个两变体的枚举,方便把客户端和服务端放进同一段代码。
五个方法分别只做一件事:
| 方法 | 做什么 | 不做什么 |
|---|---|---|
read_tls(&mut dyn io::Read) | 调一次 rd.read,把字节追加进 deframer_buffer | 不解析、不解密 |
process_new_packets() | 去帧、解密、把每条消息交给当前状态;可能产生要发的 TLS 字节和可读的明文 | 不碰 IO |
write_tls(&mut dyn io::Write) | 把 sendable_tls 里的字节写给 wr,ChunkVecBuffer::write_to 用 write_vectored | 不加密(加密在写入 sendable_tls 之前已经做完) |
reader() | 从 received_plaintext 读明文;没有时返回 WouldBlock | 不触发读网络 |
writer() | 握手完成后立即分片加密进 sendable_tls;之前先放进 sendable_plaintext | 不触发写网络 |
调用方靠三个查询决定下一步:
pub fn wants_write(&self) -> bool {
!self.sendable_tls.is_empty()
}
pub fn wants_read(&self) -> bool {
// We want to read more data all the time, except when we have unprocessed plaintext.
// This provides back-pressure to the TCP buffers. We also don't want to read more after
// the peer has sent us a close notification.
//
// In the handshake case we don't have readable plaintext before the handshake has
// completed, but also don't want to read if we still have sendable tls.
self.received_plaintext.is_empty()
&& !self.has_received_close_notify
&& (self.may_send_application_data || self.sendable_tls.is_empty())
}
is_handshaking 是 !(may_send_application_data && may_receive_application_data)。wants_read 的最后一个条件值得记住:握手期间,只要还有没发出去的 TLS 字节,它就返回假。rustls 在告诉调用方「先把我要说的话发出去,再来听对方的」。
process_new_packets:错误是粘住的
ConnectionCore::process_new_packets 先把状态取出来,在循环里一条条喂消息:
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);
}
};
// ...
match self.process_msg(msg, state, Some(sendable_plaintext)) {
Ok(new) => state = new,
Err(e) => {
self.state = Err(e.clone());
deframer_buffer.discard(buffer_progress.take_discard());
return Err(e);
}
}
状态是 Box<dyn State>,process_msg 拿走旧状态、返回新状态(为什么用这种写法见 状态机)。出错时 self.state 被换成 Err(e),以后每次调用都原样返回这个错误。文档的说法是「Future calls after an error will do no new work and will return the same error」,这时应该再调一次 write_tls,把出错时排进队列的告警发出去,然后关连接。返回值 IoState 告诉调用方现在有多少字节可写、多少明文可读。
驱动循环
本卷所有内存实验都用同一个 transfer:把一边要发的字节全部写进一个 Vec,再全部喂给另一边,然后处理:
fn transfer(from: &mut Connection, to: &mut Connection) -> usize {
let mut buf = Vec::new();
while from.wants_write() {
from.write_tls(&mut buf).unwrap();
}
let n = buf.len();
let mut rd = &buf[..];
while !rd.is_empty() {
to.read_tls(&mut rd).unwrap();
}
to.process_new_packets().unwrap();
n
}
&[u8] 实现了 io::Read,Vec<u8> 实现了 io::Write,所以「网络」可以是两块内存。主循环是 while client.is_handshaking() || server.is_handshaking() 里先 transfer(client, server) 再 transfer(server, client)。在每一步之后打印三个查询(ring,同一张 OpenSSL 生成的证书,release):
client after new wants_read=false wants_write=true is_handshaking=true
server after new wants_read=true wants_write=false is_handshaking=true
round 1: client write_tls -> 236 bytes
client wants_read=true wants_write=false is_handshaking=true
server process_new_packets -> IoState { tls_bytes_to_write: 679, plaintext_bytes_to_read: 0, peer_has_closed: false }
server wants_read=false wants_write=true is_handshaking=true
round 1: server write_tls -> 679 bytes
ClientConnection::new 返回时 ClientHello 已经在 sendable_tls 里:构造函数里就跑了 start_handshake。这时 wants_read 是假,正是上面那个条件在起作用。
client.is_handshaking() 是真还是假?服务端呢?每一轮的字节、每条记录用哪个密钥加密,见一次握手。
漏掉 write_tls
lib.rs 给的示例循环是「wants_read() 且套接字可读就 read_tls + process_new_packets;wants_write() 且套接字可写就 write_tls」。把写的那一支漏掉,在内存里转一百万圈:
doc-style loop without write_tls: 1000000 iterations, wants_read never became true
client wants_read=false wants_write=true is_handshaking=true
server (never got a byte) wants_read=true wants_write=false is_handshaking=true
ClientHello 一直留在客户端的 sendable_tls 里,wants_read 因此一直是假,循环空转,不报错。换成真套接字、不按 wants_read 判断直接阻塞读,就是两边都在等对方先开口。给两边的 TcpStream 设 2 秒读超时,看到的是:
tcp client read_tls (ClientHello never written): Err(Os { code: 35, kind: WouldBlock, message: "Resource temporarily unavailable" })
tcp server read_tls: Err(Os { code: 35, kind: WouldBlock, message: "Resource temporarily unavailable" })
没有超时就是永远卡住。rustls 没有任何日志或错误能提示「你忘了发」,唯一的线索是 wants_write() 一直为真。
漏掉 process_new_packets
服务端 read_tls 收下 236 字节的 ClientHello,但不处理:
server got 236 bytes via read_tls, no process_new_packets:
server wants_read=true wants_write=false is_handshaking=true
client wants_read=true wants_write=false is_handshaking=true
字节躺在 deframer_buffer 里,服务端不会产生回复,两边都在等读,和上一节一样卡住。
握手完成后再漏,表现不同。客户端写 30000 字节明文,变成 30044 字节密文(两条记录:16384 + 13616 字节明文,各加 5 字节头、1 字节内层类型、16 字节标签),服务端只 read_tls 不处理:
handshaken server reader().read before any data: Err(Kind(WouldBlock))
client produced 30044 TLS bytes for 30000 plaintext bytes
read_tls after 18437 bytes buffered: Err(Custom { kind: InvalidData, error: "message buffer full" }) / Display: message buffer full
server reader().read, still unprocessed: Err(Kind(WouldBlock))
after process_new_packets: IoState { tls_bytes_to_write: 0, plaintext_bytes_to_read: 16384, peer_has_closed: false }
18437 是 MAX_WIRE_SIZE:一条记录最大的密文长度 16384 + 2048 再加 5 字节头。DeframerVecBuffer::prepare_read 在缓冲已满一条最大记录时拒绝再读,返回 InvalidData 的 message buffer full(正在拼一条跨记录的握手消息时上限放宽到 64 KiB)。不处理就读不到明文,reader() 一直是 WouldBlock;处理一次,第一条记录的 16384 字节明文就出来了。
发送方向也有上限。握手完成后对客户端一次写 100000 字节,不调 write_tls:
client writer().write(100000 bytes) -> Ok(65536); again -> Ok(0)
client write_all(100000 bytes) -> Err(Error { kind: WriteZero, message: "failed to write whole buffer" })
sendable_tls 的默认上限是 DEFAULT_BUFFER_LIMIT = 64 * 1024,满了 write 就返回 Ok(0),write_all 把它当成 WriteZero。上限可以用 set_buffer_limit 改,细节见 发送路径。
complete_io、Stream 和 StreamOwned
手写这个循环很容易错,rustls 自己提供了一个:complete_io 接受任何 io::Read + io::Write,握手中就一直读写到握手完成;握手后有待发数据就写完,否则读一次。process_new_packets 出错时它先「last-gasp」地 write_tls 一次把告警发出去,再把错误包成 io::ErrorKind::InvalidData 返回。
Stream 借用一个连接和一个套接字,StreamOwned 拥有它们,两者都实现 Read、BufRead、Write,全靠 complete_io。读之前的准备是 prepare_read:
fn prepare_read(&mut self) -> Result<()> {
// Return buffered plaintext without doing any IO: `complete_io()` does not read
// while plaintext is buffered, so blocked writes would otherwise hide it.
if !self.conn.received_plaintext.is_empty() {
return Ok(());
}
self.complete_prior_io()?;
// We call complete_io() in a loop since a single call may read only
// a partial packet from the underlying transport, or may only write
// pending TLS data (like an alert queued while processing received
// records). A full packet is needed to get more plaintext, which we
// must do if EOF has not been hit.
while self.conn.wants_read() {
let (read, written) = self.conn.complete_io(self.sock)?;
if read == 0 && written == 0 {
break;
}
}
Ok(())
}
complete_prior_io 在握手没完成时先把握手做完,所以第一次 write 或 read 就会触发整个握手。在 127.0.0.1 上,服务端用 Stream::new(&mut conn, &mut sock) 读 4 字节、回 pong;客户端用 StreamOwned,套接字外面包一层打印每次 read / write 的壳:
client: write_all(b"ping")
[client] sock.write 236 -> Ok(236)
[client] sock.read -> 679
[client] sock.write 6 -> Ok(6)
[client] sock.write 74 -> Ok(74)
[client] sock.write 26 -> Ok(26)
client: read_to_end
[client] sock.read -> 210
[client] sock.read -> 0
client: read_to_end -> Err(Custom { kind: UnexpectedEof, error: "peer closed connection without sending TLS close_notify: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof" }), got "pong"
client: error kind UnexpectedEof, Display: peer closed connection without sending TLS close_notify: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof
一次 write_all(b"ping") 背后是:发 ClientHello(236),收服务端第一轮(679),发 CCS(6)和 Finished(74),握手完成,再发 ping 那条记录(26)。CCS 和 Finished 分成两次 write 是因为这层壳没实现 write_vectored,默认实现一次只写第一个缓冲;直接用 TcpStream 时是一次 writev。read_to_end 读到 210 字节(184 字节的两张票据加 26 字节的 pong),再读到 TCP 的 EOF。
服务端没有发 close_notify 就关了 TCP,Reader 把它报成 UnexpectedEof:没有 close_notify 的 EOF 可能是被截断的连接。服务端在关之前加 conn.send_close_notify() 和一次 complete_io,客户端最后一次读到 234 字节(多出 24 字节的告警记录),read_to_end -> Ok(4)。manual/howto.rs 的「Unexpected EOF」一节说明了什么时候可以把它当正常 EOF:空闲连接上,或者应用层协议自带长度、不靠关连接来分界。
0.21.1 及之前,Stream::read 在 complete_io 读到 0 字节时会自己调 process_new_packets,没有明文可读就直接返回 Ok(0)。ab685b5e(2023-05-18,首次发布于 0.21.2)删掉了这段,改成读到 0 字节就跳出循环、交给 self.conn.reader().read(...) 处理。提交说明是「In practice this has the effect of swallowing an error condition when the remote peer has closed their end of the connection without properly signalling their intent to do so. When this happens the client must read an UnexpectedEof error to prevent a truncation attack.」上面实验里的 UnexpectedEof,在 0.21.1 的 Stream 上是一个看起来正常的 Ok(0)。0.23.46 里这个循环又改过一次(1dacb34a,「stream: continue reading while we're writing」),条件变成 read == 0 && written == 0 才跳出。
其他接法
- 异步:tokio-rustls 把同一组调用接到
AsyncRead/AsyncWrite上,WouldBlock对应Poll::Pending;本卷不展开。 - 不缓冲的接口:
UnbufferedConnectionCommon不在内部持有输入输出缓冲,调用方自己给缓冲区,用ConnectionState枚举一步步推进,不依赖std::io,no_std下也能用。 - 对照:hyper 也不直接用 tokio 的 IO trait,而是定义自己的
rt::Read/rt::Write(见 hyper 卷的 IO trait);rustls 更进一步,连 trait 对象都只在read_tls/write_tls的参数里出现一下,核心状态机只和字节缓冲打交道。
动脑筋答案
接着上面的输出:
round 1: server write_tls -> 679 bytes
client process_new_packets -> IoState { tls_bytes_to_write: 80, plaintext_bytes_to_read: 0, peer_has_closed: false }
client wants_read=true wants_write=true is_handshaking=false
server wants_read=true wants_write=false is_handshaking=true
round 2: client write_tls -> 80 bytes
client wants_read=true wants_write=false is_handshaking=false
server process_new_packets -> IoState { tls_bytes_to_write: 184, plaintext_bytes_to_read: 0, peer_has_closed: false }
server wants_read=true wants_write=true is_handshaking=false
round 2: server write_tls -> 184 bytes
client process_new_packets -> IoState { tls_bytes_to_write: 0, plaintext_bytes_to_read: 0, peer_has_closed: false }
client wants_read=true wants_write=false is_handshaking=false
server wants_read=true wants_write=false is_handshaking=false
客户端是 false,服务端是 true。客户端校验完服务端的 Finished,就有了应用流量密钥,可以发也可以收,Finished 只是还排在 sendable_tls 里(tls_bytes_to_write: 80 就是 CCS 加 Finished)。这时写进 writer() 的数据会紧跟在 Finished 后面发出。服务端要等收到并校验客户端的 Finished 才结束握手,然后立刻产生 184 字节的两张票据。所以「is_handshaking() 变假」在两端不是同一时刻。
小结
- 连接对象只和字节缓冲打交道:
read_tls只收、process_new_packets只处理、write_tls只发,reader/writer只碰明文。 - 握手期间有待发字节时
wants_read()为假;漏掉write_tls的循环会空转或两边卡死,不会报错。 - 漏掉
process_new_packets:握手期间卡住;握手后缓冲到 18437 字节报message buffer full,明文一直读不到。 - 发送缓冲默认上限 64 KiB,满了
write返回Ok(0),write_all报WriteZero。 process_new_packets的错误是粘住的,出错后还要write_tls一次把告警发出去。Stream/StreamOwned靠complete_io把这些调用包成阻塞读写,第一次读写就做完握手;对端不发close_notify就关连接,读到的是UnexpectedEof。