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
+102
View File
@@ -0,0 +1,102 @@
//! Best-effort DSCP marking of the endpoint's UDP sockets.
//!
//! Background transfer traffic is marked CS1 (low priority) so routers
//! that honor DSCP deprioritize it. iroh does not expose its socket fds,
//! so this walks `/proc/self/fd` for the UDP sockets bound to the
//! endpoint's ports — Linux-only by construction, like the daemon.
//! Failures are logged at debug and ignored: some environments strip or
//! forbid DSCP (may require CAP_NET_ADMIN), and that's fine.
use std::net::SocketAddr;
use std::os::fd::RawFd;
use tracing::debug;
/// DSCP CS1 (001000) in the TOS byte.
const TOS_CS1: u32 = 0x20;
extern "C" {
fn setsockopt(fd: i32, level: i32, name: i32, value: *const u32, len: u32) -> i32;
}
const IPPROTO_IP: i32 = 0;
const IP_TOS: i32 = 1;
const IPPROTO_IPV6: i32 = 41;
const IPV6_TCLASS: i32 = 67;
/// Mark every UDP socket bound to one of `addrs` with DSCP CS1.
pub fn mark_endpoint_sockets(addrs: &[SocketAddr]) {
let ports: Vec<u16> = addrs.iter().map(|a| a.port()).collect();
let inodes = udp_socket_inodes(&ports);
if inodes.is_empty() {
debug!("dscp: no matching udp sockets found");
return;
}
let mut marked = 0;
for fd in socket_fds(&inodes) {
// Set both levels; one of them will apply depending on family.
let v4 = unsafe { setsockopt(fd, IPPROTO_IP, IP_TOS, &TOS_CS1, 4) };
let v6 = unsafe { setsockopt(fd, IPPROTO_IPV6, IPV6_TCLASS, &TOS_CS1, 4) };
if v4 == 0 || v6 == 0 {
marked += 1;
}
}
debug!(marked, "dscp: marked endpoint sockets CS1");
}
/// Inodes of UDP sockets locally bound to one of `ports`, from
/// /proc/net/udp{,6}. Format: whitespace columns, local_address is
/// `HEXIP:HEXPORT` in column 1, inode in column 9.
fn udp_socket_inodes(ports: &[u16]) -> Vec<u64> {
let mut inodes = Vec::new();
for table in ["/proc/net/udp", "/proc/net/udp6"] {
let Ok(text) = std::fs::read_to_string(table) else {
continue;
};
for line in text.lines().skip(1) {
let fields: Vec<&str> = line.split_whitespace().collect();
let (Some(local), Some(inode)) = (fields.get(1), fields.get(9)) else {
continue;
};
let Some((_, port_hex)) = local.rsplit_once(':') else {
continue;
};
let Ok(port) = u16::from_str_radix(port_hex, 16) else {
continue;
};
if ports.contains(&port) {
if let Ok(inode) = inode.parse() {
inodes.push(inode);
}
}
}
}
inodes
}
/// Our process's fds that are sockets with one of the given inodes.
fn socket_fds(inodes: &[u64]) -> Vec<RawFd> {
let mut fds = Vec::new();
let Ok(entries) = std::fs::read_dir("/proc/self/fd") else {
return fds;
};
for entry in entries.flatten() {
let Ok(target) = std::fs::read_link(entry.path()) else {
continue;
};
let target = target.to_string_lossy();
let Some(inode) = target
.strip_prefix("socket:[")
.and_then(|s| s.strip_suffix(']'))
.and_then(|s| s.parse::<u64>().ok())
else {
continue;
};
if inodes.contains(&inode) {
if let Ok(fd) = entry.file_name().to_string_lossy().parse::<RawFd>() {
fds.push(fd);
}
}
}
fds
}