rust: bssl-tls: Add convenient adapters This patch provies convenience to match rustls when the socket configuration is very simple. Bug: 479599893 Signed-off-by: Xiangfei Ding <xfding@google.com> Change-Id: Ibf8f7fc7c6de5138eaabf34910213e3f6a6a6964 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/92469 Reviewed-by: Adam Langley <agl@google.com>
diff --git a/rust/bssl-tls-tokio/src/lib.rs b/rust/bssl-tls-tokio/src/lib.rs index 87c345e..a439368 100644 --- a/rust/bssl-tls-tokio/src/lib.rs +++ b/rust/bssl-tls-tokio/src/lib.rs
@@ -113,6 +113,7 @@ }; use std::{ io, + marker::PhantomData, ops::{ Deref, DerefMut, // @@ -141,16 +142,27 @@ }; use bssl_tls::{ connection::{ + Client, + Server, TlsConnection, lifecycle::ShutdownStatus, // }, - context::TlsMode, + context::{ + DtlsMode, + TlsContext, + TlsContextBuilder, + TlsMode, // + }, + errors::Error, io::{ AbstractReader, AbstractSocket, AbstractSocketResult, AbstractWriter, IoStatus, NoAsyncContext, stdio::PollFor, }, // }; +#[cfg(test)] +mod tests; + /// Translates a `std::io::Error` into an `AbstractSocketResult`. fn translate_stdio_err(err: io::Error) -> AbstractSocketResult { match err.kind() { @@ -495,3 +507,227 @@ } } } + +/// A wrapper around `TlsContext` for creating Tokio-based client connections. +pub struct TlsConnector { + ctx: TlsContext, +} + +impl TlsConnector { + /// Construct a new `TlsConnector`. + pub(crate) fn new(ctx: TlsContext) -> Self { + Self { ctx } + } + + /// Connect to the given domain using the provided stream. + pub async fn connect<S>(&self, domain: &str, stream: S) -> Result<TlsStream<Client, S>, Error> + where + S: AsyncRead + AsyncWrite + Send + Unpin + 'static, + { + let mut conn = self.ctx.new_client_connection(None)?.build(); + conn.in_handshake().unwrap().set_host(domain)?; + + conn.set_io(TokioIo(stream))?; + + conn.async_handshake().await?; + + Ok(TlsStream { + conn: TokioTlsConnection::new(conn), + _marker: PhantomData, + }) + } +} + +/// A wrapper around `TlsContext` for creating Tokio-based server connections. +pub struct TlsAcceptor { + ctx: TlsContext, +} + +impl TlsAcceptor { + /// Construct a new `TlsAcceptor`. + pub(crate) fn new(ctx: TlsContext) -> Self { + Self { ctx } + } + + /// Accept a new connection using the provided stream. + pub async fn accept<S>(&self, stream: S) -> Result<TlsStream<Server, S>, Error> + where + S: AsyncRead + AsyncWrite + Send + Unpin + 'static, + { + let mut conn = self.ctx.new_server_connection(None)?.build(); + + conn.set_io(TokioIo(stream))?; + + conn.async_handshake().await?; + + Ok(TlsStream { + conn: TokioTlsConnection::new(conn), + _marker: PhantomData, + }) + } +} + +/// A TLS stream driven by Tokio I/O. +pub struct TlsStream<Role, Stream> { + conn: TokioTlsConnection<Role>, + _marker: PhantomData<Stream>, +} + +impl<Role, S> TlsStream<Role, S> { + /// Get a reference to the underlying `TlsConnection`. + pub fn get_ref(&self) -> &TlsConnection<Role> { + &self.conn + } + + /// Get a mutable reference to the underlying `TlsConnection`. + pub fn get_mut(&mut self) -> &mut TlsConnection<Role> { + &mut self.conn + } +} + +impl<Role, S: Unpin> AsyncRead for TlsStream<Role, S> { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll<io::Result<()>> { + Pin::new(&mut self.conn).poll_read(cx, buf) + } +} + +impl<Role, S: Unpin> AsyncWrite for TlsStream<Role, S> { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll<io::Result<usize>> { + Pin::new(&mut self.conn).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { + Pin::new(&mut self.conn).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { + Pin::new(&mut self.conn).poll_shutdown(cx) + } +} + +/// A wrapper around `TlsContext` for creating Tokio-based DTLS client connections. +pub struct DtlsConnector { + ctx: TlsContext<DtlsMode>, +} + +impl DtlsConnector { + /// Construct a new `DtlsConnector`. + pub(crate) fn new(ctx: TlsContext<DtlsMode>) -> Self { + Self { ctx } + } + + /// Connect to the given domain using the provided datagram stream. + pub async fn connect<S>(&self, domain: &str, stream: S) -> Result<DtlsStream<Client, S>, Error> + where + S: AbstractSocket + Send + Unpin + 'static, + { + let mut conn = self.ctx.new_client_connection(None)?.build(); + conn.in_handshake().unwrap().set_host(domain)?; + conn.set_io(stream)?; + conn.async_handshake().await?; + + Ok(DtlsStream { + conn, + _marker: PhantomData, + }) + } +} + +/// A wrapper around `TlsContext` for creating Tokio-based DTLS server connections. +pub struct DtlsAcceptor { + ctx: TlsContext<DtlsMode>, +} + +impl DtlsAcceptor { + /// Construct a new `DtlsAcceptor`. + pub(crate) fn new(ctx: TlsContext<DtlsMode>) -> Self { + Self { ctx } + } + + /// Accept a new connection using the provided datagram stream. + pub async fn accept<S>(&self, stream: S) -> Result<DtlsStream<Server, S>, Error> + where + S: AbstractSocket + Send + Unpin + 'static, + { + let mut conn = self.ctx.new_server_connection(None)?.build(); + conn.set_io(stream)?; + conn.async_handshake().await?; + + Ok(DtlsStream { + conn, + _marker: PhantomData, + }) + } +} + +/// A DTLS stream driven by Tokio. +pub struct DtlsStream<Role, S> { + conn: TlsConnection<Role, DtlsMode>, + _marker: PhantomData<S>, +} + +impl<Role, S> DtlsStream<Role, S> { + /// Get a reference to the underlying `TlsConnection`. + pub fn get_ref(&self) -> &TlsConnection<Role, DtlsMode> { + &self.conn + } + + /// Get a mutable reference to the underlying `TlsConnection`. + pub fn get_mut(&mut self) -> &mut TlsConnection<Role, DtlsMode> { + &mut self.conn + } + + /// Send application data over DTLS. + pub async fn send(&mut self, buf: &[u8]) -> Result<IoStatus, Error> { + self.conn.as_pin_mut().async_write(buf).await + } + + /// Receive application data over DTLS. + pub async fn recv(&mut self, buf: &mut [u8]) -> Result<IoStatus, Error> { + self.conn.as_pin_mut().async_read(buf).await + } +} + +/// Extension trait for `TlsContextBuilder` to support Tokio TLS. +pub trait TokioTlsExt { + /// Build a `TlsConnector`. + fn build_tokio_connector(self) -> TlsConnector; + /// Build a `TlsAcceptor`. + fn build_tokio_acceptor(self) -> TlsAcceptor; +} + +impl TokioTlsExt for TlsContextBuilder<TlsMode> { + fn build_tokio_connector(self) -> TlsConnector { + TlsConnector::new(self.build()) + } + + fn build_tokio_acceptor(self) -> TlsAcceptor { + TlsAcceptor::new(self.build()) + } +} + +/// Extension trait for `TlsContextBuilder` to support Tokio DTLS. +pub trait TokioDtlsExt { + /// Build a `DtlsConnector`. + fn build_dtls_tokio_connector(self) -> DtlsConnector; + /// Build a `DtlsAcceptor`. + fn build_dtls_tokio_acceptor(self) -> DtlsAcceptor; +} + +impl TokioDtlsExt for TlsContextBuilder<DtlsMode> { + fn build_dtls_tokio_connector(self) -> DtlsConnector { + DtlsConnector::new(self.build()) + } + + fn build_dtls_tokio_acceptor(self) -> DtlsAcceptor { + DtlsAcceptor::new(self.build()) + } +}
diff --git a/rust/bssl-tls-tokio/src/tests.rs b/rust/bssl-tls-tokio/src/tests.rs new file mode 100644 index 0000000..0aa204e --- /dev/null +++ b/rust/bssl-tls-tokio/src/tests.rs
@@ -0,0 +1,21 @@ +// 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. + +mod convenience; +mod datagram; +mod transport; + +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");
diff --git a/rust/bssl-tls-tokio/src/tests/convenience.rs b/rust/bssl-tls-tokio/src/tests/convenience.rs new file mode 100644 index 0000000..12a080f --- /dev/null +++ b/rust/bssl-tls-tokio/src/tests/convenience.rs
@@ -0,0 +1,94 @@ +// 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 bssl_tls::{ + context::TlsContextBuilder, + credentials::{ + Certificate, + TlsCredentialBuilder, // + }, // +}; +use bssl_x509::{ + certificates::X509Certificate, + keys::PrivateKey, + params::Trust, + store::X509StoreBuilder, // +}; +use tokio::io::{ + AsyncReadExt, + AsyncWriteExt, // +}; + +use super::{ + CA, + RSA_SERVER_CERT, + RSA_SERVER_KEY, // +}; +use crate::TokioTlsExt; + +#[tokio::test] +async fn high_level_tokio() -> Result<(), bssl_tls::errors::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 mut builder = TlsContextBuilder::new_tls(); + let ca = X509Certificate::parse_one_from_pem(CA)?; + let store = { + let mut store = X509StoreBuilder::new(); + store.set_trust(Trust::SslServer)?.add_cert(ca)?; + store.build() + }; + builder.with_certificate_store(&store); + let connector = builder.build_tokio_connector(); + let acceptor = server_ctx_builder.build_tokio_acceptor(); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + let server_task = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let mut tls_stream = acceptor.accept(stream).await.unwrap(); + + let mut buf = [0; 5]; + tls_stream.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"hello"); + + tls_stream.write_all(b"world").await.unwrap(); + tls_stream.flush().await.unwrap(); + }); + + let stream = tokio::net::TcpStream::connect(addr).await.unwrap(); + let mut tls_stream = connector.connect("www.google.com", stream).await.unwrap(); + + tls_stream.write_all(b"hello").await.unwrap(); + tls_stream.flush().await.unwrap(); + + let mut buf = [0; 5]; + tls_stream.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"world"); + + server_task.await.unwrap(); + + Ok(()) +}
diff --git a/rust/bssl-tls-tokio/tests/datagram.rs b/rust/bssl-tls-tokio/src/tests/datagram.rs similarity index 94% rename from rust/bssl-tls-tokio/tests/datagram.rs rename to rust/bssl-tls-tokio/src/tests/datagram.rs index 4c39b69..4012d48 100644 --- a/rust/bssl-tls-tokio/tests/datagram.rs +++ b/rust/bssl-tls-tokio/src/tests/datagram.rs
@@ -30,10 +30,6 @@ }, errors::Error, // }; -use bssl_tls_tokio::{ - TokioDatagramIo, - new_std_datagram_with_tokio, // -}; use bssl_x509::{ certificates::X509Certificate, keys::PrivateKey, @@ -41,9 +37,15 @@ 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"); +use super::{ + CA, + RSA_SERVER_CERT, + RSA_SERVER_KEY, // +}; +use crate::{ + TokioDatagramIo, + new_std_datagram_with_tokio, // +}; fn dumb_dtls_server_client() -> Result< ( @@ -140,6 +142,7 @@ Ok(()) } +#[cfg(unix)] #[tokio::test] #[ignore = "https://crbug.com/532601068"] async fn async_dtls() -> Result<(), Error> { @@ -151,6 +154,7 @@ async_ping_pong(server_conn, client_conn).await } +#[cfg(unix)] #[tokio::test] #[ignore = "https://crbug.com/532601068"] async fn async_dtls_over_fd() -> Result<(), Error> {
diff --git a/rust/bssl-tls-tokio/tests/transport.rs b/rust/bssl-tls-tokio/src/tests/transport.rs similarity index 92% rename from rust/bssl-tls-tokio/tests/transport.rs rename to rust/bssl-tls-tokio/src/tests/transport.rs index 2be385d..16dcf01 100644 --- a/rust/bssl-tls-tokio/tests/transport.rs +++ b/rust/bssl-tls-tokio/src/tests/transport.rs
@@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![cfg(unix)] - use bssl_tls::{ connection::{ Client, @@ -27,26 +25,27 @@ }, errors::Error, // }; -use bssl_tls_tokio::{ - TokioIo, - TokioTlsConnection, // -}; use bssl_x509::{ certificates::X509Certificate, keys::PrivateKey, params::Trust, store::X509StoreBuilder, // }; +use futures::future::FutureExt; 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"); +use super::{ + CA, + RSA_SERVER_CERT, + RSA_SERVER_KEY, // +}; +use crate::{ + TokioIo, + TokioTlsConnection, // +}; fn dumb_server_client() -> Result<(TlsConnection<Server>, TlsConnection<Client>), Error> { let ca = Certificate::parse_one_from_pem(CA, None)?; @@ -78,6 +77,7 @@ Ok((server_conn, client_conn)) } +#[cfg(unix)] #[tokio::test] async fn tokio_io() -> Result<(), Error> { let (server_conn, client_conn) = dumb_server_client()?;
diff --git a/rust/bssl-tls/src/context.rs b/rust/bssl-tls/src/context.rs index 88c0774..285f722 100644 --- a/rust/bssl-tls/src/context.rs +++ b/rust/bssl-tls/src/context.rs
@@ -416,6 +416,19 @@ } } +#[cfg(feature = "std")] +impl TlsContextBuilder<TlsMode> { + /// Builds and returns a synchronous `TlsConnector` using the configured context. + pub fn build_connector(self) -> crate::sync_io::TlsConnector { + crate::sync_io::TlsConnector::new(self.build()) + } + + /// Builds and returns a synchronous `TlsAcceptor` using the configured context. + pub fn build_acceptor(self) -> crate::sync_io::TlsAcceptor { + crate::sync_io::TlsAcceptor::new(self.build()) + } +} + impl<M> Drop for TlsContextBuilder<M> { fn drop(&mut self) { unsafe {
diff --git a/rust/bssl-tls/src/lib.rs b/rust/bssl-tls/src/lib.rs index 54fd6bd..bf949c7 100644 --- a/rust/bssl-tls/src/lib.rs +++ b/rust/bssl-tls/src/lib.rs
@@ -45,6 +45,10 @@ pub mod io; mod methods; pub mod sessions; +#[cfg(feature = "std")] +/// Synchronous I/O high-level APIs. +pub mod sync_io; + #[macro_use] #[doc(hidden)] mod macros;
diff --git a/rust/bssl-tls/src/sync_io.rs b/rust/bssl-tls/src/sync_io.rs new file mode 100644 index 0000000..b61cf80 --- /dev/null +++ b/rust/bssl-tls/src/sync_io.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. + +use crate::{ + connection::{ + Client, + Server, + TlsConnection, // + }, + context::TlsContext, + io::sync_io::{NoAsync, StdIoWithReactor}, // +}; + +use std::{ + io::{ + Read, + Write, // + }, + marker::PhantomData, // +}; + +/// A convenient wrapper around `TlsContext` for creating synchronous client connections. +pub struct TlsConnector { + ctx: TlsContext, +} + +impl TlsConnector { + /// Construct a new `TlsConnector`. + pub(crate) fn new(ctx: TlsContext) -> Self { + Self { ctx } + } + + /// Connect to the given domain using the provided stream. + pub fn connect<S>( + &self, + domain: &str, + stream: S, + ) -> Result<TlsStream<Client, S>, crate::errors::Error> + where + S: Read + Write + Send + 'static, + { + let mut conn = self.ctx.new_client_connection(None)?.build(); + { + conn.in_handshake() + .expect("connection is freshly constructed and it cannot already be established") + .set_host(domain)?; + conn.set_io(StdIoWithReactor::new(stream, NoAsync))? + .do_handshake()?; + } + + Ok(TlsStream { + conn, + _marker: PhantomData, + }) + } +} + +/// A wrapper around `TlsContext` for creating synchronous server connections. +pub struct TlsAcceptor { + ctx: TlsContext, +} + +impl TlsAcceptor { + /// Construct a new `TlsAcceptor`. + pub(crate) fn new(ctx: TlsContext) -> Self { + Self { ctx } + } + + /// Accept a new connection using the provided stream. + pub fn accept<S>(&self, stream: S) -> Result<TlsStream<Server, S>, crate::errors::Error> + where + S: Read + Write + Send + 'static, + { + let mut conn = self.ctx.new_server_connection(None)?.build(); + conn.set_io(StdIoWithReactor::new(stream, NoAsync))?; + conn.do_handshake()?; + + Ok(TlsStream { + conn, + _marker: PhantomData, + }) + } +} + +/// A TLS stream driven by synchronous I/O. +pub struct TlsStream<Role, S> { + conn: TlsConnection<Role>, + _marker: PhantomData<S>, +} + +impl<Role, S> TlsStream<Role, S> { + /// Get a reference to the underlying `TlsConnection`. + pub fn get_ref(&self) -> &TlsConnection<Role> { + &self.conn + } + + /// Get a mutable reference to the underlying `TlsConnection`. + pub fn get_mut(&mut self) -> &mut TlsConnection<Role> { + &mut self.conn + } +} + +impl<Role, S> Read for TlsStream<Role, S> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { + self.conn.read(buf) + } +} + +impl<Role, S> Write for TlsStream<Role, S> { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + self.conn.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + Write::flush(&mut self.conn) + } +}
diff --git a/rust/bssl-tls/src/tests/transport.rs b/rust/bssl-tls/src/tests/transport.rs index fc09f9e..1f24c8f 100644 --- a/rust/bssl-tls/src/tests/transport.rs +++ b/rust/bssl-tls/src/tests/transport.rs
@@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use bssl_x509::params::Trust; + use crate::tests::dumb_server_client; #[cfg(unix)] @@ -54,3 +56,68 @@ client_conn.established().unwrap().sync_shutdown().unwrap(); thread.join().unwrap(); } + +#[cfg(feature = "std")] +#[test] +fn high_level_sync() -> Result<(), crate::errors::Error> { + use crate::context::TlsContextBuilder; + use crate::credentials::{Certificate, TlsCredentialBuilder}; + use crate::tests::{CA, RSA_SERVER_CERT, RSA_SERVER_KEY}; + use bssl_x509::certificates::X509Certificate; + use bssl_x509::keys::PrivateKey; + use bssl_x509::store::X509StoreBuilder; + use std::io::{Read, Write}; + + 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 mut builder = TlsContextBuilder::new_tls(); + let ca = X509Certificate::parse_one_from_pem(CA)?; + let store = { + let mut store = X509StoreBuilder::new(); + store.set_trust(Trust::SslServer)?.add_cert(ca)?; + store.build() + }; + builder.with_certificate_store(&store); + let connector = builder.build_connector(); + let acceptor = server_ctx_builder.build_acceptor(); + + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + + let server_thread = std::thread::spawn(move || { + let (stream, _) = listener.accept().unwrap(); + let mut tls_stream = acceptor.accept(stream).unwrap(); + + let mut buf = [0; 5]; + tls_stream.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, b"hello"); + + tls_stream.write_all(b"world").unwrap(); + tls_stream.flush().unwrap(); + }); + + let stream = std::net::TcpStream::connect(addr).unwrap(); + let mut tls_stream = connector.connect("www.google.com", stream).unwrap(); + + tls_stream.write_all(b"hello").unwrap(); + tls_stream.flush().unwrap(); + + let mut buf = [0; 5]; + tls_stream.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, b"world"); + + server_thread.join().unwrap(); + + Ok(()) +}