poweroff command behind --allow-poweroff, via polkit not sudo; deploy builds natively
/ crosscheck (push) Successful in 0s
/ 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:
@@ -0,0 +1,12 @@
|
|||||||
|
# polkit (0.105, local authority): lets the svc-gw user power the host off through
|
||||||
|
# logind, with no sudo. svc-gw runs `systemctl poweroff` for the display's
|
||||||
|
# `poweroff` command (contracts BOUNDARY.md). Nothing else is granted.
|
||||||
|
# Installed to /etc/polkit-1/localauthority/50-local.d/ by scripts/deploy.sh.
|
||||||
|
# multiple-sessions: an ssh login is another session; ignore-inhibit: no inhibitor lock
|
||||||
|
# (none are taken on this host today) should leave the button silently doing nothing.
|
||||||
|
[yukart-gw: power off]
|
||||||
|
Identity=unix-user:mikkeli
|
||||||
|
Action=org.freedesktop.login1.power-off;org.freedesktop.login1.power-off-multiple-sessions;org.freedesktop.login1.power-off-ignore-inhibit
|
||||||
|
ResultAny=yes
|
||||||
|
ResultInactive=yes
|
||||||
|
ResultActive=yes
|
||||||
@@ -8,7 +8,8 @@ StartLimitIntervalSec=0
|
|||||||
[Service]
|
[Service]
|
||||||
User=mikkeli
|
User=mikkeli
|
||||||
# Add --mock only on a bench unit: every message then says so, and displays must show it.
|
# Add --mock only on a bench unit: every message then says so, and displays must show it.
|
||||||
ExecStart=/usr/local/bin/yukart-gw --listen 0.0.0.0:7700
|
# --allow-poweroff: the display may shut this host down (contracts BOUNDARY.md, poweroff). Only on the Orin.
|
||||||
|
ExecStart=/usr/local/bin/yukart-gw --listen 0.0.0.0:7700 --allow-poweroff
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
|
|
||||||
|
|||||||
+21
-7
@@ -1,29 +1,43 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build svc-gw for aarch64, check it against the car's glibc, and install it
|
# Build svc-gw natively on the development Orin, check it against the car's
|
||||||
# on an Orin as the yukart-gw service.
|
# glibc, and install it on an Orin as the yukart-gw service.
|
||||||
#
|
#
|
||||||
# usage: scripts/deploy.sh [host] (default: mikkeli-orin-nano)
|
# usage: scripts/deploy.sh [host] (default: mikkeli-orin-nano)
|
||||||
|
# env: BUILD_HOST (default: mikkeli-orin-nano-2)
|
||||||
#
|
#
|
||||||
# Cross-compiles from the workstation (the documented fallback): the development
|
# Builds what is pushed, not the working tree: the build host pulls master of
|
||||||
# Orin is the preferred native build host. Either way check-glibc.sh gates it.
|
# svc-gw and contracts under ~/dev/yukart/. Native on purpose: the poweroff command
|
||||||
|
# runs systemctl, and std::process::Command cross-compiled against the workstation's
|
||||||
|
# newer glibc links GLIBC_2.39 symbols (pidfd_spawnp) the car cannot load.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HOST="${1:-mikkeli-orin-nano}"
|
HOST="${1:-mikkeli-orin-nano}"
|
||||||
|
BUILD_HOST="${BUILD_HOST:-mikkeli-orin-nano-2}"
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
TARGET=aarch64-unknown-linux-gnu
|
BIN="$(mktemp -d)/svc-gw"
|
||||||
BIN="target/$TARGET/release/svc-gw"
|
|
||||||
|
|
||||||
cargo build --release --target "$TARGET"
|
ssh "$BUILD_HOST" 'set -e
|
||||||
|
mkdir -p ~/dev/yukart && cd ~/dev/yukart
|
||||||
|
for r in svc-gw contracts; do
|
||||||
|
[ -d $r ] || git clone -q http://192.168.2.199:3005/yukart/$r.git
|
||||||
|
git -C $r pull -q --ff-only
|
||||||
|
done
|
||||||
|
cd svc-gw && ~/.cargo/bin/cargo build --release 2>&1 | tail -1
|
||||||
|
git log --oneline -1'
|
||||||
|
scp -q "$BUILD_HOST:dev/yukart/svc-gw/target/release/svc-gw" "$BIN"
|
||||||
scripts/check-glibc.sh "$BIN"
|
scripts/check-glibc.sh "$BIN"
|
||||||
|
|
||||||
# Binary lives under ~/infra per the host repo's guidelines; /usr/local/bin gets a symlink.
|
# Binary lives under ~/infra per the host repo's guidelines; /usr/local/bin gets a symlink.
|
||||||
ssh "$HOST" 'mkdir -p ~/infra/svc-gw'
|
ssh "$HOST" 'mkdir -p ~/infra/svc-gw'
|
||||||
scp -q "$BIN" "$HOST:infra/svc-gw/svc-gw.new"
|
scp -q "$BIN" "$HOST:infra/svc-gw/svc-gw.new"
|
||||||
scp -q deploy/yukart-gw.service "$HOST:infra/svc-gw/yukart-gw.service"
|
scp -q deploy/yukart-gw.service "$HOST:infra/svc-gw/yukart-gw.service"
|
||||||
|
scp -q deploy/50-yukart-gw-poweroff.pkla "$HOST:infra/svc-gw/50-yukart-gw-poweroff.pkla"
|
||||||
ssh "$HOST" 'set -e
|
ssh "$HOST" 'set -e
|
||||||
mv ~/infra/svc-gw/svc-gw.new ~/infra/svc-gw/svc-gw
|
mv ~/infra/svc-gw/svc-gw.new ~/infra/svc-gw/svc-gw
|
||||||
sudo ln -sfn "$HOME/infra/svc-gw/svc-gw" /usr/local/bin/yukart-gw
|
sudo ln -sfn "$HOME/infra/svc-gw/svc-gw" /usr/local/bin/yukart-gw
|
||||||
sudo install -m 0644 ~/infra/svc-gw/yukart-gw.service /etc/systemd/system/yukart-gw.service
|
sudo install -m 0644 ~/infra/svc-gw/yukart-gw.service /etc/systemd/system/yukart-gw.service
|
||||||
|
# polkit, not sudo, lets the service power the host off (the poweroff command).
|
||||||
|
sudo install -m 0644 ~/infra/svc-gw/50-yukart-gw-poweroff.pkla /etc/polkit-1/localauthority/50-local.d/50-yukart-gw-poweroff.pkla
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable yukart-gw.service >/dev/null
|
sudo systemctl enable yukart-gw.service >/dev/null
|
||||||
sudo systemctl restart yukart-gw.service
|
sudo systemctl restart yukart-gw.service
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ use std::collections::VecDeque;
|
|||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::process::Command;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
@@ -24,6 +26,8 @@ pub struct Ctx {
|
|||||||
pub hub: Hub,
|
pub hub: Hub,
|
||||||
pub hello: Value,
|
pub hello: Value,
|
||||||
pub recorder_ctl: String,
|
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.
|
/// Acks by command id, shared across connections so a retry after reconnect is answered, not re-run.
|
||||||
pub acks: Mutex<VecDeque<(String, Value)>>,
|
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}")),
|
Err(e) => (false, format!("recorder unreachable: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"poweroff" => poweroff(ctx, &msg["args"]),
|
||||||
other => (false, format!("unknown command '{other}'")),
|
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> {
|
fn recorder(path: &str, line: &str) -> std::io::Result<String> {
|
||||||
let mut s = UnixStream::connect(path)?;
|
let mut s = UnixStream::connect(path)?;
|
||||||
s.set_read_timeout(Some(Duration::from_secs(3)))?;
|
s.set_read_timeout(Some(Duration::from_secs(3)))?;
|
||||||
|
|||||||
+6
-1
@@ -24,7 +24,8 @@ usage: svc-gw [options]
|
|||||||
--listen ADDR WebSocket listen address (0.0.0.0:7700)
|
--listen ADDR WebSocket listen address (0.0.0.0:7700)
|
||||||
--mock synthesise vehicle state, flagged as mock in every message (no bus)
|
--mock synthesise vehicle state, flagged as mock in every message (no bus)
|
||||||
--broker HOST:PORT MQTT broker; '' for none (127.0.0.1:1883)
|
--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 {
|
pub fn now_us() -> i64 {
|
||||||
let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
|
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 listen = "0.0.0.0:7700".to_string();
|
||||||
let mut mock = false;
|
let mut mock = false;
|
||||||
let mut recorder_ctl = "/run/yukart-recorder/ctl.sock".to_string();
|
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 broker = "127.0.0.1:1883".to_string();
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
while let Some(a) = args.next() {
|
while let Some(a) = args.next() {
|
||||||
match a.as_str() {
|
match a.as_str() {
|
||||||
"--mock" => mock = true,
|
"--mock" => mock = true,
|
||||||
|
"--allow-poweroff" => allow_poweroff = true,
|
||||||
"--listen" => listen = args.next().unwrap_or_default(),
|
"--listen" => listen = args.next().unwrap_or_default(),
|
||||||
"--recorder-ctl" => recorder_ctl = args.next().unwrap_or_default(),
|
"--recorder-ctl" => recorder_ctl = args.next().unwrap_or_default(),
|
||||||
"--broker" => broker = args.next().unwrap_or_default(),
|
"--broker" => broker = args.next().unwrap_or_default(),
|
||||||
@@ -89,12 +92,14 @@ fn main() -> ExitCode {
|
|||||||
"proto": 0,
|
"proto": 0,
|
||||||
"server": concat!("svc-gw ", env!("CARGO_PKG_VERSION")),
|
"server": concat!("svc-gw ", env!("CARGO_PKG_VERSION")),
|
||||||
"host": std::fs::read_to_string("/proc/sys/kernel/hostname").unwrap_or_default().trim(),
|
"host": std::fs::read_to_string("/proc/sys/kernel/hostname").unwrap_or_default().trim(),
|
||||||
|
"boot_id": client::boot_id(),
|
||||||
"source": source,
|
"source": source,
|
||||||
"mock": mock,
|
"mock": mock,
|
||||||
"hb_ms": client::HEARTBEAT.as_millis() as u64,
|
"hb_ms": client::HEARTBEAT.as_millis() as u64,
|
||||||
"timeout_ms": client::TIMEOUT.as_millis() as u64,
|
"timeout_ms": client::TIMEOUT.as_millis() as u64,
|
||||||
}),
|
}),
|
||||||
recorder_ctl,
|
recorder_ctl,
|
||||||
|
allow_poweroff,
|
||||||
acks: Mutex::new(Default::default()),
|
acks: Mutex::new(Default::default()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user