From c03702707bd7054945835af268ae22f359bc46eb Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 11 Aug 2026 03:46:07 +0000 Subject: [PATCH] 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) --- openfut-redirector-host/build.rs | 39 +++++++++ openfut-redirector-host/redirector.sh | 111 ++++++++++++++++++++++++++ openfut-redirector-host/src/lib.rs | 54 ++++++++++--- openfut-redirector-host/src/main.rs | 9 +++ scripts/verify-build-identity.sh | 46 +++++++++++ 5 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 openfut-redirector-host/build.rs create mode 100755 openfut-redirector-host/redirector.sh create mode 100755 scripts/verify-build-identity.sh diff --git a/openfut-redirector-host/build.rs b/openfut-redirector-host/build.rs new file mode 100644 index 0000000..774168b --- /dev/null +++ b/openfut-redirector-host/build.rs @@ -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 { + 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/, 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}"); + } + } +} diff --git a/openfut-redirector-host/redirector.sh b/openfut-redirector-host/redirector.sh new file mode 100755 index 0000000..8f686cd --- /dev/null +++ b/openfut-redirector-host/redirector.sh @@ -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=` 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 diff --git a/openfut-redirector-host/src/lib.rs b/openfut-redirector-host/src/lib.rs index be33799..daad12b 100644 --- a/openfut-redirector-host/src/lib.rs +++ b/openfut-redirector-host/src/lib.rs @@ -51,15 +51,56 @@ fn log(msg: &str) { 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. +/// +/// Machine-readable `key=value` so the launcher can extract the commit without +/// guessing at prose. pub fn banner(cfg: &RedirectorConfig) -> String { 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"), + BUILD_COMMIT, + if cfg!(debug_assertions) { + "debug" + } else { + "release" + }, tls::openssl_version(), cfg.listen_on(), cfg.adapter.endpoints.advertise, 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, ) } @@ -108,15 +149,6 @@ pub fn bind(cfg: RedirectorConfig) -> std::io::Result { let listener = TcpListener::bind(cfg.listen_on())?; let local_addr = listener.local_addr()?; 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 { local_addr, listener, @@ -218,6 +250,8 @@ mod tests { let cfg = RedirectorConfig::for_test("198.51.100.7"); let b = banner(&cfg); assert!(b.contains("openssl="), "{b}"); + assert!(b.contains("commit="), "{b}"); + assert!(b.contains("tls_min="), "{b}"); assert!(b.contains("198.51.100.7"), "{b}"); // The cipher list is part of the identity of a compatibility host. assert!(b.contains("AES256-GCM-SHA384"), "{b}"); diff --git a/openfut-redirector-host/src/main.rs b/openfut-redirector-host/src/main.rs index c6caeef..6a8e450 100644 --- a/openfut-redirector-host/src/main.rs +++ b/openfut-redirector-host/src/main.rs @@ -3,6 +3,15 @@ use openfut_redirector_host::{serve, RedirectorConfig}; 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() { Ok(c) => c, Err(e) => { diff --git a/scripts/verify-build-identity.sh b/scripts/verify-build-identity.sh new file mode 100755 index 0000000..957ea56 --- /dev/null +++ b/scripts/verify-build-identity.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Is this binary trustworthy as gate evidence? +# +# verify-build-identity.sh +# +# 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 " >&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