rustls: keep WRITABLE and drain buffered ciphertext when the socket backs up, and bound the plaintext buffer at 256KiB — rustls accepts all plaintext and buffers ciphertext internally on WouldBlock (unlike openssl, which surfaces backpressure through write); expose wants_write()/flush() so the reactor drains it and the sendq caps govern a slow reader. no-op for the openssl and plaintext paths

This commit is contained in:
Jean Chevronnet 2026-08-19 17:19:15 +00:00
parent ebd6e29589
commit 598a019620
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
3 changed files with 51 additions and 4 deletions

View file

@ -204,6 +204,21 @@ impl Sock {
Sock::Tls(t) => t.write(buf),
}
}
/// Whether a TLS session still has outbound bytes buffered internally (rustls
/// after a WouldBlock); a plaintext socket never buffers in-process.
fn wants_write(&self) -> bool {
match self {
Sock::Plain(_) => false,
Sock::Tls(t) => t.wants_write(),
}
}
/// Push a TLS session's buffered ciphertext to the socket; no-op for plaintext.
fn flush(&mut self) -> io::Result<()> {
match self {
Sock::Plain(_) => Ok(()),
Sock::Tls(t) => t.flush(),
}
}
/// Best-effort graceful close. TLS sends a close_notify alert; plaintext relies on
/// the socket's own FIN when the stream drops.
fn shutdown(&mut self) {
@ -245,8 +260,9 @@ impl Conn {
fn set_interest(poll: &mut Poll, c: &mut Conn, t: usize) {
let want_read = !c.paused;
// a TLS handshake may need to write (its flight) as well as read, so keep both
// until it completes; after that, write only when there's a backlog to drain.
let want_write = c.handshaking || !c.wbuf.is_empty() || c.paused;
// until it completes; after that, write when there's a backlog to drain — either
// our own queued plaintext, or ciphertext still buffered inside a TLS session.
let want_write = c.handshaking || !c.wbuf.is_empty() || c.paused || c.sock.wants_write();
if want_read == c.want_read && want_write == c.want_write {
return;
}
@ -979,12 +995,20 @@ fn flush_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
c.wbuf.clear();
c.wpos = 0;
}
// push any ciphertext a TLS session still holds buffered (rustls keeps it when
// the socket filled mid-write); our plaintext queue draining doesn't mean the
// socket has it all. WouldBlock leaves the rest for the next writable event.
match c.sock.flush() {
Ok(()) => {}
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {}
Err(_) => close = true,
}
if c.paused && c.pending() <= c.softsendq {
c.paused = false;
unpaused = true;
}
set_interest(poll, c, t);
if c.closing && c.wbuf.is_empty() {
if c.closing && c.wbuf.is_empty() && !c.sock.wants_write() {
close = true;
}
}