rust: bssl-tls: Carve out tokio support into its own crate

Signed-off-by: Xiangfei Ding <xfding@google.com>
Change-Id: I88b143506455afdb6c51d3914eddc0b26a6a6964
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/94508
Reviewed-by: Adam Langley <agl@google.com>
Presubmit-BoringSSL-Verified: boringssl-scoped@luci-project-accounts.iam.gserviceaccount.com <boringssl-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 964e164..c658951 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -49,12 +49,21 @@
  "futures",
  "libc",
  "once_cell",
- "tokio",
  "tracing",
  "tracing-subscriber",
 ]
 
 [[package]]
+name = "bssl-tls-tokio"
+version = "0.1.0"
+dependencies = [
+ "bssl-tls",
+ "bssl-x509",
+ "futures",
+ "tokio",
+]
+
+[[package]]
 name = "bssl-x509"
 version = "0.1.0"
 dependencies = [
@@ -425,12 +434,12 @@
 
 [[package]]
 name = "socket2"
-version = "0.6.2"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
 dependencies = [
  "libc",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -580,16 +589,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
-dependencies = [
- "windows-targets 0.53.5",
+ "windows-targets",
 ]
 
 [[package]]
@@ -607,31 +607,14 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm 0.52.6",
- "windows_i686_msvc 0.52.6",
- "windows_x86_64_gnu 0.52.6",
- "windows_x86_64_gnullvm 0.52.6",
- "windows_x86_64_msvc 0.52.6",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.53.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
-dependencies = [
- "windows-link",
- "windows_aarch64_gnullvm 0.53.1",
- "windows_aarch64_msvc 0.53.1",
- "windows_i686_gnu 0.53.1",
- "windows_i686_gnullvm 0.53.1",
- "windows_i686_msvc 0.53.1",
- "windows_x86_64_gnu 0.53.1",
- "windows_x86_64_gnullvm 0.53.1",
- "windows_x86_64_msvc 0.53.1",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
 ]
 
 [[package]]
@@ -641,96 +624,48 @@
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
-
-[[package]]
 name = "windows_aarch64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
-name = "windows_aarch64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
-
-[[package]]
 name = "windows_i686_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
 
 [[package]]
-name = "windows_i686_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
-
-[[package]]
 name = "windows_i686_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
-name = "windows_i686_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
-
-[[package]]
 name = "windows_i686_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
-name = "windows_i686_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
-
-[[package]]
 name = "windows_x86_64_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
-name = "windows_x86_64_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
-
-[[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
-
-[[package]]
 name = "windows_x86_64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
-name = "windows_x86_64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
-
-[[package]]
 name = "zeroize"
 version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index ace391c..3a88024 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -5,6 +5,7 @@
   "bssl-rustls-adapters",
   "bssl-sys",
   "bssl-tls",
+  "bssl-tls-tokio",
   "bssl-x509",
 ]
 resolver = "3"
diff --git a/rust/bssl-tls-tokio/Cargo.toml b/rust/bssl-tls-tokio/Cargo.toml
new file mode 100644
index 0000000..33af238
--- /dev/null
+++ b/rust/bssl-tls-tokio/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "bssl-tls-tokio"
+version = "0.1.0"
+edition = "2024"
+publish = false
+license = "Apache-2.0"
+
+[dependencies]
+tokio = { version = "1.0", default-features = false, features = ["net"] }
+bssl-tls = { path = "../bssl-tls" }
+
+[dev-dependencies]
+tokio = { version = "1.0", features = ["full"] }
+futures = "0.3"
+bssl-x509 = { path = "../bssl-x509" }
diff --git a/rust/bssl-tls-tokio/src/lib.rs b/rust/bssl-tls-tokio/src/lib.rs
new file mode 100644
index 0000000..a906309
--- /dev/null
+++ b/rust/bssl-tls-tokio/src/lib.rs
@@ -0,0 +1,497 @@
+// Copyright 2026 The BoringSSL Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![deny(
+    missing_docs,
+    unsafe_op_in_unsafe_fn,
+    clippy::missing_safety_doc,
+    clippy::indexing_slicing,
+    clippy::unwrap_used,
+    clippy::panic,
+    clippy::expect_used,
+    clippy::undocumented_unsafe_blocks
+)]
+
+//! `bssl-tls` / `tokio` integration
+//!
+//! This crate provides integration helpers with `tokio` and furthermore `hyper`.
+//!
+//! # Example
+//!
+//! ```rust
+//! use bssl_tls::context::TlsContextBuilder;
+//! use bssl_tls_tokio::{TokioIo, TokioTlsConnection};
+//! use tokio::net::TcpStream;
+//! # use tokio::io::{AsyncReadExt, AsyncWriteExt};
+//! # use tokio::net::TcpListener;
+//! # use bssl_tls::credentials::{Certificate, TlsCredentialBuilder};
+//! # use bssl_x509::{certificates::X509Certificate, keys::PrivateKey, params::Trust, store::X509StoreBuilder};
+//! #
+//! # #[tokio::main]
+//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
+//! #     const CA: &[u8] = include_bytes!("../../test-data/BoringSSLCATest.crt");
+//! #     const RSA_SERVER_CERT: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.crt");
+//! #     const RSA_SERVER_KEY: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.key");
+//! #
+//! #     // Server setup
+//! #     let ca = Certificate::parse_one_from_pem(CA, None)?;
+//! #     let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, None)?;
+//! #     let server_key = PrivateKey::from_pem(RSA_SERVER_KEY, || unreachable!())?;
+//! #
+//! #     let mut server_ctx_builder = TlsContextBuilder::new_tls();
+//! #     let server_cred = {
+//! #         let mut builder = TlsCredentialBuilder::new();
+//! #         builder
+//! #             .with_certificate_chain(&[server_cert, ca.clone()])?
+//! #             .with_private_key(server_key)?;
+//! #         builder.build()
+//! #     };
+//! #     server_ctx_builder.with_credential(server_cred.unwrap())?;
+//! #     let server_ctx = server_ctx_builder.build();
+//! #
+//! #     // Client setup
+//! #     let mut client_ctx_builder = TlsContextBuilder::new_tls();
+//! #     let mut cert_store = X509StoreBuilder::new();
+//! #     cert_store
+//! #         .set_trust(Trust::SslServer)?
+//! #         .add_cert(X509Certificate::parse_one_from_pem(CA)?)?;
+//! #     let cert_store = cert_store.build();
+//! #     client_ctx_builder.with_certificate_store(&cert_store);
+//! #
+//! #     // Connection
+//! #     let listener = TcpListener::bind("127.0.0.1:0").await?;
+//! #     let addr = listener.local_addr()?;
+//! #
+//! #     let server_task = tokio::spawn(async move {
+//! #         let (stream, _) = listener.accept().await.unwrap();
+//! #         let mut conn = server_ctx.new_server_connection(None).unwrap().build();
+//! #         conn.set_io(TokioIo(stream)).unwrap();
+//! #         conn.in_handshake().unwrap().async_handshake().await.unwrap();
+//! #         
+//! #         let mut tls_stream = TokioTlsConnection::new(conn);
+//! #         let mut buf = [0u8; 1024];
+//! #         let _ = tls_stream.read(&mut buf).await.unwrap();
+//! #         tls_stream.write_all(b"hello").await.unwrap();
+//! #         tls_stream.shutdown().await.unwrap();
+//! #     });
+//! #
+//!     let stream = TcpStream::connect(addr).await?;
+//!
+//!     let client_ctx = client_ctx_builder.build();
+//!
+//!     let mut conn = client_ctx.new_client_connection(None).unwrap().build();
+//!     conn.in_handshake().unwrap().set_host("www.google.com")?;
+//!     conn.set_io(TokioIo(stream))?;
+//!
+//!     let mut tls_stream = TokioTlsConnection::new(conn);
+//!
+//!     tls_stream.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").await?;
+//!     let mut buf = [0u8; 5];
+//!     tls_stream.read_exact(&mut buf).await?;
+//!     assert_eq!(&buf, b"hello");
+//! #     tls_stream.shutdown().await?;
+//! #     server_task.await?;
+//! #     Ok(())
+//! # }
+//! ```
+
+#[cfg(unix)]
+use std::os::fd::{
+    AsRawFd,
+    RawFd, //
+};
+use std::{
+    io,
+    ops::{
+        Deref,
+        DerefMut, //
+    },
+    pin::Pin,
+    task::{
+        Context,
+        Poll,
+        ready, //
+    },
+};
+#[cfg(unix)]
+use tokio::io::unix::AsyncFd;
+use tokio::io::{
+    AsyncRead,
+    AsyncWrite,
+    Interest,
+    ReadBuf,
+    Ready, //
+};
+
+#[cfg(unix)]
+use bssl_tls::io::unix::{
+    StdDatagram,
+    UseFd, //
+};
+use bssl_tls::{
+    connection::{
+        TlsConnection,
+        lifecycle::ShutdownStatus, //
+    },
+    context::TlsMode,
+    io::{
+        AbstractReader, AbstractSocket, AbstractSocketResult, AbstractWriter, IoStatus,
+        NoAsyncContext, stdio::PollFor,
+    }, //
+};
+
+/// Translates a `std::io::Error` into an `AbstractSocketResult`.
+fn translate_stdio_err(err: io::Error) -> AbstractSocketResult {
+    match err.kind() {
+        io::ErrorKind::WouldBlock => AbstractSocketResult::Retry,
+        io::ErrorKind::ConnectionReset
+        | io::ErrorKind::ConnectionRefused
+        | io::ErrorKind::ConnectionAborted
+        | io::ErrorKind::BrokenPipe
+        | io::ErrorKind::NotConnected
+        | io::ErrorKind::UnexpectedEof => AbstractSocketResult::EndOfStream,
+        _ => AbstractSocketResult::Err(Box::new(err)),
+    }
+}
+
+/// IO object implementing [`tokio::io::AsyncRead`] or [`tokio::io::AsyncWrite`] protocols.
+pub struct TokioIo<T>(pub T);
+
+fn tokio_async_read<T: AsyncRead>(
+    mut this: Pin<&mut T>,
+    ctx: &mut Context<'_>,
+    buffer: &mut [u8],
+) -> AbstractSocketResult {
+    let mut buf = ReadBuf::new(buffer);
+    loop {
+        return match this.as_mut().poll_read(ctx, &mut buf) {
+            Poll::Ready(Ok(())) => {
+                if buf.filled().is_empty() && buf.remaining() > 0 {
+                    AbstractSocketResult::EndOfStream
+                } else {
+                    AbstractSocketResult::Ok(buf.filled().len())
+                }
+            }
+            Poll::Pending => AbstractSocketResult::Retry,
+            Poll::Ready(Err(e)) => {
+                if e.kind() == io::ErrorKind::Interrupted {
+                    continue;
+                }
+                translate_stdio_err(e)
+            }
+        };
+    }
+}
+
+fn tokio_async_write<T: AsyncWrite>(
+    mut this: Pin<&mut T>,
+    ctx: &mut Context<'_>,
+    buffer: &[u8],
+) -> AbstractSocketResult {
+    loop {
+        return match this.as_mut().poll_write(ctx, buffer) {
+            Poll::Ready(Ok(bytes)) => {
+                if buffer.is_empty() {
+                    AbstractSocketResult::Ok(0)
+                } else if bytes == 0 {
+                    AbstractSocketResult::EndOfStream
+                } else {
+                    AbstractSocketResult::Ok(bytes)
+                }
+            }
+            Poll::Pending => AbstractSocketResult::Retry,
+            Poll::Ready(Err(e)) => {
+                if e.kind() == io::ErrorKind::Interrupted {
+                    continue;
+                }
+                translate_stdio_err(e)
+            }
+        };
+    }
+}
+
+fn tokio_async_flush<T: AsyncWrite>(
+    mut this: Pin<&mut T>,
+    ctx: &mut Context<'_>,
+) -> AbstractSocketResult {
+    loop {
+        return match this.as_mut().poll_flush(ctx) {
+            Poll::Ready(Ok(())) => AbstractSocketResult::Ok(0),
+            Poll::Pending => AbstractSocketResult::Retry,
+            Poll::Ready(Err(e)) => {
+                if e.kind() == io::ErrorKind::Interrupted {
+                    continue;
+                }
+                translate_stdio_err(e)
+            }
+        };
+    }
+}
+
+impl<T: AsyncRead + Send + Unpin> AbstractReader for TokioIo<T> {
+    fn read(
+        &mut self,
+        async_ctx: Option<&mut Context<'_>>,
+        buffer: &mut [u8],
+    ) -> AbstractSocketResult {
+        let Some(ctx) = async_ctx else {
+            return AbstractSocketResult::Err(Box::new(NoAsyncContext));
+        };
+        tokio_async_read(Pin::new(&mut self.0), ctx, buffer)
+    }
+}
+
+impl<T: AsyncWrite + Send + Unpin> AbstractWriter for TokioIo<T> {
+    fn write(
+        &mut self,
+        async_ctx: Option<&mut Context<'_>>,
+        buffer: &[u8],
+    ) -> AbstractSocketResult {
+        let Some(ctx) = async_ctx else {
+            return AbstractSocketResult::Err(Box::new(NoAsyncContext));
+        };
+        tokio_async_write(Pin::new(&mut self.0), ctx, buffer)
+    }
+
+    fn flush(&mut self, async_ctx: Option<&mut Context<'_>>) -> AbstractSocketResult {
+        let Some(ctx) = async_ctx else {
+            return AbstractSocketResult::Err(Box::new(NoAsyncContext));
+        };
+        tokio_async_flush(Pin::new(&mut self.0), ctx)
+    }
+}
+
+impl<T: AsyncRead + AsyncWrite + Send + Unpin> AbstractSocket for TokioIo<T> {}
+
+#[cfg(unix)]
+/// Reactor that operate over file descriptor.
+pub struct TokioOverFd(AsyncFd<RawFd>);
+
+#[cfg(unix)]
+/// Construct a datagram IO object driven by [`tokio`].
+pub fn new_std_datagram_with_tokio<T: AsRawFd>(
+    inner: T,
+) -> Result<StdDatagram<UseFd<T>, TokioOverFd>, io::Error> {
+    let reactor = TokioOverFd::new(inner.as_raw_fd())?;
+    let fd = UseFd(inner);
+    Ok(StdDatagram::new(fd, reactor))
+}
+
+#[cfg(unix)]
+impl TokioOverFd {
+    /// A trivial constructor to signal use of `tokio` reactor and register events with
+    /// file descriptors.
+    pub fn new(fd: RawFd) -> Result<Self, io::Error> {
+        Ok(Self(AsyncFd::try_with_interest(
+            fd,
+            Interest::READABLE | Interest::WRITABLE | Interest::ERROR,
+        )?))
+    }
+}
+
+#[cfg(unix)]
+impl<T> PollFor<T> for TokioOverFd {
+    fn poll_read(&mut self, async_ctx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
+        match ready!(self.0.poll_read_ready_mut(async_ctx)) {
+            Ok(mut guard) => {
+                guard.clear_ready_matching(Ready::READABLE);
+                Poll::Ready(Ok(()))
+            }
+            Err(e) => Poll::Ready(Err(e)),
+        }
+    }
+
+    fn poll_write(&mut self, async_ctx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
+        match ready!(self.0.poll_write_ready_mut(async_ctx)) {
+            Ok(mut guard) => {
+                guard.clear_ready_matching(Ready::WRITABLE);
+                Poll::Ready(Ok(()))
+            }
+            Err(e) => Poll::Ready(Err(e)),
+        }
+    }
+}
+
+/// Wrapper for datagram sockets to satisfy orphan rule.
+pub struct TokioDatagramIo<T>(pub T);
+
+macro_rules! gen_impl_datagram {
+    ($ty:ty) => {
+        impl AbstractReader for TokioDatagramIo<$ty> {
+            fn read(
+                &mut self,
+                async_ctx: Option<&mut Context<'_>>,
+                buffer: &mut [u8],
+            ) -> AbstractSocketResult {
+                let Some(cx) = async_ctx else {
+                    return AbstractSocketResult::Err(Box::new(NoAsyncContext));
+                };
+                let mut buf = ReadBuf::new(buffer);
+                match self.0.poll_recv(cx, &mut buf) {
+                    Poll::Pending => AbstractSocketResult::Retry,
+                    Poll::Ready(Ok(_)) => AbstractSocketResult::Ok(buf.filled().len()),
+                    Poll::Ready(Err(e)) => translate_stdio_err(e),
+                }
+            }
+        }
+
+        impl AbstractWriter for TokioDatagramIo<$ty> {
+            fn write(
+                &mut self,
+                async_ctx: Option<&mut Context<'_>>,
+                buf: &[u8],
+            ) -> AbstractSocketResult {
+                let Some(cx) = async_ctx else {
+                    return AbstractSocketResult::Err(Box::new(NoAsyncContext));
+                };
+                match self.0.poll_send(cx, buf) {
+                    Poll::Pending => AbstractSocketResult::Retry,
+                    Poll::Ready(Ok(len)) => AbstractSocketResult::Ok(len),
+                    Poll::Ready(Err(e)) => translate_stdio_err(e),
+                }
+            }
+
+            fn flush(&mut self, _: Option<&mut Context<'_>>) -> AbstractSocketResult {
+                AbstractSocketResult::Ok(0)
+            }
+        }
+
+        impl AbstractSocket for TokioDatagramIo<$ty> {}
+    };
+}
+
+gen_impl_datagram!(tokio::net::UdpSocket);
+#[cfg(unix)]
+gen_impl_datagram!(tokio::net::UnixDatagram);
+
+/// A wrapper around [`TlsConnection`] that implements Tokio's async I/O traits.
+pub struct TokioTlsConnection<Role> {
+    inner: TlsConnection<Role, TlsMode>,
+}
+
+impl<Role> TokioTlsConnection<Role> {
+    /// Construct a new wrapper.
+    pub fn new(inner: TlsConnection<Role, TlsMode>) -> Self {
+        Self { inner }
+    }
+
+    /// Consume the wrapper and return the inner connection.
+    pub fn into_inner(self) -> TlsConnection<Role, TlsMode> {
+        self.inner
+    }
+}
+
+impl<Role> Deref for TokioTlsConnection<Role> {
+    type Target = TlsConnection<Role, TlsMode>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
+
+impl<Role> DerefMut for TokioTlsConnection<Role> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.inner
+    }
+}
+
+impl<R> AsyncRead for TokioTlsConnection<R> {
+    fn poll_read(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &mut ReadBuf<'_>,
+    ) -> Poll<io::Result<()>> {
+        // Note: This assumes `aread_inner` is made public in `bssl-tls`.
+        let status = match self
+            .inner
+            .as_pin_mut()
+            .aread_inner(buf.initialize_unfilled(), cx)
+        {
+            Ok(Some(status)) => status,
+            Ok(None) => return Poll::Pending,
+            Err(e) => return Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))),
+        };
+        match status {
+            IoStatus::Ok(bytes) => {
+                buf.advance(bytes);
+                Poll::Ready(Ok(()))
+            }
+            IoStatus::EndOfStream => Poll::Ready(Ok(())),
+            _ => Poll::Ready(Err(io::Error::new(
+                io::ErrorKind::Other,
+                "Unexpected I/O status",
+            ))),
+        }
+    }
+}
+
+impl<R> AsyncWrite for TokioTlsConnection<R> {
+    fn poll_write(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        buf: &[u8],
+    ) -> Poll<io::Result<usize>> {
+        // Note: This assumes `awrite_inner` is made public in `bssl-tls`.
+        let status = match self.inner.as_pin_mut().awrite_inner(buf, cx) {
+            Ok(Some(status)) => status,
+            Ok(None) => return Poll::Pending,
+            Err(e) => return Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))),
+        };
+        match status {
+            IoStatus::Ok(bytes) => Poll::Ready(Ok(bytes)),
+            IoStatus::EndOfStream => Poll::Ready(Ok(0)),
+            _ => Poll::Ready(Err(io::Error::new(
+                io::ErrorKind::Other,
+                "Unexpected I/O status",
+            ))),
+        }
+    }
+
+    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+        // Note: This assumes `aflush_inner` is made public in `bssl-tls`.
+        let status = match self.inner.as_pin_mut().aflush_inner(cx) {
+            Ok(Some(status)) => status,
+            Ok(None) => return Poll::Pending,
+            Err(e) => return Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))),
+        };
+        match status {
+            IoStatus::Ok(_) => Poll::Ready(Ok(())),
+            IoStatus::EndOfStream => Poll::Ready(Ok(())),
+            _ => Poll::Ready(Err(io::Error::new(
+                io::ErrorKind::Other,
+                "Unexpected I/O status",
+            ))),
+        }
+    }
+
+    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+        // Note: This assumes `ashutdown_inner` is made public in `bssl-tls`.
+        match self.inner.as_pin_mut().ashutdown_inner(cx) {
+            Ok(Some(ShutdownStatus::CloseNotifyReceived)) => Poll::Ready(Ok(())),
+            Ok(Some(ShutdownStatus::RemainingApplicationData)) => Poll::Ready(Err(io::Error::new(
+                io::ErrorKind::Other,
+                "caller needs to drain application data before polling on shutdown again",
+            ))),
+            Ok(Some(ShutdownStatus::EndOfStream)) => Poll::Ready(Err(io::Error::new(
+                io::ErrorKind::UnexpectedEof,
+                "unexpected eof while waiting for peek close_notify",
+            ))),
+            Ok(Some(ShutdownStatus::CloseNotifyPosted)) => {
+                unreachable!()
+            }
+            Ok(None) => Poll::Pending,
+            Err(e) => Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))),
+        }
+    }
+}
diff --git a/rust/bssl-tls-tokio/tests/datagram.rs b/rust/bssl-tls-tokio/tests/datagram.rs
new file mode 100644
index 0000000..f8dec3d
--- /dev/null
+++ b/rust/bssl-tls-tokio/tests/datagram.rs
@@ -0,0 +1,173 @@
+// Copyright 2026 The BoringSSL Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![cfg(unix)]
+
+use bssl_tls::{
+    connection::{
+        Client,
+        Server,
+        TlsConnection, //
+    },
+    context::{
+        DtlsMode,
+        TlsContextBuilder, //
+    },
+    credentials::{
+        Certificate,
+        TlsCredentialBuilder, //
+    },
+    errors::Error, //
+};
+use bssl_tls_tokio::{
+    TokioDatagramIo,
+    new_std_datagram_with_tokio, //
+};
+use bssl_x509::{
+    certificates::X509Certificate,
+    keys::PrivateKey,
+    params::Trust,
+    store::X509StoreBuilder, //
+};
+
+const CA: &[u8] = include_bytes!("../../test-data/BoringSSLCATest.crt");
+const RSA_SERVER_CERT: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.crt");
+const RSA_SERVER_KEY: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.key");
+
+fn dumb_dtls_server_client() -> Result<
+    (
+        TlsConnection<Server, DtlsMode>,
+        TlsConnection<Client, DtlsMode>,
+    ),
+    Error,
+> {
+    let ca = Certificate::parse_one_from_pem(CA, None)?;
+    let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, None)?;
+    let server_key = PrivateKey::from_pem(RSA_SERVER_KEY, || unreachable!())?;
+
+    let mut server_ctx_builder = TlsContextBuilder::new_dtls();
+    let server_cred = {
+        let mut builder = TlsCredentialBuilder::new();
+        builder
+            .with_certificate_chain(&[server_cert, ca])?
+            .with_private_key(server_key)?;
+        builder.build()
+    };
+    server_ctx_builder.with_credential(server_cred.unwrap())?;
+    let server_ctx = server_ctx_builder.build();
+    let server_conn = server_ctx.new_server_connection(None)?.build();
+
+    let mut client_ctx_builder = TlsContextBuilder::new_dtls();
+    let ca = X509Certificate::parse_one_from_pem(CA)?;
+    let mut cert_store = X509StoreBuilder::new();
+    cert_store.set_trust(Trust::SslServer)?.add_cert(ca)?;
+    let cert_store = cert_store.build();
+    client_ctx_builder.with_certificate_store(&cert_store);
+    let client_ctx = client_ctx_builder.build();
+    let client_conn = client_ctx.new_client_connection(None)?.build();
+
+    Ok((server_conn, client_conn))
+}
+
+async fn async_ping_pong(
+    mut server_conn: TlsConnection<Server, DtlsMode>,
+    mut client_conn: TlsConnection<Client, DtlsMode>,
+) -> Result<(), Error> {
+    use bssl_tls::io::IoStatus;
+    use std::time::Duration;
+
+    let task = tokio::spawn(async move {
+        server_conn
+            .in_handshake()
+            .unwrap()
+            .async_handshake()
+            .await?;
+
+        let mut message = [0; 21];
+        let mut read_bytes = 0;
+        while read_bytes < 21 {
+            match server_conn
+                .as_pin_mut()
+                .async_read(&mut message[read_bytes..])
+                .await?
+            {
+                IoStatus::Ok(n) => read_bytes += n,
+                IoStatus::EndOfStream => break,
+                _ => {}
+            }
+        }
+        assert_eq!(&message, b"BoringSSL is awesome!");
+        tokio::time::sleep(Duration::from_secs(2)).await;
+        server_conn
+            .as_pin_mut()
+            .async_write(b"Oh yeah definitely!")
+            .await?;
+        server_conn.as_pin_mut().async_shutdown().await?;
+        Ok::<_, Error>(())
+    });
+
+    client_conn
+        .in_handshake()
+        .unwrap()
+        .async_handshake()
+        .await?;
+    client_conn
+        .as_pin_mut()
+        .async_write(b"BoringSSL is awesome!")
+        .await?;
+    let mut message = [0; 19];
+    let mut read_bytes = 0;
+    while read_bytes < 19 {
+        match client_conn
+            .as_pin_mut()
+            .async_read(&mut message[read_bytes..])
+            .await?
+        {
+            IoStatus::Ok(n) => read_bytes += n,
+            IoStatus::EndOfStream => break,
+            _ => {}
+        }
+    }
+    assert_eq!(&message, b"Oh yeah definitely!");
+    assert!(matches!(
+        client_conn.as_pin_mut().async_shutdown().await,
+        Ok(_) | Err(Error::Io(bssl_tls::errors::IoError::EndOfStream))
+    ));
+    task.await.unwrap()?;
+    Ok(())
+}
+
+#[tokio::test]
+async fn async_dtls() -> Result<(), Error> {
+    let (mut server_conn, mut client_conn) = dumb_dtls_server_client().unwrap();
+    let (server_sock, client_sock) = tokio::net::UnixDatagram::pair().unwrap();
+    server_conn.set_io(TokioDatagramIo(server_sock)).unwrap();
+    client_conn.set_io(TokioDatagramIo(client_sock)).unwrap();
+
+    async_ping_pong(server_conn, client_conn).await
+}
+
+#[tokio::test]
+async fn async_dtls_over_fd() -> Result<(), Error> {
+    let (mut server_conn, mut client_conn) = dumb_dtls_server_client().unwrap();
+    let (server_sock, client_sock) = std::os::unix::net::UnixDatagram::pair().unwrap();
+    server_sock.set_nonblocking(true).unwrap();
+    client_sock.set_nonblocking(true).unwrap();
+    let server_sock = new_std_datagram_with_tokio(server_sock).unwrap();
+    let client_sock = new_std_datagram_with_tokio(client_sock).unwrap();
+    server_conn.set_io(server_sock).unwrap();
+    client_conn.set_io(client_sock).unwrap();
+
+    async_ping_pong(server_conn, client_conn).await
+}
diff --git a/rust/bssl-tls-tokio/tests/transport.rs b/rust/bssl-tls-tokio/tests/transport.rs
new file mode 100644
index 0000000..2be385d
--- /dev/null
+++ b/rust/bssl-tls-tokio/tests/transport.rs
@@ -0,0 +1,128 @@
+// Copyright 2026 The BoringSSL Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![cfg(unix)]
+
+use bssl_tls::{
+    connection::{
+        Client,
+        Server,
+        TlsConnection, //
+    },
+    context::TlsContextBuilder,
+    credentials::{
+        Certificate,
+        TlsCredentialBuilder, //
+    },
+    errors::Error, //
+};
+use bssl_tls_tokio::{
+    TokioIo,
+    TokioTlsConnection, //
+};
+use bssl_x509::{
+    certificates::X509Certificate,
+    keys::PrivateKey,
+    params::Trust,
+    store::X509StoreBuilder, //
+};
+use tokio::io::{
+    AsyncReadExt,
+    AsyncWriteExt, //
+};
+
+use futures::future::FutureExt;
+
+const CA: &[u8] = include_bytes!("../../test-data/BoringSSLCATest.crt");
+const RSA_SERVER_CERT: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.crt");
+const RSA_SERVER_KEY: &[u8] = include_bytes!("../../test-data/BoringSSLServerTest-RSA.key");
+
+fn dumb_server_client() -> Result<(TlsConnection<Server>, TlsConnection<Client>), Error> {
+    let ca = Certificate::parse_one_from_pem(CA, None)?;
+    let server_cert = Certificate::parse_one_from_pem(RSA_SERVER_CERT, None)?;
+    let server_key = PrivateKey::from_pem(RSA_SERVER_KEY, || unreachable!())?;
+
+    let mut server_ctx_builder = TlsContextBuilder::new_tls();
+    let server_cred = {
+        let mut builder = TlsCredentialBuilder::new();
+        builder
+            .with_certificate_chain(&[server_cert, ca])?
+            .with_private_key(server_key)?;
+        builder.build()
+    };
+    server_ctx_builder.with_credential(server_cred.unwrap())?;
+    let server_ctx = server_ctx_builder.build();
+    let server_conn = server_ctx.new_server_connection(None)?.build();
+
+    let mut client_ctx_builder = TlsContextBuilder::new_tls();
+    let mut cert_store = X509StoreBuilder::new();
+    cert_store
+        .set_trust(Trust::SslServer)?
+        .add_cert(X509Certificate::parse_one_from_pem(&CA)?)?;
+    let cert_store = cert_store.build();
+    client_ctx_builder.with_certificate_store(&cert_store);
+    let client_ctx = client_ctx_builder.build();
+    let client_conn = client_ctx.new_client_connection(None)?.build();
+
+    Ok((server_conn, client_conn))
+}
+
+#[tokio::test]
+async fn tokio_io() -> Result<(), Error> {
+    let (server_conn, client_conn) = dumb_server_client()?;
+
+    let (server_tx, server_rx) = tokio::net::unix::pipe::pipe().unwrap();
+    let (client_tx, client_rx) = tokio::net::unix::pipe::pipe().unwrap();
+    let (send_signal, recv_signal) = tokio::sync::oneshot::channel();
+    let server_rx = TokioIo(server_rx);
+    let server_tx = TokioIo(server_tx);
+    let client_rx = TokioIo(client_rx);
+    let client_tx = TokioIo(client_tx);
+
+    let mut server_conn = TokioTlsConnection::new(server_conn);
+    let mut client_conn = TokioTlsConnection::new(client_conn);
+
+    server_conn.set_split_io(client_rx, server_tx)?;
+    client_conn.set_split_io(server_rx, client_tx)?;
+
+    let thread = tokio::spawn(async move {
+        let mut message = [0; 21];
+        server_conn.read_exact(&mut message).await.unwrap();
+        assert_eq!(message, *b"BoringSSL is awesome!");
+        server_conn.write_all(b"Oh yeah definitely!").await.unwrap();
+
+        // The original test used sync_shutdown on established()
+        // We use wrapper's shutdown instead.
+
+        let res = server_conn.shutdown().now_or_never();
+        assert!(res.is_none(), "{res:?}");
+
+        // Make the client progress
+        send_signal.send(()).unwrap();
+
+        server_conn.shutdown().await.unwrap();
+    });
+    client_conn
+        .write_all(b"BoringSSL is awesome!")
+        .await
+        .unwrap();
+    recv_signal.await.unwrap();
+    let mut message = [0; 19];
+    client_conn.read_exact(&mut message).await.unwrap();
+    assert_eq!(message, *b"Oh yeah definitely!");
+    client_conn.shutdown().await.unwrap();
+    thread.await.unwrap();
+
+    Ok(())
+}
diff --git a/rust/bssl-tls/Cargo.toml b/rust/bssl-tls/Cargo.toml
index 6820c1a..9ca2794 100644
--- a/rust/bssl-tls/Cargo.toml
+++ b/rust/bssl-tls/Cargo.toml
@@ -20,10 +20,7 @@
 [dependencies.bssl-x509]
 path = "../bssl-x509"
 
