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>
89 lines
4.3 KiB
Bash
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
|