In this article, I will provide an overview of how netbooting with the Raspberry Pi 4 works and how to configure a server off of which you can boot your Pi.

Several components are needed for netbooting a Pi:

  • The Pi itself needs to have netbooting in its boot order config
  • A DHCP server is needed to provide IPs for the Pi and the TFTP server. In this series, the IP for the Pi will be provided by my OPNsense firewall and the netboot options will be provided by DNSmasq.
  • A TFTP (Trivial File Transfer Protocol) server to provide the boot files

Prerequisites

For this article, the following things are assumed to be present:

  • A Raspberry Pi 4 you would like to netboot
  • A server which will serve as the netboot server. This can be another Pi or any other server you have running in your Homelab.
  • A DHCP server
  • An NFS server

How the Raspberry Pi netboot works

When a Raspberry Pi boots, it tries a configurable sequence of boot types and uses the first one which works. The boot order is configured in the Pi bootloader config. The documentation on all available boot options can be found in the official docs.

Interesting for us is the netboot option, 0x2.

To change the bootloader config, you need to boot into an OS and run the rpi-eeprom-config tool. As described in the Pi docs, you can run the following command:

sudo -E rpi-eeprom-config --edit

This will pop up an editor with config looking like this:

[all]
BOOT_UART=0
WAKE_ON_GPIO=1
POWER_OFF_ON_HALT=0
BOOT_ORDER=0xf241

Please note: The BOOT_ORDER option might not be present in a fresh Pi. You can simply add the line at the end.

One important point for the BOOT_ORDER variable: It is read from right to left, not from left to right. So the boot order in the above example will try the following options in order:

  1. SD card/eMMC boot (0x1)
  2. USB boot (0x4)
  3. Network boot (0x2)
  4. Start again from 1) (0xf)

I like the above BOOT_ORDER value because it allows me to short circuit the boot to a USB stick or SD card if something is wrong with the netboot setup. This setup has one caveat though: It’s not usable for booting Pi CM4s with eMMC storage. Because you can’t actually remove the eMMC storage, the Pi would always find the eMMC storage and boot off of it if there is a working image on it. For CM4s, I would move the eMMC option to the very end of the sequence or remove it completely and just rely on a USB stick when things go south with netbooting.

The netboot itself roughly follows this order:

  1. Broadcast a DHCP request
  2. Receive DHCP response and configure IP
  3. Receive DHCP proxy response
  4. Take TFTP server address from DHCP response
  5. Contact TFTP server and check if <SERIAL>/start.elf can be found
  6. If it can be found, request all further files from <SERIAL>/
  7. Otherwise, further files will be requested without a directory prefix
  8. Start downloading boot configs, initramfs and the kernel

This sequence can also be found in the official documentation.

The first interesting part to note is the Receive DHCP proxy response step. Due to this, we can setup a separate DNSmasq server which only serves the netboot relevant options, while leaving whatever main DHCP server you are using undisturbed.

The second important part is about the <SERIAL> directory the Pi tries first. This mechanism first checks whether the required files can be found in a subdirectory on the netboot server corresponding to the Pi’s serial number. This way, we can supply separate kernels and configs for different Pis in the network. The serial number can be found with this command:

cat /proc/cpuinfo | grep Serial
Serial		: 10000000bf9fed6f

The serial number the Pi checks during netboot to find device specific files is the last part starting after the long string of zeroes. In this example, the serial would be bf9fed6f.

NFS boot dir

So where are the kernel and all the other boot files coming from? The answer: An NFS mount. This is due to the following problem: When running a system update on a netbooting machine, how are the new kernel and dtbs going to find their way to the TFTP server? You could always go with just copying the files after the update, but I believe just having the /boot directory on a NFS mount shared between the netbooting Pi and the TFTP server makes system updates seamless.

So the first thing you need is an NFS server. For my setup, I’m using NFS Ganesha deployed via my Ceph cluster, backed by a CephFS volume. The documentation for setting that up can be found here. But any NFS server will do.

The following gives a brief overview of how to set up an NFS share on a Ceph cluster. To begin with, create a CephFS subvolume to back the NFS share.

ceph fs subvolume create homenet-fs picluster-boot

Here, homenet-fs is the name of my CephFS volume, while picluster-boot is the name of the new subvolume. Next, create the NFS Ganesha cluster itself, if you do not have an NFS cluster yet:

ceph nfs cluster create hn-nfs "nfs-host"

This will create a cluster called hn-nfs with an NFS Ganesha daemon being automatically deployed (via cephadm) on the cluster host nfs-host. Now, you can create the NFS export for the netboot boot files:

