use std::io::{ErrorKind, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::sync::Arc;
use std::thread;
#[cfg(feature = "server")]
use std::collections::BTreeMap;
#[cfg(feature = "server")]
use std::os::unix::net::UnixListener;
#[cfg(feature = "server")]
use std::sync::Mutex;
#[cfg(feature = "server")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "server")]
use std::thread::JoinHandle;
#[cfg(feature = "server")]
use std::time::Duration;
#[cfg(feature = "server")]
use crate::error::{Error, Result};
#[cfg(feature = "server")]
use crate::server::{StreamlocalForwardContext, StreamlocalForwardHandler};
use crate::stream::{ChannelEgress, ChannelStream};
#[cfg(feature = "server")]
const ACCEPT_POLL_INTERVAL: Duration = Duration::from_millis(100);
#[cfg(feature = "server")]
struct Binding {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
socket_path: PathBuf,
}
#[cfg(feature = "server")]
impl Drop for Binding {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
let _ = std::fs::remove_file(&self.socket_path);
}
}
#[cfg(feature = "server")]
type AllowFilter = Box<dyn Fn(&str, &str) -> bool + Send + Sync>;
#[cfg(feature = "server")]
enum Policy {
Deny,
All,
Filter(AllowFilter),
}
#[cfg(feature = "server")]
pub struct DefaultStreamlocalForwardHandler {
bindings: Mutex<BTreeMap<String, Binding>>,
policy: Policy,
}
#[cfg(feature = "server")]
impl Default for DefaultStreamlocalForwardHandler {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "server")]
impl DefaultStreamlocalForwardHandler {
pub fn new() -> Self {
Self {
bindings: Mutex::new(BTreeMap::new()),
policy: Policy::Deny,
}
}
pub fn permit_all() -> Self {
Self {
bindings: Mutex::new(BTreeMap::new()),
policy: Policy::All,
}
}
pub fn with_allow_filter<F>(mut self, filter: F) -> Self
where
F: Fn(&str, &str) -> bool + Send + Sync + 'static,
{
self.policy = Policy::Filter(Box::new(filter));
self
}
fn allowed(&self, user: &str, socket_path: &str) -> bool {
match &self.policy {
Policy::Deny => false,
Policy::All => true,
Policy::Filter(f) => f(user, socket_path),
}
}
pub fn binding_count(&self) -> usize {
self.bindings.lock().map(|m| m.len()).unwrap_or(0)
}
}
#[cfg(feature = "server")]
impl StreamlocalForwardHandler for DefaultStreamlocalForwardHandler {
fn bind(&self, user: &str, socket_path: &str, ctx: StreamlocalForwardContext) -> Result<()> {
if !self.allowed(user, socket_path) {
return Err(Error::Protocol(
"streamlocal-forward: bind refused by policy",
));
}
let path = PathBuf::from(socket_path);
if let Ok(meta) = std::fs::symlink_metadata(&path)
&& meta.file_type().is_symlink()
{
return Err(Error::Protocol(
"streamlocal-forward: refusing to bind over a symlink",
));
}
if let Ok(meta) = std::fs::symlink_metadata(&path) {
use std::os::unix::fs::FileTypeExt;
if meta.file_type().is_socket() {
let _ = std::fs::remove_file(&path);
}
}
let listener = UnixListener::bind(&path)?;
listener.set_nonblocking(true)?;
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = Arc::clone(&stop);
let bind_path_owned = socket_path.to_string();
let handle = thread::spawn(move || {
while !stop_thread.load(Ordering::SeqCst) {
match listener.accept() {
Ok((conn, _peer)) => {
match ctx.open_forwarded_streamlocal(&bind_path_owned) {
Ok(channel_stream) => spawn_unix_splice(conn, channel_stream),
Err(_) => {
let _ = conn.shutdown(std::net::Shutdown::Both);
}
}
}
Err(e) if e.kind() == ErrorKind::WouldBlock => {
thread::sleep(ACCEPT_POLL_INTERVAL);
}
Err(_) => break,
}
}
});
let mut map = self
.bindings
.lock()
.map_err(|_| Error::Protocol("streamlocal-forward: lock poisoned"))?;
if let Some(existing) = map.remove(socket_path) {
drop(existing);
}
map.insert(
socket_path.to_string(),
Binding {
stop,
handle: Some(handle),
socket_path: path,
},
);
Ok(())
}
fn unbind(&self, _user: &str, socket_path: &str) -> Result<()> {
let mut map = self
.bindings
.lock()
.map_err(|_| Error::Protocol("streamlocal-forward: lock poisoned"))?;
if let Some(binding) = map.remove(socket_path) {
drop(map);
drop(binding);
Ok(())
} else {
Err(Error::Protocol(
"cancel-streamlocal-forward: no such binding",
))
}
}
}
fn spawn_unix_splice(uds: UnixStream, stream: ChannelStream) {
let (chan_rx, chan_tx) = stream.into_raw();
let Ok(uds_in) = uds.try_clone() else {
let _ = chan_tx.send(ChannelEgress::Eof);
let _ = chan_tx.send(ChannelEgress::Close);
return;
};
let uds_out = uds;
let chan_tx_a = chan_tx.clone();
let mut uds_in_a = uds_in;
let a = thread::spawn(move || {
let mut buf = [0u8; 32 * 1024];
loop {
match uds_in_a.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if chan_tx_a
.send(ChannelEgress::Data(buf[..n].to_vec()))
.is_err()
{
break;
}
}
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
let _ = chan_tx_a.send(ChannelEgress::Eof);
});
let mut uds_out_b = uds_out;
let b = thread::spawn(move || {
while let Ok(Some(chunk)) = chan_rx.recv() {
if uds_out_b.write_all(&chunk).is_err() {
break;
}
}
let _ = uds_out_b.shutdown(std::net::Shutdown::Read);
});
thread::spawn(move || {
let _ = a.join();
let _ = b.join();
let _ = chan_tx.send(ChannelEgress::Close);
});
}
#[cfg(feature = "client")]
pub fn splice_to_unix_socket_callback(
path: PathBuf,
) -> Arc<dyn Fn(crate::client::ForwardedStreamlocalOrigin, ChannelStream) + Send + Sync + 'static> {
Arc::new(
move |_origin: crate::client::ForwardedStreamlocalOrigin, stream: ChannelStream| {
match UnixStream::connect(&path) {
Ok(uds) => spawn_unix_splice(uds, stream),
Err(_) => {
let (_rx, tx) = stream.into_raw();
let _ = tx.send(ChannelEgress::Eof);
let _ = tx.send(ChannelEgress::Close);
}
}
},
)
}
#[cfg(all(test, feature = "server"))]
mod tests {
use super::*;
struct TestTempDir {
path: PathBuf,
}
impl TestTempDir {
fn new(prefix: &str) -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let pid = std::process::id();
let path =
PathBuf::from("/tmp").join(format!("p-slf-{prefix}-{pid:x}-{:x}", nanos as u32));
std::fs::create_dir_all(&path).expect("create tempdir");
Self { path }
}
}
impl Drop for TestTempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
#[test]
fn bind_creates_socket_and_unbind_releases_it() {
let dir = TestTempDir::new("bind");
let sock = dir.path.join("fwd.sock");
let sock_str = sock.to_string_lossy().to_string();
let h = DefaultStreamlocalForwardHandler::permit_all();
h.bind(
"u",
&sock_str,
StreamlocalForwardContext::for_test_no_opens(),
)
.expect("bind");
assert!(sock.exists(), "socket should exist on disk after bind");
assert_eq!(h.binding_count(), 1);
h.unbind("u", &sock_str).expect("unbind");
assert_eq!(h.binding_count(), 0);
for _ in 0..50 {
if !sock.exists() {
break;
}
thread::sleep(Duration::from_millis(50));
}
assert!(!sock.exists(), "socket should be unlinked after unbind");
}
#[test]
fn default_constructor_is_deny_all() {
let dir = TestTempDir::new("deny");
let sock = dir.path.join("fwd.sock");
let h = DefaultStreamlocalForwardHandler::new();
assert!(
h.bind(
"u",
&sock.to_string_lossy(),
StreamlocalForwardContext::for_test_no_opens(),
)
.is_err()
);
assert_eq!(h.binding_count(), 0);
assert!(!sock.exists());
}
#[test]
fn unbind_of_unknown_binding_errors() {
let h = DefaultStreamlocalForwardHandler::new();
assert!(h.unbind("u", "/tmp/nope.sock").is_err());
}
#[test]
fn allow_filter_sees_user_and_path() {
let dir = TestTempDir::new("filter");
let sock = dir.path.join("fwd.sock");
let sock_str = sock.to_string_lossy().to_string();
let h = DefaultStreamlocalForwardHandler::new()
.with_allow_filter(|user, _path| user == "alice");
assert!(
h.bind(
"bob",
&sock_str,
StreamlocalForwardContext::for_test_no_opens()
)
.is_err()
);
h.bind(
"alice",
&sock_str,
StreamlocalForwardContext::for_test_no_opens(),
)
.expect("alice bind allowed");
h.unbind("alice", &sock_str).expect("unbind");
}
}