Confieso que soy muy fan de los sistemas de arranque en red, que me parecen verdaderamente versátiles para instanciar máquinas -físicas o virtuales- o como método de arranque de sistemas de recovery ante contingencias que puedan ocurrir. Es cierto que a día de hoy los pendrive USB dan mucha versatilidad para hacer arranques de instalaciones de sistema operativo o sistemas de recuperación, pero la ubicuidad de la red y no necesitar ningún elemento adicional siempre han tenido para servidor bastante atractivo.

Siempre he querido que a través de un sistema de arranque de red se puedan iniciar los siguientes elementos:

  • Instalación de sistemas operativos Windows.
  • Las Microsoft Diagnosticts and Recovery Toolset, más conocido como DaRT.
  • Instalación de sistemas operativos GNU/Linux.
  • Entornos de recuperación de desastres basados en Linux.

¿Cómo funciona un arranque de red?

A lo largo de la historia ha habido varias formas de arrancar equipos informáticos a través de la red, pero lo que se convirtió en estandar es el PXE, Preboot eXecution Environment, una arquitectura cliente-servidor elaborada inicialmente por Intel que implica el uso de los protocolos DHCP y TFTP. A groso modo funciona así:

  1. Un equipo informático que tenga PXE en ROM arranca un cliente DHCP para obtener dirección IP, en la solicitud se identifica como cliente PXE.
  2. Un servidor DHCP de la red le otorga una dirección IP y además mediante la opción DHCP 66 le indica a qué IP debe conectarse por TFTP y a través de la opción DHCP 67 le indica qué archivo debe solicitar. También podría pasarle la opción DHCP 60 e indicarle que hable directamente con un servidor PXE.
  3. El equipo informático descarga el programa por protocolo TFP y lo ejecuta.

El programa que descarguemos por TFTP acostumbra a ser un pequeño binario de arranque que su vez nos conectará por red a otros sistemas para continuar el proceso.

Cómo implementaba el arranque de red hasta la fecha

Para todo ello, siempre me he basado en los Windows Deployment Services (WDS), el sistema de arranque en red de Microsoft para implementaciones de Windows. WDS es fácil y rápido de poner a punto, así como un compañero ideal del Microsoft Deployment Toolkit. Mediante PXELinux es viable utilizar este sistema para instalar Windows y GNU/Linux por igual.

A continuación la captura de pantalla de un menú de arranque con WDS:

Un nuevo problema: arranque EFI

Los nuevos tiempos introducen una complejidad adicional: los arranques basados en EFI y los basados en la clásica BIOS de los IBM PC compatibles, que de momento e idealmente deben convivir juntos. A día de hoy, WDS soporta implementar Windows tanto por EFI como por BIOS, pero... en el caso del truco para que también implemente Linux, de momento no hay binario EFI que se consiga arrancar mediante WDS.

Buscando una forma de subsanar el problema es como dí con iPXE...

iPXE, un poderoso firmware de arranque en red y código abierto

iPXE es un firmware de arranque de red que desborda potencia. Está concedibo para reemplazar la ROM PXE de nuestra tarjeta de red pero... si no os queréis embarcar en esa aventura funciona igualmente bien haciendo chainload desde un arranque PXE estándar; por lo que no necesitamos jugárnosla con un flasheo.

¿Por qué es un firmware potente? Fundamentalmente por lo siguiente:

  • Soporta EFI y BIOS con o sin bypass de UNDI.
  • Puede arrancar desde una interfaz WiFi (!)
  • Puede arrancar a través de Internet u otras redes WAN (!)
  • Puede arrancar utilizando multitud de protocolos: TFTP, HTTP(S), iSCSI, Fibre Channel, Infiniband...
  • Tiene un sistema de scripting razonablemente flexible.
  • Combinado con wimboot es capaz de arrancar imágenes WIM de Windows PE, paradójicamente mucho más rápido de lo que lo hace WDS.
  • Puede hacer chainload de otro sistema de arranque, incluso del propio WDS.

Con todas estas características definitivamente se me ocurría que podía implementar un sistema de arranque de red versátil en mi entorno, cuya arquitectura a groso modo:

  • iPXE como sistema de arranque en red. Él mismo podrá iniciar cualquier entorno no-Microsoft y Microsoft con la ayuda del mencionado wimboot.
  • Como alternativa oficial al wimboot, también podrá hacer chainload con la implementación de WDS existente en mi red, de forma que ambos sistemas convivan.

¿Cómo vamos a hacerlo?

