Watch
1
0
Fork
You've already forked souveraine
0

sessiond: read tunnel health from sysfs, not wg show

Found on the phone: the bearer verb reported tunnel "off" while wiufph was up
and handshaking (4.11 KiB received). wg show needs CAP_NET_ADMIN for
WG_CMD_GET_DEVICE and sessiond is a systemd *user* unit, so it gets "Unable to
access interface wiufph: Operation not permitted" and every tunnel reads off,
forever. A daemon that cannot see the instrument it is built around.

/sys/class/net/<if>/statistics/rx_bytes is world-readable and is the same
number without the parse or the privilege; DEVTYPE=wireguard in uevent finds
the interface, and NM removes the link entirely when the profile goes down, so
its presence is the up/down signal.

The endpoint lookup had the same defect. NetworkManager hands wireguard.peers
to an unprivileged caller, and the endpoint is configured as a hostname
(sub.wiuf.net:51822), not the address the old gate hardcoded - so it is
resolved before pinning, and a failed resolution writes no pin rather than a
wrong one.
This commit is contained in:
Fimeg 2026-08-01 14:44:32 -04:00
commit ac084b461a
2 changed files with 75 additions and 33 deletions

View file

@ -310,35 +310,50 @@ fn probe_cellular() -> LinkHealth {
}
}
/// `wg show` received-bytes is the tunnel's honest instrument. A peer that has
/// sent and never received is up and deaf, not connected.
/// Received bytes are the tunnel's honest instrument: a peer that has sent and
/// never received is up and deaf, not connected.
///
/// Read from sysfs, not from `wg show`. `wg show` needs `CAP_NET_ADMIN` for
/// `WG_CMD_GET_DEVICE`, and sessiond is a systemd *user* unit — measured on the
/// phone 2026-08-01, it gets "Unable to access interface wiufph: Operation not
/// permitted" and every tunnel therefore read `off` while one was up and
/// handshaking. `/sys/class/net/*/statistics/rx_bytes` is world-readable and is
/// the same number, without the parse or the privilege.
fn probe_tunnel() -> TunnelHealth {
let out = match Command::new("wg").arg("show").output() {
Ok(o) if o.status.success() => o.stdout,
_ => return TunnelHealth::Off,
};
let text = String::from_utf8_lossy(&out);
if text.trim().is_empty() {
let Ok(entries) = std::fs::read_dir("/sys/class/net") else {
return TunnelHealth::Off;
}
// " transfer: 368 B received, 5.30 KiB sent"
let transfer = text.lines().find_map(|l| l.trim().strip_prefix("transfer:"));
match transfer {
// Up with a peer but no transfer line yet: it has not handshaken.
None => TunnelHealth::Deaf,
Some(t) => {
let received_zero = t
.split(" received")
.next()
.map(|r| r.trim().starts_with("0 "))
};
for entry in entries.flatten() {
let path = entry.path();
// DEVTYPE is how the kernel names the link's family; NM tears the
// interface down entirely when the profile goes down, so a wireguard
// DEVTYPE present at all means a tunnel is configured and up.
let is_wg = std::fs::read_to_string(path.join("uevent"))
.map(|s| s.lines().any(|l| l == "DEVTYPE=wireguard"))
.unwrap_or(false);
if received_zero {
TunnelHealth::Deaf
} else {
if !is_wg {
continue;
}
let rx = read_counter(&path.join("statistics/rx_bytes"));
// Nonzero received is the only positive evidence there is. Zero covers
// both "up and ignored" and the brief window after activation before
// anything has been sent; both are honestly "not answered yet", and
// the settling window is longer than that window, so a transient
// cannot produce an action on its own.
return if rx > 0 {
TunnelHealth::Handshaking
} else {
TunnelHealth::Deaf
};
}
TunnelHealth::Off
}
}
fn read_counter(path: &std::path::Path) -> u64 {
std::fs::read_to_string(path)
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0)
}
#[cfg(test)]

View file

@ -467,17 +467,44 @@ fn pin_tunnel_underlay(shared: &Arc<Shared>, bearer: crate::sessiond::bearer::Be
);
}
/// The peer endpoint address `wg` reports, without its port.
/// The WireGuard peer's endpoint address, as an IP.
///
/// Not from `wg show`: that needs `CAP_NET_ADMIN` and sessiond is a user unit,
/// so it can only ever report "Operation not permitted" here. NetworkManager
/// will hand the peer line to an unprivileged caller, and the endpoint is not
/// a secret — the key on the same line is, which is why only the endpoint is
/// taken.
///
/// The endpoint is configured as a *hostname* (`sub.wiuf.net:51822`), so it has
/// to be resolved before it can be pinned. That is the one circular step in
/// this file: if DNS is broken — which is precisely the failure this task
/// exists to fix — resolution fails and no pin is written. Skipping is the
/// right failure: a pin to a wrong or stale address is worse than none, and
/// the tunnel still works over whatever the default route is.
fn wg_endpoint_ip() -> Option<String> {
let out = std::process::Command::new("wg").arg("show").output().ok()?;
if !out.status.success() {
return None;
let out = std::process::Command::new("nmcli")
.args(["-g", "wireguard.peers", "connection", "show", "--active"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout);
let host = text.lines().find_map(|l| {
l.split_whitespace()
.find_map(|f| f.strip_prefix("endpoint="))
.and_then(|e| e.replace("\\:", ":").rsplit_once(':').map(|(h, _)| h.to_string()))
})?;
// Already an address? Then there is nothing to resolve.
if host.parse::<std::net::IpAddr>().is_ok() {
return Some(host);
}
let out = std::process::Command::new("getent")
.args(["ahostsv4", &host])
.output()
.ok()?;
String::from_utf8_lossy(&out.stdout)
.lines()
.find_map(|l| l.trim().strip_prefix("endpoint:"))
.and_then(|e| e.trim().rsplit_once(':'))
.map(|(host, _port)| host.to_string())
.split_whitespace()
.next()
.map(|s| s.to_string())
}
/// Act on the machine's decision that the session must lock before the panel