Milestone 5: metered awareness, DSCP, systemd, man pages, packaging

Metered detection polls NetworkManager's Metered property over D-Bus
(feature "metered", default on; builds without D-Bus via
no-default-features). While metered the daemon closes incoming blob
connections and defers every fetch; VARDE_FORCE_METERED=true forces the
state as a kill switch and test hook. Endpoint UDP sockets get DSCP CS1
best-effort by matching bound ports to /proc/net/udp inodes (iroh hides
its fds). systemd socket activation adopts LISTEN_FDS fd 3, readiness
is a hand-rolled sd_notify READY=1 (abstract + path sockets), and
standalone binding still works unchanged. dist/ ships hardened system
and user units (DynamicUser, ProtectSystem=strict, StateDirectory,
RestrictAddressFamilies), a commented config example, scdoc man pages
validated with scdoc, and an untested PKGBUILD skeleton.

Tests: activation-socket round trip via a real fd-3 handoff, READY=1
received on a NOTIFY_SOCKET, metered daemons neither serve nor fetch.

Dependencies: zbus (optional, feature-gated D-Bus client for the
NetworkManager metered flag).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bendik Lynghaug
2026-07-15 08:07:24 +02:00
co-authored by Claude Fable 5
parent 20bdf56668
commit 871280553e
19 changed files with 1031 additions and 5 deletions
+99
View File
@@ -0,0 +1,99 @@
//! Metered-connection awareness.
//!
//! When the active connection is metered, varde stops moving bytes:
//! no background fetching, no serving. Detection queries
//! NetworkManager's `Metered` property over D-Bus and is feature-gated
//! (`metered`, on by default) so the daemon still builds and runs on
//! systems without D-Bus — those simply always count as unmetered.
//!
//! `VARDE_FORCE_METERED=true` forces the metered state regardless of
//! NetworkManager; useful for testing and as a manual kill switch.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
/// Shared metered state, cheap to read on every transfer decision.
#[derive(Debug, Clone, Default)]
pub struct MeteredState(Arc<AtomicBool>);
impl MeteredState {
pub fn is_metered(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
fn set(&self, metered: bool) {
let was = self.0.swap(metered, Ordering::Relaxed);
if was != metered {
tracing::info!(metered, "metered state changed");
}
}
}
/// Start metered detection and return the shared state.
pub fn start() -> MeteredState {
let state = MeteredState::default();
if std::env::var("VARDE_FORCE_METERED").as_deref() == Ok("true") {
tracing::warn!("VARDE_FORCE_METERED=true: all transfers disabled");
state.set(true);
return state;
}
#[cfg(feature = "metered")]
{
let state = state.clone();
tokio::spawn(async move { poll_network_manager(state).await });
}
state
}
/// NetworkManager's NM_METERED enum: 1 = yes, 3 = guess-yes.
#[cfg(feature = "metered")]
async fn poll_network_manager(state: MeteredState) {
use std::time::Duration;
let connection = match zbus::Connection::system().await {
Ok(c) => c,
Err(e) => {
tracing::debug!(error = %e, "no D-Bus system bus; assuming unmetered");
return;
}
};
let proxy = match zbus::Proxy::new(
&connection,
"org.freedesktop.NetworkManager",
"/org/freedesktop/NetworkManager",
"org.freedesktop.NetworkManager",
)
.await
{
Ok(p) => p,
Err(e) => {
tracing::debug!(error = %e, "NetworkManager proxy failed; assuming unmetered");
return;
}
};
loop {
match proxy.get_property::<u32>("Metered").await {
Ok(value) => state.set(value == 1 || value == 3),
Err(e) => {
tracing::debug!(error = %e, "reading Metered property");
}
}
tokio::time::sleep(Duration::from_secs(30)).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_unmetered_and_flips() {
let state = MeteredState::default();
assert!(!state.is_metered());
state.set(true);
assert!(state.is_metered());
state.set(false);
assert!(!state.is_metered());
}
}