Progress towards implementing the rwho server
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
fn main() {
|
||||
unimplemented!()
|
||||
println!(
|
||||
"{:#?}",
|
||||
roowho2_lib::server::rwhod::generate_rwhod_status_update(),
|
||||
);
|
||||
println!(
|
||||
"{:#?}",
|
||||
roowho2_lib::server::rwhod::determine_relevant_interfaces(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod proto;
|
||||
pub mod server;
|
||||
|
||||
@@ -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
334
src/proto/rwhod_protocol.rs
Normal 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
1
src/server.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod rwhod;
|
||||
138
src/server/rwhod.rs
Normal file
138
src/server/rwhod.rs
Normal 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
|
||||
Reference in New Issue
Block a user