#!/usr/bin/env bash
# Runs ON debian1, against the disposable danger-lab VM — never against a real box.
# Reproduces the debian2 loss inside the VM and proves the deadman survives it.
set -uo pipefail

LAB=/home/user/dangerlab
DEADMAN="${1:?usage: deadman-lab-proof.sh /path/to/buildbox-deadman}"
[ -r "$DEADMAN" ] || { echo "no such deadman: $DEADMAN" >&2; exit 2; }
# shellcheck source=/dev/null
source "$LAB/dangerlab-lib.sh"

pass=0; fail=0
ok()  { pass=$((pass+1)); printf 'ok   %s\n' "$1"; }
bad() { fail=$((fail+1)); printf 'FAIL %s\n' "$1"; }
g()   { lab_ssh "$@"; }

provision_guest() {
  g 'sudo -n true' || die "guest has no passwordless sudo"
  lab_scp "$DEADMAN" /tmp/buildbox-deadman || die "deadman copy failed"
  g 'sudo -n install -m 0755 /tmp/buildbox-deadman /usr/local/sbin/buildbox-deadman'
  g 'sudo -n mkdir -p /etc/ssh/sshd_config.d && echo "Port 22" | sudo -n tee /etc/ssh/sshd_config.d/60-lab.conf >/dev/null'
}

# Each scenario reboots the guest and waits out a deadman deadline, so the whole proof
# runs inside one slot with a budget wide enough that the reaper leaves it alone.
lab_acquire 1800
provision_guest

fresh_vm() { lab_reset; provision_guest; }

# The exact shape of the change that took debian2 off the network: a new file binding an
# address that does not exist at cold boot, and the removal of the only all-interfaces door.
# Not reloaded: on debian2 this change was harmless while sshd kept running, which is why
# nothing caught it until the box was powered off hours later.
apply_bricking_change() {
  g 'echo "ListenAddress 100.64.99.99" | sudo -n tee /etc/ssh/sshd_config.d/10-tailscale.conf >/dev/null
     sudo -n rm -f /etc/ssh/sshd_config.d/60-lab.conf' >/dev/null 2>&1
}

echo "=== scenario A: a change that does survive a reboot is cancellable, and only after the reboot ==="
fresh_vm
BID_BEFORE="$(g 'cat /proc/sys/kernel/random/boot_id')"
g 'sudo -n /usr/local/sbin/buildbox-deadman arm --id proofa --deadline-sec 900' >/dev/null \
  || bad "arm"
g 'echo "# harmless" | sudo -n tee /etc/sysctl.d/99-proof.conf >/dev/null'

if g 'sudo -n /usr/local/sbin/buildbox-deadman disarm proofa' >/dev/null 2>&1; then
  bad "disarm must refuse while the boot id is unchanged"
else
  ok "disarm refused before any reboot (a live-validated change proves nothing)"
fi

g 'sudo -n systemctl reboot' >/dev/null 2>&1
# ssh answering is not proof of a boot: the guest keeps accepting on the way down.
BID_AFTER=""
A_DEADLINE=$(( $(date +%s) + 300 ))
while [ "$(date +%s)" -lt "$A_DEADLINE" ]; do
  BID_AFTER="$(g 'cat /proc/sys/kernel/random/boot_id' 2>/dev/null | tr -d '\r\n')"
  [ -n "$BID_AFTER" ] && [ "$BID_AFTER" != "$BID_BEFORE" ] && break
  sleep 5
done
[ -n "$BID_AFTER" ] || die "guest never came back from the reboot in scenario A"
[ "$BID_AFTER" != "$BID_BEFORE" ] && ok "boot id changed across the reboot" || bad "boot id unchanged"
g 'sudo -n /usr/local/sbin/buildbox-deadman disarm proofa' >/dev/null 2>&1 \
  && ok "disarm accepted once the host had actually rebooted" || bad "disarm after reboot"
g 'test -f /etc/sysctl.d/99-proof.conf' && ok "a proven change is kept, not reverted" || bad "proven change was reverted"
g 'systemctl is-enabled buildbox-deadman.timer' >/dev/null 2>&1 \
  && bad "timer still enabled after disarm" || ok "timer disarmed"

echo
echo "=== scenario B: the debian2 change, unattended — deadman restores the host ==="
fresh_vm
g 'sudo -n /usr/local/sbin/buildbox-deadman arm --id proofb --deadline-sec 150' >/dev/null || bad "arm"
apply_bricking_change
g 'test -f /etc/ssh/sshd_config.d/10-tailscale.conf' && ok "bricking change is in place" || bad "change not applied"
g 'sudo -n systemctl reboot' >/dev/null 2>&1
sleep 45

if lab_ssh true 2>/dev/null; then
  bad "guest was still reachable after the bricking reboot — the reproduction did not brick it"
else
  ok "guest is unreachable after the reboot, exactly as debian2 was"
fi

if lab_wait_ssh 300; then
  ok "guest came back on its own — the deadman restored last-known-good"
else
  bad "guest never came back; the deadman did not recover it"
