use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream};
use std::os::fd::OwnedFd;
use std::sync::atomic::{AtomicBool, Ordering};
use super::{read_frame, write_frame, PeerIdentity, RpcTransport};
use crate::rpc::RpcResult;
static INSECURE_WARNED: AtomicBool = AtomicBool::new(false);
fn warn_once() {
if !INSECURE_WARNED.swap(true, Ordering::SeqCst) {
log::warn!(
"INSECURE plaintext TCP transport — debug/interop only, \
NEVER production; use the `tls` backend for real networks"
);
}
}
pub fn insecure_warning_emitted() -> bool {
INSECURE_WARNED.load(Ordering::SeqCst)
}
pub struct TcpDebugTransport {
stream: TcpStream,
desc: String,
}
impl TcpDebugTransport {
pub fn from_stream(stream: TcpStream) -> RpcResult<Self> {
warn_once();
stream.set_nodelay(true)?;
let desc = match stream.peer_addr() {
Ok(a) => format!("tcp_debug:{a}"),
Err(_) => "tcp_debug".to_string(),
};
Ok(TcpDebugTransport { stream, desc })
}
pub fn from_owned_fd(fd: OwnedFd) -> RpcResult<Self> {
Self::from_stream(TcpStream::from(fd))
}
pub fn connect(addr: SocketAddr) -> RpcResult<Self> {
let stream = TcpStream::connect(addr)?;
Self::from_stream(stream)
}
pub fn bind_loopback() -> RpcResult<TcpListener> {
warn_once();
Ok(TcpListener::bind(SocketAddr::from((
Ipv4Addr::LOCALHOST,
0,
)))?)
}
pub fn bind_insecure(addr: SocketAddr) -> RpcResult<TcpListener> {
warn_once();
if !addr.ip().is_loopback() {
log::warn!(
"tcp_debug bound to NON-loopback {addr} — reachable off-host, \
plaintext, no peer identity; debug/interop ONLY"
);
}
Ok(TcpListener::bind(addr)?)
}
pub fn pair_loopback() -> RpcResult<(Self, Self)> {
let listener = Self::bind_loopback()?;
let addr = listener.local_addr()?;
let client = Self::connect(addr)?;
let (server_stream, _) = listener.accept()?;
let server = Self::from_stream(server_stream)?;
Ok((client, server))
}
}
impl RpcTransport for TcpDebugTransport {
fn send_frame(&self, buf: &[u8]) -> RpcResult<()> {
let mut w = &self.stream;
write_frame(&mut w, buf)
}
fn recv_frame(&self) -> RpcResult<Vec<u8>> {
let mut r = &self.stream;
read_frame(&mut r)
}
fn send_raw(&self, buf: &[u8]) -> RpcResult<()> {
use std::io::Write;
let mut w = &self.stream;
super::write_all_reporting(&mut w, buf)?;
w.flush()?;
Ok(())
}
fn recv_raw(&self, buf: &mut [u8]) -> RpcResult<usize> {
use std::io::Read;
let mut r = &self.stream;
loop {
return match r.read(buf) {
Ok(n) => Ok(n),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) if super::is_timeout(&e) => Err(crate::rpc::RpcError::Timeout),
Err(e) => Err(e.into()),
};
}
}
fn peer_identity(&self) -> PeerIdentity {
PeerIdentity::Anonymous
}
fn describe(&self) -> &str {
&self.desc
}
fn set_read_timeout(&self, timeout: Option<std::time::Duration>) -> RpcResult<()> {
self.stream.set_read_timeout(timeout)?;
Ok(())
}
fn set_write_timeout(&self, timeout: Option<std::time::Duration>) -> RpcResult<()> {
self.stream.set_write_timeout(timeout)?;
Ok(())
}
fn shutdown(&self) -> RpcResult<()> {
super::absorb_already_shut(self.stream.shutdown(std::net::Shutdown::Both))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn tcp_debug_roundtrip_and_safeguards() {
let (client, server) = TcpDebugTransport::pair_loopback().expect("loopback pair");
assert_eq!(client.peer_identity(), PeerIdentity::Anonymous);
assert_eq!(server.peer_identity(), PeerIdentity::Anonymous);
let client = Arc::new(client);
for size in [0usize, 1, 64 * 1024, 1 << 20] {
let payload: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let sender = {
let c = client.clone();
let p = payload.clone();
std::thread::spawn(move || c.send_frame(&p).unwrap())
};
assert_eq!(server.recv_frame().expect("recv"), payload, "size {size}");
sender.join().unwrap();
}
}
#[test]
fn tcp_debug_from_owned_fd_roundtrip() {
use std::os::fd::OwnedFd;
let listener = TcpDebugTransport::bind_loopback().expect("bind");
let addr = listener.local_addr().unwrap();
let client_stream = TcpStream::connect(addr).expect("connect");
let (server_stream, _) = listener.accept().expect("accept");
let client_fd: OwnedFd = client_stream.into();
let server_fd: OwnedFd = server_stream.into();
let client = TcpDebugTransport::from_owned_fd(client_fd).expect("adopt client");
let server = TcpDebugTransport::from_owned_fd(server_fd).expect("adopt server");
let payload = b"hello-accessor-tcp".to_vec();
let client = Arc::new(client);
let sender = {
let c = client.clone();
let p = payload.clone();
std::thread::spawn(move || c.send_frame(&p).unwrap())
};
assert_eq!(server.recv_frame().expect("recv"), payload);
sender.join().unwrap();
}
#[test]
fn tcp_debug_raw_bytes_roundtrip() {
let (client, server) = TcpDebugTransport::pair_loopback().expect("loopback pair");
client.send_raw(b"\x01\x02\x03").expect("send_raw");
let mut buf = [0u8; 8];
let n = server.recv_raw(&mut buf).expect("recv_raw");
assert_eq!(&buf[..n], b"\x01\x02\x03");
}
#[test]
fn tcp_debug_default_bind_is_loopback() {
let l = TcpDebugTransport::bind_loopback().expect("bind");
assert!(
l.local_addr().unwrap().ip().is_loopback(),
"default bind must be loopback"
);
}
}