use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::process::ExitCode;
use std::time::Duration;
const EXIT_ERROR: u8 = 1;
const EXIT_CONFIG: u8 = 78;
const DEFAULT_CONTROL_SOCKET: &str = "/var/run/ntpd.sock";
const VALID_TARGETS: &[&str] = &["status", "peers", "Sensors", "all"];
const IMSG_CTL_REQ: u32 = 0x0c;
const IMSG_CTL_RESP: u32 = 0x0d;
const IMSG_HEADER_SIZE: usize = 12;
const CTL_SHOW_STATUS: u32 = 0;
const CTL_SHOW_PEERS: u32 = 1;
const CTL_SHOW_SENSORS: u32 = 2;
const CTL_SHOW_ALL: u32 = 3;
fn is_prefix_of(input: &str, target: &str) -> bool {
let lower_input = input.to_lowercase();
let lower_target = target.to_lowercase();
lower_target.starts_with(&lower_input)
}
fn matching_targets(input: &str) -> Vec<&'static str> {
VALID_TARGETS
.iter()
.copied()
.filter(|&t| is_prefix_of(input, t))
.collect()
}
fn target_to_action(target: &str) -> u32 {
match target.to_lowercase().as_str() {
"status" => CTL_SHOW_STATUS,
"peers" => CTL_SHOW_PEERS,
"sensors" => CTL_SHOW_SENSORS,
"all" => CTL_SHOW_ALL,
_ => CTL_SHOW_STATUS,
}
}
fn send_control_request(socket_path: &str, action: u32) -> Result<Vec<u8>, String> {
let mut stream = UnixStream::connect(socket_path)
.map_err(|e| format!("cannot connect to {socket_path}: {e}"))?;
let mut buf = Vec::with_capacity(IMSG_HEADER_SIZE + 4);
buf.extend_from_slice(&IMSG_CTL_REQ.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&4u32.to_be_bytes());
buf.extend_from_slice(&action.to_be_bytes());
stream
.write_all(&buf)
.map_err(|e| format!("write to {socket_path}: {e}"))?;
stream
.flush()
.map_err(|e| format!("flush to {socket_path}: {e}"))?;
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.map_err(|e| format!("set read timeout: {e}"))?;
let mut header_buf = [0u8; IMSG_HEADER_SIZE];
let mut read_total = 0usize;
while read_total < IMSG_HEADER_SIZE {
match stream.read(&mut header_buf[read_total..]) {
Ok(0) => break, Ok(n) => read_total += n,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
return Err(format!("timed out waiting for response from {socket_path}"));
}
Err(e) => return Err(format!("read from {socket_path}: {e}")),
}
}
if read_total < IMSG_HEADER_SIZE {
return Err(format!(
"incomplete imsg header from {socket_path}: got {read_total} bytes"
));
}
let resp_type = u32::from_be_bytes(header_buf[0..4].try_into().unwrap());
let _resp_peer = u32::from_be_bytes(header_buf[4..8].try_into().unwrap());
let resp_len = u32::from_be_bytes(header_buf[8..12].try_into().unwrap()) as usize;
if resp_type != IMSG_CTL_RESP {
return Err(format!(
"unexpected imsg type from {socket_path}: expected {IMSG_CTL_RESP}, got {resp_type}"
));
}
let mut payload = vec![0u8; resp_len];
let mut read_payload = 0usize;
while read_payload < resp_len {
match stream.read(&mut payload[read_payload..]) {
Ok(0) => break,
Ok(n) => read_payload += n,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(e) => return Err(format!("read payload from {socket_path}: {e}")),
}
}
payload.truncate(read_payload);
Ok(payload)
}
fn format_status_response(payload: &[u8]) {
if payload.len() < 32 {
println!("status: incomplete response ({} bytes)", payload.len());
return;
}
let synced = payload[0];
let stratum = payload[1];
let peer_cnt = u32::from_be_bytes(payload[4..8].try_into().unwrap_or([0; 4]));
let sensor_cnt = u32::from_be_bytes(payload[8..12].try_into().unwrap_or([0; 4]));
println!("status:");
println!("\tsynchronized: {}", if synced != 0 { "YES" } else { "NO" });
println!("\tstratum: {}", stratum);
if payload.len() >= 24 {
let clock_offset_bytes = &payload[16..24];
if clock_offset_bytes.len() == 8 {
let clock_offset = f64::from_be_bytes(clock_offset_bytes.try_into().unwrap_or([0; 8]));
println!("\tclock offset: {:.6}s", clock_offset);
}
}
if payload.len() >= 32 {
let constraint_median = i64::from_be_bytes(payload[24..32].try_into().unwrap_or([0; 8]));
if constraint_median != 0 {
println!("\tconstraint median: {}", constraint_median);
}
}
println!("\tpeers: {peer_cnt}");
println!("\tsensors: {sensor_cnt}");
}
fn format_peers_response(payload: &[u8]) {
if payload.is_empty() {
println!("peers: (none)");
return;
}
println!("peers:");
let mut offset = 0;
let mut peer_num = 0;
while offset < payload.len() {
let addr_end = payload[offset..]
.iter()
.position(|&b| b == 0)
.map(|p| offset + p)
.unwrap_or(payload.len());
let addr = String::from_utf8_lossy(&payload[offset..addr_end]);
offset = addr_end + 1;
if offset + 20 <= payload.len() {
let reach = payload[offset];
let peer_offset =
f64::from_be_bytes(payload[offset + 1..offset + 9].try_into().unwrap_or([0; 8]));
let delay = f64::from_be_bytes(
payload[offset + 9..offset + 17]
.try_into()
.unwrap_or([0; 8]),
);
let stratum = payload[offset + 17];
let weight = payload[offset + 18];
let poll = payload[offset + 19] as i8;
println!("\tpeer {peer_num}: {addr}");
println!("\t\treach: 0x{reach:02x}");
println!("\t\toffset: {peer_offset:.6}s");
println!("\t\tdelay: {delay:.6}s");
println!("\t\tstratum: {stratum}");
println!("\t\tweight: {weight}");
println!("\t\tpoll: {}s", 1i64 << poll.max(0));
offset += 20;
peer_num += 1;
} else {
break;
}
}
if peer_num == 0 {
println!("\t(none)");
}
}
fn format_sensors_response(payload: &[u8]) {
if payload.is_empty() {
println!("Sensors: (none)");
return;
}
println!("Sensors:");
let mut offset = 0;
let mut sensor_num = 0;
while offset < payload.len() {
let dev_end = payload[offset..]
.iter()
.position(|&b| b == 0)
.map(|p| offset + p)
.unwrap_or(payload.len());
let device = String::from_utf8_lossy(&payload[offset..dev_end]);
offset = dev_end + 1;
if offset + 15 <= payload.len() {
let status = payload[offset];
let sensor_offset =
f64::from_be_bytes(payload[offset + 1..offset + 9].try_into().unwrap_or([0; 8]));
let correction = i32::from_be_bytes(
payload[offset + 9..offset + 13]
.try_into()
.unwrap_or([0; 4]),
);
let stratum = payload[offset + 13];
let weight = payload[offset + 14];
println!("\tsensor {sensor_num}: {device}");
println!("\t\toffset: {sensor_offset:.6}s");
println!("\t\tcorrection: {correction}us");
println!("\t\tstratum: {stratum}");
println!("\t\tweight: {weight}");
println!("\t\tstatus: {status}");
offset += 15;
sensor_num += 1;
} else {
break;
}
}
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
let prog = args.first().map(|s| s.as_str()).unwrap_or("ntpctl");
let control_socket =
std::env::var("NTPD_CONTROL_SOCKET").unwrap_or_else(|_| DEFAULT_CONTROL_SOCKET.to_string());
if args.len() < 3 || args[1] != "-s" {
eprintln!("Usage: {prog} -s <status|peers|Sensors|all>");
return ExitCode::from(EXIT_ERROR);
}
let what = &args[2];
if what.is_empty() {
eprintln!("{prog}: empty status type");
return ExitCode::from(EXIT_ERROR);
}
let matches = matching_targets(what);
match matches.as_slice() {
[target] => {
let action = target_to_action(target);
match send_control_request(&control_socket, action) {
Ok(payload) => {
match target.to_lowercase().as_str() {
"status" => format_status_response(&payload),
"peers" => format_peers_response(&payload),
"sensors" => format_sensors_response(&payload),
"all" => {
if payload.len() >= 32 {
format_status_response(&payload[..32]);
if payload.len() > 32 {
format_peers_response(&payload[32..]);
}
} else {
format_status_response(&payload);
}
}
_ => {
println!("{} response ({} bytes):", target, payload.len());
println!("{:?}", String::from_utf8_lossy(&payload));
}
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{prog}: {e}");
ExitCode::from(EXIT_ERROR)
}
}
}
[] => {
eprintln!("{prog}: unknown status type '{what}'");
ExitCode::from(EXIT_ERROR)
}
_ => {
eprintln!(
"{prog}: ambiguous prefix '{what}' — matches: {}",
matches.join(", ")
);
ExitCode::from(EXIT_ERROR)
}
}
}