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>
100 lines
2.9 KiB
Rust
100 lines
2.9 KiB
Rust
//! 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());
|
|
}
|
|
}
|