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}