-[dependencies.tokio]
-version = "1.0"
-optional = true
-default-features = false
+
 
 [dependencies.once_cell]
 version = "1"
@@ -39,18 +36,13 @@
 tracing-subscriber = "0.3"
 futures = "0.3"
 
-[dev-dependencies.tokio]
-version = "1.0"
-features = ["full"]
+
 
 [features]
 default = ["std"]
 # `std` depends on the Rust `std` crate, but adds some useful trait impls if
 # available.
 std = ["bssl-crypto/std", "once_cell/std", "libc"]
-# `tokio_io` enables interop with `tokio` for async I/O operations
-tokio_io = ["dep:tokio", "std"]
-# `tokio_net` enables interop with `tokio` for async network I/O operations
-tokio_net = ["tokio_io", "tokio/net", "std"]
+
 # `mlalgs` enables ML-KEM and ML-DSA support. This requires Rust 1.82.
 mlalgs = ["bssl-crypto/mlalgs"]
diff --git a/rust/bssl-tls/src/connection/io.rs b/rust/bssl-tls/src/connection/io.rs
index 80b61aa..b81135f 100644
--- a/rust/bssl-tls/src/connection/io.rs
+++ b/rust/bssl-tls/src/connection/io.rs
@@ -224,7 +224,8 @@
         self.get_connection_methods().set_pending_reason(reason);
         Ok(None)
     }
