use std::env;
use std::fs;
use std::io::{self, Read, Write};
use std::mem;
use std::net::Shutdown;
use std::os::unix::fs::DirBuilderExt;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{OnceLock, mpsc};
use std::thread;
use std::time::{Duration, Instant};
use super::Accepted;
const WRITE_CHUNK: usize = 2 * 1024;
const WRITE_WAIT: Duration = Duration::from_millis(50);
const ACCEPT_POLL: Duration = Duration::from_millis(50);
const SOCKET_PROBE_TIMEOUT: Duration = Duration::from_millis(250);
pub(crate) struct Listener {
socket: UnixListener,
unlink: Unlink,
waited: bool,
}
struct Unlink {
path: PathBuf,
ident: Option<(u64, u64)>,
}
impl Drop for Unlink {
fn drop(&mut self) {
remove_own_socket(&self.path, self.ident);
}
}
impl Listener {
pub(crate) fn bind(path: PathBuf) -> io::Result<Self> {
let path = absolutize(path)?;
taria::socket::check_path_len(&path)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err.to_string()))?;
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
ensure_private_dir(parent)?;
}
clear_socket_path(&path)?;
let socket = UnixListener::bind(&path)?;
if let Err(err) = socket.set_nonblocking(true) {
drop(socket);
let _ = fs::remove_file(&path);
return Err(err);
}
let ident = socket_ident(&path);
Ok(Self {
socket,
unlink: Unlink { path, ident },
waited: false,
})
}
pub(crate) fn path(&self) -> &Path {
&self.unlink.path
}
pub(crate) fn accept(&mut self, shutdown: impl Fn() -> bool) -> Option<Accepted> {
loop {
if shutdown() {
return None;
}
match self.socket.accept() {
Ok((stream, _addr)) => {
if shutdown() {
return None;
}
if stream.set_nonblocking(false).is_err() {
continue;
}
let waited = mem::replace(&mut self.waited, true);
return Some(Accepted {
connection: Connection { stream },
waited,
});
}
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
self.waited = false;
thread::sleep(ACCEPT_POLL);
}
Err(_) => {
if shutdown() {
return None;
}
thread::sleep(ACCEPT_POLL);
}
}
}
}
}
pub(crate) struct Connection {
stream: UnixStream,
}
impl Connection {
pub(crate) fn set_write_timeout(&self, timeout: Duration) -> io::Result<()> {
self.stream.set_write_timeout(Some(timeout))
}
pub(crate) fn split(self) -> io::Result<(Reader, Writer)> {
let writer = self.stream.try_clone()?;
Ok((
Reader {
stream: self.stream,
},
Writer { stream: writer },
))
}
}
pub(crate) struct Reader {
stream: UnixStream,
}
impl Read for Reader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.stream.read(buf)
}
}
impl Reader {
pub(crate) fn with_nonblocking_reads<R>(
&mut self,
f: impl FnOnce(&mut dyn Read) -> R,
) -> io::Result<R> {
self.stream.set_nonblocking(true)?;
let outcome = f(&mut &self.stream);
self.stream.set_nonblocking(false)?;
Ok(outcome)
}
}
pub(crate) struct Writer {
stream: UnixStream,
}
#[cfg(test)]
impl From<UnixStream> for Writer {
fn from(stream: UnixStream) -> Self {
Self { stream }
}
}
impl Writer {
pub(crate) fn write_all_before(
&mut self,
mut bytes: &[u8],
deadline: Instant,
) -> io::Result<()> {
while !bytes.is_empty() {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(io::ErrorKind::TimedOut.into());
}
self.stream.set_write_timeout(Some(left.min(WRITE_WAIT)))?;
let chunk = bytes.get(..WRITE_CHUNK).unwrap_or(bytes);
match self.stream.write(chunk) {
Ok(0) => return Err(io::ErrorKind::WriteZero.into()),
Ok(n) => bytes = bytes.get(n..).unwrap_or_default(),
Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
Err(err)
if matches!(
err.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
) => {}
Err(err) => return Err(err),
}
}
Ok(())
}
pub(crate) fn shutdown(&self) -> io::Result<()> {
self.stream.shutdown(Shutdown::Both)
}
}
fn remove_own_socket(path: &Path, bound: Option<(u64, u64)>) {
if bound.is_some() && socket_ident(path) == bound {
let _ = fs::remove_file(path);
}
}
fn socket_ident(path: &Path) -> Option<(u64, u64)> {
let meta = fs::symlink_metadata(path).ok()?;
Some((meta.dev(), meta.ino()))
}
pub(crate) fn absolutize(path: PathBuf) -> io::Result<PathBuf> {
if path.is_absolute() {
Ok(path)
} else {
Ok(env::current_dir()?.join(path))
}
}
fn clear_socket_path(path: &Path) -> io::Result<()> {
let meta = match fs::symlink_metadata(path) {
Ok(meta) => meta,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => {
return Err(io::Error::new(
err.kind(),
format!("cannot inspect {}: {err}", path.display()),
));
}
};
if !meta.file_type().is_socket() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"{} already exists and is not a socket (it is {}); refusing to delete it to \
bind there. Point $TARIA_SOCK at a path taria may create.",
path.display(),
describe_file_type(&meta)
),
));
}
match probe_socket(path) {
SocketProbe::Stale => match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(io::Error::new(
err.kind(),
format!("cannot remove the stale socket {}: {err}", path.display()),
)),
},
SocketProbe::Live => Err(io::Error::new(
io::ErrorKind::AddrInUse,
format!(
"another instance is already serving the taria socket {}; refusing to take it \
over, because that would leave it running with no way for a bridge to reach \
it. To run a second instance alongside it, give it a socket of its own: set \
TARIA_SOCK to a different path.",
path.display()
),
)),
SocketProbe::Unknown(why) => Err(io::Error::new(
io::ErrorKind::AddrInUse,
format!(
"cannot tell whether another instance is serving the taria socket {} ({why}); \
refusing to remove a socket that may still be in use. Set TARIA_SOCK to a \
different path to bind elsewhere.",
path.display()
),
)),
}
}
fn describe_file_type(meta: &fs::Metadata) -> &'static str {
let kind = meta.file_type();
if kind.is_symlink() {
"a symlink"
} else if kind.is_dir() {
"a directory"
} else if kind.is_file() {
"a regular file"
} else {
"another kind of file"
}
}
enum SocketProbe {
Live,
Stale,
Unknown(String),
}
fn probe_socket(path: &Path) -> SocketProbe {
let (tx, rx) = mpsc::channel();
let probe_path = path.to_path_buf();
let spawned = thread::Builder::new()
.name("taria-socket-probe".into())
.spawn(move || {
let outcome = UnixStream::connect(&probe_path).map(|stream| {
let _ = stream.shutdown(Shutdown::Both);
});
let _ = tx.send(outcome);
});
if let Err(err) = spawned {
return SocketProbe::Unknown(format!("the probe thread could not start: {err}"));
}
match rx.recv_timeout(SOCKET_PROBE_TIMEOUT) {
Ok(Ok(())) => SocketProbe::Live,
Ok(Err(err))
if matches!(
err.kind(),
io::ErrorKind::ConnectionRefused | io::ErrorKind::NotFound
) =>
{
SocketProbe::Stale
}
Ok(Err(err)) => SocketProbe::Unknown(format!("connecting to it failed: {err}")),
Err(_) => SocketProbe::Unknown(format!(
"connecting to it did not finish within {SOCKET_PROBE_TIMEOUT:?}"
)),
}
}
pub(crate) fn ensure_private_dir(dir: &Path) -> io::Result<()> {
let mut builder = fs::DirBuilder::new();
builder.recursive(true);
builder.mode(0o700);
let _ = builder.create(dir);
let meta = fs::symlink_metadata(dir).map_err(|err| {
io::Error::new(
err.kind(),
format!("socket directory {} is unusable: {err}", dir.display()),
)
})?;
if !meta.file_type().is_dir() {
return Err(io::Error::other(format!(
"socket directory {} is not a real directory (it may be a symlink planted by \
another user to hijack the socket); refusing to use it",
dir.display()
)));
}
let uid = current_uid()?;
if meta.uid() != uid {
return Err(io::Error::other(format!(
"socket directory {} is owned by uid {} instead of the current user (uid {}); \
its owner could replace the socket to hijack the connection; refusing to use it",
dir.display(),
meta.uid(),
uid
)));
}
if meta.mode() & 0o077 != 0 {
return Err(io::Error::other(format!(
"socket directory {} is group/world-accessible (mode {:03o}); other users could \
replace the socket to hijack the connection; re-create it with mode 0700",
dir.display(),
meta.mode() & 0o777
)));
}
Ok(())
}
fn current_uid() -> io::Result<u32> {
static UID: OnceLock<u32> = OnceLock::new();
if let Some(&uid) = UID.get() {
return Ok(uid);
}
let uid = match fs::metadata("/proc/self") {
Ok(meta) => meta.uid(),
Err(_) => uid_from_probe(&env::temp_dir())?,
};
Ok(*UID.get_or_init(|| uid))
}
fn uid_from_probe(dir: &Path) -> io::Result<u32> {
static NEXT: AtomicU64 = AtomicU64::new(0);
let probe = dir.join(format!(
"taria-uid-probe-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
let uid = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&probe)
.and_then(|file| {
let uid = file.metadata().map(|meta| meta.uid());
drop(file);
let _ = fs::remove_file(&probe);
uid
});
uid.map_err(|err| {
io::Error::new(
err.kind(),
format!(
"cannot learn this process's uid from a probe file at {}: {err}",
probe.display()
),
)
})
}
pub(crate) fn user_identity() -> String {
if let Ok(meta) = fs::metadata("/proc/self") {
return meta.uid().to_string();
}
env::var("USER")
.or_else(|_| env::var("LOGNAME"))
.unwrap_or_else(|_| "default".to_string())
}
#[cfg(test)]
mod tests {
use std::sync::Barrier;
use super::*;
#[test]
fn probes_made_at_once_all_learn_the_uid() {
const THREADS: usize = 8;
const ROUNDS: usize = 50;
let dir = tempfile::tempdir().expect("create a probe dir");
let expected = current_uid().expect("learn the uid");
let owner = fs::metadata(dir.path()).expect("stat the probe dir").uid();
assert_eq!(expected, owner);
if let Ok(meta) = fs::metadata("/proc/self") {
assert_eq!(expected, meta.uid());
}
for round in 0..ROUNDS {
let barrier = Barrier::new(THREADS);
let results: Vec<io::Result<u32>> = thread::scope(|scope| {
let probes: Vec<_> = (0..THREADS)
.map(|_| {
scope.spawn(|| {
barrier.wait();
uid_from_probe(dir.path())
})
})
.collect();
probes
.into_iter()
.map(|probe| probe.join().expect("a probe thread panicked"))
.collect()
});
for result in results {
match result {
Ok(uid) => assert_eq!(uid, expected, "round {round}"),
Err(err) => panic!("round {round}: a probe failed: {err}"),
}
}
}
let left: Vec<_> = fs::read_dir(dir.path())
.expect("list the probe dir")
.map(|entry| entry.map(|entry| entry.file_name()))
.collect::<io::Result<_>>()
.expect("read the probe dir");
assert!(left.is_empty(), "probe files left behind: {left:?}");
}
#[test]
fn a_failed_probe_names_its_path_and_purpose() {
let dir = tempfile::tempdir().expect("create a probe dir");
let missing = dir.path().join("missing");
let err = uid_from_probe(&missing).expect_err("a probe in a missing dir fails");
assert_eq!(err.kind(), io::ErrorKind::NotFound);
let message = err.to_string();
assert!(
message.starts_with(&format!(
"cannot learn this process's uid from a probe file at {}",
missing.join("taria-uid-probe-").display()
)),
"{message}"
);
}
}