fi
g 'test ! -f /etc/ssh/sshd_config.d/10-tailscale.conf' && ok "the added file is gone" || bad "added file survived the revert"
g 'test -f /etc/ssh/sshd_config.d/60-lab.conf' && ok "the deleted door is back" || bad "deleted file was not restored"
g 'systemctl is-active ssh' >/dev/null 2>&1 && ok "ssh is running again" || bad "ssh not active"

echo
echo "=== scenario C: a change that survived a reboot is cancelled at the deadline, never reverted ==="
fresh_vm
BID_BEFORE="$(g 'cat /proc/sys/kernel/random/boot_id')"
g 'sudo -n /usr/local/sbin/buildbox-deadman arm --id proofc --deadline-sec 60' >/dev/null || bad "arm"
g 'sudo -n /usr/local/sbin/buildbox-deadman status' | grep -q 'snapshotHealth=healthy' \
  && ok "a host serving a door records a healthy snapshot" || bad "healthy host recorded as unhealthy"
g 'echo "# harmless" | sudo -n tee /etc/sysctl.d/99-proofc.conf >/dev/null'
g 'sudo -n systemctl reboot' >/dev/null 2>&1
sleep 20
lab_wait_ssh 300 || die "guest never came back in scenario C"
# Past the deadline with nobody disarming: the operator crashed, went home, lost the session.
sleep 90
g 'test -f /etc/sysctl.d/99-proofc.conf' \
  && ok "the change is kept after the deadline passed on a rebooted, serving host" \
  || bad "deadman reverted a change the host had already proven"
g 'sudo -n test ! -f /var/lib/buildbox-deadman/state.json' && ok "deadman stood down" || bad "still armed after cancel"
g 'systemctl is-enabled buildbox-deadman.timer' >/dev/null 2>&1 \
  && bad "timer still enabled after cancel" || ok "timer disabled after cancel"

echo
echo "=== scenario D: a snapshot taken from a host with no door is refused, not restored ==="
fresh_vm
# Arm detached: taking the door down takes this ssh session with it, and the point is to
# capture a snapshot of a host that was already broken when it was taken.
g 'sudo -n systemd-run --collect --unit proofd-arm bash -c "systemctl stop ssh.socket ssh; sleep 2; /usr/local/sbin/buildbox-deadman arm --id proofd --deadline-sec 60; systemctl start ssh.socket || systemctl start ssh"' >/dev/null 2>&1
# The door is still up for the first couple of seconds, so waiting immediately proves
# nothing — wait out the window the arm runs in, then wait for the door to come back.
sleep 25
lab_wait_ssh 180 || die "guest never came back after the detached arm in scenario D"
g 'sudo -n /usr/local/sbin/buildbox-deadman status' | grep -q 'snapshotHealth=unhealthy' \
  && ok "a snapshot taken with no door is recorded unhealthy" || bad "broken host recorded as healthy"
g 'echo "# harmless" | sudo -n tee /etc/sysctl.d/99-proofd.conf >/dev/null'
# No reboot, so the survived-reboot cancel cannot fire: the health verdict is what decides.
sleep 100
g 'sudo -n test -f /var/lib/buildbox-deadman/refused-proofd.json' \
  && ok "the unhealthy snapshot was refused" || bad "deadman restored a snapshot taken from a broken host"
g 'test -f /etc/sysctl.d/99-proofd.conf' \
  && ok "the live state was left alone rather than replaced by a known-bad one" || bad "refused but reverted anyway"
g 'systemctl is-active ssh' >/dev/null 2>&1 && ok "the door is still served after the refusal" || bad "no door after refusal"

echo
echo "=== scenario E: a snapshot that no longer lists a guarded path is refused, not restored ==="
fresh_vm
g 'sudo -n /usr/local/sbin/buildbox-deadman arm --id proofe --deadline-sec 60' >/dev/null || bad "arm"
# Truncating the archive is how a snapshot goes bad in the field — a full disk or a crash
# mid-write. Extraction would then yield nothing, and the swap reads "absent" as "delete it".
g 'sudo -n bash -c "head -c 200 /var/lib/buildbox-deadman/lkg-proofe.tar > /tmp/t && cat /tmp/t > /var/lib/buildbox-deadman/lkg-proofe.tar && rm -f /tmp/t"' \
  || bad "could not truncate the snapshot"
g 'echo "# harmless" | sudo -n tee /etc/sysctl.d/99-proofe.conf >/dev/null'
sleep 100
g 'sudo -n test -f /var/lib/buildbox-deadman/refused-proofe.json' \
  && ok "the truncated snapshot was refused" || bad "deadman restored from a truncated snapshot"
g 'test -d /etc/ssh' && ok "/etc/ssh was not deleted by an empty extraction" || bad "/etc/ssh was removed"
g 'systemctl is-active ssh' >/dev/null 2>&1 && ok "the door is still served after the refusal" || bad "no door after refusal"

# The clone and its overlay are discarded by lab_release on exit, however this exits.
printf '\n%d passed, %d failed\n' "$pass" "$fail"
[ "$fail" = 0 ]