ceph nfs export create cephfs --cluster-id hn-nfs --pseudo-path /picluster-boot --fsname homenet-fs --path /cephfs/path/to/subvolume --client_addr 10.86.5.0/24

This command creates an NFS export reachable at /picluster-boot, using the CephFS volume picluster-boot we created above for backing storage and restricting access to my local cluster subnet. To get the right parameter for the --path argument, you can use this command:

ceph fs subvolume getpath homenet-fs picluster-boot
/cephs/path/to/subvolume

One important note on permissions. I had to configure NFS to squash all access to the nobody user. This is due to the fact that Ubuntu installs the kernel with permissions 600 and owner root. Consequently, the TFTP server not running under root was not able to read the kernel when the Pi requested it during netboot.

Another important note concerns bootstrapping your Homelab. Think about where/how you provide the NFS server. Obviously, it should not be located on a host which is netbooting itself. This also goes for putting it into your k8s cluster if your cluster nodes, or any of the services your cluster nodes need to come up, do netbooting. In my setup, the Ceph cluster is a no dependencies setup. Meaning it can always boot up without needing absolutely anything else from my Homelab. So it’s pretty save to run the NFS mount for the boot directories off of Ceph.

For the mounts themselves, I’m mounting the full NFS netboot directory on the TFTP server like this:

nfs-host:/picluster-boot /mnt/netboot nfs defaults,timeo=900,_netdev 0 0

This mounts the picluster-boot NFS export at /mnt/netboot. Again, note that I’m using nfs-host, namely a hostname instead of an IP. This means, for netbooting to work, I need working DNS because otherwise, my TFTP server is not going to be able to access the boot files. So don’t make your local DNS dependent on a netbooted machine.

On the Pi itself, the /boot directory would be mounted like this in /etc/fstab:

nfs-host:/picluster-boot/bf9fed6f /boot/firmware nfs defaults,timeo=900,_netdev 0 0

Again, as before, the serial is important here, providing a subdirectory for each device on the NFS mount.

With this setup, whenever one of the netboot hosts is updated and files in /boot/firmware are changed, they are also automatically changed on the TFTP server, not requiring out-of-band synchronization of the TFTP server and the actual host after an OS update.

DNSmasq and TFTP

For the TFTP server, I chose DNSmasq.

This fulfills two different roles: The first one is to provide the DHCP option telling the netbooting host where to find the TFTP server. The second function is providing the TFTP server itself.

The configuration file for DNSmasq looks like this:

port=0
dhcp-range=10.86.5.255,proxy
log-dhcp
enable-tftp
tftp-root=/mnt/netboot
pxe-service=0,"Raspberry Pi Boot"

The port=0 option is there to completely disable DNS functionality, as I have got another DNS server already set up on my OPNsense firewall.

The dhcp-range=10.86.5.255,proxy config tells DNSmasq not to function as a normal DHCP server. This ensures that there are no conflicts between your network’s main DHCP server and this DNSmasq instance. It will only answer with netboot info, but will not hand out IPs to hosts.

log-dhcp just enables a bit more logging.

enable-tftp and tftp-root=/mnt/netboot configure DNSmasq’s TFTP server for providing the boot files, with the root for the files being set to the previously configured NFS mount. Finally, pxe-service=0,"Raspberry Pi Boot" provides a PXE boot option. The 0 value here means that when the option is chosen in a boot menu, the netboot will be aborted. This is not relevant for our Pi netboot case here, though.

After setting up this configuration, a full netboot will produce log output like this:

