redirector: commit stamp + shared build-identity verifier that REFUSES
The binary records only the commit it was built from -- no dirty-tree flag. Cargo will not re-run a build script because another crate's source changed, so a compiled-in 'clean' claim can be stale and is not a safeguard; that was verified on the Blaze host. scripts/verify-build-identity.sh establishes both facts at LAUNCH, where they cannot go stale: the stamped commit equals HEAD, and the migration crates are clean. It REFUSES rather than warns, because for a migration gate a warning on stderr is something to scroll past. --identity prints the stamp without valid configuration. The launcher must be able to establish which commit a binary came from BEFORE deciding whether to run it; requiring a correct environment first would invert the check. redirector.sh mirrors sidecar.sh: refuses to start with an orphan present or the port busy, matches the resolved executable rather than the command line (pgrep -f matches any shell mentioning the name), and stop PROVES the process is gone and the port free. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
//! Stamp the commit this binary was built from.
|
||||||
|
//!
|
||||||
|
//! Deliberately records ONLY the commit — no dirty-tree flag. Cargo will not
|
||||||
|
//! re-run a build script because another crate's source changed, so a
|
||||||
|
//! compiled-in "clean" claim can be stale and is therefore not a safeguard.
|
||||||
|
//! (Verified on the Blaze host: editing the adapter and rebuilding left its
|
||||||
|
//! flag reading clean.)
|
||||||
|
//!
|
||||||
|
//! The authoritative checks run at launch, in `scripts/verify-build-identity.sh`,
|
||||||
|
//! which compares this stamp against the checkout's real HEAD and inspects the
|
||||||
|
//! working tree as it is at that moment.
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn git(args: &[&str]) -> Option<String> {
|
||||||
|
let out = Command::new("git").args(args).output().ok()?;
|
||||||
|
out.status
|
||||||
|
.success()
|
||||||
|
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let commit = git(&["rev-parse", "--short=7", "HEAD"]).unwrap_or_else(|| "unknown".into());
|
||||||
|
println!("cargo:rustc-env=OPENFUT_BUILD_COMMIT={commit}");
|
||||||
|
|
||||||
|
// Committing updates refs/heads/<branch>, not the HEAD file, so watching
|
||||||
|
// HEAD alone leaves the stamp a commit behind.
|
||||||
|
for p in ["../.git/HEAD", "../.git/index"] {
|
||||||
|
if std::path::Path::new(p).exists() {
|
||||||
|
println!("cargo:rerun-if-changed={p}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(rf) = git(&["symbolic-ref", "-q", "HEAD"]) {
|
||||||
|
let path = format!("../.git/{rf}");
|
||||||
|
if std::path::Path::new(&path).exists() {
|
||||||
|
println!("cargo:rerun-if-changed={path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+111
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Lifecycle for the Rust redirector host.
|
||||||
|
#
|
||||||
|
# redirector.sh start | stop | status | verify
|
||||||
|
#
|
||||||
|
# Mirrors sidecar.sh: refuses to start with an orphan present or the port busy,
|
||||||
|
# and stop PROVES the process is gone and the port free rather than assuming a
|
||||||
|
# signal worked.
|
||||||
|
#
|
||||||
|
# Additionally REFUSES TO START unless the binary's stamped commit equals HEAD
|
||||||
|
# and the migration crates are clean — evidence from an unidentifiable binary is
|
||||||
|
# not evidence.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
||||||
|
ROOT="$(cd "$HERE/.." && pwd)"
|
||||||
|
RUNDIR="${OPENFUT_REDIRECTOR_RUNDIR:-${TMPDIR:-/tmp}/openfut-redirector}"
|
||||||
|
PIDFILE="$RUNDIR/redirector.pid"
|
||||||
|
PORTFILE="$RUNDIR/redirector.port"
|
||||||
|
LOGFILE="${OPENFUT_REDIRECTOR_LOG:-$RUNDIR/redirector.log}"
|
||||||
|
BIN="$ROOT/target/debug/openfut-redirector-host"
|
||||||
|
[[ -x "$BIN" ]] || BIN="$ROOT/target/release/openfut-redirector-host"
|
||||||
|
|
||||||
|
die() { echo "redirector: $*" >&2; exit 1; }
|
||||||
|
pid_alive() { kill -0 "$1" 2>/dev/null; }
|
||||||
|
port_listening() { ss -ltn 2>/dev/null | grep -qE "[:.]${1}[[:space:]]"; }
|
||||||
|
|
||||||
|
# Match the resolved executable, not the command line: `pgrep -f` matches any
|
||||||
|
# shell whose arguments merely mention the name.
|
||||||
|
list_procs() {
|
||||||
|
local self=$$ pid exe
|
||||||
|
for d in /proc/[0-9]*; do
|
||||||
|
pid="${d#/proc/}"; [[ "$pid" == "$self" ]] && continue
|
||||||
|
exe="$(readlink -f "$d/exe" 2>/dev/null)" || continue
|
||||||
|
[[ "${exe##*/}" == "openfut-redirector-host" ]] && echo "$pid"
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The binary prints `commit=<sha>` in its banner; ask it rather than guessing.
|
||||||
|
stamped_commit() { "$BIN" --identity 2>&1 | sed -nE 's/.*commit=([0-9a-f]+).*/\1/p' | head -1; }
|
||||||
|
|
||||||
|
cmd_verify() {
|
||||||
|
local c; c="$(stamped_commit)"
|
||||||
|
[[ -n "$c" ]] || die "could not read the binary's commit stamp"
|
||||||
|
"$ROOT/scripts/verify-build-identity.sh" "$c"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_start() {
|
||||||
|
[[ -x "$BIN" ]] || die "not built: cargo build -p openfut-redirector-host"
|
||||||
|
: "${OPENFUT_REDIRECTOR_HOST_PORT:?set OPENFUT_REDIRECTOR_HOST_PORT (no default: runs beside Python)}"
|
||||||
|
local strays; strays="$(list_procs)"
|
||||||
|
[[ -z "$strays" ]] || die "orphan redirector process(es): $strays"
|
||||||
|
port_listening "$OPENFUT_REDIRECTOR_HOST_PORT" && die "port $OPENFUT_REDIRECTOR_HOST_PORT in use"
|
||||||
|
|
||||||
|
cmd_verify || die "build identity check failed — refusing to start"
|
||||||
|
|
||||||
|
mkdir -p "$RUNDIR"; echo "$OPENFUT_REDIRECTOR_HOST_PORT" > "$PORTFILE"
|
||||||
|
"$BIN" >"$LOGFILE" 2>&1 &
|
||||||
|
local pid=$!; echo "$pid" > "$PIDFILE"
|
||||||
|
local w=0
|
||||||
|
while (( w < 100 )); do
|
||||||
|
pid_alive "$pid" || { echo "died during startup:" >&2; tail -20 "$LOGFILE" >&2; rm -f "$PIDFILE"; return 1; }
|
||||||
|
if port_listening "$OPENFUT_REDIRECTOR_HOST_PORT"; then
|
||||||
|
echo "redirector started: pid $pid, port $OPENFUT_REDIRECTOR_HOST_PORT"
|
||||||
|
grep -E 'SELF-TEST|openfut-redirector-host v' "$LOGFILE" | sed 's/^/ /'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.1; w=$((w+1))
|
||||||
|
done
|
||||||
|
echo "did not listen within 10s:" >&2; tail -20 "$LOGFILE" >&2
|
||||||
|
kill "$pid" 2>/dev/null; rm -f "$PIDFILE"; return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_stop() {
|
||||||
|
local rc=0 pid="" port=""
|
||||||
|
[[ -f "$PIDFILE" ]] && pid="$(cat "$PIDFILE")"
|
||||||
|
[[ -f "$PORTFILE" ]] && port="$(cat "$PORTFILE")"
|
||||||
|
if [[ -n "$pid" ]] && pid_alive "$pid"; then
|
||||||
|
kill "$pid" 2>/dev/null
|
||||||
|
local w=0; while pid_alive "$pid" && (( w < 50 )); do sleep 0.1; w=$((w+1)); done
|
||||||
|
pid_alive "$pid" && kill -9 "$pid" 2>/dev/null
|
||||||
|
sleep 0.2
|
||||||
|
fi
|
||||||
|
[[ -n "$pid" ]] && pid_alive "$pid" && { echo "FAILED to stop $pid" >&2; rc=1; }
|
||||||
|
[[ -n "$port" ]] && port_listening "$port" && { echo "FAILED: port $port still listening" >&2; rc=1; }
|
||||||
|
local strays; strays="$(list_procs)"
|
||||||
|
[[ -n "$strays" ]] && { echo "FAILED: still running: $strays" >&2; rc=1; }
|
||||||
|
rm -f "$PIDFILE" "$PORTFILE"
|
||||||
|
[[ $rc -eq 0 ]] && echo "redirector stopped and verified gone${pid:+ (pid $pid)}"
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
if [[ -f "$PIDFILE" ]] && pid_alive "$(cat "$PIDFILE")"; then
|
||||||
|
echo "running: pid $(cat "$PIDFILE"), port $(cat "$PORTFILE" 2>/dev/null || echo '?')"
|
||||||
|
else
|
||||||
|
echo "not running"
|
||||||
|
fi
|
||||||
|
local strays; strays="$(list_procs)"
|
||||||
|
[[ -n "$strays" ]] && echo "redirector processes: $strays"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
start) cmd_start ;;
|
||||||
|
stop) cmd_stop ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
verify) cmd_verify ;;
|
||||||
|
*) sed -n '2,6p' "$0" | sed 's/^# \?//'; exit 2 ;;
|
||||||
|
esac
|
||||||
@@ -51,15 +51,56 @@ fn log(msg: &str) {
|
|||||||
eprintln!("[{}.{:03}] {msg}", ms / 1000, ms % 1000);
|
eprintln!("[{}.{:03}] {msg}", ms / 1000, ms % 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The commit this binary was built from.
|
||||||
|
///
|
||||||
|
/// Only the commit: a compiled-in cleanliness claim can go stale (cargo will
|
||||||
|
/// not re-run a build script for another crate's edit), so the authoritative
|
||||||
|
/// comparison happens at launch in `scripts/verify-build-identity.sh`.
|
||||||
|
pub const BUILD_COMMIT: &str = env!("OPENFUT_BUILD_COMMIT");
|
||||||
|
|
||||||
|
/// Build identity, printable without any configuration.
|
||||||
|
///
|
||||||
|
/// `--identity` exists so the launcher can establish which commit a binary came
|
||||||
|
/// from before deciding whether to run it. Machine-readable `key=value`.
|
||||||
|
pub fn identity() -> String {
|
||||||
|
format!(
|
||||||
|
"openfut-redirector-host v{} commit={} profile={} openssl={}",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
BUILD_COMMIT,
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
"debug"
|
||||||
|
} else {
|
||||||
|
"release"
|
||||||
|
},
|
||||||
|
tls::openssl_version(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// One line naming the binary, its linked TLS, and what it will advertise.
|
/// One line naming the binary, its linked TLS, and what it will advertise.
|
||||||
|
///
|
||||||
|
/// Machine-readable `key=value` so the launcher can extract the commit without
|
||||||
|
/// guessing at prose.
|
||||||
pub fn banner(cfg: &RedirectorConfig) -> String {
|
pub fn banner(cfg: &RedirectorConfig) -> String {
|
||||||
format!(
|
format!(
|
||||||
"openfut-redirector-host v{} openssl={} listen={} advertise={}:{} ciphers={}",
|
"openfut-redirector-host v{} commit={} profile={} openssl={} listen={} \
|
||||||
|
advertise={}:{} tls_min={:?} tls_max={:?} security_level={} ciphers={}",
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
BUILD_COMMIT,
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
"debug"
|
||||||
|
} else {
|
||||||
|
"release"
|
||||||
|
},
|
||||||
tls::openssl_version(),
|
tls::openssl_version(),
|
||||||
cfg.listen_on(),
|
cfg.listen_on(),
|
||||||
cfg.adapter.endpoints.advertise,
|
cfg.adapter.endpoints.advertise,
|
||||||
cfg.adapter.endpoints.blaze_port,
|
cfg.adapter.endpoints.blaze_port,
|
||||||
|
cfg.tls.min_version,
|
||||||
|
cfg.tls.max_version,
|
||||||
|
cfg.tls
|
||||||
|
.security_level
|
||||||
|
.map(|l| l.to_string())
|
||||||
|
.unwrap_or_else(|| "default".into()),
|
||||||
cfg.tls.cipher_list,
|
cfg.tls.cipher_list,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -108,15 +149,6 @@ pub fn bind(cfg: RedirectorConfig) -> std::io::Result<Server> {
|
|||||||
let listener = TcpListener::bind(cfg.listen_on())?;
|
let listener = TcpListener::bind(cfg.listen_on())?;
|
||||||
let local_addr = listener.local_addr()?;
|
let local_addr = listener.local_addr()?;
|
||||||
log(&banner(&cfg));
|
log(&banner(&cfg));
|
||||||
log(&format!(
|
|
||||||
"TLS min={:?} max={:?} security_level={}",
|
|
||||||
cfg.tls.min_version,
|
|
||||||
cfg.tls.max_version,
|
|
||||||
cfg.tls
|
|
||||||
.security_level
|
|
||||||
.map(|l| l.to_string())
|
|
||||||
.unwrap_or_else(|| "default (not lowered)".into())
|
|
||||||
));
|
|
||||||
Ok(Server {
|
Ok(Server {
|
||||||
local_addr,
|
local_addr,
|
||||||
listener,
|
listener,
|
||||||
@@ -218,6 +250,8 @@ mod tests {
|
|||||||
let cfg = RedirectorConfig::for_test("198.51.100.7");
|
let cfg = RedirectorConfig::for_test("198.51.100.7");
|
||||||
let b = banner(&cfg);
|
let b = banner(&cfg);
|
||||||
assert!(b.contains("openssl="), "{b}");
|
assert!(b.contains("openssl="), "{b}");
|
||||||
|
assert!(b.contains("commit="), "{b}");
|
||||||
|
assert!(b.contains("tls_min="), "{b}");
|
||||||
assert!(b.contains("198.51.100.7"), "{b}");
|
assert!(b.contains("198.51.100.7"), "{b}");
|
||||||
// The cipher list is part of the identity of a compatibility host.
|
// The cipher list is part of the identity of a compatibility host.
|
||||||
assert!(b.contains("AES256-GCM-SHA384"), "{b}");
|
assert!(b.contains("AES256-GCM-SHA384"), "{b}");
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
use openfut_redirector_host::{serve, RedirectorConfig};
|
use openfut_redirector_host::{serve, RedirectorConfig};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// Identity must be readable WITHOUT valid configuration: the launcher has
|
||||||
|
// to verify which commit a binary came from before deciding whether to run
|
||||||
|
// it at all, and refusing to reveal that until the environment is right
|
||||||
|
// would invert the check.
|
||||||
|
if std::env::args().any(|a| a == "--identity") {
|
||||||
|
println!("{}", openfut_redirector_host::identity());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let cfg = match RedirectorConfig::from_env() {
|
let cfg = match RedirectorConfig::from_env() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Is this binary trustworthy as gate evidence?
|
||||||
|
#
|
||||||
|
# verify-build-identity.sh <binary-commit>
|
||||||
|
#
|
||||||
|
# Two independent facts, both established HERE at launch rather than trusted
|
||||||
|
# from inside the binary:
|
||||||
|
#
|
||||||
|
# 1. the stamped commit equals the checkout's real HEAD
|
||||||
|
# 2. the migration crates have no uncommitted changes
|
||||||
|
#
|
||||||
|
# A compiled-in cleanliness flag cannot do this: cargo will not re-run a build
|
||||||
|
# script because another crate's source changed, so it can read "clean" for a
|
||||||
|
# binary built from edited sources. Verified on the Blaze host.
|
||||||
|
#
|
||||||
|
# REFUSES (exit non-zero) rather than warning. For a migration gate a warning on
|
||||||
|
# stderr is not a safeguard — it is something to scroll past.
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(dirname "$(readlink -f "$0")")/.."
|
||||||
|
|
||||||
|
STAMPED="${1:-}"
|
||||||
|
[[ -n "$STAMPED" ]] || { echo "usage: verify-build-identity.sh <binary-commit>" >&2; exit 2; }
|
||||||
|
|
||||||
|
CRATES=(openfut-protocol-blaze openfut-adapter-fifa17 openfut-host-config
|
||||||
|
openfut-blaze-host openfut-redirector-host)
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
HEAD_NOW="$(git rev-parse --short=7 HEAD 2>/dev/null || echo unknown)"
|
||||||
|
|
||||||
|
if [[ "$STAMPED" != "$HEAD_NOW" ]]; then
|
||||||
|
echo "REFUSING: binary was built from $STAMPED but HEAD is $HEAD_NOW" >&2
|
||||||
|
echo " Rebuild before treating this run as evidence." >&2
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DIRT="$(git status --porcelain --untracked-files=no -- "${CRATES[@]}" 2>/dev/null)"
|
||||||
|
if [[ -n "$DIRT" ]]; then
|
||||||
|
echo "REFUSING: migration crates have uncommitted changes:" >&2
|
||||||
|
sed 's/^/ /' <<<"$DIRT" >&2
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $rc -eq 0 ]]; then
|
||||||
|
echo "build identity OK: commit $STAMPED == HEAD, migration crates clean"
|
||||||
|
fi
|
||||||
|
exit $rc
|
||||||
Reference in New Issue
Block a user