use std::{
num::NonZeroUsize,
sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
},
};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use tocat_api::normalize;
use tokio::net::UnixDatagram;
use tracing::{info, warn};
use crate::{
endpoint::{
Connection, DEFAULT_MAX_CONNECTIONS, EndpointStream,
datagram::{self, Demux},
parse::{Opt, ParseEndpointError},
sys::Mode,
unix::{SocketPath, apply_mode, unlink_stale},
},
shutdown::Shutdown,
};
#[derive(Debug, Deserialize, Serialize)]
pub struct UnixDgram {
pub path: SocketPath,
#[serde(default)]
pub bind: Option<SocketPath>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub unlink: bool,
#[serde(default)]
pub mode: Option<Mode>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct UnixDgramListen {
pub path: SocketPath,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub fork: bool,
#[serde(default, rename = "max-connections")]
pub max_connections: Option<NonZeroUsize>,
#[serde(default)]
pub unlink: bool,
#[serde(default)]
pub mode: Option<Mode>,
}
impl UnixDgram {
const SCHEME: &'static str = "unix-dgram";
pub(in crate::endpoint) fn parse<'a>(
body: &str,
opts: impl Iterator<Item = Opt<'a>>,
) -> Result<Self, ParseEndpointError> {
let mut bind = None;
let mut name = None;
let mut unlink = false;
let mut mode = None;
for opt in opts {
match normalize(opt.key).as_str() {
"bind" => bind = Some(SocketPath::from_spec(opt.text()?)),
"mode" => mode = Some(opt.mode()?),
"name" => name = Some(opt.string()?),
"unlink" => unlink = opt.flag()?,
_ => return Err(opt.unsupported(Self::SCHEME)),
}
}
if unlink && bind.is_none() {
return Err(ParseEndpointError::Conflict {
scheme: Self::SCHEME,
reason: "unlink clears a stale local address, and there is no bind= to clear",
});
}
Ok(Self {
path: SocketPath::from_spec(body),
bind,
name,
unlink,
mode,
})
}
pub(in crate::endpoint) fn label(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| format!("unix-dgram://{}", self.path))
}
pub(in crate::endpoint) async fn connect(&self) -> anyhow::Result<Connection> {
let local = match &self.bind {
Some(bind) => bind.clone(),
None => temp_socket_path(),
};
if self.unlink {
unlink_stale(&local, async {
UnixDatagram::unbound().and_then(|probe| probe.connect(local.as_path()))
})
.await?;
}
let socket = bind_datagram(&local)?;
let mode = match self.bind {
Some(_) => self.mode,
None => self.mode.or(Some(Mode::PRIVATE)),
};
apply_mode(&local, mode)?;
let socket = connect_datagram(socket, &self.path)?;
info!(local = %local, peer = %self.path, "sending datagrams");
Ok(EndpointStream::unix_dgram(UnixDgramSocket::dialled(socket))
.into_connection_with_guard(local.guard()))
}
}
impl UnixDgramListen {
const SCHEME: &'static str = "unix-dgram-listen";
pub(in crate::endpoint) fn parse<'a>(
body: &str,
opts: impl Iterator<Item = Opt<'a>>,
) -> Result<Self, ParseEndpointError> {
let mut name = None;
let mut fork = false;
let mut max_connections = None;
let mut unlink = false;
let mut mode = None;
for opt in opts {
match normalize(opt.key).as_str() {
"fork" => fork = opt.flag()?,
"maxconnections" | "maxconn" => {
max_connections = Some(opt.count()?);
}
"mode" => mode = Some(opt.mode()?),
"name" => name = Some(opt.string()?),
"unlink" => unlink = opt.flag()?,
_ => return Err(opt.unsupported(Self::SCHEME)),
}
}
Ok(Self {
path: SocketPath::from_spec(body),
name,
fork,
max_connections,
unlink,
mode,
})
}
pub(in crate::endpoint) fn label(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| format!("unix-dgram://{}", self.path))
}
pub fn max_connections(&self) -> NonZeroUsize {
self.max_connections.unwrap_or(DEFAULT_MAX_CONNECTIONS)
}
pub async fn bind(&self) -> anyhow::Result<UnixDatagram> {
if self.unlink {
unlink_stale(&self.path, async {
UnixDatagram::unbound().and_then(|probe| probe.connect(self.path.as_path()))
})
.await?;
}
let socket = bind_datagram(&self.path)?;
apply_mode(&self.path, self.mode)?;
Ok(socket)
}
pub async fn demux(&self, buffer: usize, shutdown: Shutdown) -> anyhow::Result<Demux> {
let socket = Arc::new(self.bind().await?);
info!(path = %self.path, "listening for datagrams");
Ok(datagram::demux(
datagram::Socket::Unix(socket),
self.max_connections(),
buffer,
shutdown,
))
}
pub(in crate::endpoint) async fn connect(&self, buffer: usize) -> anyhow::Result<Connection> {
let socket = self.bind().await?;
let guard = self.path.guard();
info!(path = %self.path, "listening for datagrams");
let mut first = vec![0u8; buffer];
let (n, peer) = socket.recv_from(&mut first).await?;
first.truncate(n);
let connected = !peer.is_unnamed();
let socket = if connected {
info!(peer = ?peer, "peered");
connect_addr(socket, &peer.into()).context("peering with the first sender")?
} else {
warn!(
"the first sender has no address of its own, so this path can receive but not \
send; a peer that expects replies has to bind before it sends",
);
socket
};
Ok(
EndpointStream::unix_dgram(UnixDgramSocket::listening(socket, first, connected))
.into_connection_with_guard(guard),
)
}
}
pub struct UnixDgramSocket {
socket: UnixDatagram,
pending: Mutex<Option<Vec<u8>>>,
connected: bool,
}
impl UnixDgramSocket {
pub(in crate::endpoint) fn dialled(socket: UnixDatagram) -> Self {
Self {
socket,
pending: Mutex::new(None),
connected: true,
}
}
pub(in crate::endpoint) fn listening(
socket: UnixDatagram,
first: Vec<u8>,
connected: bool,
) -> Self {
Self {
socket,
pending: Mutex::new(Some(first)),
connected,
}
}
pub(in crate::endpoint) async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> {
let pending = self
.pending
.lock()
.expect("the pending message lock is never held across a panic")
.take();
if let Some(message) = pending {
let n = message.len().min(buf.len());
buf[..n].copy_from_slice(&message[..n]);
return Ok(n);
}
self.socket.recv(buf).await
}
pub(in crate::endpoint) async fn send(&self, buf: &[u8]) -> std::io::Result<usize> {
if !self.connected {
return Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"the peer is unnamed, so there is no address to send to",
));
}
self.socket.send(buf).await
}
}
fn temp_socket_path() -> SocketPath {
static NEXT: AtomicU64 = AtomicU64::new(0);
let seq = NEXT.fetch_add(1, Ordering::Relaxed);
let since_epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| since.as_nanos())
.unwrap_or_default();
SocketPath::from_path(std::env::temp_dir().join(format!(
"tocat-{}-{since_epoch}-{seq}.sock",
std::process::id()
)))
}
fn bind_datagram(path: &SocketPath) -> anyhow::Result<UnixDatagram> {
path.supported()?;
let addr = path
.addr()
.with_context(|| format!("{path} is not a usable socket address"))?;
let socket = std::os::unix::net::UnixDatagram::bind_addr(&addr)
.with_context(|| format!("binding {path}"))?;
socket
.set_nonblocking(true)
.with_context(|| format!("setting {path} non-blocking"))?;
UnixDatagram::from_std(socket).with_context(|| format!("registering {path}"))
}
fn connect_datagram(socket: UnixDatagram, peer: &SocketPath) -> anyhow::Result<UnixDatagram> {
peer.supported()?;
let addr = peer
.addr()
.with_context(|| format!("{peer} is not a usable socket address"))?;
connect_addr(socket, &addr).with_context(|| format!("connecting to {peer}"))
}
fn connect_addr(
socket: UnixDatagram,
addr: &std::os::unix::net::SocketAddr,
) -> std::io::Result<UnixDatagram> {
let socket = socket.into_std()?;
socket.connect_addr(addr)?;
UnixDatagram::from_std(socket)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::endpoint::EndpointSpec;
fn dial(s: &str) -> UnixDgram {
match s.parse::<EndpointSpec>().expect("parses") {
EndpointSpec::UnixDgram(e) => e,
other => panic!("wrong variant: {other:?}"),
}
}
fn listen(s: &str) -> UnixDgramListen {
match s.parse::<EndpointSpec>().expect("parses") {
EndpointSpec::UnixDgramListen(e) => e,
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn the_scheme_answers_to_its_spellings() {
for spec in [
"unix-dgram:/dev/log",
"unix-datagram:/dev/log",
"uds-dgram:/dev/log",
] {
assert_eq!(dial(spec).path, SocketPath::from_spec("/dev/log"));
}
assert!(listen("unix-dgram-listen:/tmp/tocat.sock,fork").fork);
}
#[test]
fn the_local_address_is_optional_and_explicit_when_given() {
assert_eq!(dial("unix-dgram:/dev/log").bind, None);
assert_eq!(
dial("unix-dgram:/dev/log,bind=@tocat").bind,
Some(SocketPath::from_spec("@tocat"))
);
}
#[test]
fn unlink_without_a_local_address_is_a_contradiction() {
assert!(matches!(
"unix-dgram:/dev/log,unlink"
.parse::<EndpointSpec>()
.expect_err("rejected"),
ParseEndpointError::Conflict { .. }
));
assert!(dial("unix-dgram:/dev/log,bind=/tmp/reply.sock,unlink").unlink);
}
#[test]
fn the_listening_options_match_the_other_listening_schemes() {
let e = listen("unix-dgram-listen:/tmp/tocat.sock,fork,unlink,mode=660,max-conn=4");
assert!(e.fork);
assert!(e.unlink);
assert_eq!(e.max_connections, NonZeroUsize::new(4));
assert_eq!(e.mode, Some("660".parse().expect("valid mode")));
}
#[test]
fn generated_addresses_are_distinct() {
assert_ne!(temp_socket_path(), temp_socket_path());
assert!(!temp_socket_path().is_abstract());
}
}