Para construir todo lo mencionado necesitamos:

  • Un servidor DHCP y un servidor TFTP. Yo utilizo los que lleva el RouterOS de mi router Mikrotik.
  • El código fuente del iPXE, que necesitaremos compilar desde una máquina GNU/Linux. Me temo que este es uno de los pocos casos incompatibles con WSL 1, así que si no estamos con WSL 2, necesitaremos un kernel de Linux completo, aunque sea en una máquina virtual.

Dicho esto, configuramos un servidor TFTP y el servidor DHCP para que las opción 66 apunte a él, y la 67 solicite el nombre de un archivo. Hay multitud de tutoriales para hacerlo y no es menester de este artículo explicarlo; si tenéis dudas siempre me podéis dejar un comentario.

Con esto ya estamos preparados para servir un binario de iPXE por TFTP e implementar iPXE... ¿o no?

Saliendo de ese bucle eterno...

He mencionado que no íbamos a flashear ninguna tarjeta de red y que en su lugar arrancaríamos con PXE y a su vez haríamos chainload a iPXE. Si optamos por descargar el binario y servirlo por TFTP, cuando arranquemos por red nos daremos cuenta de que entramos en un bucle infinito... ¿por qué? Porque el comportamiento que se da es el siguiente:

  1. Arranco mi máquina con la ROM PXE de la tarjeta de red.
  2. Obtengo el binario del iPXE y lo ejecuto.
  3. Sin ninguna configuración adicional, lo primero que hace iPXE es una petición DHCP para arrancar por red. Recuerda que está pensado para sustituir a nuestra ROM PXE.
  4. El servidor DHCP le responde con la IP a la que hacer TFTP y el programa a arrancar... que resulta a ser él mismo. Volvemos al paso 2.

Hay dos formas de romper este bucle infinito:

  • Implementar en nuestro servidor DHCP un condicional User Class que nos permita identificar que si el solicitante es iPXE no le dirija más a sí mismo.
  • Incrustar un script de arranque en el binario de iPXE, que implica compilárnoslo. Esta es lo que he utilizado.

Creando un script de arranque

El scripting de iPXE es extremadamente sencillo y no nos llevará nada aprenderlo. Si ponemos una URL fija de arranque evitaremos el dichoso bucle. Un ejemplo de script para este propósito puede ser tan simple cono este:

#!ipxe
  
dhcp
chain http://boot.ipxe.org/demo/boot.php

Aunque también podemos hacer uno bastante más elaborado como el que servidor utiliza:

