use super::Transport;
use crate::error::{Error, Result};
use bytes::{Bytes, BytesMut};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex as StdMutex};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use tokio::time::timeout;
const MAX_TCP_MESSAGE_SIZE: usize = 0x7FFF_FFFF;
const DEFAULT_MAX_ALLOCATION_SIZE: usize = 10 * 1024 * 1024;
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
pub struct TcpOptions {
pub max_allocation_size: usize,
}
impl Default for TcpOptions {
fn default() -> Self {
Self {
max_allocation_size: DEFAULT_MAX_ALLOCATION_SIZE,
}
}
}
#[derive(Debug)]
pub struct TcpTransportBuilder {
timeout: Option<Duration>,
options: TcpOptions,
}
impl TcpTransportBuilder {
#[must_use]
pub fn new() -> Self {
Self {
timeout: None,
options: TcpOptions::default(),
}
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
#[must_use]
pub fn max_allocation_size(mut self, size: usize) -> Self {
self.options.max_allocation_size = size;
self
}
pub async fn connect(self, target: SocketAddr) -> Result<TcpTransport> {
let stream = match self.timeout {
Some(t) => timeout(t, TcpStream::connect(target))
.await
.map_err(|_| {
Error::Timeout {
target,
elapsed: t,
retries: 0,
}
.boxed()
})?
.map_err(|e| Error::Network { target, source: e }.boxed())?,
None => TcpStream::connect(target)
.await
.map_err(|e| Error::Network { target, source: e }.boxed())?,
};
let local_addr = stream
.local_addr()
.map_err(|e| Error::Network { target, source: e }.boxed())?;
Ok(TcpTransport {
inner: Arc::new(TcpTransportInner {
stream: Arc::new(Mutex::new(stream)),
pending_timeouts: StdMutex::new(HashMap::new()),
target,
local_addr,
max_allocation_size: self.options.max_allocation_size,
poisoned: AtomicBool::new(false),
}),
})
}
}
impl Default for TcpTransportBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct TcpTransport {
inner: Arc<TcpTransportInner>,
}
struct TcpTransportInner {
stream: Arc<Mutex<TcpStream>>,
pending_timeouts: StdMutex<HashMap<i32, Duration>>,
target: SocketAddr,
local_addr: SocketAddr,
max_allocation_size: usize,
poisoned: AtomicBool,
}
impl TcpTransportInner {
fn is_poisoned(&self) -> bool {
self.poisoned.load(Ordering::Acquire)
}
fn take_timeout(&self, request_id: i32) -> Duration {
self.pending_timeouts
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&request_id)
.unwrap_or(DEFAULT_REQUEST_TIMEOUT)
}
async fn poison(&self, stream: &mut TcpStream) {
self.poisoned.store(true, Ordering::Release);
let _ = stream.shutdown().await;
}
}
impl TcpTransport {
pub async fn connect(target: SocketAddr) -> Result<Self> {
Self::builder().connect(target).await
}
pub async fn connect_timeout(target: SocketAddr, connect_timeout: Duration) -> Result<Self> {
Self::builder()
.timeout(connect_timeout)
.connect(target)
.await
}
#[must_use]
pub fn builder() -> TcpTransportBuilder {
TcpTransportBuilder::new()
}
pub async fn from_socket(
socket: tokio::net::TcpSocket,
target: SocketAddr,
options: TcpOptions,
) -> Result<Self> {
let stream = socket
.connect(target)
.await
.map_err(|e| Error::Network { target, source: e }.boxed())?;
let local_addr = stream
.local_addr()
.map_err(|e| Error::Network { target, source: e }.boxed())?;
Ok(Self {
inner: Arc::new(TcpTransportInner {
stream: Arc::new(Mutex::new(stream)),
pending_timeouts: StdMutex::new(HashMap::new()),
target,
local_addr,
max_allocation_size: options.max_allocation_size,
poisoned: AtomicBool::new(false),
}),
})
}
}
impl Transport for TcpTransport {
async fn send(&self, data: &[u8]) -> Result<()> {
let mut stream = self.inner.stream.clone().lock_owned().await;
let target = self.inner.target;
stream
.write_all(data)
.await
.map_err(|e| Error::Network { target, source: e }.boxed())?;
stream
.flush()
.await
.map_err(|e| Error::Network { target, source: e }.boxed())?;
Ok(())
}
fn register_request(&self, request_id: i32, timeout: Duration) {
self.inner
.pending_timeouts
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(request_id, timeout);
}
async fn recv(&self, request_id: i32) -> Result<(Bytes, SocketAddr)> {
let recv_timeout = self.inner.take_timeout(request_id);
let target = self.inner.target;
let mut stream = self.inner.stream.clone().lock_owned().await;
if self.inner.is_poisoned() {
return Err(Error::Closed { target }.boxed());
}
let max_alloc = self.inner.max_allocation_size;
let result = timeout(
recv_timeout,
read_ber_message(&mut stream, target, max_alloc),
)
.await;
match result {
Ok(Ok(data)) => Ok((data, target)),
Ok(Err(e)) => {
self.inner.poison(&mut stream).await;
Err(e)
}
Err(_) => {
tracing::debug!(target: "async_snmp::transport::tcp", { request_id, %target, elapsed = ?recv_timeout }, "transport timeout");
self.inner.poison(&mut stream).await;
Err(Error::Timeout {
target,
elapsed: recv_timeout,
retries: 0,
}
.boxed())
}
}
}
async fn request(&self, data: &[u8], request_id: i32) -> Result<(Bytes, SocketAddr)> {
let recv_timeout = self.inner.take_timeout(request_id);
let target = self.inner.target;
let max_alloc = self.inner.max_allocation_size;
let mut stream = self.inner.stream.clone().lock_owned().await;
if self.inner.is_poisoned() {
return Err(Error::Closed { target }.boxed());
}
if let Err(e) = stream.write_all(data).await {
self.inner.poison(&mut stream).await;
return Err(Error::Network { target, source: e }.boxed());
}
if let Err(e) = stream.flush().await {
self.inner.poison(&mut stream).await;
return Err(Error::Network { target, source: e }.boxed());
}
let result = timeout(
recv_timeout,
read_ber_message(&mut stream, target, max_alloc),
)
.await;
match result {
Ok(Ok(response)) => Ok((response, target)),
Ok(Err(e)) => {
self.inner.poison(&mut stream).await;
Err(e)
}
Err(_) => {
tracing::debug!(target: "async_snmp::transport::tcp", { request_id, %target, elapsed = ?recv_timeout }, "transport timeout");
self.inner.poison(&mut stream).await;
Err(Error::Timeout {
target,
elapsed: recv_timeout,
retries: 0,
}
.boxed())
}
}
}
fn peer_addr(&self) -> SocketAddr {
self.inner.target
}
fn local_addr(&self) -> SocketAddr {
self.inner.local_addr
}
fn is_reliable(&self) -> bool {
true
}
fn max_message_size(&self) -> u32 {
self.inner.max_allocation_size.min(MAX_TCP_MESSAGE_SIZE) as u32
}
}
async fn read_ber_message(
stream: &mut TcpStream,
target: SocketAddr,
max_allocation_size: usize,
) -> Result<Bytes> {
let mut tag_buf = [0u8; 1];
stream
.read_exact(&mut tag_buf)
.await
.map_err(|e| Error::Network { target, source: e }.boxed())?;
let tag = tag_buf[0];
if tag != 0x30 {
tracing::debug!(target: "async_snmp::transport::tcp", { expected_tag = 0x30, actual_tag = tag, %target }, "invalid SNMP message tag");
return Err(Error::MalformedResponse { target }.boxed());
}
let mut first_len_byte = [0u8; 1];
stream
.read_exact(&mut first_len_byte)
.await
.map_err(|e| Error::Network { target, source: e }.boxed())?;
let (content_len, len_bytes) = match first_len_byte[0].cmp(&0x80) {
std::cmp::Ordering::Less => {
(first_len_byte[0] as usize, vec![first_len_byte[0]])
}
std::cmp::Ordering::Equal => {
tracing::debug!(target: "async_snmp::transport::tcp", { %target }, "indefinite length encoding not supported");
return Err(Error::MalformedResponse { target }.boxed());
}
std::cmp::Ordering::Greater => {
let num_len_bytes = (first_len_byte[0] & 0x7F) as usize;
if num_len_bytes > 4 {
tracing::debug!(target: "async_snmp::transport::tcp", { octets = num_len_bytes, %target }, "length encoding too long");
return Err(Error::MalformedResponse { target }.boxed());
}
let mut len_bytes_buf = vec![0u8; num_len_bytes];
stream
.read_exact(&mut len_bytes_buf)
.await
.map_err(|e| Error::Network { target, source: e }.boxed())?;
let mut length: usize = 0;
for &b in &len_bytes_buf {
length = (length << 8) | (b as usize);
}
let mut all_len_bytes = vec![first_len_byte[0]];
all_len_bytes.extend_from_slice(&len_bytes_buf);
(length, all_len_bytes)
}
};
if content_len > max_allocation_size {
tracing::warn!(target: "async_snmp::transport::tcp", { size = content_len, max = max_allocation_size, %target }, "message size exceeds limit");
return Err(Error::MalformedResponse { target }.boxed());
}
let mut content = vec![0u8; content_len];
stream
.read_exact(&mut content)
.await
.map_err(|e| Error::Network { target, source: e }.boxed())?;
let total_len = 1 + len_bytes.len() + content_len;
let mut message = BytesMut::with_capacity(total_len);
message.extend_from_slice(&[tag]);
message.extend_from_slice(&len_bytes);
message.extend_from_slice(&content);
Ok(message.freeze())
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
#[tokio::test]
async fn test_tcp_send_recv() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buf = vec![0u8; 1024];
let n = socket.read(&mut buf).await.unwrap();
let response = [
0x30, 0x1c, 0x02, 0x01, 0x01, 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0xa2, 0x0f, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x04, 0x30, 0x02, 0x05, 0x00, ];
socket.write_all(&response).await.unwrap();
n
});
let transport = TcpTransport::connect(server_addr).await.unwrap();
let request = [
0x30, 0x1a, 0x02, 0x01, 0x01, 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0xa0, 0x0d, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x02, 0x30, 0x00,
];
transport.send(&request).await.unwrap();
transport.register_request(1, Duration::from_secs(5));
let (response, source) = transport.recv(1).await.unwrap();
assert_eq!(source, server_addr);
assert_eq!(response[0], 0x30); assert!(response.len() > 10);
server.await.unwrap();
}
#[tokio::test]
async fn test_tcp_long_length_form() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 1];
let _ = socket.read(&mut buf).await;
let mut response = vec![0x30, 0x81, 0xc8]; response.extend(vec![0x00; 200]); socket.write_all(&response).await.unwrap();
});
let transport = TcpTransport::connect(server_addr).await.unwrap();
transport.send(&[0x00]).await.unwrap();
transport.register_request(1, Duration::from_secs(5));
let (response, _) = transport.recv(1).await.unwrap();
assert_eq!(response.len(), 203);
assert_eq!(response[0], 0x30);
assert_eq!(response[1], 0x81);
assert_eq!(response[2], 0xc8);
server.await.unwrap();
}
#[tokio::test]
async fn test_tcp_advertised_max_matches_accepted_limit() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let mut conns = Vec::new();
while let Ok((socket, _)) = listener.accept().await {
conns.push(socket);
}
});
let transport = TcpTransport::connect(server_addr).await.unwrap();
assert_eq!(
transport.max_message_size() as usize,
transport.inner.max_allocation_size,
"advertised msgMaxSize must equal the accepted allocation limit"
);
assert_eq!(
transport.max_message_size() as usize,
DEFAULT_MAX_ALLOCATION_SIZE
);
let custom = 512 * 1024;
let transport = TcpTransport::builder()
.max_allocation_size(custom)
.connect(server_addr)
.await
.unwrap();
assert_eq!(transport.max_message_size() as usize, custom);
assert_eq!(
transport.max_message_size() as usize,
transport.inner.max_allocation_size
);
}
#[tokio::test]
async fn test_tcp_is_reliable() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let _ = listener.accept().await;
});
let transport = TcpTransport::connect(server_addr).await.unwrap();
assert!(transport.is_reliable());
}
#[tokio::test]
async fn test_tcp_concurrent_requests() {
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering};
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let request_counter = Arc::new(AtomicI32::new(0));
let counter_clone = request_counter.clone();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
for _ in 0..5 {
let mut tag = [0u8; 1];
if socket.read_exact(&mut tag).await.is_err() {
break;
}
let mut len_byte = [0u8; 1];
socket.read_exact(&mut len_byte).await.unwrap();
let content_len = len_byte[0] as usize;
let mut content = vec![0u8; content_len];
socket.read_exact(&mut content).await.unwrap();
let request_id = counter_clone.fetch_add(1, Ordering::SeqCst) + 1;
let response = build_response_with_id(request_id);
socket.write_all(&response).await.unwrap();
}
});
let transport = TcpTransport::connect(server_addr).await.unwrap();
let mut handles = vec![];
for i in 0..5 {
let transport = transport.clone();
let handle = tokio::spawn(async move {
let request_id = i + 1;
let request = build_request_with_id(request_id);
transport.register_request(request_id, Duration::from_secs(5));
let (response, _) = transport.request(&request, request_id).await?;
assert_eq!(response[0], 0x30, "Response should be SEQUENCE");
Ok::<_, Box<Error>>(i)
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles).await;
let success_count = results
.iter()
.filter(|r| r.as_ref().is_ok_and(std::result::Result::is_ok))
.count();
assert_eq!(
success_count, 5,
"All 5 concurrent requests should succeed (serialized)"
);
server.await.unwrap();
}
fn build_request_with_id(request_id: i32) -> Vec<u8> {
let id_bytes = request_id.to_be_bytes();
vec![
0x30,
0x1d, 0x02,
0x01,
0x01, 0x04,
0x06,
0x70,
0x75,
0x62,
0x6c,
0x69,
0x63, 0xa0,
0x10, 0x02,
0x04,
id_bytes[0],
id_bytes[1],
id_bytes[2],
id_bytes[3], 0x02,
0x01,
0x00, 0x02,
0x01,
0x00, 0x30,
0x02,
0x30,
0x00, ]
}
fn build_response_with_id(request_id: i32) -> Vec<u8> {
let id_bytes = request_id.to_be_bytes();
vec![
0x30,
0x1d, 0x02,
0x01,
0x01, 0x04,
0x06,
0x70,
0x75,
0x62,
0x6c,
0x69,
0x63, 0xa2,
0x10, 0x02,
0x04,
id_bytes[0],
id_bytes[1],
id_bytes[2],
id_bytes[3], 0x02,
0x01,
0x00, 0x02,
0x01,
0x00, 0x30,
0x02,
0x30,
0x00, ]
}
#[tokio::test]
async fn test_tcp_rejects_excessive_claimed_size() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 64];
let _ = socket.read(&mut buf).await;
let malicious_response = [
0x30, 0x84, 0x06, 0x40, 0x00,
0x00, ];
let _ = socket.write_all(&malicious_response).await;
tokio::time::sleep(Duration::from_millis(100)).await;
});
let transport = TcpTransport::connect(server_addr).await.unwrap();
let request = build_request_with_id(1);
transport.send(&request).await.unwrap();
transport.register_request(1, Duration::from_secs(5));
let result = transport.recv(1).await;
assert!(result.is_err(), "Should reject excessive claimed size");
let err = result.unwrap_err();
assert!(
matches!(*err, Error::MalformedResponse { .. }),
"Expected MalformedResponse error, got: {err:?}"
);
server.await.unwrap();
}
#[tokio::test]
async fn test_read_ber_message_rejects_bad_tag() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
socket.write_all(&[0x31, 0x00]).await.unwrap();
});
let mut client = TcpStream::connect(server_addr).await.unwrap();
let result = timeout(
Duration::from_secs(5),
read_ber_message(&mut client, server_addr, DEFAULT_MAX_ALLOCATION_SIZE),
)
.await
.expect("read_ber_message should not hang");
assert!(result.is_err(), "Should reject non-0x30 tag byte");
let err = result.unwrap_err();
assert!(
matches!(*err, Error::MalformedResponse { .. }),
"Expected MalformedResponse error, got: {err:?}"
);
server.await.unwrap();
}
#[tokio::test]
async fn test_read_ber_message_rejects_indefinite_length() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
socket.write_all(&[0x30, 0x80]).await.unwrap();
});
let mut client = TcpStream::connect(server_addr).await.unwrap();
let result = timeout(
Duration::from_secs(5),
read_ber_message(&mut client, server_addr, DEFAULT_MAX_ALLOCATION_SIZE),
)
.await
.expect("read_ber_message should not hang");
assert!(result.is_err(), "Should reject indefinite length encoding");
let err = result.unwrap_err();
assert!(
matches!(*err, Error::MalformedResponse { .. }),
"Expected MalformedResponse error, got: {err:?}"
);
server.await.unwrap();
}
#[tokio::test]
async fn test_read_ber_message_rejects_length_encoding_too_long() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
socket
.write_all(&[0x30, 0x85, 0x00, 0x00, 0x00, 0x00, 0x00])
.await
.unwrap();
});
let mut client = TcpStream::connect(server_addr).await.unwrap();
let result = timeout(
Duration::from_secs(5),
read_ber_message(&mut client, server_addr, DEFAULT_MAX_ALLOCATION_SIZE),
)
.await
.expect("read_ber_message should not hang");
assert!(
result.is_err(),
"Should reject length encoding over 4 octets"
);
let err = result.unwrap_err();
assert!(
matches!(*err, Error::MalformedResponse { .. }),
"Expected MalformedResponse error, got: {err:?}"
);
server.await.unwrap();
}
#[tokio::test]
async fn test_read_ber_message_reassembles_segmented_content() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
socket.write_all(&[0x30, 0x04]).await.unwrap();
socket.flush().await.unwrap();
tokio::time::sleep(Duration::from_millis(20)).await;
socket.write_all(&[0x01, 0x02]).await.unwrap();
socket.flush().await.unwrap();
tokio::time::sleep(Duration::from_millis(20)).await;
socket.write_all(&[0x03, 0x04]).await.unwrap();
socket.flush().await.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
});
let mut client = TcpStream::connect(server_addr).await.unwrap();
let result = timeout(
Duration::from_secs(5),
read_ber_message(&mut client, server_addr, DEFAULT_MAX_ALLOCATION_SIZE),
)
.await
.expect("read_ber_message should not hang");
let bytes = result.expect("segmented message should reassemble successfully");
assert_eq!(bytes.as_ref(), &[0x30, 0x04, 0x01, 0x02, 0x03, 0x04]);
server.await.unwrap();
}
#[tokio::test]
async fn test_read_ber_message_truncated_stream_is_network_error() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
socket.write_all(&[0x30, 0x05]).await.unwrap();
socket.write_all(&[0x01, 0x02]).await.unwrap();
socket.flush().await.unwrap();
drop(socket);
});
let mut client = TcpStream::connect(server_addr).await.unwrap();
let result = timeout(
Duration::from_secs(5),
read_ber_message(&mut client, server_addr, DEFAULT_MAX_ALLOCATION_SIZE),
)
.await
.expect("read_ber_message should not hang");
assert!(result.is_err(), "Should error on truncated content stream");
let err = result.unwrap_err();
assert!(
matches!(*err, Error::Network { .. }),
"Expected Network error, got: {err:?}"
);
server.await.unwrap();
}
#[tokio::test]
async fn test_tcp_builder_custom_allocation_limit() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 64];
let _ = socket.read(&mut buf).await;
let response = [
0x30, 0x82, 0x28, 0x00, ];
let _ = socket.write_all(&response).await;
tokio::time::sleep(Duration::from_millis(100)).await;
});
let transport = TcpTransport::builder()
.max_allocation_size(1024) .connect(server_addr)
.await
.unwrap();
let request = build_request_with_id(1);
transport.send(&request).await.unwrap();
transport.register_request(1, Duration::from_secs(5));
let result = transport.recv(1).await;
assert!(
result.is_err(),
"Should reject message exceeding custom limit"
);
let err = result.unwrap_err();
assert!(
matches!(*err, Error::MalformedResponse { .. }),
"Expected MalformedResponse error, got: {err:?}"
);
server.await.unwrap();
}
#[tokio::test]
async fn test_tcp_cancelled_request_does_not_wedge_next() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut hdr = [0u8; 2];
socket.read_exact(&mut hdr).await.unwrap();
let mut body = vec![0u8; hdr[1] as usize];
socket.read_exact(&mut body).await.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
socket.read_exact(&mut hdr).await.unwrap();
let mut body2 = vec![0u8; hdr[1] as usize];
socket.read_exact(&mut body2).await.unwrap();
let response = build_response_with_id(2);
socket.write_all(&response).await.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
});
let transport = TcpTransport::connect(server_addr).await.unwrap();
let request = build_request_with_id(1);
transport.register_request(1, Duration::from_secs(30));
let cancelled = timeout(Duration::from_millis(50), transport.request(&request, 1)).await;
assert!(cancelled.is_err(), "outer timeout should elapse (cancel)");
assert!(
transport.inner.stream.clone().try_lock_owned().is_ok(),
"stream lock leaked after a cancelled request"
);
let request2 = build_request_with_id(2);
transport.register_request(2, Duration::from_secs(5));
let result = timeout(Duration::from_secs(5), transport.request(&request2, 2))
.await
.expect("second request should not hang");
let (response, _) = result.expect("second request should succeed");
assert_eq!(response[0], 0x30);
server.await.unwrap();
}
#[tokio::test]
async fn test_tcp_malformed_frame_poisons_stream() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut hdr = [0u8; 2];
socket.read_exact(&mut hdr).await.unwrap();
let mut body = vec![0u8; hdr[1] as usize];
socket.read_exact(&mut body).await.unwrap();
socket
.write_all(&[0x31, 0x02, 0xde, 0xad, 0x30, 0x00])
.await
.unwrap();
socket.flush().await.unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
});
let transport = TcpTransport::connect(server_addr).await.unwrap();
let request = build_request_with_id(1);
transport.register_request(1, Duration::from_secs(5));
let first = transport.request(&request, 1).await;
let err = first.expect_err("malformed frame should error");
assert!(
matches!(*err, Error::MalformedResponse { .. }),
"Expected MalformedResponse, got: {err:?}"
);
assert!(
transport.inner.is_poisoned(),
"stream should be poisoned after a malformed frame"
);
let request2 = build_request_with_id(2);
transport.register_request(2, Duration::from_secs(5));
let second = timeout(Duration::from_secs(5), transport.request(&request2, 2))
.await
.expect("second request should not hang");
let err2 = second.expect_err("poisoned stream should reject the next request");
assert!(
matches!(*err2, Error::Closed { .. }),
"Expected Closed on poisoned stream, got: {err2:?}"
);
server.await.unwrap();
}
#[tokio::test]
async fn test_tcp_per_request_timeout_not_overwritten_by_clone() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (_socket, _) = listener.accept().await.unwrap();
tokio::time::sleep(Duration::from_secs(30)).await;
});
let transport_a = TcpTransport::connect(server_addr).await.unwrap();
let transport_b = transport_a.clone();
transport_a.register_request(1, Duration::from_millis(150));
transport_b.register_request(2, Duration::from_secs(20));
let start = std::time::Instant::now();
let result = timeout(Duration::from_secs(5), transport_a.recv(1))
.await
.expect("recv should honor A's short timeout, not B's long one");
let elapsed = start.elapsed();
let err = result.expect_err("recv should time out");
assert!(
matches!(*err, Error::Timeout { .. }),
"Expected Timeout, got: {err:?}"
);
assert!(
elapsed < Duration::from_secs(2),
"recv honored the wrong timeout; elapsed {elapsed:?}"
);
server.abort();
}
#[tokio::test]
async fn test_tcp_timeout_mid_frame_poisons_stream() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let server_addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 64];
let _ = socket.read(&mut buf).await;
socket.write_all(&[0x30, 0x08, 0x01, 0x02]).await.unwrap();
socket.flush().await.unwrap();
tokio::time::sleep(Duration::from_millis(500)).await;
});
let transport = TcpTransport::connect(server_addr).await.unwrap();
let request = build_request_with_id(1);
transport.send(&request).await.unwrap();
transport.register_request(1, Duration::from_millis(100));
let first = transport.recv(1).await;
let err = first.expect_err("mid-frame read should time out");
assert!(
matches!(*err, Error::Timeout { .. }),
"Expected Timeout, got: {err:?}"
);
assert!(
transport.inner.is_poisoned(),
"stream should be poisoned after a mid-frame timeout"
);
transport.register_request(1, Duration::from_secs(5));
let second = timeout(Duration::from_secs(5), transport.recv(1))
.await
.expect("second recv should not hang");
let err2 = second.expect_err("poisoned stream should reject the next recv");
assert!(
matches!(*err2, Error::Closed { .. }),
"Expected Closed on poisoned stream, got: {err2:?}"
);
server.await.unwrap();
}
}