-    fn aread_inner(
+    #[doc(hidden)]
+    pub fn aread_inner(
         self: Pin<&mut Self>,
         buffer: &mut [u8],
         cx: &mut Context<'_>,
@@ -233,7 +234,8 @@
         self.do_async_io(cx, move |this| this.sync_read(&mut buffer))
     }
 
-    fn awrite_inner(
+    #[doc(hidden)]
+    pub fn awrite_inner(
         self: Pin<&mut Self>,
         buffer: &[u8],
         cx: &mut Context<'_>,
@@ -241,7 +243,11 @@
         self.do_async_io(cx, move |this| this.sync_write(buffer))
     }
 
-    fn aflush_inner(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<Option<IoStatus>, Error> {
+    #[doc(hidden)]
+    pub fn aflush_inner(
+        self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+    ) -> Result<Option<IoStatus>, Error> {
         self.do_async_io(cx, move |this| this.flush())
     }
 
@@ -289,7 +295,8 @@
         })
     }
 
-    fn ashutdown_inner(
+    #[doc(hidden)]
+    pub fn ashutdown_inner(
         mut self: Pin<&mut Self>,
         cx: &mut Context<'_>,
     ) -> Result<Option<ShutdownStatus>, Error> {
@@ -326,6 +333,3 @@
 
 #[cfg(feature = "std")]
 mod stdio;
-
-#[cfg(feature = "tokio_io")]
-mod tokio_io_impl;
diff --git a/rust/bssl-tls/src/connection/io/tokio_io_impl.rs b/rust/bssl-tls/src/connection/io/tokio_io_impl.rs
deleted file mode 100644
index 40a3c34..0000000
--- a/rust/bssl-tls/src/connection/io/tokio_io_impl.rs
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright 2026 The BoringSSL Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-use std::{
-    fmt::Display,
-    io,
-    pin::Pin,
-    task::{
-        Context,
-        Poll, //
-    }, //
-};
-
-use tokio::io::{
-    AsyncRead,
-    AsyncWrite,
-    ReadBuf, //
-};
-
-use super::{
-    IoStatus,
-    TlsConnectionRef,
-    TlsMode, //
-};
-use crate::connection::lifecycle::ShutdownStatus;
-
-#[inline]
-fn handle_io_status<T>(status: IoStatus) -> Poll<io::Result<T>> {
-    match status {
-        IoStatus::EndOfStream | IoStatus::Ok(_) => unreachable!(),
-        IoStatus::Retry(_) => unreachable!("we should have handled retry earlier"),
-        IoStatus::Err => Poll::Ready(Err(io::Error::new(
-            io::ErrorKind::Other,
-            "The transport has failed the I/O operation",
-        ))),
-        IoStatus::Empty => Poll::Ready(Err(io::Error::new(
-            io::ErrorKind::ConnectionReset,
-            "TlsConnection has no backing transport or the transport has panicked",
-        ))),
-    }
-}
-
-impl<R> AsyncRead for TlsConnectionRef<R, TlsMode> {
-    fn poll_read(
-        mut self: Pin<&mut Self>,
-        cx: &mut Context<'_>,
-        buf: &mut ReadBuf<'_>,
-    ) -> Poll<io::Result<()>> {
-        let status = match self.as_mut().aread_inner(buf.initialize_unfilled(), cx) {
-            Ok(status) => status,
-            Err(e) => return Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))),
-        };
-        match status {
-            None => Poll::Pending,
-            Some(IoStatus::Ok(bytes)) => {
-                buf.advance(bytes);
-                Poll::Ready(Ok(()))
-            }
-            Some(IoStatus::EndOfStream) => Poll::Ready(Ok(())),
-            Some(status) => handle_io_status(status),
-        }
-    }
-}
-
-impl<R> AsyncWrite for TlsConnectionRef<R, TlsMode> {
-    fn poll_write(
-        mut self: Pin<&mut Self>,
-        cx: &mut Context<'_>,
-        buf: &[u8],
-    ) -> Poll<io::Result<usize>> {
-        let status = match self.as_mut().awrite_inner(buf, cx) {
-            Ok(status) => status,
-            Err(e) => return Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))),
-        };
-        match status {
-            None => Poll::Pending,
-            Some(IoStatus::Ok(bytes)) => Poll::Ready(Ok(bytes)),
-            Some(IoStatus::EndOfStream) => Poll::Ready(Ok(0)),
-            Some(status) => handle_io_status(status),
-        }
-    }
-
-    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
-        let status = match self.as_mut().aflush_inner(cx) {
-            Ok(status) => status,
-            Err(e) => return Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))),
-        };
-        match status {
-            None => Poll::Pending,
-            Some(IoStatus::Ok(_)) => Poll::Ready(Ok(())),
-            Some(IoStatus::EndOfStream) => Poll::Ready(Ok(())),
-            Some(status) => handle_io_status(status),
-        }
-    }
-
-    /// # Warning ⚠️
-    ///
-    /// Calling this may fail with error [`NeedToDrainAppData`].
-    /// This is not a hard error.
-    /// Caller should continue reading from the connection until the end of the stream.
-    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
-        match self.as_mut().ashutdown_inner(cx) {
-            Ok(Some(ShutdownStatus::CloseNotifyReceived)) => Poll::Ready(Ok(())),
-            Ok(Some(ShutdownStatus::RemainingApplicationData)) => Poll::Ready(Err(io::Error::new(
-                io::ErrorKind::Other,
-                NeedToDrainAppData,
-            ))),
-            Ok(Some(ShutdownStatus::EndOfStream)) => Poll::Ready(Err(io::Error::new(
-                io::ErrorKind::UnexpectedEof,
-                "unexpected eof while waiting for peek close_notify",
-            ))),
-            Ok(Some(ShutdownStatus::CloseNotifyPosted)) => unreachable!(),
-            Ok(None) => Poll::Pending,
-            Err(e) => Poll::Ready(Err(io::Error::new(io::ErrorKind::Other, e))),
-        }
-    }
-}
-
-#[derive(Debug)]
-pub struct NeedToDrainAppData;
-
-impl Display for NeedToDrainAppData {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "caller needs to drain application data before polling on shutdown again"
-        )
-    }
-}
-
-impl std::error::Error for NeedToDrainAppData {}
diff --git a/rust/bssl-tls/src/io.rs b/rust/bssl-tls/src/io.rs
index ce0c5d7..d454e2f 100644
--- a/rust/bssl-tls/src/io.rs
+++ b/rust/bssl-tls/src/io.rs
@@ -49,9 +49,7 @@
 pub mod stdio;
 /// Synchronous I/O adapters.
 pub mod sync_io;
