poweroff command behind --allow-poweroff, via polkit not sudo; deploy builds natively
/ crosscheck (push) Successful in 0s

The display can shut the Orin down: ack first, systemctl poweroff 2 s later.
Refused without --allow-poweroff (only the Orin's unit sets it), without
confirm, or with another boot's boot_id (hello now carries it), so a resend
after the reboot cannot shut the next boot down. polkit grants the service
user logind's power-off; no passwordless sudo involved at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
2026-10-13 18:35:06 +09:00
parent b92beabd70
commit a2d6ad5aaf
5 changed files with 76 additions and 9 deletions
+35
View File
@@ -9,7 +9,9 @@ use std::collections::VecDeque;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::os::unix::net::UnixStream;
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use serde_json::{json, Value};
@@ -24,6 +26,8 @@ pub struct Ctx {
pub hub: Hub,
pub hello: Value,
pub recorder_ctl: String,
/// `--allow-poweroff`: off by default, so a gateway on a workstation can never shut it down.
pub allow_poweroff: bool,
/// Acks by command id, shared across connections so a retry after reconnect is answered, not re-run.
pub acks: Mutex<VecDeque<(String, Value)>>,
}
@@ -126,10 +130,41 @@ fn run(ctx: &Ctx, msg: &Value) -> (bool, String) {
Err(e) => (false, format!("recorder unreachable: {e}")),
}
}
"poweroff" => poweroff(ctx, &msg["args"]),
other => (false, format!("unknown command '{other}'")),
}
}
/// The host's boot id: a poweroff command must carry the current one, so a resend
/// from before a reboot cannot shut the next boot down.
pub fn boot_id() -> String {
std::fs::read_to_string("/proc/sys/kernel/random/boot_id").unwrap_or_default().trim().to_string()
}
fn poweroff(ctx: &Ctx, args: &Value) -> (bool, String) {
if !ctx.allow_poweroff {
return (false, "poweroff not enabled on this gateway (--allow-poweroff)".into());
}
if args["confirm"].as_str() != Some("poweroff") {
return (false, "poweroff needs args.confirm = \"poweroff\"".into());
}
if args["boot_id"].as_str() != Some(boot_id().as_str()) {
return (false, "poweroff refused: boot_id is not this boot's".into());
}
eprintln!("svc-gw: poweroff requested by a client");
// Ack first: the reply has to reach the client before the services stop.
thread::spawn(|| {
thread::sleep(Duration::from_secs(2));
// Unprivileged: polkit grants this user logind's power-off (deploy/50-yukart-gw-poweroff.pkla).
match Command::new("systemctl").args(["poweroff", "--no-ask-password"]).status() {
Ok(s) if s.success() => {}
Ok(s) => eprintln!("svc-gw: systemctl poweroff exited {s}"),
Err(e) => eprintln!("svc-gw: systemctl poweroff: {e}"),
}
});
(true, "shutting down in 2 s".into())
}
fn recorder(path: &str, line: &str) -> std::io::Result<String> {
let mut s = UnixStream::connect(path)?;
s.set_read_timeout(Some(Duration::from_secs(3)))?;
+6 -1
View File
@@ -24,7 +24,8 @@ usage: svc-gw [options]
--listen ADDR WebSocket listen address (0.0.0.0:7700)
--mock synthesise vehicle state, flagged as mock in every message (no bus)
--broker HOST:PORT MQTT broker; '' for none (127.0.0.1:1883)
--recorder-ctl PATH recorder control socket, for 'mark' (/run/yukart-recorder/ctl.sock)";
--recorder-ctl PATH recorder control socket, for 'mark' (/run/yukart-recorder/ctl.sock)
--allow-poweroff accept the 'poweroff' command (the Orin's unit only; never on a workstation)";
pub fn now_us() -> i64 {
let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
@@ -53,11 +54,13 @@ fn main() -> ExitCode {
let mut listen = "0.0.0.0:7700".to_string();
let mut mock = false;
let mut recorder_ctl = "/run/yukart-recorder/ctl.sock".to_string();
let mut allow_poweroff = false;
let mut broker = "127.0.0.1:1883".to_string();
let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
match a.as_str() {
"--mock" => mock = true,
"--allow-poweroff" => allow_poweroff = true,
"--listen" => listen = args.next().unwrap_or_default(),
"--recorder-ctl" => recorder_ctl = args.next().unwrap_or_default(),
"--broker" => broker = args.next().unwrap_or_default(),
@@ -89,12 +92,14 @@ fn main() -> ExitCode {
"proto": 0,
"server": concat!("svc-gw ", env!("CARGO_PKG_VERSION")),
"host": std::fs::read_to_string("/proc/sys/kernel/hostname").unwrap_or_default().trim(),
"boot_id": client::boot_id(),
"source": source,
"mock": mock,
"hb_ms": client::HEARTBEAT.as_millis() as u64,
"timeout_ms": client::TIMEOUT.as_millis() as u64,
}),
recorder_ctl,
allow_poweroff,
acks: Mutex::new(Default::default()),
});