pub mod probe;
use crate::config::{Config, Monitor as Spec, UnixPaths};
use crate::{log_debug, log_info};
use std::ffi::OsString;
use std::fs;
use std::io;
use std::net::{IpAddr, SocketAddr};
use std::os::fd::{AsRawFd, RawFd};
use std::os::unix::fs::{DirBuilderExt, MetadataExt};
use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::net::{TcpListener, TcpStream, UnixListener, UnixStream};
use tokio::time::timeout;
const MAX_TRIES: u32 = 3;
const RETRY_PAUSE_DIVISOR: u32 = 10;
const RETRY_PAUSE_MAX: Duration = Duration::from_secs(1);
enum Inbound {
Tcp(TcpListener),
Unix(UnixListener),
}
pub struct Monitor {
spec: Spec,
host: IpAddr,
unix: Option<UnixPaths>,
inbound: Option<Inbound>,
}
impl Monitor {
pub async fn bind(cfg: &Config) -> io::Result<Self> {
let inbound = match (cfg.monitor, cfg.unix.as_ref()) {
(Spec::Loop { port }, _) => {
let addr = SocketAddr::new(cfg.monitor_host, port + 1);
let listener = TcpListener::bind(addr).await?;
log_debug!("monitor listening on {addr}");
Some(Inbound::Tcp(listener))
}
(Spec::Unix, Some(paths)) => {
prepare_socket_dir(&paths.local_in)?;
let _ = fs::remove_file(&paths.local_in);
let listener = UnixListener::bind(&paths.local_in)?;
log_debug!("monitor listening on {}", paths.local_in.display());
Some(Inbound::Unix(listener))
}
_ => None,
};
Ok(Self {
spec: cfg.monitor,
host: cfg.monitor_host,
unix: cfg.unix.clone(),
inbound,
})
}
pub fn enabled(&self) -> bool {
!matches!(self.spec, Spec::Disabled)
}
pub fn listener_fd(&self) -> Option<RawFd> {
match &self.inbound {
Some(Inbound::Tcp(l)) => Some(l.as_raw_fd()),
Some(Inbound::Unix(l)) => Some(l.as_raw_fd()),
None => None,
}
}
pub fn next_forwards(&self) -> Vec<OsString> {
if let Some(u) = &self.unix {
let _ = fs::remove_file(&u.local_out);
}
let remote = self.remote_sock();
self.spec.forwards(self.host, self.unix.as_ref(), &remote)
}
fn remote_sock(&self) -> PathBuf {
match &self.unix {
Some(u) => u
.remote_dir
.join(format!("rash-{:016x}.sock", probe::nonce())),
None => PathBuf::new(),
}
}
pub async fn probe(&self, cfg: &Config) -> bool {
let pause = (cfg.net_timeout / RETRY_PAUSE_DIVISOR).min(RETRY_PAUSE_MAX);
for attempt in 1..=MAX_TRIES {
if self.attempt(cfg).await {
log_debug!("connection ok");
return true;
}
log_debug!("monitor attempt {attempt} of {MAX_TRIES} failed");
if attempt < MAX_TRIES {
tokio::time::sleep(pause).await;
}
}
log_info!("tried connection {MAX_TRIES} times and failed");
false
}
async fn attempt(&self, cfg: &Config) -> bool {
let net = cfg.net_timeout;
let msg = probe::message(cfg);
match self.spec {
Spec::Disabled => true,
Spec::Echo { port, .. } => {
let addr = SocketAddr::new(self.host, port);
let Some(mut s) = connect_tcp(addr, net).await else {
return false;
};
let (mut r, mut w) = s.split();
matches!(
timeout(net, probe::exchange(&mut w, &mut r, &msg)).await,
Ok(true)
)
}
Spec::Loop { port } => {
let Some(Inbound::Tcp(listener)) = &self.inbound else {
return false;
};
let addr = SocketAddr::new(self.host, port);
let Some(mut w) = connect_tcp(addr, net).await else {
return false;
};
let Some(mut r) = accept_tcp(listener, net).await else {
return false;
};
matches!(
timeout(net, probe::exchange(&mut w, &mut r, &msg)).await,
Ok(true)
)
}
Spec::Unix => {
let (Some(u), Some(Inbound::Unix(listener))) = (&self.unix, &self.inbound) else {
return false;
};
let Some(mut w) = connect_unix(&u.local_out, net).await else {
return false;
};
let Some(mut r) = accept_unix(listener, net).await else {
return false;
};
matches!(
timeout(net, probe::exchange(&mut w, &mut r, &msg)).await,
Ok(true)
)
}
}
}
}
impl Drop for Monitor {
fn drop(&mut self) {
if let Some(u) = &self.unix {
let _ = fs::remove_file(&u.local_in);
let _ = fs::remove_file(&u.local_out);
}
}
}
fn prepare_socket_dir(sock: &Path) -> io::Result<()> {
let Some(dir) = sock.parent() else {
return Ok(());
};
match fs::metadata(dir) {
Ok(md) => {
let me = unsafe { libc::getuid() };
if md.uid() != me {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"socket directory {} belongs to uid {}, not to us ({me})",
dir.display(),
md.uid()
),
));
}
Ok(())
}
Err(e) if e.kind() == io::ErrorKind::NotFound => fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(dir),
Err(e) => Err(e),
}
}
async fn connect_tcp(addr: SocketAddr, net: Duration) -> Option<TcpStream> {
match timeout(net, TcpStream::connect(addr)).await {
Ok(Ok(s)) => Some(s),
Ok(Err(e)) => {
log_info!("{addr}: {e}");
None
}
Err(_) => {
log_info!("{addr}: connect timed out");
None
}
}
}
async fn connect_unix(path: &Path, net: Duration) -> Option<UnixStream> {
match timeout(net, UnixStream::connect(path)).await {
Ok(Ok(s)) => Some(s),
Ok(Err(e)) => {
log_info!("{}: {e}", path.display());
None
}
Err(_) => {
log_info!("{}: connect timed out", path.display());
None
}
}
}
async fn accept_tcp(listener: &TcpListener, net: Duration) -> Option<TcpStream> {
match timeout(net, listener.accept()).await {
Ok(Ok((s, _))) => Some(s),
Ok(Err(e)) => {
log_debug!("error accepting read connection: {e}");
None
}
Err(_) => {
log_info!("timeout polling to accept read connection");
None
}
}
}
async fn accept_unix(listener: &UnixListener, net: Duration) -> Option<UnixStream> {
match timeout(net, listener.accept()).await {
Ok(Ok((s, _))) => Some(s),
Ok(Err(e)) => {
log_debug!("error accepting read connection: {e}");
None
}
Err(_) => {
log_info!("timeout polling to accept read connection");
None
}
}
}