-/// Tokio-based async I/O adapters.
-#[cfg(feature = "tokio_net")]
-pub mod tokio;
+
 #[cfg(all(unix, feature = "std"))]
 pub mod unix;
 
diff --git a/rust/bssl-tls/src/io/tokio.rs b/rust/bssl-tls/src/io/tokio.rs
deleted file mode 100644
index 4953edf..0000000
--- a/rust/bssl-tls/src/io/tokio.rs
+++ /dev/null
@@ -1,131 +0,0 @@
-// Copyright 2026 The BoringSSL Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#![cfg(feature = "tokio_net")]
-
-use std::{
-    pin::Pin,
-    task::{
-        Context,
-        Poll, //
-    }, //
-};
-
-use tokio::io::{
-    AsyncRead,
-    AsyncWrite,
-    ReadBuf, //
-};
-
-use crate::io::{
-    AbstractReader,
-    AbstractSocket,
-    AbstractSocketResult,
-    AbstractWriter,
-    NoAsyncContext, //
-};
-
-/// IO object implementing [`tokio::io::AsyncRead`] or [`tokio::io::AsyncWrite`] protocol.
-pub struct TokioIo<T>(pub T);
-
-fn tokio_async_read<T: AsyncRead>(
-    mut this: Pin<&mut T>,
-    ctx: &mut Context<'_>,
-    buffer: &mut [u8],
-) -> AbstractSocketResult {
-    let mut buf = ReadBuf::new(buffer);
-    loop {
-        return match this.as_mut().poll_read(ctx, &mut buf) {
-            Poll::Ready(Ok(())) => {
-                if buf.filled().is_empty() && buf.remaining() > 0 {
-                    AbstractSocketResult::EndOfStream
-                } else {
-                    AbstractSocketResult::Ok(buf.filled().len())
-                }
-            }
-            Poll::Pending => AbstractSocketResult::Retry,
-            Poll::Ready(Err(e)) => crate::retry_on_interrupt!(e),
-        };
-    }
-}
-
-fn tokio_async_write<T: AsyncWrite>(
-    mut this: Pin<&mut T>,
-    ctx: &mut Context<'_>,
-    buffer: &[u8],
-) -> AbstractSocketResult {
-    loop {
-        return match this.as_mut().poll_write(ctx, buffer) {
-            Poll::Ready(Ok(bytes)) => {
-                if buffer.is_empty() {
-                    AbstractSocketResult::Ok(0)
-                } else if bytes == 0 {
-                    AbstractSocketResult::EndOfStream
-                } else {
-                    AbstractSocketResult::Ok(bytes)
-                }
-            }
-            Poll::Pending => AbstractSocketResult::Retry,
-            Poll::Ready(Err(e)) => crate::retry_on_interrupt!(e),
-        };
-    }
-}
-
-fn tokio_async_flush<T: AsyncWrite>(
-    mut this: Pin<&mut T>,
-    ctx: &mut Context<'_>,
-) -> AbstractSocketResult {
-    loop {
-        return match this.as_mut().poll_flush(ctx) {
-            Poll::Ready(Ok(())) => AbstractSocketResult::Ok(0),
-            Poll::Pending => AbstractSocketResult::Retry,
-            Poll::Ready(Err(e)) => crate::retry_on_interrupt!(e),
-        };
-    }
-}
-
-impl<T: AsyncRead + Send + Unpin> AbstractReader for TokioIo<T> {
-    fn read(
-        &mut self,
-        async_ctx: Option<&mut Context<'_>>,
-        buffer: &mut [u8],
-    ) -> AbstractSocketResult {
-        let Some(ctx) = async_ctx else {
-            return AbstractSocketResult::Err(Box::new(NoAsyncContext));
-        };
-        tokio_async_read(Pin::new(&mut self.0), ctx, buffer)
-    }
-}
-
-impl<T: AsyncWrite + Send + Unpin> AbstractWriter for TokioIo<T> {
-    fn write(
-        &mut self,
-        async_ctx: Option<&mut Context<'_>>,
-        buffer: &[u8],
-    ) -> AbstractSocketResult {
-        let Some(ctx) = async_ctx else {
-            return AbstractSocketResult::Err(Box::new(NoAsyncContext));
-        };
-        tokio_async_write(Pin::new(&mut self.0), ctx, buffer)
-    }
-
-    fn flush(&mut self, async_ctx: Option<&mut Context<'_>>) -> AbstractSocketResult {
-        let Some(ctx) = async_ctx else {
-            return AbstractSocketResult::Err(Box::new(NoAsyncContext));
-        };
-        tokio_async_flush(Pin::new(&mut self.0), ctx)
-    }
-}
-
-impl<T: AsyncRead + AsyncWrite + Send + Unpin> AbstractSocket for TokioIo<T> {}
diff --git a/rust/bssl-tls/src/io/unix.rs b/rust/bssl-tls/src/io/unix.rs
index 8f3627a..5dd4a4b 100644
--- a/rust/bssl-tls/src/io/unix.rs
+++ b/rust/bssl-tls/src/io/unix.rs
@@ -41,11 +41,6 @@
     DatagramSocket,
     PollFor, //
 };
