WIP
This commit is contained in:
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -85,6 +85,12 @@ version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.51"
|
||||
@@ -556,6 +562,7 @@ name = "roowho2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"nix",
|
||||
@@ -753,9 +760,21 @@ dependencies = [
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "type-map"
|
||||
version = "0.5.1"
|
||||
|
||||
@@ -15,10 +15,11 @@ autolib = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
bytes = "1.11.0"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
nix = { version = "0.30.1", features = ["hostname", "net"] }
|
||||
tokio = { version = "1.49.0", features = ["net", "rt-multi-thread"] }
|
||||
tokio = { version = "1.49.0", features = ["macros", "net", "rt-multi-thread"] }
|
||||
# onc-rpc = "0.3.2"
|
||||
# sd-notify = "0.4.5"
|
||||
# serde = { version = "1.0.228", features = ["derive"] }
|
||||
|
||||
@@ -1,10 +1,41 @@
|
||||
fn main() {
|
||||
println!(
|
||||
"{:#?}",
|
||||
roowho2_lib::server::rwhod::generate_rwhod_status_update(),
|
||||
);
|
||||
println!(
|
||||
"{:#?}",
|
||||
roowho2_lib::server::rwhod::determine_relevant_interfaces(),
|
||||
);
|
||||
use std::net::SocketAddrV4;
|
||||
|
||||
use anyhow::Context;
|
||||
use chrono::Timelike;
|
||||
use roowho2_lib::proto::{Whod, WhodStatusUpdate};
|
||||
|
||||
const RWHOD_BROADCAST_PORT: u16 = 513;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let addr = SocketAddrV4::new(std::net::Ipv4Addr::UNSPECIFIED, RWHOD_BROADCAST_PORT);
|
||||
let socket = tokio::net::UdpSocket::bind(addr).await?;
|
||||
socket.set_broadcast(true)?;
|
||||
|
||||
let mut buf = [0u8; Whod::MAX_SIZE];
|
||||
loop {
|
||||
let (len, src) = socket.recv_from(&mut buf).await?;
|
||||
if len < Whod::HEADER_SIZE {
|
||||
eprintln!(
|
||||
"Received too short packet from {src}: {len} bytes (needs to be at least {} bytes)",
|
||||
Whod::HEADER_SIZE
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let result: WhodStatusUpdate = Whod::from_bytes(&buf[..len])
|
||||
.context("Failed to parse whod packet")?
|
||||
.try_into()
|
||||
.map(|mut status_update: WhodStatusUpdate| {
|
||||
let timestamp = chrono::Utc::now()
|
||||
.with_nanosecond(0)
|
||||
.unwrap_or(chrono::Utc::now());
|
||||
status_update.recvtime = Some(timestamp);
|
||||
status_update
|
||||
})
|
||||
.map_err(|e| anyhow::anyhow!("Invalid whod packet: {}", e))?;
|
||||
|
||||
println!("Received whod packet from {src}:\n{result:#?}");
|
||||
|
||||
buf = [0u8; Whod::MAX_SIZE];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use std::array;
|
||||
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
|
||||
/// Classic C struct for utmp data for a single user session.
|
||||
///
|
||||
/// This struct is used in the rwhod protocol by being interpreted as raw bytes to be sent over UDP.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(C)]
|
||||
pub struct Outmp {
|
||||
@@ -16,6 +21,9 @@ impl Outmp {
|
||||
pub const MAX_USER_ID_LEN: usize = 8;
|
||||
}
|
||||
|
||||
/// Classic C struct for a single user session.
|
||||
///
|
||||
/// This struct is used in the rwhod protocol by being interpreted as raw bytes to be sent over UDP.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(C)]
|
||||
pub struct Whoent {
|
||||
@@ -25,6 +33,31 @@ pub struct Whoent {
|
||||
pub we_idle: i32,
|
||||
}
|
||||
|
||||
impl Whoent {
|
||||
pub const SIZE: usize = std::mem::size_of::<Self>();
|
||||
|
||||
fn zeroed() -> Self {
|
||||
Self {
|
||||
we_utmp: Outmp {
|
||||
out_line: [0u8; Outmp::MAX_TTY_NAME_LEN],
|
||||
out_name: [0u8; Outmp::MAX_USER_ID_LEN],
|
||||
out_time: 0,
|
||||
},
|
||||
we_idle: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_zeroed(&self) -> bool {
|
||||
self.we_utmp.out_line.iter().all(|&b| b == 0)
|
||||
&& self.we_utmp.out_name.iter().all(|&b| b == 0)
|
||||
&& self.we_utmp.out_time == 0
|
||||
&& self.we_idle == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Classic C struct for a rwhod status update.
|
||||
///
|
||||
/// This struct is used in the rwhod protocol by being interpreted as raw bytes to be sent over UDP.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(C)]
|
||||
pub struct Whod {
|
||||
@@ -47,7 +80,9 @@ pub struct Whod {
|
||||
}
|
||||
|
||||
impl Whod {
|
||||
pub const SIZE: usize = std::mem::size_of::<Whod>();
|
||||
pub const HEADER_SIZE: usize = 1 + 1 + 2 + 4 + 4 + Self::MAX_HOSTNAME_LEN + 4 * 3 + 4;
|
||||
pub const MAX_SIZE: usize = std::mem::size_of::<Self>();
|
||||
|
||||
pub const MAX_HOSTNAME_LEN: usize = 32;
|
||||
pub const MAX_WHOENTRIES: usize = 1024 / std::mem::size_of::<Whoent>();
|
||||
|
||||
@@ -56,42 +91,172 @@ impl Whod {
|
||||
// NOTE: there was probably meant to be more packet types, but only status is defined.
|
||||
pub const WHODTYPE_STATUS: u8 = 1;
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; std::mem::size_of::<Whod>()] {
|
||||
unsafe { std::mem::transmute_copy(self) }
|
||||
pub fn new(
|
||||
sendtime: i32,
|
||||
recvtime: i32,
|
||||
hostname: [u8; Self::MAX_HOSTNAME_LEN],
|
||||
loadav: [i32; 3],
|
||||
boottime: i32,
|
||||
whoentries: [Whoent; Self::MAX_WHOENTRIES],
|
||||
) -> Self {
|
||||
debug_assert!(
|
||||
whoentries
|
||||
.iter()
|
||||
.skip_while(|entry| !entry.is_zeroed())
|
||||
.all(|entry| entry.is_zeroed())
|
||||
);
|
||||
|
||||
Self {
|
||||
wd_vers: Self::WHODVERSION,
|
||||
wd_type: Self::WHODTYPE_STATUS,
|
||||
wd_pad: [0u8; 2],
|
||||
wd_sendtime: sendtime,
|
||||
wd_recvtime: recvtime,
|
||||
wd_hostname: hostname,
|
||||
wd_loadav: loadav,
|
||||
wd_boottime: boottime,
|
||||
wd_we: whoentries,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we should probably make a safer parser.
|
||||
pub fn from_bytes(bytes: &[u8; std::mem::size_of::<Whod>()]) -> Self {
|
||||
unsafe { std::mem::transmute_copy(bytes) }
|
||||
pub fn to_bytes(&self) -> [u8; Whod::MAX_SIZE] {
|
||||
let mut buf = BytesMut::with_capacity(Whod::MAX_SIZE);
|
||||
buf.put_u8(self.wd_vers);
|
||||
buf.put_u8(self.wd_type);
|
||||
buf.put_slice(&self.wd_pad);
|
||||
buf.put_i32(self.wd_sendtime);
|
||||
buf.put_i32(self.wd_recvtime);
|
||||
buf.put_slice(&self.wd_hostname);
|
||||
buf.put_i32(self.wd_loadav[0]);
|
||||
buf.put_i32(self.wd_loadav[1]);
|
||||
buf.put_i32(self.wd_loadav[2]);
|
||||
buf.put_i32(self.wd_boottime);
|
||||
|
||||
for whoent in &self.wd_we {
|
||||
buf.put_slice(&whoent.we_utmp.out_line);
|
||||
buf.put_slice(&whoent.we_utmp.out_name);
|
||||
buf.put_i32(whoent.we_utmp.out_time);
|
||||
buf.put_i32(whoent.we_idle);
|
||||
}
|
||||
|
||||
// SAFETY: this should never happen, Whod::MAX_SIZE is computed from the struct size
|
||||
let result = buf
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("Buffer length mismatch, this should never happen");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn from_bytes(input: &[u8]) -> anyhow::Result<Self> {
|
||||
if input.len() < Self::HEADER_SIZE {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Not enough bytes to parse packet header: {} < {}",
|
||||
input.len(),
|
||||
Self::HEADER_SIZE
|
||||
));
|
||||
}
|
||||
|
||||
if input.len() > Self::MAX_SIZE {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Too many bytes to parse packet: {} > {}",
|
||||
input.len(),
|
||||
Self::MAX_SIZE
|
||||
));
|
||||
}
|
||||
|
||||
if (input.len() - Self::HEADER_SIZE) % Whoent::SIZE != 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Invalid packet length: {} (not aligned with struct sizes, should be {} + N * {})",
|
||||
input.len(),
|
||||
Self::HEADER_SIZE,
|
||||
Whoent::SIZE,
|
||||
));
|
||||
}
|
||||
|
||||
let mut bytes = bytes::Bytes::copy_from_slice(input);
|
||||
|
||||
let wd_vers = bytes.get_u8();
|
||||
debug_assert!(wd_vers == Self::WHODVERSION);
|
||||
|
||||
let wd_type = bytes.get_u8();
|
||||
debug_assert!(wd_type == Self::WHODTYPE_STATUS);
|
||||
|
||||
bytes.advance(2); // skip wd_pad
|
||||
|
||||
let wd_sendtime = bytes.get_i32();
|
||||
let wd_recvtime = bytes.get_i32();
|
||||
let mut wd_hostname = [0u8; Self::MAX_HOSTNAME_LEN];
|
||||
bytes.copy_to_slice(&mut wd_hostname);
|
||||
let wd_loadav = [bytes.get_i32(), bytes.get_i32(), bytes.get_i32()];
|
||||
let wd_boottime = bytes.get_i32();
|
||||
|
||||
debug_assert!(bytes.remaining() + Self::HEADER_SIZE == input.len());
|
||||
|
||||
let mut wd_we = array::from_fn(|_| Whoent::zeroed());
|
||||
|
||||
for (byte_chunk, whoent) in bytes.chunks_exact(Whoent::SIZE).zip(wd_we.iter_mut()) {
|
||||
let mut chunk_bytes = bytes::Bytes::copy_from_slice(byte_chunk);
|
||||
|
||||
let mut out_line = [0u8; Outmp::MAX_TTY_NAME_LEN];
|
||||
chunk_bytes.copy_to_slice(&mut out_line);
|
||||
let mut out_name = [0u8; Outmp::MAX_USER_ID_LEN];
|
||||
chunk_bytes.copy_to_slice(&mut out_name);
|
||||
let out_time = chunk_bytes.get_i32();
|
||||
|
||||
let we_utmp = Outmp {
|
||||
out_line,
|
||||
out_name,
|
||||
out_time,
|
||||
};
|
||||
let we_idle = chunk_bytes.get_i32();
|
||||
|
||||
*whoent = Whoent { we_utmp, we_idle };
|
||||
}
|
||||
|
||||
let result = Whod::new(
|
||||
wd_sendtime,
|
||||
wd_recvtime,
|
||||
wd_hostname,
|
||||
wd_loadav,
|
||||
wd_boottime,
|
||||
wd_we,
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
/// High-level representation of a rwhod status update.
|
||||
///
|
||||
/// This struct is intended for easier use in Rust code, with proper types and dynamic arrays.
|
||||
/// It can be converted to and from the low-level [`Whod`] struct used for network transmission.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WhodStatusUpdate {
|
||||
// NOTE: there is only one defined packet type, so we just omit it here
|
||||
/// Timestamp by sender
|
||||
sendtime: chrono::DateTime<chrono::Utc>,
|
||||
pub sendtime: chrono::DateTime<chrono::Utc>,
|
||||
|
||||
/// Timestamp applied by receiver
|
||||
recvtime: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub recvtime: Option<chrono::DateTime<chrono::Utc>>,
|
||||
|
||||
/// Name of the host sending the status update (max 32 characters)
|
||||
hostname: String,
|
||||
pub hostname: String,
|
||||
|
||||
/// load average over 5 minutes multiplied by 100
|
||||
load_average_5_min: i32,
|
||||
pub load_average_5_min: i32,
|
||||
/// load average over 10 minutes multiplied by 100
|
||||
load_average_10_min: i32,
|
||||
pub load_average_10_min: i32,
|
||||
/// load average over 15 minutes multiplied by 100
|
||||
load_average_15_min: i32,
|
||||
pub load_average_15_min: i32,
|
||||
|
||||
/// Which time the system was booted
|
||||
boot_time: chrono::DateTime<chrono::Utc>,
|
||||
pub boot_time: chrono::DateTime<chrono::Utc>,
|
||||
|
||||
/// List of users currently logged in to the host (max 42 entries)
|
||||
users: Vec<WhodUserEntry>,
|
||||
pub users: Vec<WhodUserEntry>,
|
||||
}
|
||||
|
||||
impl WhodStatusUpdate {
|
||||
@@ -118,19 +283,23 @@ impl WhodStatusUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
/// High-level representation of a single user session in a rwhod status update.
|
||||
///
|
||||
/// This struct is intended for easier use in Rust code, with proper types.
|
||||
/// It can be converted to and from the low-level [`Whoent`] struct used for network transmission.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WhodUserEntry {
|
||||
/// TTY name (max 8 characters)
|
||||
tty: String,
|
||||
pub tty: String,
|
||||
|
||||
/// User ID (max 8 characters)
|
||||
user_id: String,
|
||||
pub user_id: String,
|
||||
|
||||
/// Time when the user logged in
|
||||
login_time: chrono::DateTime<chrono::Utc>,
|
||||
pub login_time: chrono::DateTime<chrono::Utc>,
|
||||
|
||||
/// Idle time in seconds
|
||||
idle_time_seconds: i32,
|
||||
pub idle_time_seconds: i32,
|
||||
}
|
||||
|
||||
impl WhodUserEntry {
|
||||
@@ -224,11 +393,13 @@ impl TryFrom<Whod> for WhodStatusUpdate {
|
||||
format!("Invalid boot time timestamp: {}", value.wd_boottime),
|
||||
)?;
|
||||
|
||||
let mut users = Vec::new();
|
||||
for whoent in &value.wd_we {
|
||||
let user_entry = WhodUserEntry::try_from(whoent.clone())?;
|
||||
users.push(user_entry);
|
||||
}
|
||||
let users = value
|
||||
.wd_we
|
||||
.iter()
|
||||
.take_while(|whoent| !whoent.is_zeroed())
|
||||
.cloned()
|
||||
.map(|whoent| WhodUserEntry::try_from(whoent))
|
||||
.collect::<Result<Vec<WhodUserEntry>, String>>()?;
|
||||
|
||||
Ok(WhodStatusUpdate {
|
||||
sendtime,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
Reference in New Issue
Block a user