diff --git a/Cargo.lock b/Cargo.lock index aeb7a5e..3adf68c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -67,7 +67,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -205,9 +205,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.2" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +checksum = "0fb99565819980999fb7b4a1796046a5c949e6d4ff132cf5fadf5a641e20d776" dependencies = [ "clap_builder", "clap_derive", @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "32f2392eae7f16557a3d727ef3a12e57b2b2ca6f98566a5f4fb41ffe305df077" dependencies = [ "heck", "proc-macro2", @@ -359,7 +359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -440,12 +440,48 @@ dependencies = [ "thiserror", ] +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.33" @@ -494,9 +530,13 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -742,6 +782,54 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "netlink-packet-audit" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca9e80389b423ef073a74f393a57c37ffef7bcf058212c233412f19a026109b8" +dependencies = [ + "bytes", + "log", + "netlink-packet-core", + "netlink-proto", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + [[package]] name = "nix" version = "0.31.3" @@ -761,7 +849,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -815,6 +903,12 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "phf" version = "0.12.1" @@ -929,6 +1023,9 @@ dependencies = [ "futures-util", "indoc", "itertools", + "netlink-packet-audit", + "netlink-proto", + "netlink-sys", "nix", "sd-notify", "serde", @@ -961,7 +1058,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1018,14 +1115,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.1", + "syn 3.0.2", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1093,7 +1190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1115,9 +1212,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5edbec4ed188954a10c12c038215f8ce7606b2d5c973cd8dc43e8795065c5f2f" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", @@ -1134,7 +1231,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1144,7 +1241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1164,7 +1261,7 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.1", + "syn 3.0.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e7943cf..a587bf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ autolib = false anyhow = "1.0.104" bytes = "1.12.1" chrono = { version = "0.4.45", features = ["serde"] } -clap = { version = "4.6.2", features = ["derive"] } +clap = { version = "4.6.3", features = ["derive"] } futures-util = "0.3.33" nix = { version = "0.31.3", features = ["hostname", "net", "fs", "user"] } serde = { version = "1.0.229", features = ["derive"] } @@ -29,7 +29,7 @@ tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } # onc-rpc = "0.3.2" sd-notify = { version = "0.5.0", optional = true } -serde_json = "1.0.150" +serde_json = "1.0.151" uucore = { version = "0.9.0", features = ["utmpx"] } zlink = { version = "0.7.0", features = ["introspection"] } clap_complete = "4.6.7" @@ -39,6 +39,9 @@ caps = "0.5.6" users = { version = "0.11.0", default-features = false } tracing-journald = "0.3.2" chrono-tz = "0.10.4" +netlink-proto = "0.12.0" +netlink-sys = { version = "0.8.8", features = ["tokio_socket"] } +netlink-packet-audit = "0.6.1" [dev-dependencies] indoc = "2.0.7" diff --git a/assets/systemd/roowho2-audit.socket b/assets/systemd/roowho2-audit.socket new file mode 100644 index 0000000..f0075a6 --- /dev/null +++ b/assets/systemd/roowho2-audit.socket @@ -0,0 +1,13 @@ +[Unit] +Description=Roowho2 Audit Netlink Socket + +ConditionKernelCommandLine=!audit=0 +ConditionKernelCommandLine=!audit=off + +[Socket] +FileDescriptorName=audit_socket +ListenNetlink=audit 1 +Service=roowho2.service + +[Install] +WantedBy=sockets.target diff --git a/nix/module.nix b/nix/module.nix index 4a12210..dcb126f 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -79,6 +79,16 @@ in { ''; }; + realtime_updates = lib.mkOption { + type = lib.types.bool; + default = true; + example = false; + description = '' + Whether to react to Linux audit log activity (e.g. logins/logouts) + and push status updates immediately. + ''; + }; + ignoreUsers = lib.mkOption { type = with lib.types; listOf (either str ints.unsigned); default = [ ]; @@ -276,6 +286,22 @@ in { } // cfg.settings.rwhod.socketConfig; }; + systemd.sockets.roowho2-audit = lib.mkIf (cfg.settings.rwhod.enable && cfg.settings.rwhod.realtime_updates) { + wantedBy = [ "sockets.target" ]; + description = "Roowho2 Audit Netlink Socket"; + unitConfig = { + ConditionKernelCommandLine = [ + "!audit=0" + "!audit=off" + ]; + }; + socketConfig = { + Service = "roowho2.service"; + FileDescriptorName = "audit_socket"; + ListenNetlink = "audit 1"; # AUDIT_NLGRP_READLOG + }; + }; + # systemd.sockets.roowho2-fingerd = lib.mkIf cfg.settings.fingerd.enable && cfg.settings.fingerd.enableRemoteFinger { # wantedBy = [ "sockets.target" ]; # description = "Roowho2 Fingerd Socket"; diff --git a/nix/vm.nix b/nix/vm.nix index 67d86f7..f3d9d63 100644 --- a/nix/vm.nix +++ b/nix/vm.nix @@ -19,9 +19,13 @@ nixpkgs.lib.nixosSystem { ({ config, lib, ... }: { system.stateVersion = config.system.nixos.release; virtualisation.graphics = false; - virtualisation.memorySize = 256; + boot.kernelParams = [ + "audit=1" + "audit_backlog_limit=1024" + ]; + users.users.alice.extraGroups = [ "wheel" ]; users.users.bob.uid = 1001; diff --git a/src/bin/roowhod.rs b/src/bin/roowhod.rs index 77cb167..16dba3e 100644 --- a/src/bin/roowhod.rs +++ b/src/bin/roowhod.rs @@ -105,12 +105,20 @@ async fn main() -> anyhow::Result<()> { }) .context("RWHOD server is enabled, but socket fd not provided by systemd")??; + let audit_socket: Option = fd_map + .get("audit_socket") + .map(|fd| fd.try_clone()) + .transpose() + .context("Failed to clone audit socket fd")?; + join_set.spawn(rwhod_server( socket, whod_status_store.clone(), rwhod_ignore_list.clone(), config.rwhod.interfaces.clone(), config.rwhod.send_interval(), + config.rwhod.realtime_updates_enabled(), + audit_socket, )); } else { tracing::debug!("RWHOD server is disabled in configuration"); @@ -148,13 +156,31 @@ async fn rwhod_server( ignore_list: Option, allowed_interfaces: Option>, send_interval: Duration, + realtime_updates_enabled: bool, + audit_socket: Option, ) -> anyhow::Result<()> { let socket = Arc::new(socket); let interfaces = roowho2_lib::server::rwhod::determine_relevant_interfaces(allowed_interfaces.as_ref())?; + + let (tx, rx) = tokio::sync::mpsc::channel(1); + match (realtime_updates_enabled, audit_socket) { + (true, Some(fd)) => { + tokio::spawn(roowho2_lib::server::rwhod::audit_change_notifier(tx, fd)); + } + (true, None) => { + tracing::warn!( + "rwhod.realtime_updates is enabled, but no audit socket fd was provided by \ + systemd (e.g. the kernel might not have been booted with `audit=1`); \ + falling back to interval-only updates" + ); + } + (false, _) => {} + } + let sender_task = - rwhod_packet_sender_task(socket.clone(), interfaces, ignore_list, send_interval); + rwhod_packet_sender_task(socket.clone(), interfaces, ignore_list, send_interval, rx); let receiver_task = rwhod_packet_receiver_task(socket.clone(), whod_status_store); tokio::select! { diff --git a/src/server/config.rs b/src/server/config.rs index 2b81b3e..06e4621 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -61,6 +61,13 @@ pub struct RwhodConfig { /// /// If left as `None`, defaults to 4096. pub max_status_entries: Option, + + /// Whether to react to Linux audit log activity (e.g. logins/logouts) + /// and push a status update immediately, instead of only on the + /// regular `send_interval_seconds` interval. + /// + /// If left as `None`, defaults to `false`. + pub realtime_updates: Option, } impl RwhodConfig { @@ -95,6 +102,11 @@ impl RwhodConfig { None => DEFAULT_MAX_STATUS_ENTRIES, } } + + /// Resolves [`Self::realtime_updates`] into a concrete flag. + pub fn realtime_updates_enabled(&self) -> bool { + self.realtime_updates.unwrap_or(false) + } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/src/server/rwhod.rs b/src/server/rwhod.rs index 43a5603..8ca08ce 100644 --- a/src/server/rwhod.rs +++ b/src/server/rwhod.rs @@ -1,8 +1,10 @@ +mod audit_watcher; mod packet_receiver; mod packet_sender; mod rwhod_status; mod status_registry; +pub use audit_watcher::audit_change_notifier; pub use packet_receiver::rwhod_packet_receiver_task; pub use packet_sender::{determine_relevant_interfaces, rwhod_packet_sender_task}; pub use rwhod_status::{generate_rwhod_status_update, generate_rwhod_user_entries}; diff --git a/src/server/rwhod/audit_watcher.rs b/src/server/rwhod/audit_watcher.rs new file mode 100644 index 0000000..9e84dc5 --- /dev/null +++ b/src/server/rwhod/audit_watcher.rs @@ -0,0 +1,57 @@ +use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd}; + +use futures_util::stream::StreamExt; +use netlink_packet_audit::AuditMessage; +use netlink_proto::sys::TokioSocket; +use tokio::sync::mpsc; + +/// `AUDIT_USER_START` is emitted when a PAM session is opened +const AUDIT_USER_START: u16 = 1105; +/// `AUDIT_USER_END` is emitted when a PAM session is closed +const AUDIT_USER_END: u16 = 1106; + +/// Listens for audit messages on the systemd-provided netlink socket and sends a +/// notification on the provided channel whenever a user session is opened or closed, +/// indicating that there rwhod status may have changed. +pub async fn audit_change_notifier(sender: mpsc::Sender<()>, socket_fd: OwnedFd) { + // SAFETY: `socket_fd` is a systemd-provided netlink socket, already + // bound and subscribed to the audit multicast group. + let socket = unsafe { TokioSocket::from_raw_fd(socket_fd.into_raw_fd()) }; + + let (connection, _handle, mut messages) = netlink_proto::from_socket_with_codec::< + AuditMessage, + TokioSocket, + netlink_packet_audit::NetlinkAuditCodec, + >(socket); + + tokio::spawn(connection); + + tracing::info!("Listening for realtime session updates via the Linux audit log"); + + loop { + match messages.next().await { + Some((msg, _addr)) + if matches!(msg.header.message_type, AUDIT_USER_START | AUDIT_USER_END) => + { + tracing::debug!("Received session-related audit message: {:?}", msg); + if sender.send(()).await.is_err() { + tracing::debug!("Realtime update receiver dropped, stopping audit watcher"); + return; + } + } + Some((msg, _addr)) => { + tracing::trace!( + "Ignoring audit message unrelated to sessions (type {})", + msg.header.message_type + ); + } + None => { + tracing::warn!( + "Audit netlink connection closed unexpectedly; realtime updates disabled \ + for the rest of this run" + ); + return; + } + } + } +} diff --git a/src/server/rwhod/packet_sender.rs b/src/server/rwhod/packet_sender.rs index b62957c..4ed9480 100644 --- a/src/server/rwhod/packet_sender.rs +++ b/src/server/rwhod/packet_sender.rs @@ -6,6 +6,7 @@ use std::{ }; use tokio::{ net::UdpSocket, + sync::mpsc, time::{Duration as TokioDuration, interval}, }; @@ -115,11 +116,31 @@ pub async fn rwhod_packet_sender_task( interfaces: Vec, ignore_list: Option, send_interval: TokioDuration, + mut realtime_update_trigger: mpsc::Receiver<()>, ) -> anyhow::Result<()> { let mut interval = interval(send_interval); + let mut trigger_closed = false; loop { - interval.tick().await; + if trigger_closed { + interval.tick().await; + } else { + tokio::select! { + _ = interval.tick() => {} + triggered = realtime_update_trigger.recv() => { + if triggered.is_some() { + tracing::debug!("Sending an early rwhod update due to realtime trigger"); + interval.reset(); + } else { + tracing::warn!( + "Realtime update channel closed unexpectedly; falling back to interval-only updates" + ); + trigger_closed = true; + continue; + } + } + } + } let status_update = generate_rwhod_status_update(ignore_list.as_ref())?;