sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
1#!/usr/bin/env bash
2# ssh-e2e-test.sh — SPIKE de validación (sucio, temporal) del Paso 1: probar el handshake SSH REAL
3# contra un takana booteado en QEMU, con sshd supervisado por arje-zero como genesis card.
4#
5# Esto NO es plomería de producto: arma un `work/rootfs-ssh-test` desechable = el 4/4 limpio del
6# store + openssh + netup (todos PRECOMPILADOS de la caché del lab, no rebuild) + una seed con un
7# card `sshd`, y lo bootea como initramfs. La red es QEMU slirp con hostfwd :2222->:22, así que el
8# host hace `ssh -p 2222 root@localhost` SIN tocar firewall/bridge (la lección de netup). El chroot
9# de privsep —que el bwrap anidado no pudo— acá corre como root REAL del guest.
10#
11# Quema tres incógnitas que el sandbox de bwrap dejó abiertas:
12# 1. el handshake completo (post-chroot, post-setuid sshd-user) ocurre,
13# 2. arje maneja el ciclo de vida del daemon (Restart) como cualquier card Native,
14# 3. los fds de red de sshd no segfaultean contra el musl del target.
15#
16# Variables: KERNEL (def store/*-linux/boot/bzImage) MEM (def 2048) KVM (1 si /dev/kvm)
17# PORT (hostfwd, def 2222) DEADLINE (def 150s)
18set -euo pipefail
19ROOT="$(cd "$(dirname "$0")/.." && pwd)"
20cd "$ROOT"
21
22KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}"
23MEM="${MEM:-2048}"
24KVM="${KVM:-1}"
25PORT="${PORT:-2222}"
26DEADLINE="${DEADLINE:-150}"
27WORK="$ROOT/work/ssh-e2e"
28RFS="$ROOT/work/rootfs-ssh-test"
29LOG="$WORK/console.log"
30
31pick() { ls -d store/*-"$1" 2>/dev/null | head -1; }
32MUSL=$(pick musl); BB=$(pick busybox); ARJE="store/69e2cab8ac00c33cde7680eb393952e2c7f9ae9af24c180c6593d7a86482efb9-arje-zero"
33NETUP=$(pick netup); OSSH=$(pick openssh)
34for v in KERNEL MUSL BB ARJE NETUP OSSH; do
35 p="${!v}"; [ -n "$p" ] && [ -e "$p" ] || { echo "falta $v ($p)"; exit 1; }
36done
37echo "==> kernel $KERNEL"
38echo "==> componentes: musl busybox arje-zero netup openssh (precompilados del store)"
39
40# ── 1) ensamblar el rootfs desechable ───────────────────────────────────────────────────────────
41rm -rf "$RFS" "$WORK"; mkdir -p "$RFS" "$WORK"
42for src in "$MUSL" "$BB" "$ARJE" "$NETUP" "$OSSH"; do
43 cp -a "$src"/. "$RFS"/ 2>/dev/null || true
44done
45chmod -R u+w "$RFS" # el store está sellado read-only; el staging debe ser escribible
46rm -rf "$RFS/.hammer"
47mkdir -p "$RFS"/{proc,sys,dev,dev/pts,run,tmp,etc,etc/ssh,root,root/.ssh,var,var/empty,ente,sbin,bin,usr/bin}
48chmod 0711 "$RFS/var/empty"
49ln -sf /usr/bin/arje-zero "$RFS/sbin/init"
50# busybox a /bin/sh + applets que la getty/seed necesitan (por si el cp no dejó symlinks en /bin)
51[ -e "$RFS/bin/busybox" ] || cp -a "$BB"/bin/busybox "$RFS/bin/busybox"
52for a in sh getty; do ln -sf busybox "$RFS/bin/$a"; done
53
54# usuarios: root (login por llave) + sshd (privsep). shells reales para que sshd ejecute el comando.
55cat > "$RFS/etc/passwd" <<'EOF'
56root:x:0:0:root:/root:/bin/sh
57sshd:x:74:74:sshd privsep:/var/empty:/bin/false
58EOF
59cat > "$RFS/etc/group" <<'EOF'
60root:x:0:
61sshd:x:74:
62EOF
63cat > "$RFS/etc/ssh/sshd_config" <<'EOF'
64Port 22
65ListenAddress 0.0.0.0
66PermitRootLogin yes
67PubkeyAuthentication yes
68PasswordAuthentication no
69AuthorizedKeysFile /root/.ssh/authorized_keys
70StrictModes no
71Subsystem sftp /usr/libexec/sftp-server
72EOF
73
74# ── 2) llave de cliente desechable (host) → authorized_keys del guest ────────────────────────────
75"$OSSH/usr/bin/ssh-keygen" -t ed25519 -N '' -f "$WORK/clientkey" -q
76cat "$WORK/clientkey.pub" > "$RFS/root/.ssh/authorized_keys"
77chmod 0600 "$RFS/root/.ssh/authorized_keys"
78
79# ── 3) seed card: console-getty + un card `sshd` (Native, Restart) ───────────────────────────────
80# El card sshd arranca la red (netup, DHCP slirp), genera host keys (ssh-keygen -A) y exec sshd -D.
81# Mismo esquema validado de STAGE1_SEED_CARD (takana-bootstrap); ULIDs nuevos A10/A11/A12.
82SSHD_CMD='/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e'
83cat > "$RFS/ente/seed.card.json" <<EOF
84{
85 "schema_version": 1,
86 "id": "01HQAR53D4M2NBV8KZTYXFQA10",
87 "lineage": null,
88 "label": "hammer-ssh-spike",
89 "provides": ["Spawn", "Journal"],
90 "requires": [],
91 "permissions": { "networking": "full", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
92 "soma": {
93 "namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
94 "rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
95 "cgroup": { "path": "arje.slice/hammer", "cpu_weight": null, "io_weight": null },
96 "cpu_affinity": null
97 },
98 "payload": "Virtual",
99 "supervision": "OneShot",
100 "lifecycle": "daemon",
101 "priority": "normal",
102 "flow": { "input": [], "output": [] },
103 "genesis": [
104 {
105 "schema_version": 1,
106 "id": "01HQAR53D4M2NBV8KZTYXFQA11",
107 "lineage": null,
108 "label": "console-getty",
109 "provides": [],
110 "requires": [],
111 "permissions": { "networking": "none", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
112 "soma": {
113 "namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
114 "rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
115 "cgroup": { "path": "arje.slice/getty", "cpu_weight": null, "io_weight": null },
116 "cpu_affinity": null
117 },
118 "payload": { "Native": { "exec": "/bin/busybox", "argv": ["getty", "-n", "-l", "/bin/sh", "115200", "console"], "envp": [] }},
119 "supervision": { "Restart": { "initial": 200, "max": 10000 } },
120 "lifecycle": "daemon",
121 "priority": "normal",
122 "flow": { "input": [], "output": [] },
123 "genesis": []
124 },
125 {
126 "schema_version": 1,
127 "id": "01HQAR53D4M2NBV8KZTYXFQA12",
128 "lineage": null,
129 "label": "sshd",
130 "provides": [],
131 "requires": [],
132 "permissions": { "networking": "full", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
133 "soma": {
134 "namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
135 "rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
136 "cgroup": { "path": "arje.slice/sshd", "cpu_weight": null, "io_weight": null },
137 "cpu_affinity": null
138 },
139 "payload": { "Native": { "exec": "/bin/busybox", "argv": ["sh", "-c", "${SSHD_CMD}"], "envp": [["HOME", "/root"]] }},
140 "supervision": { "Restart": { "initial": 500, "max": 20000 } },
141 "lifecycle": "daemon",
142 "priority": "normal",
143 "flow": { "input": [], "output": [] },
144 "genesis": []
145 }
146 ]
147}
148EOF
149
150# ── 4) empaquetar initramfs ──────────────────────────────────────────────────────────────────────
151# -R 0:0: el cpio corre como uid no-root; sin esto el rootfs queda con nuestro uid y sshd rechaza
152# /var/empty ("must be owned by root"). Reasigna todo a root:root dentro del archivo.
153( cd "$RFS" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz"
154echo "==> initramfs $(du -h "$WORK/initramfs.cpio.gz" | cut -f1)"
155
156# ── 5) boot QEMU (slirp + hostfwd) en background ─────────────────────────────────────────────────
157accel=(-cpu Broadwell)
158if [ "$KVM" = 1 ] && [ -w /dev/kvm ]; then accel=(-enable-kvm -cpu host); fi
159echo "==> boot QEMU (mem ${MEM}, kvm $([ "${accel[0]}" = -enable-kvm ] && echo sí || echo no)), hostfwd :$PORT->:22"
160qemu-system-x86_64 -m "$MEM" -no-reboot -nographic "${accel[@]}" \
161 -kernel "$KERNEL" -initrd "$WORK/initramfs.cpio.gz" \
162 -append "console=ttyS0 rdinit=/sbin/init" \
163 -netdev "user,id=n0,hostfwd=tcp::$PORT-:22" -device e1000,netdev=n0 \
164 > "$LOG" 2>&1 &
165QPID=$!
166trap 'kill $QPID 2>/dev/null || true' EXIT
167
168# ── 6) esperar a sshd y hacer el handshake real ──────────────────────────────────────────────────
169SSHOPTS=(-p "$PORT" -i "$WORK/clientkey" -o StrictHostKeyChecking=no
170 -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null
171 -o PasswordAuthentication=no -o ConnectTimeout=4 -o LogLevel=ERROR)
172echo "==> esperando handshake (deadline ${DEADLINE}s)…"
173start=$(date +%s); ok=0
174while [ $(( $(date +%s) - start )) -lt "$DEADLINE" ]; do
175 if ! kill -0 $QPID 2>/dev/null; then echo "!! QEMU murió"; break; fi
176 if out=$(ssh "${SSHOPTS[@]}" root@localhost \
177 'echo HANDSHAKE_OK uid=$(id -u) user=$(id -un); uname -srm; cat /etc/os-release 2>/dev/null | head -1' 2>/dev/null); then
178 echo "================ RESPUESTA DEL GUEST ================"
179 echo "$out"
180 echo "===================================================="
181 echo "$out" | grep -q HANDSHAKE_OK && ok=1
182 break
183 fi
184 sleep 3
185done
186
187# ── 7) veredicto + diagnóstico ───────────────────────────────────────────────────────────────────
188echo "---- consola (cola) ----"; tail -25 "$LOG" 2>/dev/null
189if [ "$ok" = 1 ]; then
190 echo; echo "*** SSH E2E OK — handshake real contra hammer+arje, sshd como card ***"; exit 0
191else
192 echo; echo "*** SSH E2E FALLÓ — ver $LOG ***"; exit 1
193fi