diff --git a/src/crypto/crypto_connection.rs b/src/crypto/crypto_connection.rs
index 935c04c..1bfde6d 100644
@@ -200,18 +200,6 @@ impl CryptoConnection {
}
}
- /// Check if the connection wants to read data
- ///
- /// If true, the application should call `read_tls()` as soon as possible.
- pub fn wants_read(&self) -> bool {
- match self {
- CryptoConnection::RustlsServer(conn) => conn.wants_read(),
- CryptoConnection::RustlsClient(conn) => conn.wants_read(),
- CryptoConnection::RealityServer(conn) => conn.wants_read(),
- CryptoConnection::RealityClient(conn) => conn.wants_read(),
- }
- }
-
/// Check whether the handshake is complete
///
/// For both client and server connections, this returns true when
diff --git a/src/crypto/crypto_tls_stream.rs b/src/crypto/crypto_tls_stream.rs
index edad9f6..227b472 100644
@@ -124,6 +124,27 @@ where
) -> Poll<io::Result<()>> {
let this = self.get_mut();
+ // Check if session already has decrypted data available
+ {
+ let mut reader = this.session.reader();
+ match reader.fill_buf() {
+ Ok(available) if !available.is_empty() => {
+ // Copy directly from session buffer to user buffer
+ let len = buf.remaining().min(available.len());
+ buf.put_slice(&available[..len]);
+ reader.consume(len);
+ return Poll::Ready(Ok(()));
+ }
+ Ok(_) => {
+ // Empty buffer, need to read more
+ }
+ Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
+ // No data available yet
+ }
+ Err(e) => return Poll::Ready(Err(e)),
+ }
+ }
+
if this.is_read_eof {
return Poll::Ready(Ok(()));
}
@@ -137,69 +158,80 @@ where
}
}
- // Track whether we did any I/O that returned Pending
- let mut io_pending = false;
-
- // Read from TCP while the session wants more data (tokio-rustls pattern)
- while !this.is_read_eof && this.session.wants_read() {
- let mut adapter = SyncReadAdapter {
- io: &mut this.io,
- cx,
- };
- match this.session.read_tls(&mut adapter) {
- Ok(0) => {
- this.is_read_eof = true;
- break;
- }
- Ok(_) => {
- // Process encrypted data - try to send alerts on error
- if let Err(e) = this.session.process_new_packets() {
- // Try last-gasp write to send any pending TLS alerts (tokio-rustls pattern)
- let _ = this.drain_all_writes(cx);
- return Poll::Ready(Err(e));
+ // No data in session buffer, read from TCP and decrypt
+ // Split borrows manually to avoid borrow checker issues
+
+ loop {
+ let bytes_read = {
+ let mut adapter = SyncReadAdapter {
+ io: &mut this.io,
+ cx,
+ };
+ match this.session.read_tls(&mut adapter) {
+ Ok(n) => n,
+ Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
+ return Poll::Pending;
}
+ Err(e) => return Poll::Ready(Err(e)),
}
- Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
- io_pending = true;
- break;
- }
- Err(e) => return Poll::Ready(Err(e)),
- }
- }
+ };
- // Try to get decrypted data from session buffer
- let mut reader = this.session.reader();
- match reader.fill_buf() {
- Ok(available) if !available.is_empty() => {
- // Copy directly from session buffer to user buffer
- let len = buf.remaining().min(available.len());
- buf.put_slice(&available[..len]);
- reader.consume(len);
- Poll::Ready(Ok(()))
+ if bytes_read == 0 {
+ // EOF
+ this.is_read_eof = true;
+ return Poll::Ready(Ok(()));
}
- Ok(_) => {
- // Empty buffer - check if EOF
- if this.is_read_eof {
- Poll::Ready(Ok(()))
- } else if io_pending {
- Poll::Pending
- } else {
- // Edge case: wants_read() returned false but no data available.
- // Wake ourselves to retry (tokio-rustls pattern to prevent hangs).
- cx.waker().wake_by_ref();
- Poll::Pending
+
+ // Process encrypted data - try to send alerts on error
+ let io_state = match this.session.process_new_packets() {
+ Ok(state) => state,
+ Err(e) => {
+ // Try last-gasp write to send any pending TLS alerts (tokio-rustls pattern)
+ // Make best-effort attempt to drain all pending writes without blocking.
+ // This increases the chance that the peer receives an alert explaining
+ // the protocol error. We ignore write failures to ensure the primary
+ // TLS protocol error (e) is always returned.
+ while this.session.wants_write() {
+ let mut adapter = SyncWriteAdapter {
+ io: &mut this.io,
+ cx,
+ };
+ match this.session.write_tls(&mut adapter) {
+ Ok(_) => {} // Continue draining
+ Err(ref write_err) if write_err.kind() == io::ErrorKind::WouldBlock => {
+ break;
+ }
+ Err(_) => break, // Give up on write error
+ }
+ }
+ return Poll::Ready(Err(e));
}
+ };
+
+ if io_state.plaintext_bytes_to_read() == 0 {
+ // No plaintext yet, need more data
+ continue;
}
- Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
- if !io_pending {
- // If wants_read() is satisfied, rustls will not return WouldBlock.
- // But if it does, we can try again. Wake ourselves to prevent hang.
- // Tokio's cooperative budget will prevent infinite wakeup.
- cx.waker().wake_by_ref();
+
+ // Extract plaintext from session
+ let mut reader = this.session.reader();
+ match reader.fill_buf() {
+ Ok(available) => {
+ if available.is_empty() {
+ return Poll::Ready(Err(io::Error::new(
+ io::ErrorKind::UnexpectedEof,
+ "Read zero bytes when plaintext is available",
+ )));
+ }
+ let len = buf.remaining().min(available.len());
+ buf.put_slice(&available[..len]);
+ reader.consume(len);
+ return Poll::Ready(Ok(()));
+ }
+ Err(e) => {
+ return Poll::Ready(Err(e));
}
- Poll::Pending
}
- Err(e) => Poll::Ready(Err(e)),
}
}
}
@@ -236,11 +268,7 @@ where
// Drain TLS output to TCP stream
while self.session.wants_write() {
match self.write_tls_direct(cx) {
- Poll::Ready(Ok(0)) => {
- // WriteZero: underlying socket can't accept data
- return Poll::Ready(Err(io::ErrorKind::WriteZero.into()));
- }
- Poll::Pending => {
+ Poll::Ready(Ok(0)) | Poll::Pending => {
would_block = true;
break;
}
@@ -260,20 +288,11 @@ where
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
- // Flush the session writer first (tokio-rustls pattern)
- // This ensures any buffered plaintext is moved to the TLS output buffer
- self.session.writer().flush()?;
-
// Drain all pending TLS writes
- while self.session.wants_write() {
- match self.write_tls_direct(cx) {
- Poll::Ready(Ok(0)) => {
- return Poll::Ready(Err(io::ErrorKind::WriteZero.into()));
- }
- Poll::Ready(Ok(_)) => {}
- Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
- Poll::Pending => return Poll::Pending,
- }
+ match self.drain_all_writes(cx) {
+ Poll::Ready(Ok(())) => {}
+ Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+ Poll::Pending => return Poll::Pending,
}
// Flush underlying stream
diff --git a/src/reality/reality_client_connection.rs b/src/reality/reality_client_connection.rs
index ad2f0c0..c9ff8f6 100644
@@ -847,21 +847,6 @@ impl RealityClientConnection {
!matches!(self.handshake_state, HandshakeState::Complete)
}
- /// Check if the connection wants to read more TLS data
- ///
- /// Returns true if we need more data to make progress (handshake or decryption).
- /// This mirrors rustls::Connection::wants_read().
- pub fn wants_read(&self) -> bool {
- // During handshake, we always want to read
- if self.is_handshaking() {
- return true;
- }
-
- // After handshake, we want to read if plaintext buffer is empty
- // (we need more ciphertext to decrypt)
- self.plaintext_read_buf.len() == 0
- }
-
/// Queue a close notification alert
pub fn send_close_notify(&mut self) {
// In TLS 1.3, alerts must be encrypted like application data
diff --git a/src/reality/reality_server_connection.rs b/src/reality/reality_server_connection.rs
index 425dac2..2c212b8 100644
@@ -864,21 +864,6 @@ impl RealityServerConnection {
!matches!(self.handshake_state, HandshakeState::Complete)
}
- /// Check if the connection wants to read more TLS data
- ///
- /// Returns true if we need more data to make progress (handshake or decryption).
- /// This mirrors rustls::Connection::wants_read().
- pub fn wants_read(&self) -> bool {
- // During handshake, we always want to read
- if self.is_handshaking() {
- return true;
- }
-
- // After handshake, we want to read if plaintext buffer is empty
- // (we need more ciphertext to decrypt)
- self.plaintext_read_buf.len() == 0
- }
-
/// Queue a close notification alert
pub fn send_close_notify(&mut self) {
// In TLS 1.3, alerts must be encrypted like application data