use std::fs;
use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use crate::shared::{OwnedChannelStream, SharedClient};
use super::client::{ProbeOutcome, probe_master};
use super::codec::{Frame, PROTOCOL_VERSION};
use super::{read_frame, write_frame};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Persist {
No,
Yes,
Seconds(u64),
}
#[non_exhaustive]
pub struct MasterConfig {
pub control_path: PathBuf,
pub persist: Persist,
pub on_exit: Option<Arc<dyn Fn() + Send + Sync>>,
pub on_forward: Option<ForwardHook>,
pub on_conninfo: Option<Arc<dyn Fn() -> String + Send + Sync>>,
pub peer_check: Option<PeerCheck>,
}
pub type ForwardHook = Arc<dyn Fn(u8, &str, bool) -> Result<u16, String> + Send + Sync>;
pub type PeerCheck = Arc<dyn Fn(&UnixStream) -> bool + Send + Sync>;
impl MasterConfig {
pub fn new(control_path: PathBuf, persist: Persist) -> Self {
Self {
control_path,
persist,
on_exit: None,
on_forward: None,
on_conninfo: None,
peer_check: None,
}
}
pub fn with_on_forward(
mut self,
f: impl Fn(u8, &str, bool) -> Result<u16, String> + Send + Sync + 'static,
) -> Self {
self.on_forward = Some(Arc::new(f));
self
}
pub fn with_on_conninfo(mut self, f: impl Fn() -> String + Send + Sync + 'static) -> Self {
self.on_conninfo = Some(Arc::new(f));
self
}
pub fn with_on_exit(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
self.on_exit = Some(Arc::new(f));
self
}
pub fn with_peer_check(
mut self,
f: impl Fn(&UnixStream) -> bool + Send + Sync + 'static,
) -> Self {
self.peer_check = Some(Arc::new(f));
self
}
}
struct MasterState {
live_clients: AtomicU64,
foreground_done: AtomicBool,
shutdown: AtomicBool,
last_idle: Mutex<Option<Instant>>,
control_path: PathBuf,
on_exit: Option<Arc<dyn Fn() + Send + Sync>>,
on_forward: Option<ForwardHook>,
on_conninfo: Option<Arc<dyn Fn() -> String + Send + Sync>>,
peer_check: Option<PeerCheck>,
}
fn bind_socket(path: &Path) -> Result<UnixListener, String> {
match probe_master(path) {
ProbeOutcome::Live => {
return Err(format!(
"ControlPath {} already has a live master; refusing to clobber it",
path.display()
));
}
ProbeOutcome::Stale => {
let _ = fs::remove_file(path);
}
ProbeOutcome::Absent => {}
}
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
&& !parent.exists()
{
return Err(format!(
"ControlPath parent directory {} does not exist",
parent.display()
));
}
match bind_private(path) {
Ok(l) => return Ok(l),
Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => {}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
return Err(format!(
"ControlPath {} already has a live master; refusing to clobber it",
path.display()
));
}
Err(e) => return Err(format!("bind control socket {}: {e}", path.display())),
}
let listener = UnixListener::bind(path)
.map_err(|e| format!("bind control socket {}: {e}", path.display()))?;
if let Err(e) = fs::set_permissions(path, fs::Permissions::from_mode(0o600)) {
let _ = fs::remove_file(path);
return Err(format!("chmod 0600 {}: {e}", path.display()));
}
Ok(listener)
}
fn bind_private(path: &Path) -> std::io::Result<UnixListener> {
use std::os::unix::fs::DirBuilderExt;
let parent = match path.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => Path::new("."),
};
let seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.subsec_nanos())
^ std::process::id().rotate_left(16);
let mut dir = None;
for i in 0..16u32 {
let d = parent.join(format!(".m{:06x}", seed.wrapping_add(i) & 0xff_ffff));
match fs::DirBuilder::new().mode(0o700).create(&d) {
Ok(()) => {
dir = Some(d);
break;
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
}
let Some(dir) = dir else {
return Err(std::io::Error::other("no free temporary directory name"));
};
let tmp = dir.join("s");
let res = UnixListener::bind(&tmp).and_then(|l| {
fs::set_permissions(&tmp, fs::Permissions::from_mode(0o600))?;
fs::hard_link(&tmp, path)?;
Ok(l)
});
let _ = fs::remove_file(&tmp);
let _ = fs::remove_dir(&dir);
res
}
pub fn run_master<F>(cfg: MasterConfig, shared: SharedClient, foreground: F) -> Result<i32, String>
where
F: FnOnce(&SharedClient) -> i32 + Send + 'static,
{
let listener = bind_socket(&cfg.control_path)?;
let state = Arc::new(MasterState {
live_clients: AtomicU64::new(0),
foreground_done: AtomicBool::new(false),
shutdown: AtomicBool::new(false),
last_idle: Mutex::new(Some(Instant::now())),
control_path: cfg.control_path.clone(),
on_exit: cfg.on_exit.clone(),
on_forward: cfg.on_forward.clone(),
on_conninfo: cfg.on_conninfo.clone(),
peer_check: cfg.peer_check.clone(),
});
let accept_shared = shared.clone();
let accept_state = state.clone();
let accept = thread::spawn(move || {
accept_loop(listener, accept_shared, accept_state);
});
let reaper_state = state.clone();
let persist = cfg.persist;
let reaper_path = cfg.control_path.clone();
let reaper = thread::spawn(move || {
reaper_loop(persist, reaper_state, &reaper_path);
});
let status = foreground(&shared);
state.foreground_done.store(true, Ordering::SeqCst);
match cfg.persist {
Persist::No => {
state.shutdown.store(true, Ordering::SeqCst);
let _ = fs::remove_file(&cfg.control_path);
let _ = accept.join();
let _ = reaper.join();
Ok(status)
}
Persist::Yes | Persist::Seconds(_) => {
drop(accept);
drop(reaper);
Ok(status)
}
}
}
pub fn run_master_daemon(cfg: MasterConfig, shared: SharedClient) -> Result<(), String> {
let listener = bind_socket(&cfg.control_path)?;
let state = Arc::new(MasterState {
live_clients: AtomicU64::new(0),
foreground_done: AtomicBool::new(true),
shutdown: AtomicBool::new(false),
last_idle: Mutex::new(Some(Instant::now())),
control_path: cfg.control_path.clone(),
on_exit: cfg.on_exit.clone(),
on_forward: cfg.on_forward.clone(),
on_conninfo: cfg.on_conninfo.clone(),
peer_check: cfg.peer_check.clone(),
});
let accept_shared = shared.clone();
let accept_state = state.clone();
let accept = thread::spawn(move || {
accept_loop(listener, accept_shared, accept_state);
});
let reaper_state = state.clone();
let persist = cfg.persist;
let reaper_path = cfg.control_path.clone();
let reaper = thread::spawn(move || {
reaper_loop(persist, reaper_state, &reaper_path);
});
let pump_shared = shared.clone();
let pump_state = state.clone();
let pump = thread::spawn(move || {
let _ = pump_shared.pump_while(|| !pump_state.shutdown.load(Ordering::SeqCst));
pump_state.shutdown.store(true, Ordering::SeqCst);
});
while !state.shutdown.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(100));
}
let _ = fs::remove_file(&cfg.control_path);
let _ = accept.join();
let _ = reaper.join();
let _ = pump.join();
Ok(())
}
const ACCEPT_POLL: Duration = Duration::from_millis(50);
fn accept_loop(listener: UnixListener, shared: SharedClient, state: Arc<MasterState>) {
if listener.set_nonblocking(true).is_err() {
return;
}
while !state.shutdown.load(Ordering::SeqCst) {
let stream = match listener.accept() {
Ok((s, _)) => s,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(ACCEPT_POLL);
continue;
}
Err(_) => {
thread::sleep(ACCEPT_POLL);
continue;
}
};
if stream.set_nonblocking(false).is_err() {
continue;
}
if let Some(check) = &state.peer_check
&& !check(&stream)
{
continue;
}
let s = shared.clone();
let st = state.clone();
thread::spawn(move || {
handle_client(stream, s, st);
});
}
}
fn reaper_loop(persist: Persist, state: Arc<MasterState>, path: &Path) {
loop {
if state.shutdown.load(Ordering::SeqCst) {
let _ = fs::remove_file(path);
return;
}
thread::sleep(Duration::from_millis(200));
let live = state.live_clients.load(Ordering::SeqCst);
let fg_done = state.foreground_done.load(Ordering::SeqCst);
match persist {
Persist::Yes => {
}
Persist::No => {
}
Persist::Seconds(n) => {
if !fg_done {
continue;
}
if live > 0 {
*state.last_idle.lock().unwrap() = None;
continue;
}
let mut idle = state.last_idle.lock().unwrap();
let since = idle.get_or_insert_with(Instant::now);
if since.elapsed() >= Duration::from_secs(n) {
drop(idle);
state.shutdown.store(true, Ordering::SeqCst);
let _ = fs::remove_file(path);
return;
}
}
}
}
}
fn handle_client(stream: UnixStream, shared: SharedClient, state: Arc<MasterState>) {
let mut ctrl = stream;
match read_frame(&mut ctrl) {
Ok(Some(Frame::Hello { version })) if version == PROTOCOL_VERSION => {}
_ => return, }
if write_frame(
&mut ctrl,
&Frame::Hello {
version: PROTOCOL_VERSION,
},
)
.is_err()
{
return;
}
let open = match read_frame(&mut ctrl) {
Ok(Some(f)) => f,
_ => return,
};
let (want_pty, term, cols, rows, env, command, subsystem) = match open {
Frame::OpenSession {
want_pty,
term,
cols,
rows,
env,
command,
subsystem,
} => (want_pty, term, cols, rows, env, command, subsystem),
Frame::ExitRequest => {
state.shutdown.store(true, Ordering::SeqCst);
if let Some(f) = &state.on_exit {
let _ = fs::remove_file(&state.control_path);
f();
}
return;
}
Frame::AliveCheck => {
let _ = write_frame(&mut ctrl, &Frame::AliveOk);
return;
}
Frame::Forward { kind, spec, cancel } => {
let reply = match &state.on_forward {
Some(_) if !cancel && shared.host_key_mismatched() => Frame::OpenFail {
reason: crate::client::HOST_KEY_MISMATCH_DISABLED.into(),
},
Some(f) => match f(kind, &spec, cancel) {
Ok(port) => Frame::ForwardOk { port: port as u32 },
Err(reason) => Frame::OpenFail { reason },
},
None => Frame::OpenFail {
reason: "forwarding requests are not supported by this master".into(),
},
};
let _ = write_frame(&mut ctrl, &reply);
return;
}
Frame::ConnInfo => {
let text = state.on_conninfo.as_ref().map_or_else(String::new, |f| f());
let _ = write_frame(&mut ctrl, &Frame::ConnInfoReply { text });
return;
}
Frame::OpenDirectTcpip {
dest_host,
dest_port,
orig_host,
orig_port,
} => {
handle_direct_tcpip(
ctrl, shared, state, &dest_host, dest_port, &orig_host, orig_port,
);
return;
}
_ => return,
};
if !env.is_empty() {
shared.with_session_env(env);
}
let what = match command.as_deref() {
Some(name) if subsystem => crate::shared::SessionRequest::Subsystem(name),
Some(cmd) => crate::shared::SessionRequest::Exec(cmd),
None => crate::shared::SessionRequest::Shell,
};
let pty = want_pty.then(|| crate::shared::PtyRequest::new(term.as_str(), cols, rows));
let stream_res = shared.session_stream(what, pty);
let chan = match stream_res {
Ok(c) => c,
Err(e) => {
let _ = write_frame(
&mut ctrl,
&Frame::StderrData(format!("mux: open session failed: {e}\n").into_bytes()),
);
let _ = write_frame(&mut ctrl, &Frame::ExitStatus { code: 255 });
return;
}
};
state.live_clients.fetch_add(1, Ordering::SeqCst);
*state.last_idle.lock().unwrap() = None;
splice(ctrl, chan, &shared);
state.live_clients.fetch_sub(1, Ordering::SeqCst);
if state.live_clients.load(Ordering::SeqCst) == 0 {
*state.last_idle.lock().unwrap() = Some(Instant::now());
}
}
#[allow(clippy::too_many_arguments)]
fn handle_direct_tcpip(
mut ctrl: UnixStream,
shared: SharedClient,
state: Arc<MasterState>,
dest_host: &str,
dest_port: u32,
orig_host: &str,
orig_port: u32,
) {
if dest_port > 0xFFFF || orig_port > 0xFFFF {
let _ = write_frame(
&mut ctrl,
&Frame::OpenFail {
reason: format!(
"direct-tcpip {dest_host}:{dest_port}: invalid port (out of range)"
),
},
);
return;
}
let chan =
match shared.open_direct_tcpip(dest_host, dest_port as u16, orig_host, orig_port as u16) {
Ok(c) => c,
Err(e) => {
let _ = write_frame(
&mut ctrl,
&Frame::OpenFail {
reason: format!("direct-tcpip {dest_host}:{dest_port}: {e}"),
},
);
return;
}
};
if write_frame(&mut ctrl, &Frame::OpenOk).is_err() {
return;
}
state.live_clients.fetch_add(1, Ordering::SeqCst);
*state.last_idle.lock().unwrap() = None;
splice(ctrl, chan, &shared);
state.live_clients.fetch_sub(1, Ordering::SeqCst);
if state.live_clients.load(Ordering::SeqCst) == 0 {
*state.last_idle.lock().unwrap() = Some(Instant::now());
}
}
fn splice(ctrl: UnixStream, chan: OwnedChannelStream, shared: &SharedClient) {
let channel_id = chan.channel_id();
let _ = shared.set_read_timeout(Some(Duration::from_millis(50)));
let ctrl_read = match ctrl.try_clone() {
Ok(c) => c,
Err(_) => return,
};
let ctrl_write = Arc::new(Mutex::new(ctrl));
let in_shared = shared.clone();
let mut ctrl_in = ctrl_read;
let t_in = thread::spawn(move || {
loop {
match read_frame(&mut ctrl_in) {
Ok(Some(Frame::StdinData(d))) => {
let mut off = 0;
while off < d.len() {
match in_shared.channel_send_data(channel_id, &d[off..]) {
Ok(0) | Err(_) => return,
Ok(n) => off += n,
}
}
}
Ok(Some(Frame::Eof)) => {
let _ = in_shared.channel_send_eof(channel_id);
}
Ok(Some(Frame::WindowChange { cols, rows })) => {
let _ = in_shared.send_window_change(channel_id, cols, rows, 0, 0);
}
Ok(Some(Frame::ExitRequest)) => return,
Ok(Some(_)) => { }
Ok(None) | Err(_) => return,
}
}
});
let err_shared = shared.clone();
let err_write = ctrl_write.clone();
let t_err = thread::spawn(move || {
let mut buf = [0u8; 32 * 1024];
loop {
match err_shared.channel_recv_stderr(channel_id, &mut buf) {
Ok(0) => break,
Ok(n) => {
let mut g = match err_write.lock() {
Ok(g) => g,
Err(_) => break,
};
if write_frame(&mut *g, &Frame::StderrData(buf[..n].to_vec())).is_err() {
break;
}
}
Err(_) => break,
}
}
});
let mut chan = chan;
let mut buf = [0u8; 32 * 1024];
loop {
match chan.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let mut g = match ctrl_write.lock() {
Ok(g) => g,
Err(_) => break,
};
if write_frame(&mut *g, &Frame::StdoutData(buf[..n].to_vec())).is_err() {
break;
}
}
Err(_) => break,
}
}
let _ = chan.wait_exit_status(None);
let _ = t_err.join();
if let Ok(mut g) = ctrl_write.lock() {
if let Some(sig) = chan.exit_signal() {
let _ = write_frame(&mut *g, &Frame::ExitSignal { name: sig });
}
let code = chan.exit_status().unwrap_or(255);
let _ = write_frame(&mut *g, &Frame::ExitStatus { code: code as u32 });
let _ = write_frame(&mut *g, &Frame::Eof);
let _ = g.flush();
let _ = g.shutdown(std::net::Shutdown::Both);
}
drop(t_in);
drop(chan);
}
#[cfg(test)]
mod bind_tests {
use super::*;
#[test]
fn control_socket_is_bound_owner_only_and_never_clobbered() {
let dir = PathBuf::from("/tmp").join(format!("p-muxb-{:x}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let sock = dir.join("control-socket");
let l = bind_private(&sock).unwrap();
let mode = fs::symlink_metadata(&sock).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600);
let names: Vec<_> = fs::read_dir(&dir)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert_eq!(names, vec![std::ffi::OsString::from("control-socket")]);
let _c = UnixStream::connect(&sock).unwrap();
assert!(l.accept().is_ok());
let e = bind_private(&sock).unwrap_err();
assert_eq!(e.kind(), std::io::ErrorKind::AlreadyExists);
assert_eq!(fs::read_dir(&dir).unwrap().count(), 1);
let _ = fs::remove_dir_all(&dir);
}
}