Progress towards implementing the rwho server
Some checks failed
Build and test / check (push) Failing after 1m7s
Build and test / build (push) Successful in 1m12s
Build and test / docs (push) Failing after 1m52s
Build and test / test (push) Has been cancelled

This commit is contained in:
2026-01-04 17:09:00 +09:00
parent 5a0a65c3cf
commit 8436bffce0
9 changed files with 1363 additions and 27 deletions

View File

@@ -1,4 +1,10 @@
fn main() {
unimplemented!()
println!(
"{:#?}",
roowho2_lib::server::rwhod::generate_rwhod_status_update(),
);
println!(
"{:#?}",
roowho2_lib::server::rwhod::determine_relevant_interfaces(),
);
}

View File

@@ -1 +1,2 @@
pub mod proto;
pub mod server;

View File

@@ -1,3 +1,9 @@
pub mod finger_protocol;
pub mod rusers_protocol;
pub mod rwhod_protocol;
pub mod write_protocol;
pub use finger_protocol::*;
pub use rusers_protocol::*;
pub use rwhod_protocol::*;
pub use write_protocol::*;

334
src/proto/rwhod_protocol.rs Normal file
View File

@@ -0,0 +1,334 @@
use std::array;
#[derive(Debug, Clone)]
#[repr(C)]
pub struct Outmp {
/// tty name
pub out_line: [u8; Self::MAX_TTY_NAME_LEN],
/// user id
pub out_name: [u8; Self::MAX_USER_ID_LEN],
/// time on
pub out_time: i32,
}
impl Outmp {
pub const MAX_TTY_NAME_LEN: usize = 8;
pub const MAX_USER_ID_LEN: usize = 8;
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct Whoent {
/// active tty info
pub we_utmp: Outmp,
/// tty idle time
pub we_idle: i32,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct Whod {
/// protocol version
pub wd_vers: u8,
/// packet type, see below
pub wd_type: u8,
pub wd_pad: [u8; 2],
/// time stamp by sender
pub wd_sendtime: i32,
/// time stamp applied by receiver
pub wd_recvtime: i32,
/// host's name
pub wd_hostname: [u8; Self::MAX_HOSTNAME_LEN],
/// load average as in uptime
pub wd_loadav: [i32; 3],
/// time system booted
pub wd_boottime: i32,
pub wd_we: [Whoent; Self::MAX_WHOENTRIES],
}
impl Whod {
pub const SIZE: usize = std::mem::size_of::<Whod>();
pub const MAX_HOSTNAME_LEN: usize = 32;
pub const MAX_WHOENTRIES: usize = 1024 / std::mem::size_of::<Whoent>();
pub const WHODVERSION: u8 = 1;
// 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) }
}
// 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) }
}
}
// ------------------------------------------------
#[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>,
/// Timestamp applied by receiver
recvtime: Option<chrono::DateTime<chrono::Utc>>,
/// Name of the host sending the status update (max 32 characters)
hostname: String,
/// load average over 5 minutes multiplied by 100
load_average_5_min: i32,
/// load average over 10 minutes multiplied by 100
load_average_10_min: i32,
/// load average over 15 minutes multiplied by 100
load_average_15_min: i32,
/// Which time the system was booted
boot_time: chrono::DateTime<chrono::Utc>,
/// List of users currently logged in to the host (max 42 entries)
users: Vec<WhodUserEntry>,
}
impl WhodStatusUpdate {
pub fn new(
sendtime: chrono::DateTime<chrono::Utc>,
recvtime: Option<chrono::DateTime<chrono::Utc>>,
hostname: String,
load_average_5_min: i32,
load_average_10_min: i32,
load_average_15_min: i32,
boot_time: chrono::DateTime<chrono::Utc>,
users: Vec<WhodUserEntry>,
) -> Self {
Self {
sendtime,
recvtime,
hostname,
load_average_5_min,
load_average_10_min,
load_average_15_min,
boot_time,
users,
}
}
}
#[derive(Debug, Clone)]
pub struct WhodUserEntry {
/// TTY name (max 8 characters)
tty: String,
/// User ID (max 8 characters)
user_id: String,
/// Time when the user logged in
login_time: chrono::DateTime<chrono::Utc>,
/// Idle time in seconds
idle_time_seconds: i32,
}
impl WhodUserEntry {
pub fn new(
tty: String,
user_id: String,
login_time: chrono::DateTime<chrono::Utc>,
idle_time_seconds: i32,
) -> Self {
Self {
tty,
user_id,
login_time,
idle_time_seconds,
}
}
}
impl TryFrom<Whoent> for WhodUserEntry {
type Error = String;
fn try_from(value: Whoent) -> Result<Self, Self::Error> {
let tty_end = value
.we_utmp
.out_line
.iter()
.position(|&c| c == 0)
.unwrap_or(value.we_utmp.out_line.len());
let tty = String::from_utf8(value.we_utmp.out_line[..tty_end].to_vec())
.map_err(|e| format!("Invalid UTF-8 in TTY name: {}", e))?;
let user_id_end = value
.we_utmp
.out_name
.iter()
.position(|&c| c == 0)
.unwrap_or(value.we_utmp.out_name.len());
let user_id = String::from_utf8(value.we_utmp.out_name[..user_id_end].to_vec())
.map_err(|e| format!("Invalid UTF-8 in user ID: {}", e))?;
let login_time = chrono::DateTime::from_timestamp_secs(value.we_utmp.out_time as i64)
.ok_or(format!(
"Invalid login time timestamp: {}",
value.we_utmp.out_time
))?;
Ok(WhodUserEntry {
tty,
user_id,
login_time,
idle_time_seconds: value.we_idle,
})
}
}
impl TryFrom<Whod> for WhodStatusUpdate {
type Error = String;
fn try_from(value: Whod) -> Result<Self, Self::Error> {
if value.wd_vers != Whod::WHODVERSION {
return Err(format!(
"Unsupported whod protocol version: {}",
value.wd_vers
));
}
let sendtime = chrono::DateTime::from_timestamp_secs(value.wd_sendtime as i64).ok_or(
format!("Invalid send time timestamp: {}", value.wd_sendtime),
)?;
let recvtime = if value.wd_recvtime == 0 {
None
} else {
Some(
chrono::DateTime::from_timestamp_secs(value.wd_recvtime as i64).ok_or(format!(
"Invalid receive time timestamp: {}",
value.wd_recvtime
))?,
)
};
let hostname_end = value
.wd_hostname
.iter()
.position(|&c| c == 0)
.unwrap_or(value.wd_hostname.len());
let hostname = String::from_utf8(value.wd_hostname[..hostname_end].to_vec())
.map_err(|e| format!("Invalid UTF-8 in hostname: {}", e))?;
let boot_time = chrono::DateTime::from_timestamp_secs(value.wd_boottime as i64).ok_or(
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);
}
Ok(WhodStatusUpdate {
sendtime,
recvtime,
hostname,
load_average_5_min: value.wd_loadav[0],
load_average_10_min: value.wd_loadav[1],
load_average_15_min: value.wd_loadav[2],
boot_time,
users,
})
}
}
// TODO: support less strict conversions with truncation
impl TryFrom<WhodUserEntry> for Whoent {
type Error = String;
fn try_from(value: WhodUserEntry) -> Result<Self, Self::Error> {
let mut out_line = [0u8; 8];
let tty_bytes = value.tty.as_bytes();
if tty_bytes.len() > out_line.len() {
return Err(format!(
"TTY name too long: {} (max {})",
value.tty,
out_line.len()
));
}
out_line[..tty_bytes.len()].copy_from_slice(tty_bytes);
let mut out_name = [0u8; 8];
let user_id_bytes = value.user_id.as_bytes();
if user_id_bytes.len() > out_name.len() {
return Err(format!(
"User ID too long: {} (max {})",
value.user_id,
out_name.len()
));
}
out_name[..user_id_bytes.len()].copy_from_slice(user_id_bytes);
Ok(Whoent {
we_utmp: Outmp {
out_line,
out_name,
out_time: value.login_time.timestamp() as i32,
},
we_idle: value.idle_time_seconds,
})
}
}
// TODO: support less strict conversions with truncation
impl TryFrom<WhodStatusUpdate> for Whod {
type Error = String;
fn try_from(value: WhodStatusUpdate) -> Result<Self, Self::Error> {
let mut wd_hostname = [0u8; Whod::MAX_HOSTNAME_LEN];
let hostname_bytes = value.hostname.as_bytes();
if hostname_bytes.len() > wd_hostname.len() {
return Err(format!(
"Hostname too long: {} (max {})",
value.hostname,
wd_hostname.len()
));
}
wd_hostname[..hostname_bytes.len()].copy_from_slice(hostname_bytes);
let mut wd_we = array::from_fn(|_| Whoent {
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,
});
for (i, user_entry) in value.users.into_iter().enumerate() {
if i >= Whod::MAX_WHOENTRIES {
break;
}
wd_we[i] = Whoent::try_from(user_entry)?;
}
Ok(Whod {
wd_vers: Whod::WHODVERSION,
wd_type: Whod::WHODTYPE_STATUS,
wd_pad: [0u8; 2],
wd_sendtime: value.sendtime.timestamp() as i32,
wd_recvtime: value.recvtime.map_or(0, |dt| dt.timestamp() as i32),
wd_hostname,
wd_loadav: [
value.load_average_5_min,
value.load_average_10_min,
value.load_average_15_min,
],
wd_boottime: value.boot_time.timestamp() as i32,
wd_we,
})
}
}

