use anyhow::{anyhow, Result};
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::TcpStream;
pub const IPPROTO_SCTP: i32 = 132;
pub const M3UA_PPID: u32 = 3;
pub const DATA_STREAM: u16 = 1;
pub fn sctp_socket(addr: &SocketAddr, role: &str) -> Result<socket2::Socket> {
use socket2::{Domain, Protocol, Socket, Type};
let domain = if addr.is_ipv4() {
Domain::IPV4
} else {
Domain::IPV6
};
Socket::new(domain, Type::STREAM, Some(Protocol::from(IPPROTO_SCTP))).map_err(|e| {
anyhow!(
"M3UA {role} cannot open an SCTP socket on this host: {e}. M3UA's transport is SCTP \
(RFC 4666 section 1.4.1, IANA port 2905) and this operating system has no SCTP \
stack available to this process — macOS ships neither kernel support nor headers \
for SCTP, and a Linux kernel without the sctp module fails the same way \
(`modprobe sctp`). NetGet refuses rather than pretend it is speaking SIGTRAN. \
Either run on a host with SCTP, or, for local protocol work only, pass the startup \
parameter transport=\"tcp\" — a NON-STANDARD framing that exercises the M3UA layer \
and that no real SIGTRAN peer speaks."
)
})
}
pub async fn connect(
addr: SocketAddr,
sctp: bool,
local_port: Option<u16>,
deadline: Duration,
) -> Result<TcpStream> {
let socket = if sctp {
sctp_socket(&addr, "ASP")?
} else {
let domain = if addr.is_ipv4() {
socket2::Domain::IPV4
} else {
socket2::Domain::IPV6
};
socket2::Socket::new(domain, socket2::Type::STREAM, Some(socket2::Protocol::TCP))?
};
if let Some(port) = local_port {
let local: SocketAddr = if addr.is_ipv4() {
(std::net::Ipv4Addr::UNSPECIFIED, port).into()
} else {
(std::net::Ipv6Addr::UNSPECIFIED, port).into()
};
socket.set_reuse_address(true)?;
socket
.bind(&local.into())
.map_err(|e| anyhow!("binding the association's local port {port}: {e}"))?;
}
let std_stream = tokio::task::spawn_blocking(move || -> Result<std::net::TcpStream> {
socket
.connect_timeout(&addr.into(), deadline)
.map_err(|e| anyhow!("association to {addr} failed: {e}"))?;
socket.set_nonblocking(true)?;
Ok(socket.into())
})
.await
.map_err(|e| anyhow!("SCTP connect task failed: {e}"))??;
Ok(TcpStream::from_std(std_stream)?)
}
pub fn split(stream: TcpStream, sctp: bool) -> (Reader, Writer) {
let stream = Arc::new(stream);
(
Reader {
stream: stream.clone(),
},
Writer { stream, sctp },
)
}
pub struct Reader {
stream: Arc<TcpStream>,
}
impl AsyncRead for Reader {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
loop {
std::task::ready!(self.stream.poll_read_ready(cx))?;
match self.stream.try_read(buf.initialize_unfilled()) {
Ok(n) => {
buf.advance(n);
return Poll::Ready(Ok(()));
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => return Poll::Ready(Err(e)),
}
}
}
}
pub struct Writer {
stream: Arc<TcpStream>,
sctp: bool,
}
impl Writer {
pub fn close(&self) {
let _ = socket2::SockRef::from(&*self.stream).shutdown(std::net::Shutdown::Write);
}
}
pub fn stream_for(message: &[u8]) -> u16 {
match super::codec::peek_class_type(message) {
Some((super::codec::CLASS_TRANSFER, _)) => DATA_STREAM,
_ => 0,
}
}
#[cfg(unix)]
fn sctp_send(stream: &TcpStream, message: &[u8], sid: u16) -> std::io::Result<usize> {
use std::os::fd::AsRawFd;
#[repr(C)]
struct SndRcvInfo {
stream: u16,
ssn: u16,
flags: u16,
_pad: u16,
ppid: u32,
context: u32,
timetolive: u32,
tsn: u32,
cumtsn: u32,
assoc_id: i32,
}
const SCTP_SNDRCV: i32 = 1;
let info = SndRcvInfo {
stream: sid,
ssn: 0,
flags: 0,
_pad: 0,
ppid: M3UA_PPID.to_be(),
context: 0,
timetolive: 0,
tsn: 0,
cumtsn: 0,
assoc_id: 0,
};
let space = unsafe { libc::CMSG_SPACE(std::mem::size_of::<SndRcvInfo>() as u32) } as usize;
let mut control = vec![0u8; space];
let mut iov = libc::iovec {
iov_base: message.as_ptr() as *mut libc::c_void,
iov_len: message.len(),
};
unsafe {
let mut msg: libc::msghdr = std::mem::zeroed();
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = control.as_mut_ptr() as *mut libc::c_void;
msg.msg_controllen = space as _;
let cmsg = libc::CMSG_FIRSTHDR(&msg);
(*cmsg).cmsg_level = IPPROTO_SCTP;
(*cmsg).cmsg_type = SCTP_SNDRCV;
(*cmsg).cmsg_len = libc::CMSG_LEN(std::mem::size_of::<SndRcvInfo>() as u32) as _;
std::ptr::copy_nonoverlapping(
&info as *const SndRcvInfo as *const u8,
libc::CMSG_DATA(cmsg),
std::mem::size_of::<SndRcvInfo>(),
);
let n = libc::sendmsg(stream.as_raw_fd(), &msg, 0);
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}
}
#[cfg(not(unix))]
fn sctp_send(_stream: &TcpStream, _message: &[u8], _sid: u16) -> std::io::Result<usize> {
Err(std::io::Error::other(
"SCTP is not available on this platform",
))
}
impl AsyncWrite for Writer {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
loop {
std::task::ready!(self.stream.poll_write_ready(cx))?;
let result = if self.sctp {
let preferred = stream_for(buf);
let stream = self.stream.clone();
let mut r = stream.try_io(tokio::io::Interest::WRITABLE, || {
sctp_send(&stream, buf, preferred)
});
if preferred != 0 && matches!(&r, Err(e) if e.raw_os_error() == Some(libc::EINVAL))
{
r = stream.try_io(tokio::io::Interest::WRITABLE, || sctp_send(&stream, buf, 0));
}
r
} else {
self.stream.try_write(buf)
};
match result {
Ok(n) => return Poll::Ready(Ok(n)),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
Err(e) => return Poll::Ready(Err(e)),
}
}
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
self.close();
Poll::Ready(Ok(()))
}
}