#!ipxe
set esc:hex 1b
set bold ${esc:string}[1m
set boldoff ${esc:string}[22m
set fg_gre ${esc:string}[32m
set fg_cya ${esc:string}[36m
set fg_whi ${esc:string}[37m
set HTTPS_ERR HTTPS appears to have failed... attempting HTTP
set HTTP_ERR HTTP has failed, localbooting...

:start
echo ${bold}${fg_gre}HispaMSX Network Boot Services ${fg_whi}(based on netboot.xyz)${boldoff}
prompt --key m --timeout 4000 Hit the ${bold}m${boldoff} key to open failsafe menu... && goto failsafe || goto dhcp

:dhcp
echo
dhcp || goto netconfig
goto menu

:failsafe
menu netboot.xyz Failsafe Menu
item localboot Boot to local drive
item netconfig Manual network configuration
item retry Retry boot
item debug iPXE Debug Shell
item reboot Reboot System
choose failsafe_choice || exit
goto ${failsafe_choice}

:netconfig
echo Network Configuration:
echo Available interfaces...
ifstat
imgfree
echo -n Set network interface number [0 for net0, defaults to 0]: ${} && read net
isset ${net} || set net 0
echo -n IP: && read net${net}/ip
echo -n Subnet mask: && read net${net}/netmask
echo -n Gateway: && read net${net}/gateway
echo -n DNS: && read dns
ifopen net${net}
echo Attempting chainload of netboot.xyz...
goto menu || goto failsafe

:menu
set conn_type https
chain --autofree https://www.hispamsx.org/ipxe/main.ipxe || echo ${HTTPS_ERR}
sleep 5
set conn_type http
chain --autofree http://www.hispamsx.org/ipxe/main.ipxe || echo ${HTTP_ERR}
sleep 5
goto localboot

:localboot
exit

:retry
goto start

:reboot
reboot
goto start

:debug
echo Type "exit" to return to menu
shell
goto failsafe

Atención a la sentencia chain --autofree http://www.hispamsx.org/ipxe/main.ipxe || echo ${HTTP_ERR}, que junto con su variante HTTPS es la que nos dice verdaderamente de dónde vamos a coger el script de arranque. En el ejecutable incrustamos la URL, pero no el archivo, por lo que después somos libres de cambiarlo en tiempo real. Por otro lado, fíjate que el protocolo es HTTP (o HTTPS), ¡cómodo, práctico y si es HTTPS además seguro!

¿Cómo lo incrustamos? Imaginad que a este script le llamamos embed.ipxe, sólo tenemos que especificarlo a la hora de compilar el binario, que se haría de la siguiente manera:

$ git clone git://git.ipxe.org/ipxe.git
$ cd git/src
$ vim embed.ipxe # Aquí creamos nuestro script
$ make bin/snp.efi EMBED=embed.ipxe # Si queremos generar ejecutable EFI
$ make bin/undionly.kpxe EMBED=embed.ipxe # Si queremos generar ejecutable BIOS

¿Qué tenemos como resultado si arrancamos ese ejecutable? Algo como lo siguiente:

Creando un script de arranque

main.ipxe es mi script de arranque, que en este caso es todo lo siguiente:

#!ipxe
set esc:hex 1b            # ANSI escape character - "^["
set cls ${esc:string}[2J  # ANSI clear screen sequence - "^[[2J"
set esc:hex 1b
set bold ${esc:string}[1m
set boldoff ${esc:string}[22m
set fg_red ${esc:string}[31m
set fg_gre ${esc:string}[32m
set fg_cya ${esc:string}[36m
set fg_yel ${esc:string}[33m
set fg_whi ${esc:string}[37m
set base_url http://www.hispamsx.org/ipxe
cpuid --ext 29 && set arch x86_64 || set arch i386
echo ${bold}${fg_cya}IP: ${fg_whi}${ip}
echo ${bold}${fg_cya}Gateway: ${fg_whi}${gateway}
echo ${bold}${fg_cya}DNS: ${fg_whi}${dns}
echo ${bold}${fg_cya}Domain: ${fg_whi}${domain}
echo ${bold}${fg_cya}Boot filename: ${fg_whi}${filename}
echo ${bold}${fg_cya}TFTP server: ${fg_whi}${next-server}
echo ${bold}${fg_cya}Platform: ${fg_whi}${platform}${boldoff}
sleep 3
cpair --foreground 7 --background 0 2

:main
clear menu
set space:hex 20:20
set space ${space:string}
menu HispaMSX Network Boot Services - ${platform} ${arch}
item --gap ${bold}${fg_cya}Microsoft Windows${boldoff}
item wds ${space} Windows Deployment Services
item dart ${space} Microsoft DaRT 10
item hirenpe ${space} Hiren's BootCD PE 1.0.1
item --gap ${bold}${fg_cya}GNU/Linux${boldoff}
item ubuntu ${space} Ubuntu GNU/Linux
item --gap ${bold}${fg_cya}Utilities${boldoff}
iseq ${platform} pcbios && item memtest ${space} Memtest86+ 5.01 ||
item --gap ${bold}${fg_cya}Others${boldoff}
item netbootxyz ${space} netboot.xyz
item shell ${space} iPXE shell
item exit ${bold}${fg_red}Exit${boldoff}
choose --default wds --timeout 30000 target && goto ${target}

# Windows Deployment Services chainload: http://ipxe.org/appnote/chainload_wds
# Goto HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/WDSServer/Providers/WDSTFTP/ReadFilter
# Add ReadFilters: boot/* boot\* /boot/* \boot\* /boot\*
# Set DHCP options in the WDS server
# Be careful with TFTP block size until Microsoft fixes it
:wds
imgfree
iseq ${gateway} 10.0.0.1 && set net0/next-server 10.0.0.22 || set net0/next-server 10.0.1.22
iseq ${platform} efi && iseq ${arch} x86_64 && imgexec tftp://${net0/next-server}/boot/x64/wdsmgfw.efi ||
iseq ${platform} pcbios && iseq ${arch} x86_64 && imgexec tftp://${net0/next-server}/boot/x64/wdsnbp.com ||
iseq ${platform} efi && iseq ${arch} i386 && imgexec tftp://${net0/next-server}/boot/x86/wdsmgfw.efi ||
iseq ${platform} pcbios && iseq ${arch} i386 && imgexec tftp://${net0/next-server}/boot/x86/wdsnbp.com ||
echo Something went wrong chainloading to Windows Deployment Services :(
sleep 5
goto main

:ubuntu
chain ${base_url}/ubuntu.ipxe
goto main

:dart
cpuid --ext 29 && set arch_wds x64 || set arch_wds x86
imgfree
kernel ${base_url}/wimboot
imgfetch --name boot.sdi ${base_url}/wim/boot.sdi boot.sdi
imgfetch --name BCD ${base_url}/wim/${arch_wds}/Images/boot.wim.wimboot.bcd BCD
imgfetch --name boot.wim ${base_url}/wim/${arch_wds}/Images/boot-(2).wim boot.wim
boot

:hirenpe
cpuid --ext 29 && set arch_wds x64 || set arch_wds x86
imgfree
kernel ${base_url}/wimboot
imgfetch --name boot.sdi ${base_url}/wim/boot.sdi boot.sdi
imgfetch --name BCD ${base_url}/wim/${arch_wds}/Images/boot.wim.wimboot.bcd BCD
imgfetch --name boot.wim ${base_url}/wim/${arch_wds}/Images/boot-(3).wim boot.wim
boot

:memtest
kernel ${base_url}/memdisk
initrd ${base_url}/memtest86p.iso
imgargs memdisk iso raw
boot

:netbootxyz
imgfree
iseq ${platform} efi && chain --autofree https://boot.netboot.xyz || echo Netbootxyz requires DOWNLOAD_PROTO_HTTPS
sleep 5
goto main

:shell
shell
goto main

:exit

¡Muchas cosas interesantes aquí! Vamos por partes:

  • Líneas 1-22. Definimos una serie de variables, sobre todo para hacernos la vida más fácil introducciendo secuencias de escape ANSI para colorear el texto de la pantalla. También establecemos la arquitectura de la CPU que está arrancando.
  • Líneas 24-41. El menú de arranque. Algunos condicionales como el de la línea 36 nos sirvan para decidir si mostrar algo o no. Por ejemplo, Memtest86+ sólo funciona en BIOS.
  • Líneas 43-57. Chainload de WDS. Hablaré de ello al detalle más adelante.
  • Líneas 59-61. No es realmente un chainload, sino una llamada al archivo ubuntu.ipxe que contiene el script para arrancar la instalación o la recuperación de desastres de distintas versiones de Ubuntu GNU/Linux.
  • Líneas 63-70. Arranque de Microsoft DaRT utilizando wimboot; efectivamente, sin depender de WDS. Detalle que cogemos el archivo WIM desde protocolo HTTP con una diferencia de velocidad notable versus el TFTP.
  • Líneas 72-79. Arranque del Hiren's BootCD PE, que igualmente lo hace desde archivo WIM.
  • Líneas 81-85. Arranque del Memtest86+ utilizando el genial MEMDISK que... desafortunadamente no tiene versión EFI.
  • Líneas 87-91. Arranque del netboot.xyz, una distribución de iPXE lista para utilizar.
  • Líneas 93-95. La shell de iPXE, en caso de que necesitemos realizar operaciones manualmente.

El archivo ubuntu.ipxe es un poco más sencillo:

#!ipxe
cpuid --ext 29 && set arch_a amd64 || set arch_a i386
set ubuntu_mirror archive.ubuntu.com
set ubuntu_base_dir ubuntu

:ubuntu_main
clear menu
set space:hex 20:20
set space ${space:string}
menu HispaMSX Network Boot Services - Ubuntu GNU/Linux ${arch_a}
item bionic Ubuntu 18.04 LTS Bionic Beaver
item disco Ubuntu 19.04 Disco Dingo
item
item changebits Architecture: ${arch_a}
item back Back
choose ubuntu_version || goto ubuntu_exit

:mirrorcfg
set mirrorcfg mirror/suite=${ubuntu_version}
set dir ${ubuntu_base_dir}/dists/${ubuntu_version}-updates/main/installer-${arch_a}/current/images/netboot/ubuntu-installer/${arch_a}
iseq ${ubuntu_version} disco && set dir ${ubuntu_base_dir}/dists/${ubuntu_version}/main/installer-${arch_a}/current/images/netboot/ubuntu-installer/${arch_a} ||

:ubuntu_boot_type
menu ${os} [${ubuntu_version}] Installer
item --gap Install types
item install ${space} Install
item rescue ${space} Rescue Mode
item expert ${space} Expert Install
item preseed ${space} Unattended: specify preseed url...
choose --default ${type} type || goto ubuntu_main
echo ${cls}
goto ubuntu_${type}

:ubuntu_preseed
echo -n Specify preseed URL for ${ubuntu_version}: && read preseedurl
set kernelargs auto=true priority=critical preseed/url=${preseedurl}
goto ubuntu_boot

:ubuntu_expert
set kernelargs priority=low 
goto ubuntu_boot

:ubuntu_rescue
set kernelargs rescue/enabled=true
goto ubuntu_boot

:ubuntu_install
:ubuntu_boot
imgfree
kernel http://${ubuntu_mirror}/${dir}/linux
initrd http://${ubuntu_mirror}/${dir}/initrd.gz
imgargs linux initrd=initrd.gz vga=788 ${kernelargs}
boot

:changebits
iseq ${arch} amd64 && set arch i386 || set arch amd64
goto main_ubuntu

:ubuntu_exit
clear menu
exit 0

El inicio del archivo es prácticamente inicialización mientras que el arranque del kernel ocurre en las líneas 50 a 53. Simplificándolo un poco, para arrancar un kernel de Linux sólo necesitamos algo así:

set ubuntu_installer_url_amd64 archive.ubuntu.com/ubuntu/dists/bionic-updates/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64
kernel http://${ubuntu_installer_url_amd64}/linux
initrd http://${ubuntu_installer_url_amd64}/initrd.gz
imgargs linux initrd=initrd.gz vga=788
boot

Fíjate como iPXE es capaz de retirar la imagen del kernel directamente de los servidores de Ubuntu por protocolo HTTP y arrancarla.

Chainload de Windows Deployment Services

Me sorprendió muchísimo lo fácil que es arrancar WDS desde iPXE, para lo que además tenemos instrucciones específicas aquí. Resumiendo lo que la documentación cuenta al detalle, el proceso es:

  1. Ir al servidor de WDS, abrir regedit.exe e ir a la clave HKLM/SYSTEM/CurrentControlSet/Services/WDSServer/Providers/WDSTFTP/ReadFilter.
  2. Asegurarse de que los siguientes patrones están incluidos, uno por línea: boot/*, boot\*, /boot/*, \boot\*, /boot\*.
  3. Reiniciar el servicio de TFTP en Windows Server.

Hecho esto en nuestro script de iPXE ya podemos poner algo como lo siguiente:

set net0/next-server 10.0.1.22
imgexec tftp://${net0/next-server}/boot/x86/wdsnbp.com

Sin embargo, es verdad que WDS es capaz de servir múltiples arquitecturas, así como EFI y BIOS, así que si queremos ser flexibles con esto podemos poner varios condicionales:

# Definimos IP del servidor WDS
set net0/next-server 10.0.1.22
# EFI + x86_64
iseq ${platform} efi && iseq ${arch} x86_64 && imgexec tftp://${net0/next-server}/boot/x64/wdsmgfw.efi ||
# BIOS + x86_64
iseq ${platform} pcbios && iseq ${arch} x86_64 && imgexec tftp://${net0/next-server}/boot/x64/wdsnbp.com ||
# EFI + x86
iseq ${platform} efi && iseq ${arch} i386 && imgexec tftp://${net0/next-server}/boot/x86/wdsmgfw.efi ||
# BIOS + x86
iseq ${platform} pcbios && iseq ${arch} i386 && imgexec tftp://${net0/next-server}/boot/x86/wdsnbp.com ||
echo Something went wrong chainloading to Windows Deployment Services :(
sleep 5
goto main

Como podéis ver, la ruta y el ejecutable cambian en función de la arquitectura y si BIOS o EFI.

Secure Boot

El único aspecto negativo que le he encontrado a iPXE es que no soporta Secure Boot. Esto es un poco obvio porque, al menos en las instrucciones de este artículo, estamos generando nosotros mismos el binario con un script integrado, por lo que resultaría sospechoso -y preocupante- que pudiéramos saltarnos el Secure Boot. Nuestro binario personalizado no arrancará en sistemas que tengan habilitada esta característica.

Una alternativa sería que carguemos en nuestro firmware nuestra propia clave que utilizaríamos para firmar el ejecutable, tal como comenta el Microsoft Dubai Security blog aquí, apartado 3. Entrar en detalle, no obstante está fuera de este artículo.

El resultado

Un sistema de arranque en red muy flexible, que puede ser utilizado en múltiples arquitecturas, múltipes sistemas operativos y convivir en perfecta armonía con los Windows Deployment Services.

El aspecto final es el siguiente:

Happy netbooting!