Files
hammer/scripts/labnet.sh
T
sergioandClaude Opus 4.8 6886f810bd crates/netup: red mínima Rust-nativa (Etapa C pieza 5)
netup = binario hammer-propio (sync, sólo libc, sin tokio) que configura la red:
levanta el link (netlink RTM_NEWLINK), negocia un lease DHCPv4 a mano sobre UDP
(DISCOVER/OFFER/REQUEST/ACK con flag broadcast), y aplica IP + ruta default +
/etc/resolv.conf (RTM_NEWADDR/RTM_NEWROUTE). Hand-roll de netlink y DHCP porque
no hay cliente DHCP Rust "maduro" para adoptar al estilo ripgrep, y el workspace
es 100% sync (rtnetlink arrastraría tokio). Reemplaza el `ip` estático de busybox.

Validado in-VM contra el DHCP de QEMU slirp: ✓ lease 10.0.2.15 + ruta + DNS +
egress TCP. Autodetecta la NIC (primera no-loopback) y espera carrier por sysfs.

Infra de prueba:
- scripts/drive-netup.py: bootea la VM, corre netup y verifica lease/NAT/DNS.
  SLIRP=1 ⇒ red user-mode (cero setup de host); si no, tap del lab.
- scripts/labnet.sh: lab de LAN real opcional (tap directo + dnsmasq + NAT) para
  fidelidad de hardware; no requerido para validar netup.
- scripts/netdiag.sh: diagnóstico del camino DHCP del lab (tcpdump+nft+dnsmasq).

Lección: para validar el CÓDIGO conviene slirp primero (cero plomería de host);
la LAN real (firewall INPUT, sutilezas del bridge, perms) es fidelidad posterior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:20:22 -04:00

89 lines
4.3 KiB
Bash

#!/bin/sh
# labnet.sh — lab de red para probar `netup` (y luego OpenSSH) in-VM con DHCP/DNS/NAT REALES, sin poner
# la VM en la LAN física (el host sale por WiFi, que no se puede bridgear a L2).
#
# Modelo TAP DIRECTO (sin bridge): para UNA VM no hace falta bridge. El host pone su IP en el tap
# (192.168.100.1/24) y corre dnsmasq escuchando ahí; la VM y el host son pares L2 sobre el tap. Esto
# evita la sutileza del Linux bridge (broadcast entregado localmente desde un puerto con skb_iif =
# puerto, que el SO_BINDTODEVICE de dnsmasq al bridge no matchea ⇒ no recibía el DISCOVER).
#
# Da DHCP+DNS reales (dnsmasq) + salida a internet por NAT hacia el uplink. Ejercita exactamente los
# code paths de netup: netlink real, DHCP DISCOVER/OFFER/REQUEST/ACK reales, ruta + DNS reales.
#
# REQUIERE ROOT. Levantar: sudo sh scripts/labnet.sh up Bajar: sudo sh scripts/labnet.sh down
# El tap queda owned por $LAB_USER ⇒ QEMU (no-root) lo abre.
#
# Variables: TAP (def hmtap0) SUBNET (def 192.168.100) UPLINK (def wlan0)
# LAB_USER (dueño del tap, def $SUDO_USER) DNS_UP (upstream DNS, def 192.168.1.1)
set -eu
TAP="${TAP:-hmtap0}"
SUBNET="${SUBNET:-192.168.100}"
UPLINK="${UPLINK:-wlan0}"
LAB_USER="${LAB_USER:-${SUDO_USER:-root}}"
DNS_UP="${DNS_UP:-192.168.1.1}"
PIDF="/run/hmdnsmasq.pid"
[ "$(id -u)" = 0 ] || { echo "labnet: requiere root (sudo sh scripts/labnet.sh $*)" >&2; exit 1; }
fw_clean() { # quita las reglas "hmlab" que insertamos en el firewall del host
for ch in input forward; do
while h=$(nft -a list chain inet filter "$ch" 2>/dev/null | awk '/comment "hmlab"/{print $NF; exit}'); [ -n "${h:-}" ]; do
nft delete rule inet filter "$ch" handle "$h" 2>/dev/null || break
done
done
}
up() {
echo "==> tap $TAP ($SUBNET.1/24, owner $LAB_USER) — modelo tap directo, sin bridge"
# auto-limpieza de estado viejo (bridge de versiones previas + tap que pudo quedar enslaved)
ip link del hmbr0 2>/dev/null || true
ip link del "$TAP" 2>/dev/null || true
ip tuntap add dev "$TAP" mode tap user "$LAB_USER"
ip addr replace "$SUBNET.1/24" dev "$TAP"
ip link set "$TAP" up
echo "==> forwarding + NAT (nftables) $SUBNET.0/24 → $UPLINK"
sysctl -wq net.ipv4.ip_forward=1
nft list table ip hmnat >/dev/null 2>&1 || nft add table ip hmnat
nft flush table ip hmnat
nft add chain ip hmnat hmpost '{ type nat hook postrouting priority 100 ; }'
nft add rule ip hmnat hmpost ip saddr "$SUBNET.0/24" oifname "$UPLINK" masquerade
# El host tiene su PROPIO firewall (inet filter, policy drop en input/forward). Insertamos accepts
# ACOTADOS al tap del lab DENTRO de inet filter (al tope, vía `insert`), reversibles por su comment
# "hmlab". input: el DISCOVER llega a dnsmasq. forward: el NAT VM→internet pasa.
if nft list chain inet filter input >/dev/null 2>&1; then
echo "==> permitir $TAP en el firewall del host (inet filter input/forward)"
fw_clean
nft insert rule inet filter input iifname "$TAP" accept comment "hmlab"
nft insert rule inet filter forward iifname "$TAP" accept comment "hmlab"
nft insert rule inet filter forward oifname "$TAP" accept comment "hmlab"
fi
echo "==> dnsmasq (DHCP $SUBNET.50-150 + DNS→$DNS_UP) en $TAP"
[ -f "$PIDF" ] && kill "$(cat "$PIDF")" 2>/dev/null || true
dnsmasq --interface="$TAP" --bind-interfaces --except-interface=lo \
--dhcp-range="$SUBNET.50,$SUBNET.150,12h" --dhcp-authoritative \
--dhcp-option=3,"$SUBNET.1" --dhcp-option=6,"$SUBNET.1" \
--listen-address="$SUBNET.1" --no-resolv --server="$DNS_UP" \
--dhcp-leasefile=/tmp/hmdnsmasq.leases \
--pid-file="$PIDF" --log-dhcp --log-facility=/tmp/hmdnsmasq.log
chmod 644 /tmp/hmdnsmasq.log /tmp/hmdnsmasq.leases 2>/dev/null || true
echo "==> listo. QEMU: -netdev tap,id=n0,ifname=$TAP,script=no,downscript=no -device e1000,netdev=n0"
}
down() {
echo "==> teardown"
[ -f "$PIDF" ] && kill "$(cat "$PIDF")" 2>/dev/null && rm -f "$PIDF" || true
nft delete table ip hmnat 2>/dev/null || true
fw_clean
ip link del "$TAP" 2>/dev/null || true
echo "==> red del lab desmontada"
}
case "${1:-}" in
up) up ;;
down) down ;;
*) echo "uso: sudo sh scripts/labnet.sh {up|down}" >&2; exit 1 ;;
esac