pub(super) mod dgram;
pub(super) mod seqpacket;
use std::{
ffi::OsStr,
future::Future,
num::NonZeroUsize,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
};
use anyhow::Context;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tocat_api::normalize;
use tokio::net::{UnixListener, UnixStream};
use tracing::info;
use crate::endpoint::{
Connection, EndpointStream,
parse::{Opt, ParseEndpointError},
sys::{Mode, PathGuard},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SocketPath(PathBuf);
impl SocketPath {
pub(super) fn from_spec(body: &str) -> Self {
match body.strip_prefix('@') {
Some(name) => {
let mut bytes = Vec::with_capacity(name.len() + 1);
bytes.push(0);
bytes.extend_from_slice(name.as_bytes());
Self(PathBuf::from(OsStr::from_bytes(&bytes)))
}
None => Self(PathBuf::from(body)),
}
}
pub(super) fn from_path(path: PathBuf) -> Self {
Self(path)
}
pub(super) fn as_path(&self) -> &Path {
&self.0
}
pub fn is_abstract(&self) -> bool {
self.abstract_name().is_some()
}
fn abstract_name(&self) -> Option<&[u8]> {
self.0.as_os_str().as_bytes().strip_prefix(b"\0")
}
pub fn guard(&self) -> Option<PathGuard> {
(!self.is_abstract()).then(|| PathGuard(self.0.clone()))
}
pub(super) fn addr(&self) -> std::io::Result<std::os::unix::net::SocketAddr> {
#[cfg(target_os = "linux")]
if let Some(name) = self.abstract_name() {
use std::os::linux::net::SocketAddrExt as _;
return std::os::unix::net::SocketAddr::from_abstract_name(name);
}
std::os::unix::net::SocketAddr::from_pathname(&self.0)
}
pub(super) fn supported(&self) -> anyhow::Result<()> {
#[cfg(not(target_os = "linux"))]
if self.is_abstract() {
anyhow::bail!("{self} names the abstract namespace, which only Linux has");
}
Ok(())
}
}
impl std::fmt::Display for SocketPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.abstract_name() {
Some(name) => write!(f, "@{}", String::from_utf8_lossy(name)),
None => write!(f, "{}", self.0.display()),
}
}
}
impl<'de> Deserialize<'de> for SocketPath {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Self::from_spec(&String::deserialize(deserializer)?))
}
}
impl Serialize for SocketPath {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.is_abstract() {
serializer.serialize_str(&self.to_string())
} else {
self.0.serialize(serializer)
}
}
}
pub(super) async fn unlink_stale(
path: &SocketPath,
probe: impl Future<Output = std::io::Result<()>>,
) -> anyhow::Result<()> {
if path.is_abstract() || !path.as_path().exists() {
return Ok(());
}
match probe.await {
Ok(()) => anyhow::bail!("{path} is already in use"),
Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => {
std::fs::remove_file(path.as_path())
.with_context(|| format!("removing stale socket {path}"))?;
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e).with_context(|| format!("probing {path}")),
}
Ok(())
}
pub(super) fn apply_mode(path: &SocketPath, mode: Option<Mode>) -> anyhow::Result<()> {
let Some(mode) = mode else {
return Ok(());
};
if path.is_abstract() {
anyhow::bail!(
"mode cannot be applied to {path}: an abstract name has no filesystem entry and no \
permission check, so anything in the network namespace can reach it",
);
}
mode.apply(path.as_path())
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Unix {
pub path: SocketPath,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct UnixListen {
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 Unix {
const SCHEME: &'static str = "unix";
pub(super) fn parse<'a>(
body: &str,
opts: impl Iterator<Item = Opt<'a>>,
) -> Result<Self, ParseEndpointError> {
let mut name = None;
for opt in opts {
match normalize(opt.key).as_str() {
"name" => name = Some(opt.string()?),
_ => return Err(opt.unsupported(Self::SCHEME)),
}
}
Ok(Self {
path: SocketPath::from_spec(body),
name,
})
}
pub(super) fn label(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| format!("unix://{}", self.path))
}
pub(super) async fn connect(&self) -> anyhow::Result<Connection> {
self.path.supported()?;
let stream = UnixStream::connect(self.path.as_path())
.await
.with_context(|| format!("connecting to {}", self.path))?;
Ok(EndpointStream::unix(stream).into_connection())
}
}
impl UnixListen {
const SCHEME: &'static str = "unix-listen";
pub(super) 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(super) fn label(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| format!("unix://{}", self.path))
}
pub async fn bind(&self) -> anyhow::Result<UnixListener> {
self.path.supported()?;
if self.unlink {
unlink_stale(&self.path, async {
UnixStream::connect(self.path.as_path()).await.map(drop)
})
.await?;
}
let listener = UnixListener::bind(self.path.as_path())
.with_context(|| format!("binding {}", self.path))?;
apply_mode(&self.path, self.mode)?;
Ok(listener)
}
pub(super) async fn connect(&self) -> anyhow::Result<Connection> {
let listener = self.bind().await?;
let guard = self.path.guard();
info!(path = %self.path, "listening");
let (stream, _) = listener.accept().await?;
Ok(EndpointStream::unix(stream).into_connection_with_guard(guard))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::endpoint::EndpointSpec;
fn dial(s: &str) -> Unix {
match s.parse::<EndpointSpec>().expect("parses") {
EndpointSpec::Unix(e) => e,
other => panic!("wrong variant: {other:?}"),
}
}
fn listen(s: &str) -> UnixListen {
match s.parse::<EndpointSpec>().expect("parses") {
EndpointSpec::UnixListen(e) => e,
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn a_plain_path_is_a_path() {
let path = SocketPath::from_spec("/run/app/app.sock");
assert!(!path.is_abstract());
assert_eq!(path.as_path(), Path::new("/run/app/app.sock"));
assert_eq!(path.to_string(), "/run/app/app.sock");
}
#[test]
fn an_at_sign_becomes_the_abstract_namespace() {
let path = SocketPath::from_spec("@tocat");
assert!(path.is_abstract());
assert_eq!(path.as_path().as_os_str().as_bytes(), b"\0tocat");
assert_eq!(path.to_string(), "@tocat");
}
#[test]
fn a_relative_at_sign_is_still_a_file() {
assert!(!SocketPath::from_spec("./@tocat").is_abstract());
assert!(!SocketPath::from_path(PathBuf::from("@tocat")).is_abstract());
}
#[test]
fn only_a_real_path_has_a_guard() {
assert!(SocketPath::from_spec("/tmp/tocat.sock").guard().is_some());
assert!(SocketPath::from_spec("@tocat").guard().is_none());
}
#[test]
fn the_table_form_reads_addresses_the_same_way() {
let spec: EndpointSpec =
toml::from_str("type = \"unix-listen\"\npath = \"@tocat\"").expect("deserialises");
match spec {
EndpointSpec::UnixListen(e) => assert!(e.path.is_abstract()),
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn an_abstract_address_round_trips_through_toml() {
let encoded = toml::to_string(&Unix {
path: SocketPath::from_spec("@tocat"),
name: Some("control".to_owned()),
})
.expect("serialises");
assert!(encoded.contains("\"@tocat\""), "{encoded}");
}
#[test]
fn the_listening_options_are_all_accepted() {
let e = listen("unix-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 dialling_rejects_the_listening_options() {
assert!(matches!(
"unix:/tmp/tocat.sock,fork"
.parse::<EndpointSpec>()
.expect_err("rejected"),
ParseEndpointError::UnsupportedOption { .. }
));
assert_eq!(dial("unix:/tmp/tocat.sock,name=control").label(), "control");
}
}