-use crate::io::sync_io::translate_stdio_err;
-#[cfg(feature = "tokio_net")]
-pub use crate::io::tokio::TokioIo;
-#[cfg(feature = "tokio_net")]
-pub use tokio_impl::TokioOverFd;
 
 use super::{
     AbstractReader,
@@ -54,9 +49,6 @@
     AbstractWriter, //
 };
 
-#[cfg(feature = "tokio_net")]
-mod tokio_impl;
-
 // ============
 // Datagrams
 // ============
diff --git a/rust/bssl-tls/src/io/unix/tokio_impl.rs b/rust/bssl-tls/src/io/unix/tokio_impl.rs
deleted file mode 100644
index 037d7af..0000000
--- a/rust/bssl-tls/src/io/unix/tokio_impl.rs
+++ /dev/null
@@ -1,106 +0,0 @@
-use std::{
-    io,
-    os::fd::{AsRawFd, RawFd},
-    task::{Context, Poll, ready},
-};
-
-use tokio::io::{Interest, ReadBuf, Ready, unix::AsyncFd};
-
-use crate::io::{
-    AbstractReader, AbstractSocket, AbstractSocketResult, AbstractWriter, NoAsyncContext,
-    stdio::PollFor,
-    unix::{StdDatagram, UseFd, translate_stdio_err},
-};
-
-/// Reactor that operate over file descriptor.
-pub struct TokioOverFd(AsyncFd<RawFd>);
-
-impl<T: AsRawFd> StdDatagram<UseFd<T>, TokioOverFd> {
-    /// Construct a datagram IO object driven by [`tokio`].
-    pub fn new_with_tokio(inner: T) -> Result<Self, io::Error> {
-        let reactor = TokioOverFd::new(inner.as_raw_fd())?;
-        let fd = UseFd(inner);
-        Ok(Self::new(fd, reactor))
-    }
-}
-
-impl TokioOverFd {
-    /// A trivial constructor to signal use of `tokio` reactor and register events with
-    /// file descriptors.
-    pub fn new(fd: RawFd) -> Result<Self, io::Error> {
-        Ok(Self(AsyncFd::try_with_interest(
-            fd,
-            Interest::READABLE | Interest::WRITABLE | Interest::ERROR,
-        )?))
-    }
-}
-
-impl<T> PollFor<T> for TokioOverFd {
-    fn poll_read(&mut self, async_ctx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
-        match ready!(self.0.poll_read_ready_mut(async_ctx)) {
-            Ok(mut guard) => {
-                guard.clear_ready_matching(Ready::READABLE);
-                Poll::Ready(Ok(()))
-            }
-            Err(e) => Poll::Ready(Err(e)),
-        }
-    }
-
-    fn poll_write(&mut self, async_ctx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
-        match ready!(self.0.poll_write_ready_mut(async_ctx)) {
-            Ok(mut guard) => {
-                guard.clear_ready_matching(Ready::WRITABLE);
-                Poll::Ready(Ok(()))
-            }
-            Err(e) => Poll::Ready(Err(e)),
-        }
-    }
-}
-
-macro_rules! gen_impl_datagram {
-    ($ty:ty) => {
-        impl AbstractReader for $ty {
-            fn read(
-                &mut self,
-                async_ctx: Option<&mut Context<'_>>,
-                buffer: &mut [u8],
-            ) -> AbstractSocketResult {
-                let Some(cx) = async_ctx else {
-                    return AbstractSocketResult::Err(Box::new(NoAsyncContext));
-                };
-                let mut buf = ReadBuf::new(buffer);
-                match self.poll_recv(cx, &mut buf) {
-                    Poll::Pending => AbstractSocketResult::Retry,
-                    Poll::Ready(Ok(_)) => AbstractSocketResult::Ok(buf.filled().len()),
-                    Poll::Ready(Err(e)) => translate_stdio_err(e),
-                }
-            }
-        }
-
-        impl AbstractWriter for $ty {
-            fn write(
-                &mut self,
-                async_ctx: Option<&mut Context<'_>>,
-                buf: &[u8],
-            ) -> AbstractSocketResult {
-                let Some(cx) = async_ctx else {
-                    return AbstractSocketResult::Err(Box::new(NoAsyncContext));
-                };
-                match self.poll_send(cx, buf) {
-                    Poll::Pending => AbstractSocketResult::Retry,
-                    Poll::Ready(Ok(len)) => AbstractSocketResult::Ok(len),
-                    Poll::Ready(Err(e)) => translate_stdio_err(e),
-                }
-            }
-
-            fn flush(&mut self, _: Option<&mut Context<'_>>) -> AbstractSocketResult {
-                AbstractSocketResult::Ok(0)
-            }
-        }
-
-        impl AbstractSocket for $ty {}
-    };
-}
-
-gen_impl_datagram!(tokio::net::UdpSocket);
-gen_impl_datagram!(tokio::net::UnixDatagram);
diff --git a/rust/bssl-tls/src/tests.rs b/rust/bssl-tls/src/tests.rs
index 084637d..f626fb8 100644
--- a/rust/bssl-tls/src/tests.rs
+++ b/rust/bssl-tls/src/tests.rs
@@ -135,63 +135,6 @@
     Ok(())
 }
 
