rwhod: react to audit pam events to broadcast updates
Build and test / build (push) Successful in 1m40s
Build and test / test (push) Successful in 2m1s
Build and test / check (push) Successful in 2m22s
Build and test / docs (push) Successful in 5m13s

Listen to read-only audit packets from the kernel to react on user
logins/logouts. This way, we can immediately notify other machines on
the network whenever there is a change to the user session list.
This commit is contained in:
2026-07-21 02:04:03 +09:00
parent 026c19acc7
commit 51d1fb1ffb
9 changed files with 271 additions and 23 deletions
Generated
+115 -18
View File
@@ -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]]
+5 -2
View File
@@ -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"
+26
View File
@@ -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";
+5 -1
View File
@@ -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;
+27 -1
View File
@@ -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<OwnedFd> = 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<IgnoreList>,
allowed_interfaces: Option<HashSet<String>>,
send_interval: Duration,
realtime_updates_enabled: bool,
audit_socket: Option<OwnedFd>,
) -> 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! {
+12
View File
@@ -61,6 +61,13 @@ pub struct RwhodConfig {
///
/// If left as `None`, defaults to 4096.
pub max_status_entries: Option<usize>,
/// 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<bool>,
}
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)]
+2
View File
@@ -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};
+57
View File
@@ -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;
}
}
}
}
+22 -1
View File
@@ -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<RwhodSendTarget>,
ignore_list: Option<IgnoreList>,
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())?;