use std::io::{self, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use super::codec::{Frame, MuxError, PROTOCOL_VERSION};
use super::{read_frame, write_frame};
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProbeOutcome {
Live,
Absent,
Stale,
}
pub fn validate_control_socket(path: &Path) -> io::Result<()> {
use std::os::unix::fs::{FileTypeExt, MetadataExt};
let md = std::fs::symlink_metadata(path)?;
let reject = |why: String| {
io::Error::new(
io::ErrorKind::PermissionDenied,
format!("control socket {}: {why}", path.display()),
)
};
if md.file_type().is_symlink() {
return Err(reject("is a symlink; refusing to follow it".into()));
}
if !md.file_type().is_socket() {
return Err(reject("is not a Unix-domain socket".into()));
}
let euid = crate::sys::effective_uid()?;
if md.uid() != euid {
return Err(reject(format!(
"is owned by uid {} but we are euid {euid}; refusing to trust another user's master",
md.uid()
)));
}
let mode = md.mode() & 0o777;
if (mode & 0o077) != 0 {
return Err(reject(format!(
"is group/world-accessible (mode {mode:04o}); refusing to use it"
)));
}
Ok(())
}
fn connect_control_socket(path: &Path) -> io::Result<UnixStream> {
validate_control_socket(path)?;
UnixStream::connect(path)
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct SessionRequest {
pub want_pty: bool,
pub term: String,
pub cols: u32,
pub rows: u32,
pub env: Vec<(String, String)>,
pub command: Option<String>,
pub subsystem: bool,
}
pub fn open_forward(
path: &Path,
dest_host: &str,
dest_port: u16,
orig_host: &str,
orig_port: u16,
) -> Result<UnixStream, MuxError> {
let mut sock = hello(path)?;
write_frame(
&mut sock,
&Frame::OpenDirectTcpip {
dest_host: dest_host.to_string(),
dest_port: dest_port as u32,
orig_host: orig_host.to_string(),
orig_port: orig_port as u32,
},
)?;
match read_frame(&mut sock)? {
Some(Frame::OpenOk) => Ok(sock),
Some(Frame::OpenFail { reason }) => Err(MuxError::ForwardFailed(reason)),
_ => Err(MuxError::Unexpected(
"expected OPEN_OK/OPEN_FAIL from master",
)),
}
}
fn hello(path: &Path) -> Result<UnixStream, MuxError> {
let mut sock = connect_control_socket(path).map_err(MuxError::Io)?;
write_frame(
&mut sock,
&Frame::Hello {
version: PROTOCOL_VERSION,
},
)?;
match read_frame(&mut sock)? {
Some(Frame::Hello { version }) if version == PROTOCOL_VERSION => Ok(sock),
Some(Frame::Hello { version }) => Err(MuxError::VersionMismatch {
ours: PROTOCOL_VERSION,
theirs: version,
}),
_ => Err(MuxError::Unexpected("expected HELLO from master")),
}
}
pub fn request_forward(path: &Path, kind: u8, spec: &str, cancel: bool) -> Result<u16, MuxError> {
let mut sock = hello(path)?;
write_frame(
&mut sock,
&Frame::Forward {
kind,
spec: spec.to_string(),
cancel,
},
)?;
match read_frame(&mut sock)? {
Some(Frame::ForwardOk { port }) => Ok(port as u16),
Some(Frame::OpenFail { reason }) => Err(MuxError::ForwardFailed(reason)),
_ => Err(MuxError::Unexpected(
"expected FORWARD_OK/OPEN_FAIL from master",
)),
}
}
pub fn conninfo(path: &Path) -> Result<String, MuxError> {
let mut sock = hello(path)?;
write_frame(&mut sock, &Frame::ConnInfo)?;
match read_frame(&mut sock)? {
Some(Frame::ConnInfoReply { text }) => Ok(text),
_ => Err(MuxError::Unexpected("expected CONNINFO_REPLY from master")),
}
}
pub fn splice_forward<S>(sock: UnixStream, local: S) -> Result<(), MuxError>
where
S: Read + Write + TryCloneStream + Send + 'static,
{
let local_read = local.try_clone_stream().map_err(MuxError::Io)?;
let mut local_write = local;
let sock_read = sock.try_clone().map_err(MuxError::Io)?;
let sock_write = Arc::new(std::sync::Mutex::new(sock));
let up_write = sock_write.clone();
let t_up = thread::spawn(move || {
let mut local_read = local_read;
let mut buf = [0u8; 32 * 1024];
loop {
match local_read.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let mut g = match up_write.lock() {
Ok(g) => g,
Err(_) => break,
};
if write_frame(&mut *g, &Frame::StdinData(buf[..n].to_vec())).is_err() {
break;
}
}
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
if let Ok(mut g) = up_write.lock() {
let _ = write_frame(&mut *g, &Frame::Eof);
}
});
let mut sock_read = sock_read;
loop {
match read_frame(&mut sock_read) {
Ok(Some(Frame::StdoutData(d))) => {
if local_write.write_all(&d).is_err() {
break;
}
let _ = local_write.flush();
}
Ok(Some(Frame::Eof)) | Ok(None) => break,
Ok(Some(_)) => { }
Err(_) => break,
}
}
if let Ok(g) = sock_write.lock() {
let _ = g.shutdown(std::net::Shutdown::Both);
}
let _ = t_up.join();
Ok(())
}
pub trait TryCloneStream {
fn try_clone_stream(&self) -> io::Result<Self>
where
Self: Sized;
}
impl TryCloneStream for std::net::TcpStream {
fn try_clone_stream(&self) -> io::Result<Self> {
self.try_clone()
}
}
pub fn probe_master(path: &Path) -> ProbeOutcome {
if std::fs::symlink_metadata(path).is_err() {
return ProbeOutcome::Absent;
}
if validate_control_socket(path).is_err() {
return ProbeOutcome::Stale;
}
match UnixStream::connect(path) {
Ok(mut sock) => {
let _ = sock.set_read_timeout(Some(Duration::from_millis(500)));
let _ = sock.set_write_timeout(Some(Duration::from_millis(500)));
if write_frame(
&mut sock,
&Frame::Hello {
version: PROTOCOL_VERSION,
},
)
.is_err()
{
return ProbeOutcome::Stale;
}
match read_frame(&mut sock) {
Ok(Some(Frame::Hello { version })) if version == PROTOCOL_VERSION => {
ProbeOutcome::Live
}
_ => ProbeOutcome::Stale,
}
}
Err(_) => ProbeOutcome::Stale,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ControlCommand {
Check,
Exit,
}
pub fn send_control_command(path: &Path, cmd: ControlCommand) -> Result<bool, MuxError> {
let mut sock = match connect_control_socket(path) {
Ok(s) => s,
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => return Err(MuxError::Io(e)),
Err(_) if cmd == ControlCommand::Check => return Ok(false),
Err(e) => return Err(MuxError::Io(e)),
};
let _ = sock.set_read_timeout(Some(Duration::from_millis(2000)));
let _ = sock.set_write_timeout(Some(Duration::from_millis(2000)));
write_frame(
&mut sock,
&Frame::Hello {
version: PROTOCOL_VERSION,
},
)?;
match read_frame(&mut sock) {
Ok(Some(Frame::Hello { version })) if version == PROTOCOL_VERSION => {}
_ if cmd == ControlCommand::Check => return Ok(false),
Ok(Some(Frame::Hello { version })) => {
return Err(MuxError::VersionMismatch {
ours: PROTOCOL_VERSION,
theirs: version,
});
}
Ok(_) => return Err(MuxError::Unexpected("expected HELLO from master")),
Err(e) => return Err(e),
}
match cmd {
ControlCommand::Check => {
write_frame(&mut sock, &Frame::AliveCheck)?;
match read_frame(&mut sock) {
Ok(Some(Frame::AliveOk)) => Ok(true),
_ => Ok(false),
}
}
ControlCommand::Exit => {
write_frame(&mut sock, &Frame::ExitRequest)?;
let _ = read_frame(&mut sock);
Ok(true)
}
}
}
pub fn run_client(
path: &Path,
req: &SessionRequest,
resize: Option<Arc<dyn Fn() -> (u32, u32) + Send + Sync>>,
) -> Result<i32, MuxError> {
let mut sock = connect_control_socket(path).map_err(MuxError::Io)?;
write_frame(
&mut sock,
&Frame::Hello {
version: PROTOCOL_VERSION,
},
)?;
match read_frame(&mut sock)? {
Some(Frame::Hello { version }) if version == PROTOCOL_VERSION => {}
Some(Frame::Hello { version }) => {
return Err(MuxError::VersionMismatch {
ours: PROTOCOL_VERSION,
theirs: version,
});
}
_ => return Err(MuxError::Unexpected("expected HELLO from master")),
}
write_frame(
&mut sock,
&Frame::OpenSession {
want_pty: req.want_pty,
term: req.term.clone(),
cols: req.cols,
rows: req.rows,
env: req.env.clone(),
command: req.command.clone(),
subsystem: req.subsystem,
},
)?;
let done = Arc::new(AtomicBool::new(false));
let write_half = Arc::new(std::sync::Mutex::new(
sock.try_clone().map_err(MuxError::Io)?,
));
let in_write = write_half.clone();
let in_done = done.clone();
let t_in = thread::spawn(move || {
let mut buf = [0u8; 32 * 1024];
let mut stdin = io::stdin();
loop {
if in_done.load(Ordering::Relaxed) {
break;
}
match stdin.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let mut g = match in_write.lock() {
Ok(g) => g,
Err(_) => break,
};
if write_frame(&mut *g, &Frame::StdinData(buf[..n].to_vec())).is_err() {
break;
}
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
if let Ok(mut g) = in_write.lock() {
let _ = write_frame(&mut *g, &Frame::Eof);
}
});
let t_winch = resize.map(|get| {
let w = write_half.clone();
let stop = done.clone();
thread::spawn(move || {
let mut last = get();
while !stop.load(Ordering::Relaxed) {
thread::sleep(Duration::from_millis(100));
let cur = get();
if cur != last {
last = cur;
if let Ok(mut g) = w.lock() {
let _ = write_frame(
&mut *g,
&Frame::WindowChange {
cols: cur.0,
rows: cur.1,
},
);
}
}
}
})
});
let mut exit_code: i32 = 255;
let mut stdout = io::stdout();
let mut stderr = io::stderr();
loop {
match read_frame(&mut sock) {
Ok(Some(Frame::StdoutData(d))) => {
if stdout.write_all(&d).is_err() {
break;
}
let _ = stdout.flush();
}
Ok(Some(Frame::StderrData(d))) => {
if stderr.write_all(&d).is_err() {
break;
}
let _ = stderr.flush();
}
Ok(Some(Frame::ExitStatus { code })) => {
exit_code = code as i32;
}
Ok(Some(Frame::ExitSignal { name })) => {
let _ = writeln!(stderr, "\r\nsession terminated by signal: {name}");
exit_code = 255;
}
Ok(Some(Frame::Eof)) => {
break;
}
Ok(Some(Frame::AliveOk)) => { }
Ok(Some(_)) => { }
Ok(None) => break, Err(_) => break,
}
}
done.store(true, Ordering::Relaxed);
let _ = sock.shutdown(std::net::Shutdown::Both);
drop(t_in);
if let Some(h) = t_winch {
drop(h);
}
Ok(exit_code)
}
#[cfg(test)]
mod control_socket_tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::UnixListener;
fn scratch(tag: &str) -> std::path::PathBuf {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::path::PathBuf::from("/tmp").join(format!(
"p-mux-{tag}-{:x}-{:x}",
std::process::id(),
nanos as u32
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
dir
}
#[test]
fn missing_path_is_not_found() {
let dir = scratch("missing");
let e = validate_control_socket(&dir.join("nope")).unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::NotFound);
assert_eq!(probe_master(&dir.join("nope")), ProbeOutcome::Absent);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn regular_file_is_rejected() {
let dir = scratch("file");
let p = dir.join("sock");
std::fs::write(&p, b"").unwrap();
let e = validate_control_socket(&p).unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::PermissionDenied);
assert!(e.to_string().contains("not a Unix-domain socket"), "{e}");
assert_eq!(probe_master(&p), ProbeOutcome::Stale);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn symlink_to_socket_is_rejected() {
let dir = scratch("link");
let real = dir.join("real");
let _l = UnixListener::bind(&real).unwrap();
std::fs::set_permissions(&real, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(validate_control_socket(&real).is_ok());
let link = dir.join("link");
std::os::unix::fs::symlink(&real, &link).unwrap();
let e = validate_control_socket(&link).unwrap_err();
assert!(e.to_string().contains("symlink"), "{e}");
assert_eq!(probe_master(&link), ProbeOutcome::Stale);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn group_or_world_accessible_socket_is_rejected() {
let dir = scratch("mode");
let p = dir.join("sock");
let _l = UnixListener::bind(&p).unwrap();
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o660)).unwrap();
let e = validate_control_socket(&p).unwrap_err();
assert!(e.to_string().contains("group/world-accessible"), "{e}");
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(validate_control_socket(&p).is_ok());
let _ = std::fs::remove_dir_all(&dir);
}
}