Self-contained PXE boot server for macOS: DHCP + TFTP + HTTP in a single binary. Tested and working with HPE Gen9 UEFI PXE boot.
cd go-pxe
go build -o go-pxe .
sudo ./go-pxe \
-iface en7 \
-ip 10.0.0.1 \
-dhcp-start 10.0.0.100 \
-dhcp-end 10.0.0.200 \
-tftp-root ../tftp \
-http-root ../http \
-http-port 8080 \
-boot-file grubx64.efipxeboot/
├── dnsmasq.conf # Alternative dnsmasq config (if not using go-pxe)
├── go-pxe/ # This Go project (DHCP + TFTP + HTTP)
├── tftp/
│ ├── grubx64.efi # UEFI GRUB bootloader
│ ├── vmlinuz # Linux kernel (from AlmaLinux or Ubuntu ISO)
│ ├── initrd.img # AlmaLinux initramfs
│ ├── initrd # Ubuntu initramfs
│ └── grub/
│ └── grub.cfg # GRUB boot menu (multi-OS)
└── http/
├── almalinux97/ # Extracted AlmaLinux 9.7 DVD (repo for Anaconda)
├── AlmaLinux-9.7-x86_64-dvd.iso # AlmaLinux DVD ISO (12 GB)
├── ubuntu-24.04.4-live-server-amd64.iso # Ubuntu live server ISO (3.2 GB)
└── almalinux.img # AlmaLinux raw disk image (10 GB, for dd workflow)
PXE boots directly into the AlmaLinux Anaconda installer GUI. You walk through disk partitioning, user setup, and package selection interactively.
Setup:
# Download AlmaLinux DVD ISO
# Iranian mirrors: https://mirror.0-1.ir/almalinux/9/isos/x86_64/
# Official: https://repo.almalinux.org/almalinux/9/isos/x86_64/
curl -LO https://repo.almalinux.org/almalinux/9/isos/x86_64/AlmaLinux-9.7-x86_64-dvd.iso
mv AlmaLinux-9.7-x86_64-dvd.iso http/
# Extract PXE boot files (vmlinuz + initrd.img)
7z e http/AlmaLinux-9.7-x86_64-dvd.iso images/pxeboot/vmlinuz images/pxeboot/initrd.img -otftp/ -y
# Extract ISO contents for HTTP repo (Anaconda needs directory structure, not raw ISO)
mkdir -p http/almalinux97
cd http/almalinux97 && 7z x ../AlmaLinux-9.7-x86_64-dvd.iso -yGRUB config (tftp/grub/grub.cfg):
menuentry "Install AlmaLinux 9.7 (Anaconda)" {
linux vmlinuz ip=dhcp inst.repo=http://10.0.0.1:8080/almalinux97/
initrd initrd.img
}
Important:
inst.repomust point to the extracted directory (containing.treeinfo), NOT to the raw ISO file. Anaconda cannot fetch a repo from an ISO URL over HTTP.
Boot flow:
- PXE → GRUB loads AlmaLinux
vmlinuz+initrd.imgvia TFTP - Kernel boots, Anaconda fetches repo from
http://10.0.0.1:8080/almalinux97/ - Anaconda GUI launches — configure disk, users, packages interactively
- Installer writes AlmaLinux to disk and reboots
PXE boots Ubuntu 24.04 live server. Use this either for a standard Ubuntu install, as a rescue shell, or to dd a raw disk image (like almalinux.img) to the target.
Setup:
# Download Ubuntu ISO
curl -LO https://releases.ubuntu.com/24.04.4/ubuntu-24.04.4-live-server-amd64.iso
mv ubuntu-24.04.4-live-server-amd64.iso http/
# Extract kernel/initrd from ISO
7z e http/ubuntu-24.04.4-live-server-amd64.iso casper/vmlinuz casper/initrd -otftp/ -y
# Rename: casper/vmlinuz -> vmlinuz, casper/initrd -> initrd (already in tftp/)GRUB config:
menuentry "Ubuntu 24.04 Live (for dd deploy or rescue)" {
linux vmlinuz ip=dhcp url=http://10.0.0.1:8080/ubuntu-24.04.4-live-server-amd64.iso
initrd initrd
}
dd workflow (to deploy pre-built AlmaLinux disk image):
# From the live Ubuntu shell on the target server:
curl http://10.0.0.1:8080/almalinux.img | dd of=/dev/sda bs=4M status=progress
reboot- Connect Mac to target server via Ethernet (USB-C/Thunderbolt adapter → direct cable)
- Set static IP on the Ethernet interface:
# Find your interface (look for USB/Thunderbolt Ethernet)
networksetup -listallhardwareports
# Set static IP
sudo ifconfig en7 10.0.0.1 netmask 255.255.255.0 up- Boot target server, select PXE IPv4 (UEFI mode) from boot menu (F12/F11)
HPE Gen9 UEFI PXE ROMs filter on IP destination and reject subnet-directed broadcasts (e.g., 10.0.0.255). They only accept packets addressed to 255.255.255.255.
Fix: Send DHCP replies to 255.255.255.255:68 (global broadcast) instead of subnet broadcast. Fall back to subnet broadcast only if global fails.
HP UEFI PXE clients request blksize and tsize options in TFTP RRQ. Without an OACK response, the client either aborts or falls back to 512-byte blocks (causing 2.3MB grubx64.efi to transfer extremely slowly or time out).
Fix: Implement OACK responses supporting:
blksize— negotiate up to 1468 bytes (Ethernet MTU - headers)tsize— report file size before transfer
This reduced grubx64.efi transfer from timing out to completing in <1 second.
If you prefer dnsmasq over the Go binary:
# Start dnsmasq (DHCP + TFTP)
sudo dnsmasq --no-daemon --conf-file=../dnsmasq.conf
# Start HTTP server separately
cd ../http && python3 -m http.server 8080Note: On macOS, dnsmasq with
bind-interfacescan get stuck at 100% CPU on some setups. The go-pxe binary avoids this issue entirely.
To give the PXE-booted server internet access through your Mac:
# Enable IP forwarding
sudo sysctl -w net.inet.ip.forwarding=1
# NAT via your Mac's internet interface (e.g., en0 = Wi-Fi)
echo "nat on en0 from 10.0.0.0/24 to any -> (en0)" | sudo pfctl -ef -| Problem | Solution |
|---|---|
| DISCOVER loops (no REQUEST) | Ensure replies go to 255.255.255.255, not subnet broadcast |
| TFTP transfer never completes | Server must support blksize/tsize OACK negotiation |
| Anaconda "failed to fetch" | inst.repo must point to extracted ISO directory, not raw ISO file |
GRUB shows "file not found" for .lst files |
Non-fatal — GRUB modules command.lst, fs.lst, etc. are optional |
| Server doesn't PXE boot | Check BIOS: must be UEFI mode, network boot enabled |
| dnsmasq 100% CPU on macOS | Use go-pxe binary instead |
| TFTP timeout on large files | Ensure blksize negotiation is working (check for OACK in logs) |
- PXE ROMs filter on IP destination: HP UEFI only accepts
255.255.255.255broadcast, not subnet-directed10.0.0.255— even though both arrive as ethernet broadcast frames. - TFTP option negotiation is mandatory for UEFI: Modern PXE clients request
blksize/tsize. Without OACK support, large file transfers fail. - Anaconda needs a repo directory, not an ISO URL:
inst.repo=http://server/file.isodoes not work. Extract the ISO and serve the directory structure (must contain.treeinfo). - macOS broadcast routing: Use
IP_BOUND_IFto pin UDP sockets to the correct interface. - BOOTP minimum packet size: Some PXE ROMs reject DHCP packets smaller than 548 bytes.
- dnsmasq on macOS is unreliable: With
bind-interfacesit can spin at 100% CPU. The Go binary is more predictable.