-#[cfg(feature = "tokio_io")]
-async fn async_ping_pong<
-    M: crate::connection::methods::HasTlsConnectionMethod
-        + crate::context::SupportedMode
-        + crate::context::HasBasicIo
-        + 'static,
->(
-    mut server_conn: TlsConnection<Server, M>,
-    mut client_conn: TlsConnection<Client, M>,
-) -> Result<(), Error> {
-    use std::time::Duration;
-
-    let task = tokio::spawn(async move {
-        server_conn
-            .in_handshake()
-            .unwrap()
-            .async_handshake()
-            .await?;
-
-        let mut message = [0; 21];
-        assert!(matches!(
-            server_conn.as_pin_mut().async_read(&mut message).await?,
-            IoStatus::Ok(21)
-        ));
-        assert_eq!(message, *b"BoringSSL is awesome!");
-        tokio::time::sleep(Duration::from_secs(2)).await;
-        server_conn
-            .as_pin_mut()
-            .async_write(b"Oh yeah definitely!")
-            .await?;
-        server_conn.as_pin_mut().async_shutdown().await?;
-        Ok::<_, Error>(())
-    });
-
-    client_conn
-        .in_handshake()
-        .unwrap()
-        .async_handshake()
-        .await?;
-    client_conn
-        .as_pin_mut()
-        .async_write(b"BoringSSL is awesome!")
-        .await?;
-    let mut message = [0; 19];
-    assert!(matches!(
-        client_conn.as_pin_mut().async_read(&mut message).await?,
-        IoStatus::Ok(19)
-    ));
-    assert_eq!(message, *b"Oh yeah definitely!");
-    assert!(matches!(
-        client_conn.as_pin_mut().async_shutdown().await,
-        Ok(_) | Err(Error::Io(crate::errors::IoError::EndOfStream))
-    ));
-    task.await.unwrap()?;
-    Ok(())
-}
-
 pub const TEST_DATA: &[u8] = b"BoringSSL is awesome!";
 
 /// A mocked pipe that will never close or emit EOF.