2408886387 available DHCP subnet: 10.86.5.255/255.255.255.0
2408886387 vendor class: PXEClient:Arch:00000:UNDI:002001
2408886387 PXE(eth0) e4:5f:01:98:e0:82 proxy
2408886387 tags: eth0
2408886387 broadcast response
2408886387 sent size:  1 option: 53 message-type  2
2408886387 sent size:  4 option: 54 server-identifier  10.86.5.152
2408886387 sent size:  9 option: 60 vendor-class  50:58:45:43:6c:69:65:6e:74
2408886387 sent size: 17 option: 97 client-machine-id  00:34:69:50:52:15:31:d0:00:01:98:e0:82:6f...
2408886387 sent size: 32 option: 43 vendor-encap  06:01:03:0a:04:00:50:58:45:09:14:00:00:11...
2408886387 available DHCP subnet: 10.86.5.255/255.255.255.0
2408886387 vendor class: PXEClient:Arch:00000:UNDI:002001
error 0 Early terminate received from 10.86.5.151
sent /mnt/netboot/bf9fed6f/start4.elf to 10.86.5.151
sent /mnt/netboot/bf9fed6f/config.txt to 10.86.5.151
file /mnt/netboot/bf9fed6f/pieeprom.sig not found
file /mnt/netboot/bf9fed6f/recover4.elf not found
file /mnt/netboot/bf9fed6f/recovery.elf not found
sent /mnt/netboot/bf9fed6f/start4.elf to 10.86.5.151
sent /mnt/netboot/bf9fed6f/fixup4.dat to 10.86.5.151
file /mnt/netboot/bf9fed6f/recovery.elf not found
error 0 Early terminate received from 10.86.5.151
sent /mnt/netboot/bf9fed6f/config.txt to 10.86.5.151
sent /mnt/netboot/bf9fed6f/config.txt to 10.86.5.151
file /mnt/netboot/bf9fed6f/dt-blob.bin not found
file /mnt/netboot/bf9fed6f/recovery.elf not found
error 0 Early terminate received from 10.86.5.151
sent /mnt/netboot/bf9fed6f/config.txt to 10.86.5.151
sent /mnt/netboot/bf9fed6f/config.txt to 10.86.5.151
file /mnt/netboot/bf9fed6f/bootcfg.txt not found
error 0 Early terminate received from 10.86.5.151
failed sending /mnt/netboot/bf9fed6f/initrd.img to 10.86.5.151
sent /mnt/netboot/bf9fed6f/initrd.img to 10.86.5.151
error 0 Early terminate received from 10.86.5.151
failed sending /mnt/netboot/bf9fed6f/bcm2711-rpi-4-b.dtb to 10.86.5.151
sent /mnt/netboot/bf9fed6f/bcm2711-rpi-4-b.dtb to 10.86.5.151
error 0 Early terminate received from 10.86.5.151
failed sending /mnt/netboot/bf9fed6f/overlays/overlay_map.dtb to 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/overlay_map.dtb to 10.86.5.151
error 0 Early terminate received from 10.86.5.151
sent /mnt/netboot/bf9fed6f/config.txt to 10.86.5.151
sent /mnt/netboot/bf9fed6f/config.txt to 10.86.5.151
error 0 Early terminate received from 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/dwc2.dtbo to 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/dwc2.dtbo to 10.86.5.151
error 0 Early terminate received from 10.86.5.151
failed sending /mnt/netboot/bf9fed6f/overlays/disable-bt.dtbo to 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/disable-bt.dtbo to 10.86.5.151
error 0 Early terminate received from 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/disable-wifi.dtbo to 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/disable-wifi.dtbo to 10.86.5.151
error 0 Early terminate received from 10.86.5.151
sent /mnt/netboot/bf9fed6f/cmdline.txt to 10.86.5.151
sent /mnt/netboot/bf9fed6f/cmdline.txt to 10.86.5.151
error 0 Early terminate received from 10.86.5.151
failed sending /mnt/netboot/bf9fed6f/vmlinuz to 10.86.5.151
file /mnt/netboot/bf9fed6f/armstub8-gic.bin not found
error 0 Early terminate received from 10.86.5.151
failed sending /mnt/netboot/bf9fed6f/vmlinuz to 10.86.5.151
error 0 Early terminate received from 10.86.5.151
failed sending /mnt/netboot/bf9fed6f/vmlinuz to 10.86.5.151
sent /mnt/netboot/bf9fed6f/vmlinuz to 10.86.5.151

Important for us is the 2408886387 sent size: 4 option: 54 server-identifier 10.86.5.152 message. It informs the client requesting netboot about where it can find the TFTP server. In this case, this is just the IP of my local boot server.

Right after the DHCP message is send, file transfers for boot files start:

sent /mnt/netboot/bf9fed6f/start4.elf to 10.86.5.151
sent /mnt/netboot/bf9fed6f/config.txt to 10.86.5.151
sent /mnt/netboot/bf9fed6f/initrd.img to 10.86.5.151
sent /mnt/netboot/bf9fed6f/bcm2711-rpi-4-b.dtb to 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/overlay_map.dtb to 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/dwc2.dtbo to 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/disable-bt.dtbo to 10.86.5.151
sent /mnt/netboot/bf9fed6f/overlays/disable-wifi.dtbo to 10.86.5.151
sent /mnt/netboot/bf9fed6f/cmdline.txt to 10.86.5.151
sent /mnt/netboot/bf9fed6f/vmlinuz to 10.86.5.151

I’m honestly still not sure where the Early terminate errors are coming from. The file seems to be retried and succeed later, but I don’t see any dropped packets or similar.

This concludes the second part of the Pi netboot series. An overview of all parts can be found in Part I of the series.