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
+215
View File
@@ -0,0 +1,215 @@
//! Milestone 5: systemd socket activation, readiness notification, and
//! metered behavior.
mod support;
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::Command;
use std::time::{Duration, Instant};
use varde_proto::{PinPolicy, Request, ResponseData};
extern "C" {
fn dup2(oldfd: i32, newfd: i32) -> i32;
}
/// Spawn the daemon with an activation socket on fd 3, the LISTEN_FDS
/// protocol systemd uses.
#[test]
fn systemd_socket_activation() {
let dir = tempfile::tempdir().unwrap();
let socket_path = dir.path().join("activated.sock");
let listener = std::os::unix::net::UnixListener::bind(&socket_path).unwrap();
let fd = listener.as_raw_fd();
std::fs::write(dir.path().join("no-config.toml"), "").unwrap();
let mut command = Command::new(env!("CARGO_BIN_EXE_varde-daemon"));
command
.arg("--user")
.arg("--store")
.arg(dir.path().join("store"))
// Deliberately point --socket elsewhere: the activation fd must
// win over self-binding.
.arg("--socket")
.arg(dir.path().join("ignored.sock"))
.env("VARDE_CONFIG", dir.path().join("no-config.toml"))
.env("VARDE_DISCOVERY", "false")
.env("LISTEN_FDS", "1")
.env_remove("LISTEN_PID");
// SAFETY: dup2 is async-signal-safe; we place the listener on fd 3
// in the child, which also clears CLOEXEC on the duplicate.
unsafe {
command.pre_exec(move || {
if dup2(fd, 3) < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let mut child = command.spawn().expect("spawning daemon");
// The daemon must answer on the activation socket.
let deadline = Instant::now() + Duration::from_secs(10);
let mut answered = false;
while Instant::now() < deadline {
if std::os::unix::net::UnixStream::connect(&socket_path).is_ok() {
let mut client = support::Client::connect(&socket_path);
let reply = client.request(&Request::Status { hash: None });
if reply.ok {
answered = true;
break;
}
}
std::thread::sleep(Duration::from_millis(50));
}
let _ = child.kill();
let _ = child.wait();
assert!(answered, "daemon did not serve on the activation socket");
assert!(
!dir.path().join("ignored.sock").exists(),
"daemon bound its own socket despite activation"
);
}
/// With NOTIFY_SOCKET set, the daemon reports READY=1 once serving.
#[test]
fn sd_notify_ready_is_sent() {
let dir = tempfile::tempdir().unwrap();
let notify_path = dir.path().join("notify.sock");
let notify = std::os::unix::net::UnixDatagram::bind(&notify_path).unwrap();
notify
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
std::fs::write(dir.path().join("no-config.toml"), "").unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_varde-daemon"))
.arg("--user")
.arg("--store")
.arg(dir.path().join("store"))
.arg("--socket")
.arg(dir.path().join("varde.sock"))
.env("VARDE_CONFIG", dir.path().join("no-config.toml"))
.env("VARDE_DISCOVERY", "false")
.env("NOTIFY_SOCKET", &notify_path)
.spawn()
.expect("spawning daemon");
let mut buf = [0u8; 64];
let result = notify.recv(&mut buf);
let _ = child.kill();
let _ = child.wait();
let n = result.expect("no readiness notification received");
let message = std::str::from_utf8(&buf[..n]).unwrap();
assert!(
message.contains("READY=1"),
"unexpected notify payload: {message}"
);
}
fn wait_complete(socket: &Path, hash: &str, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let mut client = support::Client::connect(socket);
let reply = client.request(&Request::Status {
hash: Some(hash.to_string()),
});
if let Some(ResponseData::HashStatus(s)) = reply.data {
if s.complete {
return true;
}
}
std::thread::sleep(Duration::from_millis(100));
}
false
}
/// A metered daemon serves nothing, even openly exported content.
#[test]
fn metered_daemon_stops_serving() {
let provider_dir = tempfile::tempdir().unwrap();
let provider =
support::spawn_daemon_with_env(provider_dir.path(), &[("VARDE_FORCE_METERED", "true")]);
let fetcher = support::spawn_daemon();
let mut provider_client = support::Client::connect(&provider.socket);
let mut fetcher_client = support::Client::connect(&fetcher.socket);
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("f.bin");
std::fs::write(&src, vec![4u8; 200_000]).unwrap();
let reply = provider_client.request(&Request::Add {
path: src.display().to_string(),
recursive: false,
});
let hash = match reply.data {
Some(ResponseData::Added { hash, .. }) => hash,
other => panic!("unexpected add reply: {other:?}"),
};
let reply = provider_client.request(&Request::Pin {
hash: hash.clone(),
policy: PinPolicy::default(),
});
assert!(reply.ok);
let reply = provider_client.request(&Request::TicketExport { hash: hash.clone() });
let ticket = match reply.data {
Some(ResponseData::Ticket { ticket }) => ticket,
other => panic!("unexpected ticket reply: {other:?}"),
};
let reply = fetcher_client.request(&Request::TicketImport {
ticket,
pin_policy: PinPolicy::default(),
});
assert!(reply.ok, "import should be accepted (pin recorded)");
assert!(
!wait_complete(&fetcher.socket, &hash, Duration::from_secs(8)),
"metered provider must not serve"
);
}
/// A metered daemon defers its own fetches too.
#[test]
fn metered_daemon_defers_fetching() {
let provider = support::spawn_daemon();
let fetcher_dir = tempfile::tempdir().unwrap();
let fetcher =
support::spawn_daemon_with_env(fetcher_dir.path(), &[("VARDE_FORCE_METERED", "true")]);
let mut provider_client = support::Client::connect(&provider.socket);
let mut fetcher_client = support::Client::connect(&fetcher.socket);
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("f.bin");
std::fs::write(&src, vec![6u8; 200_000]).unwrap();
let reply = provider_client.request(&Request::Add {
path: src.display().to_string(),
recursive: false,
});
let hash = match reply.data {
Some(ResponseData::Added { hash, .. }) => hash,
other => panic!("unexpected add reply: {other:?}"),
};
assert!(
provider_client
.request(&Request::Pin {
hash: hash.clone(),
policy: PinPolicy::default(),
})
.ok
);
let reply = provider_client.request(&Request::TicketExport { hash: hash.clone() });
let ticket = match reply.data {
Some(ResponseData::Ticket { ticket }) => ticket,
other => panic!("unexpected ticket reply: {other:?}"),
};
let reply = fetcher_client.request(&Request::TicketImport {
ticket,
pin_policy: PinPolicy::default(),
});
assert!(reply.ok, "import records the pin even while metered");
assert!(
!wait_complete(&fetcher.socket, &hash, Duration::from_secs(8)),
"metered fetcher must not download"
);
}