1
src/server.rs Normal file
View File

@@ -0,0 +1 @@
pub mod rwhod;

138
src/server/rwhod.rs Normal file
View File

@@ -0,0 +1,138 @@
use std::{collections::HashSet, net::IpAddr, path::Path};
use nix::{ifaddrs::getifaddrs, net::if_::InterfaceFlags, sys::stat::stat};
use uucore::utmpx::Utmpx;
use crate::proto::{Whod, WhodStatusUpdate, WhodUserEntry};
/// Reads utmp entries to determine currently logged-in users.
pub fn generate_rwhod_user_entries() -> anyhow::Result<Vec<WhodUserEntry>> {
Utmpx::iter_all_records()
.filter(|entry| entry.is_user_process())
.map(|entry| {
let login_time = entry
.login_time()
.checked_to_utc()
.and_then(|t| {
chrono::DateTime::<chrono::Utc>::from_timestamp_secs(t.unix_timestamp())
})
.ok_or_else(|| anyhow::anyhow!("Failed to convert login time to UTC"))?;
let idle_time = stat(&Path::new("/dev").join(entry.tty_device()))
.map(|st| (chrono::Utc::now().timestamp() - st.st_atime) as i32)
.unwrap_or(0);
Ok(WhodUserEntry::new(
entry.tty_device(),
entry.user(),
login_time,
idle_time,
))
})
.collect()
}
/// Generate a rwhod status update packet representing the current system state.
pub fn generate_rwhod_status_update() -> anyhow::Result<Whod> {
let sysinfo = nix::sys::sysinfo::sysinfo().unwrap();
let load_average = sysinfo.load_average();
let uptime = sysinfo.uptime();
let hostname = nix::unistd::gethostname()?.to_str().unwrap().to_string();
let result = WhodStatusUpdate::new(
chrono::Utc::now(),
None,
hostname,
(load_average.0 * 100.0).abs() as i32,
(load_average.1 * 100.0).abs() as i32,
(load_average.2 * 100.0).abs() as i32,
chrono::Utc::now() - uptime,
generate_rwhod_user_entries()?,
)
.try_into()
.map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(result)
}
#[derive(Debug)]
pub struct RwhodSendTarget {
/// Name of the network interface.
pub name: String,
/// Address to send rwhod packets to.
/// This is either the broadcast address (for broadcast interfaces)
/// or the point-to-point destination address (for point-to-point interfaces).
pub addr: IpAddr,
}
/// Find all networks network interfaces suitable for rwhod communication.
pub fn determine_relevant_interfaces() -> anyhow::Result<Vec<RwhodSendTarget>> {
getifaddrs().map_err(|e| e.into()).map(|ifaces| {
ifaces
// interface must be up
.filter(|iface| iface.flags.contains(InterfaceFlags::IFF_UP))
// interface must be broadcast or point-to-point
.filter(|iface| {
iface
.flags
.intersects(InterfaceFlags::IFF_BROADCAST | InterfaceFlags::IFF_POINTOPOINT)
})
.filter_map(|iface| {
let neighbor_addr = if iface.flags.contains(InterfaceFlags::IFF_BROADCAST) {
iface.broadcast
} else if iface.flags.contains(InterfaceFlags::IFF_POINTOPOINT) {
iface.destination
} else {
None
};
match neighbor_addr {
Some(addr) => addr
.as_sockaddr_in()
.map(|sa| IpAddr::V4(sa.ip().into()))
.or_else(|| addr.as_sockaddr_in6().map(|sa| IpAddr::V6(sa.ip().into())))
.map(|ip_addr| RwhodSendTarget {
name: iface.interface_name,
addr: ip_addr,
}),
None => None,
}
})
/* keep first occurrence per interface name */
.scan(HashSet::new(), |seen, n| {
if seen.insert(n.name.clone()) {
Some(n)
} else {
None
}
})
.collect::<Vec<RwhodSendTarget>>()
})
}
pub async fn send_rwhod_packet_to_interface(
socket: &mut tokio::net::UdpSocket,
interface: &RwhodSendTarget,
packet: &Whod,
) -> anyhow::Result<()> {
let serialized_packet = packet.to_bytes();
let target_addr = match interface.addr {
IpAddr::V4(addr) => std::net::SocketAddr::new(IpAddr::V4(addr), 0),
IpAddr::V6(addr) => std::net::SocketAddr::new(IpAddr::V6(addr), 0),
};
socket
.send_to(&serialized_packet, &target_addr)
.await
.map_err(|e| anyhow::anyhow!("Failed to send rwhod packet: {}", e))?;
Ok(())
}
// TODO: implement receiving rwhod packets from other hosts
// TODO: implement storing and loading rwhod packets to/from file
// TODO: implement protocol for cli - daemon communication