use std::net::SocketAddr;
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::panic::AssertUnwindSafe;
use async_trait::async_trait;
use bytes::{BufMut, Bytes, BytesMut};
use futures_util::FutureExt;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use crate::bus_timing::BusTiming;
use crate::client::ModbusClient;
use crate::error::ModbusError;
use crate::error::*;
use crate::frame::{Request, Response};
use crate::options::ClientOptions;
use crate::transport::send_recv;
use crate::transport::sniff_io::SniffIo;
use crate::transport::{MAX_ADU_SIZE, MAX_TCP_ADU_SIZE, MBAP_HEADER_SIZE, MBAP_PREFIX_SIZE};
use crate::wire_tap::WireTap;
#[derive(Debug)]
pub enum TidMode {
Fixed(u16),
Auto,
}
impl Clone for TidMode {
fn clone(&self) -> Self {
match self {
TidMode::Fixed(v) => TidMode::Fixed(*v),
TidMode::Auto => TidMode::Auto,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LengthMode {
Standard,
PduOnly,
}
#[derive(Debug, Clone)]
pub struct TcpConfig {
pub tid: TidMode,
pub unit_id_in_body: bool,
pub length_mode: LengthMode,
}
impl Default for TcpConfig {
fn default() -> Self {
Self {
tid: TidMode::Fixed(0),
unit_id_in_body: false,
length_mode: LengthMode::Standard,
}
}
}
impl TcpConfig {
pub fn standard() -> Self {
Self::default()
}
pub fn gateway() -> Self {
Self {
tid: TidMode::Auto,
unit_id_in_body: true,
length_mode: LengthMode::Standard,
}
}
}
fn next_tid(mode: &TidMode, counter: &AtomicU16) -> u16 {
match mode {
TidMode::Fixed(v) => *v,
TidMode::Auto => counter.fetch_add(1, Ordering::Relaxed),
}
}
pub fn encode_tcp_frame(data: &[u8], buf: &mut BytesMut, config: &TcpConfig, tid: u16) {
let (unit_id, pdu) = if data.is_empty() {
(1u8, &[] as &[u8])
} else {
(data[0], &data[1..])
};
let extra = if config.unit_id_in_body { 1u16 } else { 0u16 };
let len = match config.length_mode {
LengthMode::Standard => pdu.len() as u16 + 1 + extra,
LengthMode::PduOnly => pdu.len() as u16 + extra,
};
buf.put_u16(tid);
buf.put_u16(0); buf.put_u16(len);
buf.put_u8(unit_id);
if config.unit_id_in_body {
buf.put_u8(unit_id);
}
buf.extend_from_slice(pdu);
}
fn is_tcp_header_corrupt(buf: &[u8]) -> bool {
if buf.len() < MBAP_HEADER_SIZE {
return false; }
let proto_id = u16::from_be_bytes([buf[2], buf[3]]);
let payload_len = u16::from_be_bytes([buf[4], buf[5]]) as usize;
proto_id != 0 || !(1..=MAX_TCP_ADU_SIZE).contains(&payload_len)
}
const MAX_TCP_BUF_BYTES: usize = MAX_TCP_ADU_SIZE * 4;
pub fn try_parse_tcp_frame(buf: &[u8]) -> Option<(u16, u8, Bytes, usize)> {
if buf.len() < MBAP_HEADER_SIZE {
return None;
}
let tid = u16::from_be_bytes([buf[0], buf[1]]);
let payload_len = u16::from_be_bytes([buf[4], buf[5]]) as usize;
let proto_id = u16::from_be_bytes([buf[2], buf[3]]);
if proto_id != 0 || payload_len > MAX_TCP_ADU_SIZE {
return None;
}
let unit_id = buf[6];
for &fl in &[
MBAP_HEADER_SIZE + payload_len,
MBAP_PREFIX_SIZE + payload_len,
] {
if fl > MBAP_HEADER_SIZE && buf.len() >= fl {
let body = &buf[MBAP_HEADER_SIZE..fl];
if body.is_empty() {
continue;
}
return try_extract_tcp_pdu(tid, unit_id, body, fl);
}
}
None
}
fn try_extract_tcp_pdu(
tid: u16,
unit_id: u8,
body: &[u8],
consumed: usize,
) -> Option<(u16, u8, Bytes, usize)> {
if body.len() >= 2 && body[0] == unit_id && crate::frame::is_known_function_code(body[1]) {
let pdu = Bytes::copy_from_slice(&body[1..]);
if crate::frame::Request::try_from(pdu.clone()).is_ok() {
return Some((tid, body[0], pdu, consumed));
}
}
if crate::frame::is_known_function_code(body[0]) {
let pdu = Bytes::copy_from_slice(body);
if body[0] == unit_id && Request::try_from(pdu.clone()).is_err() {
return None;
}
return Some((tid, unit_id, pdu, consumed));
}
None
}
pub struct TcpClient {
inner: Mutex<TcpInner>,
addr: SocketAddr,
timeout: Duration,
reconnect: Option<crate::reconnect::ReconnectConfig>,
tcp_config: TcpConfig,
tcp_counter: AtomicU16,
tap: Option<Arc<dyn WireTap>>,
bus_timing: Option<Arc<BusTiming>>,
}
struct TcpInner {
stream: SniffIo<TcpStream>,
write_buf: BytesMut,
read_buf: BytesMut,
}
impl std::fmt::Debug for TcpClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("TcpClient");
d.field("addr", &self.addr);
d.field("timeout", &self.timeout);
if let Some(cfg) = &self.reconnect {
d.field("reconnect_max_retries", &cfg.max_retries());
d.field("reconnect_interval", &cfg.interval());
}
d.finish()
}
}
impl TcpClient {
pub async fn connect(addr: SocketAddr) -> std::io::Result<Self> {
Self::connect_with_timeout(addr, Duration::from_secs(5)).await
}
pub async fn connect_with_timeout(
addr: SocketAddr,
timeout: Duration,
) -> std::io::Result<Self> {
Self::connect_with_config(addr, timeout, TcpConfig::default()).await
}
async fn connect_with_config(
addr: SocketAddr,
timeout: Duration,
tcp_config: TcpConfig,
) -> std::io::Result<Self> {
let stream = TcpStream::connect(addr).await?;
stream.set_nodelay(true)?;
Ok(Self {
inner: Mutex::new(TcpInner {
stream: SniffIo::new(stream, None, None),
write_buf: BytesMut::with_capacity(MAX_ADU_SIZE),
read_buf: BytesMut::with_capacity(MAX_ADU_SIZE),
}),
addr,
timeout,
reconnect: None,
tcp_config: tcp_config.clone(),
tcp_counter: AtomicU16::new(1),
tap: None,
bus_timing: None,
})
}
pub fn with_config(mut self, config: TcpConfig) -> Self {
self.tcp_config = config;
self
}
pub fn with_gateway_mode(self) -> Self {
self.with_config(TcpConfig::gateway())
}
pub fn with_reconnect(mut self, max_retries: u32, backoff: Duration) -> Self {
self.reconnect = Some(crate::reconnect::ReconnectConfig::new(max_retries, backoff));
self
}
async fn send_recv(
&self,
slave_id: u8,
request: &Request<'_>,
) -> Result<Response, ModbusError> {
let mut inner = self.inner.lock().await;
let inner = &mut *inner;
let mut scratch = [0u8; MAX_ADU_SIZE];
send_recv::drain_stale_data(&mut inner.stream, &mut scratch).await?;
let tcp_cfg = self.tcp_config.clone();
let tid = next_tid(&tcp_cfg.tid, &self.tcp_counter);
send_recv::send_frame(
&mut inner.stream,
&mut inner.write_buf,
slave_id,
self.timeout,
request,
|data, buf| encode_tcp_frame(data, buf, &tcp_cfg, tid),
)
.await?;
inner.read_buf.clear();
let deadline = Instant::now() + self.timeout;
send_recv::read_at_least(
&mut inner.stream,
&mut inner.read_buf,
deadline,
MBAP_HEADER_SIZE,
)
.await?;
if inner.read_buf.len() < MBAP_HEADER_SIZE {
return Err(ModbusError::timeout(TCP_RECV_TIMEOUT));
}
let proto_id = u16::from_be_bytes([inner.read_buf[2], inner.read_buf[3]]);
if proto_id != 0 {
return Err(ModbusError::protocol("TCP: invalid Protocol ID"));
}
let payload_len = u16::from_be_bytes([inner.read_buf[4], inner.read_buf[5]]) as usize;
if payload_len > MAX_TCP_ADU_SIZE {
return Err(ModbusError::protocol("TCP: MBAP Length exceeds max ADU"));
}
let min_total = MBAP_PREFIX_SIZE + payload_len;
send_recv::read_at_least(&mut inner.stream, &mut inner.read_buf, deadline, min_total)
.await?;
let max_total = MBAP_HEADER_SIZE + payload_len;
if inner.read_buf.len() < max_total {
let remaining = deadline.saturating_duration_since(Instant::now());
if !remaining.is_zero() {
match tokio::time::timeout(
remaining.min(Duration::from_millis(200)),
inner.stream.read(&mut scratch),
)
.await
{
Ok(Ok(n)) if n > 0 => {
inner.read_buf.extend_from_slice(&scratch[..n]);
}
_ => {}
}
}
}
let available = inner.read_buf.len();
let mut pdu = if available >= max_total {
Bytes::copy_from_slice(&inner.read_buf[MBAP_HEADER_SIZE..max_total])
} else if available >= min_total {
Bytes::copy_from_slice(&inner.read_buf[MBAP_HEADER_SIZE..min_total])
} else {
return Err(ModbusError::timeout(TCP_RECV_TIMEOUT));
};
if pdu.is_empty() {
return Err(ModbusError::protocol(TCP_EMPTY_RESP));
}
if tcp_cfg.unit_id_in_body && pdu.len() > 1 && pdu[0] == slave_id {
pdu = pdu.slice(1..);
}
Response::try_from(pdu)
.map_err(|e| ModbusError::protocol(format!("{PDU_DECODE_ERROR} {e}")))
}
}
#[async_trait]
impl ModbusClient for TcpClient {
async fn call(&self, slave: u8, request: Request<'_>) -> Result<Response, ModbusError> {
let request = request.into_owned();
let slave_id = slave;
send_recv::run_with_reconnect(
self.reconnect.as_ref(),
|| self.send_recv(slave_id, &request),
|| async {
let mut inner = self.inner.lock().await;
if let Ok(stream) = TcpStream::connect(self.addr).await {
stream.set_nodelay(true).ok();
inner.stream = SniffIo::new(stream, self.tap.clone(), self.bus_timing.clone());
inner.write_buf.clear();
inner.read_buf.clear();
true
} else {
false
}
},
ModbusError::connection,
)
.await
}
}
pub async fn with_options(addr: SocketAddr, opts: ClientOptions) -> std::io::Result<TcpClient> {
let tap = opts.tap().cloned();
let timing = opts.bus_timing.clone();
let stream = TcpStream::connect(addr).await?;
stream.set_nodelay(true)?;
let mut sniff = SniffIo::new(stream, tap, timing);
if let Some(cap) = opts.data_channel_capacity {
sniff = sniff.with_channel_capacity(cap);
}
if let Some(cap) = opts.tap_channel_capacity {
sniff = sniff.with_tap_channel_capacity(cap);
}
Ok(TcpClient {
inner: Mutex::new(TcpInner {
stream: sniff,
write_buf: BytesMut::with_capacity(MAX_ADU_SIZE),
read_buf: BytesMut::with_capacity(MAX_ADU_SIZE),
}),
addr,
timeout: opts.timeout,
reconnect: opts.reconnect,
tcp_config: TcpConfig::default(),
tcp_counter: AtomicU16::new(1),
tap: opts.tap().cloned(),
bus_timing: opts.bus_timing.clone(),
})
}
pub struct TcpServer {
listener: tokio::net::TcpListener,
tcp_config: TcpConfig,
}
impl TcpServer {
pub async fn bind(addr: SocketAddr) -> std::io::Result<Self> {
let listener = tokio::net::TcpListener::bind(addr).await?;
Ok(Self {
listener,
tcp_config: TcpConfig::default(),
})
}
pub fn with_config(mut self, config: TcpConfig) -> Self {
self.tcp_config = config;
self
}
pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
self.listener.local_addr()
}
pub async fn serve_forever<S>(self, service: S) -> std::io::Result<()>
where
S: crate::server::Service + Send + Sync + Clone + 'static,
{
loop {
let (stream, addr) = self.listener.accept().await?;
log::info!("TCP server: new connection from {}", addr);
let svc = service.clone();
let tcp_cfg = self.tcp_config.clone();
tokio::spawn(async move {
let result = AssertUnwindSafe(handle_tcp_connection(stream, svc, tcp_cfg))
.catch_unwind()
.await;
match result {
Ok(Ok(())) => {}
Ok(Err(e)) => {
log::warn!("TCP server: connection {} error: {}", addr, e)
}
Err(_) => {
log::error!("TCP server: handler task panicked");
}
}
});
}
}
}
async fn handle_tcp_connection<S>(
stream: TcpStream,
service: S,
tcp_cfg: TcpConfig,
) -> std::io::Result<()>
where
S: crate::server::Service + Send + Sync + 'static,
{
let mut sniff = SniffIo::new(stream, None, None);
let mut buf = BytesMut::with_capacity(MAX_ADU_SIZE);
let mut rsp_buf = BytesMut::with_capacity(MAX_ADU_SIZE);
let mut frame_buf = BytesMut::with_capacity(MAX_ADU_SIZE);
loop {
let mut tmp = [0u8; MAX_ADU_SIZE];
match sniff.read(&mut tmp).await {
Ok(0) => break,
Ok(n) => {
buf.extend_from_slice(&tmp[..n]);
while let Some((tid, slave_id, pdu, consumed)) = try_parse_tcp_frame(&buf) {
if let Some(rsp_data) =
send_recv::process_server_request(&pdu, slave_id, &service, &mut rsp_buf)
.await
{
frame_buf.clear();
encode_tcp_frame(&rsp_data, &mut frame_buf, &tcp_cfg, tid);
if sniff.write_all(&frame_buf).await.is_err() {
return Ok(());
}
}
let _ = buf.split_to(consumed);
}
if is_tcp_header_corrupt(&buf) || buf.len() > MAX_TCP_BUF_BYTES {
buf.clear();
}
}
Err(_) => break,
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_parse_tcp_frame_empty_buffer() {
assert!(try_parse_tcp_frame(&[]).is_none());
}
#[test]
fn try_parse_tcp_frame_incomplete_header() {
assert!(try_parse_tcp_frame(&[0x00, 0x01, 0x00]).is_none());
assert!(try_parse_tcp_frame(&[0x00, 0x01, 0x00, 0x00, 0x00, 0x01]).is_none());
}
#[test]
fn try_parse_tcp_frame_nonzero_protocol_id() {
let buf = [0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x01, 0x03, 0x00];
assert!(try_parse_tcp_frame(&buf).is_none());
}
#[test]
fn try_parse_tcp_frame_payload_too_large() {
let mut buf = [0u8; MBAP_HEADER_SIZE + 1];
buf[0] = 0x00;
buf[1] = 0x01; buf[2] = 0x00;
buf[3] = 0x00; buf[4] = 0x01;
buf[5] = 0x05; buf[6] = 0x01; buf[7] = 0x03; assert!(try_parse_tcp_frame(&buf).is_none());
}
#[test]
fn try_parse_tcp_frame_payload_len_zero() {
let buf = [0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01];
assert!(try_parse_tcp_frame(&buf).is_none());
}
#[test]
fn try_parse_tcp_frame_payload_len_one_no_pdu() {
let buf = [0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x46, 0x46];
assert!(try_parse_tcp_frame(&buf).is_none());
}
#[test]
fn try_parse_tcp_frame_unknown_function_code() {
let buf = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x01, 0x46, 0x00, ];
assert!(try_parse_tcp_frame(&buf).is_none());
}
#[test]
fn try_parse_tcp_frame_valid_minimal() {
let buf = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, ];
let (tid, slave, pdu, consumed) = try_parse_tcp_frame(&buf).unwrap();
assert_eq!(tid, 1);
assert_eq!(slave, 1);
assert_eq!(pdu.len(), 5);
assert_eq!(consumed, 12);
}
#[test]
fn try_parse_tcp_frame_with_unit_id_in_body() {
let buf = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, ];
let (tid, slave, pdu, _) = try_parse_tcp_frame(&buf).unwrap();
assert_eq!(tid, 1);
assert_eq!(slave, 1);
assert_eq!(pdu[0], 0x03);
assert_eq!(pdu.len(), 5);
}
#[test]
fn try_parse_tcp_frame_pdu_only_length() {
let buf = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, ];
let (tid, slave, pdu, consumed) = try_parse_tcp_frame(&buf).unwrap();
assert_eq!(tid, 1);
assert_eq!(slave, 1);
assert_eq!(pdu.len(), 5);
assert_eq!(consumed, 12);
}
#[test]
fn try_parse_tcp_frame_not_enough_data() {
let buf = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x01, 0x03, 0x00, 0x00, ];
assert!(try_parse_tcp_frame(&buf).is_none());
}
#[test]
fn try_parse_tcp_frame_max_valid_length() {
let mut buf = vec![0u8; MBAP_HEADER_SIZE + 260];
buf[0] = 0x00;
buf[1] = 0x01; buf[2] = 0x00;
buf[3] = 0x00; buf[4] = 0x01;
buf[5] = 0x04; buf[6] = 0x01; buf[7] = 0x03; assert!(try_parse_tcp_frame(&buf).is_some());
}
}