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:
2026-07-15 08:07:24 +02:00
parent 20bdf56668
commit 871280553e
19 changed files with 1031 additions and 5 deletions
+18 -2
View File
@@ -27,6 +27,7 @@ use varde_proto::{Direction, Event};
use crate::config::Config;
use crate::meta::Meta;
use crate::metered::MeteredState;
use crate::shaped::{ServeFilter, ShapedStore, TokenBucket};
use crate::store::BlobStore;
@@ -39,6 +40,7 @@ pub struct Transfer {
router: Router,
downloader: Downloader,
events: broadcast::Sender<Event>,
metered: MeteredState,
discovery_rx: std::sync::Mutex<Option<mpsc::Receiver<NodeId>>>,
}
@@ -54,6 +56,7 @@ struct GatedProvider {
events: EventSender,
pool: LocalPoolHandle,
serving_enabled: bool,
metered: MeteredState,
}
impl ProtocolHandler for GatedProvider {
@@ -61,8 +64,8 @@ impl ProtocolHandler for GatedProvider {
let this = self.clone();
Box::pin(async move {
let remote = connection.remote_node_id()?;
if !this.serving_enabled {
debug!(%remote, "serving disabled, closing connection");
if !this.serving_enabled || this.metered.is_metered() {
debug!(%remote, "serving disabled or metered, closing connection");
connection.close(0u32.into(), b"serving disabled");
return Ok(());
}
@@ -116,6 +119,7 @@ impl Transfer {
open_filter: ServeFilter,
pool: LocalPoolHandle,
events: broadcast::Sender<Event>,
metered: MeteredState,
) -> Result<Transfer> {
let secret = load_or_create_secret(&config.store_dir.join("secret.key"))?;
let mut builder = Endpoint::builder().secret_key(secret.clone());
@@ -146,6 +150,11 @@ impl Transfer {
let endpoint = builder.bind().await.context("binding iroh endpoint")?;
info!(node_id = %endpoint.node_id(), discovery = config.discovery, "endpoint up");
// Best-effort low-priority marking of our UDP traffic.
let (v4, v6) = endpoint.bound_sockets();
let bound: Vec<std::net::SocketAddr> = std::iter::once(v4).chain(v6).collect();
crate::dscp::mark_endpoint_sockets(&bound);
let upload_bucket = TokenBucket::new(config.max_upload_bytes_per_sec);
let download_bucket = TokenBucket::new(config.max_download_bytes_per_sec);
let shaped = ShapedStore::new(store.inner().clone(), upload_bucket, download_bucket);
@@ -158,6 +167,7 @@ impl Transfer {
events: provider_events,
pool: pool.clone(),
serving_enabled: config.max_upload_bytes_per_sec > 0,
metered: metered.clone(),
};
let downloader = Downloader::new(shaped, endpoint.clone(), pool);
@@ -170,6 +180,7 @@ impl Transfer {
router,
downloader,
events,
metered,
discovery_rx: std::sync::Mutex::new(Some(discovery_rx)),
})
}
@@ -200,6 +211,11 @@ impl Transfer {
/// Queue a background fetch of `content` from `providers`. Emits
/// progress events and [`Event::PinComplete`] when fully verified.
pub fn spawn_fetch(&self, content: HashAndFormat, providers: Vec<NodeAddr>) {
if self.metered.is_metered() {
// The pin stays recorded; auto-sync retries once unmetered.
info!(hash = %content.hash.to_hex(), "metered connection: fetch deferred");
return;
}
let downloader = self.downloader.clone();
let events = self.events.clone();
let endpoint = self.endpoint.clone();