UEFI PXE Boot в виртуальном окружении
Установка PXE‑сервера для тестирования edk2/OvmfPkg в виртуальном окружении может оказаться не совсем тривиальной задачей. Например мы столкнулись с тем что TFTP‑сервер, работающий на одной виртуальной машине, оказался недоступным для других виртуальных машин. Решение данной проблемы находится в конфигурации брандмауэра. В Linux netfilter есть вспомогательные модули nf_conntrack_tftp и nf_nat_tftp, которые после загрузки позволяют отслеживать обратные TFTP‑соединения. Однако и здесь настройка может потребовать дополнительных затрат времени.

Допустим мы работаем на персональной машине в сети с адресом 192.168.9.0/24, где установлены Qemu, Libvirt, Virtual Machine Manager и нам необходимо создать виртуальную сеть 192.168.49.0/24, в которой настроен PXE‑сервер для загрузки виртуальных машин подключаемых к этой сети.
tags
Создание такой сети, с использованием Libvirt, выглядит достаточно просто. Главным моментом здесь является то, что мы используем встроенный TFTP‑сервер и отказываемся от DHCP‑сервера.
Создание сети
Создадим XML‑файл pxe‑net.xml:
<network> <name>pxe-net</name> <forward mode='nat'> <port start='1' end='65536'/> </forward> <bridge name='pxe0' stp='on' delay='0'/> <mac address='00:16:3e:69:d4:a6'/> <domain name='radix-linux.su'/> <ip address='192.168.49.1' netmask='255.255.255.0'> <tftp root='/srv/tftp'/> </ip> </network>
и выполним команду:
virsh net-define --file pxe-net.xml
После этого в каталоге /etc/libvirt/qemu/networks/ будет создан файл описания сети pxe‑net.xml.
Для того чтобы данная сеть была доступна после каждой перезагрузки рабочей машины, выполним еще одну команду:
virsh net-autostart --network pxe-net
Поднять сеть можно с помощью команды:
virsh net-start --network pxe-net
Теперь остается разрешить Qemu использовать мост pxe0. Для этого в файл /etc/qemu/bridge.conf необходимо добавить строку:
allow pxe0
Теперь сеть 192.168.49.0/24 готова к использованию.
Напомним, что теперь каталог /etc/tftp/ на рабочей машине будет доступен всем виртуальным машинам сети 192.168.49.0/24 и загрузить файл из этого каталога можно будет, например, с помощью команды:
tftp 192.168.49.1 -c get pxelinux.0
При этом, роль TFTP‑сервера 192.168.49.1 будет выполнять Libvirt.
DHCP‑сервер
Поскольку в сети 192.168.49.0/24 не предусмотрен DHCP‑сервер, нам понадобится некая виртуальная машина с настроенным DHCP‑сервером. Ее можно создать с помощью Virtual Machine Manager или взять готовую. Рассмотрим настройку и запуск DHCP‑сервера на примере Slackware‑current.
Допустим, что мы имеем образ диска slackware‑20250205.qcow2, на котором установлена свежая версия дистрибутива. Приготовим скрипт запуска:
#!/bin/sh qemu-system-x86_64 \ -enable-kvm -bios OVMF.fd \ -cpu host -smp 4 -m 8192 \ -drive file=slackware-20250205.qcow2,media=disk,if=ide \ -netdev bridge,id=pxe-net,br=pxe0,helper=/usr/libexec/qemu-bridge-helper \ -device e1000,netdev=pxe-net,mac=00:16:3e:21:ac:4b \ -display sdl
Здесь, файл OVMF.fd – это образ, полученный в результате сборки edk2 (OvmfPkgX64.dsc).
Итак, мы имеем Slackware‑машину с одним сетевым интерфейсом. Сразу после запуска этой машины, следует назначить статический IP‑адрес.
Для этого надо отредактировать файл /etc/rc.d/rc.inet1.conf добавив адрес 192.168.49.2 так, чтобы получилась запись:
# IPv4 config options for eth0: IPADDRS[0]="192.168.49.2"
в начале файла.
Теперь можно приступать к настройке DHCP‑сервера. ISC DHCP‑сервер позволяет выбирать загрузочные конфигурации и образы в зависимости от архитектуры процессора клиентской машины, а также типа загрузки. Список доступных архитектур можно найти на странице Processor Architecture Types.
Например, тип 00:07 соответствует UEFI загрузчику с архитектурой x64. При обращении такого UEFI PXE загрузчика по IPv4 к DHCP‑серверу, в файле /var/state/dhcp/dhcp.leases будет оставлена запись, например, следующего содержания:
lease 192.168.49.50 { starts 0 2025/02/09 06:49:13; ends 1 2025/02/10 01:49:13; cltt 0 2025/02/09 06:49:13; binding state active; next binding state free; rewind binding state free; hardware ethernet 00:16:3e:5f:bd:52; set vendor-class-identifier = "PXEClient:Arch:00007:UNDI:003001"; }
Где, vendor‑class‑identifier показывает тип клиента "PXEClient" и архитектуру процессора "00:07".
Таким образом DHCP‑сервер может быть настроен на работу сразу с несколькими клиентами, имеющими различную архитектуру. В качестве примера, приведем выбор параметров загрузки в конфигурационном файле /etc/dhcpd.conf для архитектуры PowerPC:
if option arch = 00:0e { # POWER OPAL v3 option path-prefix "powerpc/"; option conf-file "netboot.cfg"; } else if option arch = 00:0c { # PowerPC Open Firmware filename "pxelinux.0"; }
Итак, займемся конфигурированием DHCP‑сервера. Для этого надо отредактировать файл /etc/dhcpd.conf. В начале файла зададим общие настройки:
authoritative; ddns-update-style none; ignore bootp; option domain-name-servers 192.168.49.1;
Напомним, что роль сервера 192.168.49.1 выполняет Libvirt, обслуживая сеть 192.168.49.0/24.
Далее опишем подсеть:
subnet 192.168.49.0 netmask 255.255.255.0 { option domain-name "radix-linux.su"; option broadcast-address 192.168.49.255; option subnet-mask 255.255.255.0; option routers 192.168.49.1; # We reserve the range 192.168.49.1 to 192.168.49.49 for static IP addresses pool { # Known clients (i.e. configured with a 'host' statement) # that request an IP address via DHCP range 192.168.49.50 192.168.49.100; # Default lease is 1 week (604800 sec.) default-lease-time 604800; # Max lease is 4 weeks (2419200 sec.) max-lease-time 2419200; deny unknown clients; } pool { # Guests range 192.168.49.150 192.168.49.200; # Default lease is 8 hours (28800 sec.) default-lease-time 28800; # Max lease is 24 hours (86400 sec.) max-lease-time 86400; deny known clients; } }
Здесь мы зарезервировали диапазон 192.168.49.1 – 192.168.49.48 под статические IP‑адреса, а блоки pool { } определяют диапазон адресов для известных нам клиентов [192.168.49.50 – 192.168.49.100] и диапазон гостевых машин [192.168.49.150 – 192.168.49.200]. Для гостевых машин мы выбрали более короткие интервалы жизни присвоенных IP‑адресов.
Теперь нам остается разделить клиентов на группы. Первая группа создана для машин, не требующих PXE‑загрузки:
group { # Non-PXE default-lease-time 68400; max-lease-time 172800; }
Вторая группа предназначена для клиентов, которые будут обращаться к нашему серверу для получения загрузочных образов:
group { # PXEBoot default-lease-time 68400; max-lease-time 172800; allow bootp; next-server 192.168.49.1; use-host-decl-names on; if substring (option vendor-class-identifier, 0, 9) = "PXEClient" { filename "grubx64.efi"; } host t47 { hardware ethernet 00:16:3e:5f:bd:52; fixed-address 192.168.49.7; } }
Параметр next-server указывает адрес TFTP‑сервера, в то время как DHCP‑сервер работает на машине с адресом 192.168.49.2.
Блок:
if substring (option vendor-class-identifier, 0, 9) = "PXEClient" { filename "grubx64.efi"; }
предлагает PXE‑клиентам загрузчик grubx64.efi, расположенный на нашей рабочей машине в каталоге /srv/tftp/.
Подобных блоков может быть несколько, например, можно добавить блок для клиентов с идентификатором "Etherboot":
else if substring (option vendor-class-identifier, 0, 9) = "Etherboot" { filename "bzImage"; }
Кроме того, DHCP‑сервер может предлагать клиетнам фиксированные IP‑адреса по их MAC‑адресу. Пример такой выдачи описан в блоке t47 { }:
host t47 { hardware ethernet 00:16:3e:5f:bd:52; fixed-address 192.168.49.7; }
То есть клиенту с MAC‑адресом 00:16:3e:5f:bd:52 DHCP‑сервер всегда будет предлагать IP‑адрес, равный IP=192.168.49.7.
Приведем итоговый конфигурационный файл /etc/dhcpd.conf целиком:
# dhcpd.conf # # Configuration file for ISC dhcpd (see 'man dhcpd.conf') # authoritative; ddns-update-style none; ignore bootp; option domain-name-servers 192.168.49.1; subnet 192.168.49.0 netmask 255.255.255.0 { option domain-name "radix-linux.su"; option broadcast-address 192.168.49.255; option subnet-mask 255.255.255.0; option routers 192.168.49.1; # We reserve the range 192.168.49.1 to 192.168.49.49 for static IP addresses pool { # Known clients (i.e. configured with a 'host' statement) # that request an IP address via DHCP range 192.168.49.50 192.168.49.100; # Default lease is 1 week (604800 sec.) default-lease-time 604800; # Max lease is 4 weeks (2419200 sec.) max-lease-time 2419200; deny unknown clients; } pool { # Guests range 192.168.49.150 192.168.49.200; # Default lease is 8 hours (28800 sec.) default-lease-time 28800; # Max lease is 24 hours (86400 sec.) max-lease-time 86400; deny known clients; } } group { # Non-PXE default-lease-time 68400; max-lease-time 172800; } group { # PXEBoot default-lease-time 68400; max-lease-time 172800; allow bootp; next-server 192.168.49.1; use-host-decl-names on; if substring (option vendor-class-identifier, 0, 9) = "PXEClient" { filename "grubx64.efi"; } host t47 { hardware ethernet 00:16:3e:5f:bd:52; fixed-address 192.168.49.7; } }
Автозапуск
Запустить настроенный DHCP‑сервер в работу можно с помощью команды:
/usr/sbin/dhcpd -q eth0
Для того чтобы DHCP‑сервер стартовал при каждой загрузке операционной ситемы, необходимо составить еще один файл /etc/rc.d/rc.dhcpd:
#!/bin/sh DHCPD_OPTIONS="-q eth0" [ -x /usr/sbin/dhcpd ] || exit 0 [ -f /etc/dhcpd.conf ] || exit 0 start() { echo -n "Starting dhcpd: /usr/sbin/dhcpd ${DHCPD_OPTIONS} " /usr/sbin/dhcpd ${DHCPD_OPTIONS} echo } stop() { echo -n "Shutting down dhcpd: " killall -TERM dhcpd echo } status() { PIDS=$(pidof dhcpd) if [ "${PIDS}" == "" ]; then echo "dhcpd is not running!" else echo "dhcpd is running at pid(s) ${PIDS}." fi } restart() { stop start } case "${1}" in start) start ;; stop) stop ;; restart) restart ;; status) status ;; *) echo "Usage: $0 {start|stop|status|restart}" ;; esac exit 0
и добавить в файл /etc/rc.d/rc.local следующие строки:
route add -net 0.0.0.0 netmask 0.0.0.0 gw 192.168.49.1 if [ -x /etc/rc.d/rc.dhcpd ]; then /etc/rc.d/rc.dhcpd start fi
Теперь, если предоставить файлу /etc/rc.d/rc.dhcpd право исполнения:
chmod a+x /etc/rc.d/rc.dhcpd
при каждом старте системы будет запускаться и DHCP‑сервер.
Отдельно следует напомнить, что Libvirt не предоставляет собственный DHCP‑сервер в сети 192.168.49.0/24 и не прокладывает маршруты. Поэтому мы должны вручную задать маршрут во внешнюю сеть:
route add -net 0.0.0.0 netmask 0.0.0.0 gw 192.168.49.1
что мы и сделали в начале файла /etc/rc.d/rc.local.
TFTP‑сервер
Содержимое каталога /srv/tftp/ на рабочей машине зависит от выбора операционной системы. Здесь можно создать множество подкаталогов для загрузки различных OS. Мы же рассмотрим самый простой случай, когда в каталоге /srv/tftp/ располагается лишь один инсталлятор системы Debian‑12.
Чтобы получить все необходимые для загрузки файлы, надо скачать архив netboot.tar.gz и распаковать его непосредственно в каталоге /srv/tftp/:
cd /srv/tftp wget https://deb.debian.org/debian/dists/bookworm/main/installer-amd64/current/images/netboot/netboot.tar.gz tar xzvf netboot.tar.gz rm -f netboot.tar.gz
В результате в каталоге /srv/tftp/ будут расположены следующие файлы и подкаталги:
/srv/tftp/ ├── debian-installer/ ├── ldlinux.c32 -> debian-installer/amd64/boot-screens/ldlinux.c32 ├── netboot.tar.gz ├── pxelinux.0 -> debian-installer/amd64/pxelinux.0 ├── pxelinux.cfg -> debian-installer/amd64/pxelinux.cfg ├── splash.png -> debian-installer/amd64/boot-screens/splash.png └── version.info
Для того чтобы иметь возможность загружать GRUB, нам необходимо дополнительно создать еще две символические ссылки:
cd /srv/tftp ln -s debian-installer/amd64/grubx64.efi . ln -s debian-installer/amd64/grub .
Виртуальная машина
Итак, в сети 192.168.49.0/24 работает наш DHCP‑сервер по адресу 192.168.49.2 и мы можем приготовить свежую виртуальную машину для того, чтобы на ней инсталлировать систему Debian‑12. Создадим пустой образ диска disk0.qcow2:
fallocate --length 64GiB disk0.qcow2
выберем подходящий образ OVMF.fd и создадим скрипт запуска машины run‑pxe‑net.sh:
#!/bin/sh qemu-system-x86_64 \ -enable-kvm -bios OVMF.fd \ -cpu host -smp 4 -m 8192 \ -drive format=raw,file=disk0.qcow2,media=disk,if=ide \ -netdev bridge,id=pxe-net,br=pxe0,helper=/usr/libexec/qemu-bridge-helper \ -device e1000,netdev=pxe-net,mac=00:16:3e:5f:bd:52 \ -display sdl
после запуска машины по клавише Esc мы сможем перейти в меню Boot Manager:
и выбрать загрузку по сети "UEFI PXEv4 (MAC:00163E5FBD52)":
Через некоторое время будет загружен GRUB, путь к которому мы указали в файле /etc/dhcpf.conf:
filename "grubx64.efi";
и мы увидим следующее меню:
Проведя инсталляцию системы на диск disk0.qcow2, мы получим рабочую виртуальную машину в сети 192.168.49.0/24.
UEFI HTTP Boot
В рассмотренной выше конфигурации DHCP‑сервера предусматривается только PXE‑загрузка и только для машин с MAC‑адресами представленными в блоках:
host t47 { hardware ethernet 00:16:3e:5f:bd:52; fixed-address 192.168.49.7; }
Если же образ OVMF.fd предусматривает еще и UEFI HTTP загрузку и, мы хотим разрешить всем машинам в сети загружаться по HTTP и PXE протоколам на выбор, то конфигурацию DHCP‑сервера /etc/dhcpd.conf можно существенно упростить:
# dhcpd.conf # # Configuration file for ISC dhcpd (see 'man dhcpd.conf') # option domain-name-servers 192.168.49.1; option routers 192.168.49.1; default-lease-time 14400; ddns-update-style none; subnet 192.168.49.0 netmask 255.255.255.0 { range dynamic-bootp 192.168.49.50 192.168.49.100; default-lease-time 14400; max-lease-time 172800; class "pxeclients" { match if substring (option vendor-class-identifier, 0, 9) = "PXEClient"; next-server 192.168.49.1; filename "grubx64.efi"; } class "httpclients" { match if substring (option vendor-class-identifier, 0, 10) = "HTTPClient"; option vendor-class-identifier "HTTPClient"; filename "http://boot.kx-home.su/openSUSE-Leap-15.4-NET-x86_64-Media.iso"; } }
Здесь, наряду с TFTP‑сервером, подразумевается наличие HTTP‑сервера. HTTP‑сервер может быть расположен в любой доступной сети поскольку мы указали путь:
route add -net 0.0.0.0 netmask 0.0.0.0 gw 192.168.49.1
в файле /etc/rc.d/rc.local сервера 192.168.49.2.
В случае использования NGINX, создание HTTP‑сервера выглядит просто:
server { listen 80; server_name boot.kx-home.su; root /srv/nginx/vhosts/boot; error_log /var/log/nginx/boot.kx-home.su-error.log; access_log /var/log/nginx/boot.kx-home.su-access.log; location / { autoindex on; } }
Загрузочный образ openSUSE‑Leap‑15.4‑NET‑x86_64‑Media.iso можно найти на официальном сайте download.opensuse.org и скопировать его в каталог /srv/nginx/vhosts/boot/ на машине, где работает сервер NGINX.
Выполнив все настройки и создав новую виртуальную машину, мы сможем инсталлировать openSUSE Leap 15.4. Для этого надо всего лишь выбрать пункт "UEFI HTTPv4 (MAC:00163E5FBD52)" в меню Boot Manager.
Клиенты с архитектурой x64 осуществляющие загрузку по HTTP оставляют след в файле /var/state/dhcp/dhcpd.leases с идентификатором, равным:
vendor-class-identifier = "HTTPClient:Arch:00016:UNDI:003001";
Здесь тип архитектуры 0x00:0x10 представлен в десятичном формате 00016.
Литература:
- NetworkPkg Getting Started Guide
- UEFI HTTP Boot
- UEFI HTTPBoot Server Setup
- PXE Boot Install
- GNU GRUB Manual 2.12
- Processor Architecture Types
Следует отметить, что на сервере 192.168.49.2 можно настроить NFS, HTTP и другие сервисы, которые будут необходимы для запуска виртуальных машин с различной архитектурой CPU и, таким образом, построить виртуальное тестовое окружение для повседневной работы.
Enjoy.