diff --git a/rust/bssl-tls/src/tests/datagram.rs b/rust/bssl-tls/src/tests/datagram.rs
index 9359892..7f8775e 100644
--- a/rust/bssl-tls/src/tests/datagram.rs
+++ b/rust/bssl-tls/src/tests/datagram.rs
@@ -74,36 +74,6 @@
     sync_ping_pong(server_conn, client_conn).unwrap();
 }
 
-#[cfg(all(unix, feature = "tokio_net"))]
-#[tokio::test]
-async fn async_dtls() -> Result<(), Error> {
-    use crate::tests::async_ping_pong;
-
-    let (mut server_conn, mut client_conn) = dumb_dtls_server_client().unwrap();
-    let (server_sock, client_sock) = tokio::net::UnixDatagram::pair().unwrap();
-    server_conn.set_io(server_sock).unwrap();
-    client_conn.set_io(client_sock).unwrap();
-
-    async_ping_pong(server_conn, client_conn).await
-}
-
-#[cfg(all(feature = "tokio_net", unix))]
-#[tokio::test]
-async fn async_dtls_over_fd() -> Result<(), Error> {
-    use crate::{io::unix::StdDatagram, tests::async_ping_pong};
-
-    let (mut server_conn, mut client_conn) = dumb_dtls_server_client().unwrap();
-    let (server_sock, client_sock) = std::os::unix::net::UnixDatagram::pair().unwrap();
-    server_sock.set_nonblocking(true).unwrap();
-    client_sock.set_nonblocking(true).unwrap();
-    let server_sock = StdDatagram::new_with_tokio(server_sock).unwrap();
-    let client_sock = StdDatagram::new_with_tokio(client_sock).unwrap();
-    server_conn.set_io(server_sock).unwrap();
-    client_conn.set_io(client_sock).unwrap();
-
-    async_ping_pong(server_conn, client_conn).await
-}
-
 #[test]
 fn test_async_dtls() -> Result<(), Error> {
     use crate::io::IoStatus;
diff --git a/rust/bssl-tls/src/tests/transport.rs b/rust/bssl-tls/src/tests/transport.rs
index a50a9db..fc09f9e 100644
--- a/rust/bssl-tls/src/tests/transport.rs
+++ b/rust/bssl-tls/src/tests/transport.rs
@@ -54,52 +54,3 @@
     client_conn.established().unwrap().sync_shutdown().unwrap();
     thread.join().unwrap();
 }
