shoes 0.2.2

A multi-protocol proxy server.
diff --git a/src/crypto/crypto_connection.rs b/src/crypto/crypto_connection.rs
index 935c04c..1bfde6d 100644
--- a/src/crypto/crypto_connection.rs
+++ b/src/crypto/crypto_connection.rs
@@ -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
--- a/src/crypto/crypto_tls_stream.rs
+++ b/src/crypto/crypto_tls_stream.rs
@@ -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
--- a/src/reality/reality_client_connection.rs
+++ b/src/reality/reality_client_connection.rs
@@ -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
--- a/src/reality/reality_server_connection.rs
+++ b/src/reality/reality_server_connection.rs
@@ -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