Skip to main content

rash/monitor/
probe.rs

1//! The probe payload and the exchange that carries it.
2//!
3//! The message is something identifiable as ours (autossh.c:1301-1311), so a
4//! stray process writing to the monitor port cannot be mistaken for a healthy
5//! tunnel. A probe succeeds only when the bytes that come back are byte-for-byte
6//! the bytes that went out.
7
8use crate::config::Config;
9use std::fs::File;
10use std::io::Read;
11use std::time::{SystemTime, UNIX_EPOCH};
12use tokio::io::{AsyncReadExt, AsyncWriteExt};
13
14/// Build one probe message: `hostname rash pid nonce message\r\n`.
15pub fn message(cfg: &Config) -> Vec<u8> {
16    format!(
17        "{} rash {} {} {}\r\n",
18        hostname(),
19        std::process::id(),
20        nonce(),
21        cfg.message
22    )
23    .into_bytes()
24}
25
26/// Send `msg` on `w`, read the same number of bytes from `r`, and say whether
27/// they match.
28///
29/// autossh hand-rolls this over `poll()` with a "too many loops without data"
30/// guard for the case where traffic is black-holed and `poll()` cannot tell
31/// (autossh.c:1517-1529). The caller here wraps the whole exchange in a timeout,
32/// which covers that case without the heuristic.
33pub async fn exchange<W, R>(w: &mut W, r: &mut R, msg: &[u8]) -> bool
34where
35    W: AsyncWriteExt + Unpin,
36    R: AsyncReadExt + Unpin,
37{
38    if w.write_all(msg).await.is_err() || w.flush().await.is_err() {
39        return false;
40    }
41
42    let mut back = vec![0u8; msg.len()];
43    if r.read_exact(&mut back).await.is_err() {
44        return false;
45    }
46
47    back == msg
48}
49
50/// This machine's name, as `uname(2)` reports it — the same source autossh uses.
51fn hostname() -> String {
52    // SAFETY: uname fills a caller-provided struct, and a zeroed `utsname` is a
53    // valid thing to hand it.
54    let uts = unsafe {
55        let mut uts: libc::utsname = std::mem::zeroed();
56        if libc::uname(&mut uts) != 0 {
57            return String::new();
58        }
59        uts
60    };
61
62    // `c_char` is i8 on x86-64 and on every Apple target, but u8 on aarch64
63    // Linux, where clippy would otherwise call this cast unnecessary. It is
64    // needed on the platforms where it is needed.
65    #[allow(clippy::unnecessary_cast)]
66    let bytes: Vec<u8> = uts
67        .nodename
68        .iter()
69        .take_while(|&&c| c != 0)
70        .map(|&c| c as u8)
71        .collect();
72    String::from_utf8_lossy(&bytes).into_owned()
73}
74
75/// 64 bits of randomness, so two probes are never confused for one another.
76///
77/// autossh seeds `random()` with `pid ^ tv_usec ^ tv_sec` (autossh.c:721-722),
78/// which is guessable; `/dev/urandom` costs nothing here.
79pub(crate) fn nonce() -> u64 {
80    let mut buf = [0u8; 8];
81    if let Ok(mut f) = File::open("/dev/urandom")
82        && f.read_exact(&mut buf).is_ok()
83    {
84        return u64::from_ne_bytes(buf);
85    }
86
87    let nanos = SystemTime::now()
88        .duration_since(UNIX_EPOCH)
89        .map_or(0, |d| d.as_nanos() as u64);
90    nanos ^ u64::from(std::process::id())
91}