-
-#[cfg(all(unix, feature = "tokio_net"))]
-#[tokio::test]
-async fn tokio_io() -> Result<(), crate::errors::Error> {
-    use crate::io::unix::TokioIo;
-
-    use futures::prelude::*;
-    use tokio::io::{AsyncReadExt, AsyncWriteExt};
-
-    let (mut server_conn, mut client_conn) = dumb_server_client()?;
-
-    let (server_tx, server_rx) = tokio::net::unix::pipe::pipe().unwrap();
-    let (client_tx, client_rx) = tokio::net::unix::pipe::pipe().unwrap();
-    let (send_signal, recv_signal) = tokio::sync::oneshot::channel();
-    let server_rx = TokioIo(server_rx);
-    let server_tx = TokioIo(server_tx);
-    let client_rx = TokioIo(client_rx);
-    let client_tx = TokioIo(client_tx);
-
-    server_conn.set_split_io(client_rx, server_tx)?;
-    client_conn.set_split_io(server_rx, client_tx)?;
-
-    let thread = tokio::spawn(async move {
-        let mut message = [0; 21];
-        server_conn.read_exact(&mut message).await.unwrap();
-        assert_eq!(message, *b"BoringSSL is awesome!");
-        server_conn.write_all(b"Oh yeah definitely!").await.unwrap();
-        server_conn.established().unwrap().sync_shutdown().unwrap();
-
-        // The following shutdown call should not make progress.
-        let res = server_conn.established().unwrap().shutdown().now_or_never();
-        assert!(res.is_none(), "{res:?}");
-        // Make the client progress.
-        send_signal.send(()).unwrap();
-        server_conn.established().unwrap().shutdown().await.unwrap();
-    });
-    client_conn
-        .write_all(b"BoringSSL is awesome!")
-        .await
-        .unwrap();
-    recv_signal.await.unwrap();
-    let mut message = [0; 19];
-    client_conn.read_exact(&mut message).await.unwrap();
-    assert_eq!(message, *b"Oh yeah definitely!");
-    client_conn.established().unwrap().sync_shutdown().unwrap();
-    thread.await.unwrap();
-
-    Ok(())
-}