diff --git a/scripts/ssh-e2e-test.sh b/scripts/ssh-e2e-test.sh new file mode 100755 index 00000000..52cd63b4 --- /dev/null +++ b/scripts/ssh-e2e-test.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash +# ssh-e2e-test.sh — SPIKE de validación (sucio, temporal) del Paso 1: probar el handshake SSH REAL +# contra un hammer booteado en QEMU, con sshd supervisado por arje-zero como genesis card. +# +# Esto NO es plomería de producto: arma un `work/rootfs-ssh-test` desechable = el 4/4 limpio del +# store + openssh + netup (todos PRECOMPILADOS de la caché del lab, no rebuild) + una seed con un +# card `sshd`, y lo bootea como initramfs. La red es QEMU slirp con hostfwd :2222->:22, así que el +# host hace `ssh -p 2222 root@localhost` SIN tocar firewall/bridge (la lección de netup). El chroot +# de privsep —que el bwrap anidado no pudo— acá corre como root REAL del guest. +# +# Quema tres incógnitas que el sandbox de bwrap dejó abiertas: +# 1. el handshake completo (post-chroot, post-setuid sshd-user) ocurre, +# 2. arje maneja el ciclo de vida del daemon (Restart) como cualquier card Native, +# 3. los fds de red de sshd no segfaultean contra el musl del target. +# +# Variables: KERNEL (def store/*-linux/boot/bzImage) MEM (def 2048) KVM (1 si /dev/kvm) +# PORT (hostfwd, def 2222) DEADLINE (def 150s) +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}" +MEM="${MEM:-2048}" +KVM="${KVM:-1}" +PORT="${PORT:-2222}" +DEADLINE="${DEADLINE:-150}" +WORK="$ROOT/work/ssh-e2e" +RFS="$ROOT/work/rootfs-ssh-test" +LOG="$WORK/console.log" + +pick() { ls -d store/*-"$1" 2>/dev/null | head -1; } +MUSL=$(pick musl); BB=$(pick busybox); ARJE="store/69e2cab8ac00c33cde7680eb393952e2c7f9ae9af24c180c6593d7a86482efb9-arje-zero" +NETUP=$(pick netup); OSSH=$(pick openssh) +for v in KERNEL MUSL BB ARJE NETUP OSSH; do + p="${!v}"; [ -n "$p" ] && [ -e "$p" ] || { echo "falta $v ($p)"; exit 1; } +done +echo "==> kernel $KERNEL" +echo "==> componentes: musl busybox arje-zero netup openssh (precompilados del store)" + +# ── 1) ensamblar el rootfs desechable ─────────────────────────────────────────────────────────── +rm -rf "$RFS" "$WORK"; mkdir -p "$RFS" "$WORK" +for src in "$MUSL" "$BB" "$ARJE" "$NETUP" "$OSSH"; do + cp -a "$src"/. "$RFS"/ 2>/dev/null || true +done +chmod -R u+w "$RFS" # el store está sellado read-only; el staging debe ser escribible +rm -rf "$RFS/.hammer" +mkdir -p "$RFS"/{proc,sys,dev,dev/pts,run,tmp,etc,etc/ssh,root,root/.ssh,var,var/empty,ente,sbin,bin,usr/bin} +chmod 0711 "$RFS/var/empty" +ln -sf /usr/bin/arje-zero "$RFS/sbin/init" +# busybox a /bin/sh + applets que la getty/seed necesitan (por si el cp no dejó symlinks en /bin) +[ -e "$RFS/bin/busybox" ] || cp -a "$BB"/bin/busybox "$RFS/bin/busybox" +for a in sh getty; do ln -sf busybox "$RFS/bin/$a"; done + +# usuarios: root (login por llave) + sshd (privsep). shells reales para que sshd ejecute el comando. +cat > "$RFS/etc/passwd" <<'EOF' +root:x:0:0:root:/root:/bin/sh +sshd:x:74:74:sshd privsep:/var/empty:/bin/false +EOF +cat > "$RFS/etc/group" <<'EOF' +root:x:0: +sshd:x:74: +EOF +cat > "$RFS/etc/ssh/sshd_config" <<'EOF' +Port 22 +ListenAddress 0.0.0.0 +PermitRootLogin yes +PubkeyAuthentication yes +PasswordAuthentication no +AuthorizedKeysFile /root/.ssh/authorized_keys +StrictModes no +Subsystem sftp /usr/libexec/sftp-server +EOF + +# ── 2) llave de cliente desechable (host) → authorized_keys del guest ──────────────────────────── +"$OSSH/usr/bin/ssh-keygen" -t ed25519 -N '' -f "$WORK/clientkey" -q +cat "$WORK/clientkey.pub" > "$RFS/root/.ssh/authorized_keys" +chmod 0600 "$RFS/root/.ssh/authorized_keys" + +# ── 3) seed card: console-getty + un card `sshd` (Native, Restart) ─────────────────────────────── +# El card sshd arranca la red (netup, DHCP slirp), genera host keys (ssh-keygen -A) y exec sshd -D. +# Mismo esquema validado de STAGE1_SEED_CARD (hammer-bootstrap); ULIDs nuevos A10/A11/A12. +SSHD_CMD='/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e' +cat > "$RFS/ente/seed.card.json" </dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz" +echo "==> initramfs $(du -h "$WORK/initramfs.cpio.gz" | cut -f1)" + +# ── 5) boot QEMU (slirp + hostfwd) en background ───────────────────────────────────────────────── +accel=(-cpu Broadwell) +if [ "$KVM" = 1 ] && [ -w /dev/kvm ]; then accel=(-enable-kvm -cpu host); fi +echo "==> boot QEMU (mem ${MEM}, kvm $([ "${accel[0]}" = -enable-kvm ] && echo sí || echo no)), hostfwd :$PORT->:22" +qemu-system-x86_64 -m "$MEM" -no-reboot -nographic "${accel[@]}" \ + -kernel "$KERNEL" -initrd "$WORK/initramfs.cpio.gz" \ + -append "console=ttyS0 rdinit=/sbin/init" \ + -netdev "user,id=n0,hostfwd=tcp::$PORT-:22" -device e1000,netdev=n0 \ + > "$LOG" 2>&1 & +QPID=$! +trap 'kill $QPID 2>/dev/null || true' EXIT + +# ── 6) esperar a sshd y hacer el handshake real ────────────────────────────────────────────────── +SSHOPTS=(-p "$PORT" -i "$WORK/clientkey" -o StrictHostKeyChecking=no + -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null + -o PasswordAuthentication=no -o ConnectTimeout=4 -o LogLevel=ERROR) +echo "==> esperando handshake (deadline ${DEADLINE}s)…" +start=$(date +%s); ok=0 +while [ $(( $(date +%s) - start )) -lt "$DEADLINE" ]; do + if ! kill -0 $QPID 2>/dev/null; then echo "!! QEMU murió"; break; fi + if out=$(ssh "${SSHOPTS[@]}" root@localhost \ + 'echo HANDSHAKE_OK uid=$(id -u) user=$(id -un); uname -srm; cat /etc/os-release 2>/dev/null | head -1' 2>/dev/null); then + echo "================ RESPUESTA DEL GUEST ================" + echo "$out" + echo "====================================================" + echo "$out" | grep -q HANDSHAKE_OK && ok=1 + break + fi + sleep 3 +done + +# ── 7) veredicto + diagnóstico ─────────────────────────────────────────────────────────────────── +echo "---- consola (cola) ----"; tail -25 "$LOG" 2>/dev/null +if [ "$ok" = 1 ]; then + echo; echo "*** SSH E2E OK — handshake real contra hammer+arje, sshd como card ***"; exit 0 +else + echo; echo "*** SSH E2E FALLÓ — ver $LOG ***"; exit 1 +fi