PXE Boot RetroPie

SD cards do fail … and often at inconvenient times. Rather than face the loss of saved games, I recently configured my RetroPie to boot off the network using PXE, TFTP, and a NFS root volume.

To start, read the existing documentation on this topic at https://www.raspberrypi.org/documentation/hardware/raspberrypi/bootmodes/net_tutorial.md. Based on that, I did the “client” work of configuring the Pi for USB boot. As I had existing DHCP (MicroTik), as well as TFTP and NFS (FreeNAS), I only needed the file system from my Pi, so I rsync’d that over ssh to the location I wanted to use on the FreeNAS host. Since I am moving the existing root filesystem from the SD card to NFS, I did not remove the ssh host config. After copying the files, I did edit /etc/fstab on the network copy, removing the /dev/mmcblkp1 and p2 lines (only proc should be left) as covered in the tutorial. Finally, before shutting down the Pi and removing the SD Card, grab the serial number by looking at /proc/cpuinfo because we will use it when configuring the TFTP server.

Next, I configured NFS within Services on FreeNAS (number of servers=4, serve UDP NFS clients=checked, Bind IP Address=checked the one listed, allow non-root mount=unchecked, enabled NFSv4=unchecked, NFSv3 ownership model for NFSv4=unchecked, require kerberos for NFSv4=unchecked, mountd bind port=, rpc.statd bind port=, rpc.lockd bind port=, support >16 groups=unchecked, log mountd requests=unchecked, log rpc.statd and rpc.lockd=unchecked). Then in Services, click Start Now to start NFS, and select Start on boot.

For the NFS Share, within Sharing on FreeNAS, I pointed to the path I wanted to use (/mnt/storage/user/retropie/root), and configured mapall user=root, mapall group=wheel, taking defaults for the rest (delete=unchecked, authorized ips/networks=, all directories=unchecked, read only=unchecked, quiet=unchecked, maproot user=, maproot group=).

Now, I’d like to say that all I had to do was copy some files over to the TFTP server and it worked; however, that is not what happened. After a bit of tcpdump and other troubleshooting, I came to find that the Pi insisted on having the TFTP server address be the same as the DHCP server address. This insistence was true for both the MicroTik router as well as an Ubiquiti EdgeRouter 6P. Regardless, what I did instead to get this working was put the Pi’s TFTP files onto the router and use TFTP on the router (instead of the FreeNAS I already had setup).

I will cover the MicroTik config first, as that is what I originally got this working on, and the EdgeRouter next. Within MicroTik, I setup the special DHCP option for the Pi. In DHCP Server, added a new Option (name=piboot, code=43, value=’Raspberry Pi Boot ‘) – note, there are three extra spaces at the end of the value based on reading https://www.raspberrypi.org/documentation/hardware/raspberrypi/bootmodes/net.md. Then, I edited the DHCP Network to add my piboot DHCP Option. Note, on my MicroTik I have the Boot File Name set to pxelinux.0 and Next Server as 192.168.88.217, which still work (the Pi does not use these).

Then, from the Pi’s /boot directory I added these files to the MicroTik under TFTP (all are allow=checked and read only=checked):

Request Filename                 Real Filename
--------------------------------------------------------------
bootcode.bin                     bootcode.bin
bf7072fe/bcm2710-rpi-3-b.dtb     bcm2710-rpi-3-b.dtb
bf7072fe/cmdline.txt             cmdline.txt
bf7072fe/config.txt              config.txt
bf7072fe/fixup.dat               fixup.dat
bf7072fe/kernel.img              kernel.img
bf7072fe/kernel7.img             kernel7.img
bf7072fe/start.elf               start.elf

A couple notes on the above:

  • bf7072fe is the serial number of my Pi (yours will be different, check /proc/cpuinfo)
  • To figure out which files were needed, I used Tools -> Packet Sniffer on the MicroTik with Filter on ports 67, 68, 69 (click Apply, then Start, then Packets)
  • There were several iterations of start packet sniffer filter, reboot Pi, watch Pi fail to boot, stop packet sniffer, view Packets, tweak, try again
  • There were a couple files it was looking for (based on packet sniffing) that did not exist in the /boot directory to begin with … they are not on the TFTP server and it is booting just fine

To configure the EdgeRouter, I started with the DHCP option 43. There are a couple ways you can do this from CLI to GUI, and I did both. I will cover the GUI here since it may be easier for some folks. In EdgeOS, go to the Config Tree and drill down to service, dhcp-server. We will add a global-parameter (click Add):

option option-43 code 43 = string;

Next, drill down into shared-network-name, the LAN the Pi is on, subnet, the subnet address, and add a subnet-parameter:

option option-43 "Raspberry Pi Boot   ";

Click Preview then Save. If you want another reference, there is a support article on Defining Custom DHCP Options.

You can confirm your changes (and the formatting result of " if you wish) by looking at /opt/vyatta/etc/dhcpd.conf (on the CLI, cat /opt/vyatta/etc/dhcpd.conf):

# ... only showing relative lines here ...

# The following 1 lines were added as global-parameters in the CLI and
# have not been validated
option option-43 code 43 = string;

shared-network LAN_1 {
  not authoritative;
  subnet 192.168.1.0 netmask 255.255.255.0 {
    option domain-name-servers 192.168.88.81, 192.168.88.90;
# The following 1 lines were added as subnet-parameters in the CLI and have not been validated
    option option-43 "Raspberry Pi Boot   ";
    option routers 192.168.1.1;
    option bootfile-name "pxelinux.0";
    filename "pxelinux.0";
    next-server 192.168.88.217;
    default-lease-time 86400;
    max-lease-time 86400;
    range 192.168.1.100 192.168.1.254;
  }
}

You can see I still have the “normal” bootfile-name and bootfile-server configured on this subnet, because the Pi ignores them.

Next, I had to install TFTP on EdgeOS (because it is not one of the “built-in” services). There is a support article that covers Adding Debian Packages to EdgeOS:

configure
set system package repository wheezy components 'main contrib non-free'
set system package repository wheezy distribution wheezy
set system package repository wheezy url http://http.us.debian.org/debian
commit
save
sudo apt-get update
sudo apt-get install tftpd-hpa

To then configure TFTP on EdgeOS, I setup the directory structure:

sudo mkdir -p /config/user-data/tftp/bf7072fe
sudo cp bootcode.bin /config/user-data/tftp/bootcode.bin
sudo cp bcm2710-rpi-3-b.dtb /config/user-data/tftp/bf7072fe/bcm2710-rpi-3-b.dtb
sudo cp cmdline.txt /config/user-data/tftp/bf7072fe/cmdline.txt
sudo cp config.txt /config/user-data/tftp/bf7072fe/config.txt
sudo cp fixup.dat /config/user-data/tftp/bf7072fe/fixup.dat
sudo cp kernel.img /config/user-data/tftp/bf7072fe/kernel.img
sudo cp kernel7.img /config/user-data/tftp/bf7072fe/kernel7.img
sudo cp start.elf /config/user-data/tftp/bf7072fe/start.elf
sudo chown -R root:vyattacfg /config/user-data/tftp

… and update the configuration file /etc/default/tftpd-hpa:

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/config/user-data/tftp"
TFTP_ADDRESS="192.168.1.1:69"
TFTP_OPTIONS="--secure"

You will see I modified the default address from 0.0.0.0 to a specific internal only address on the LAN where the Pi was because I did not want the TFTP server running on the Internet. Finally, a quick restart was all that was needed to get TFTP running (sudo /etc/init.d/tftpd-hpa stop ; /etc/init.d/tftpd-hpa start).

Once you have all the files the Pi needs on the TFTP server, and the NFS volume working, it should network boot as expected.

PXE Network Boot with MicroTik Router and FreeNAS TFTP

Having done a fair amount of computer building and troubleshooting, I find it is helpful to have bootable media with common tools like Memtest. Even more convenient than finding that boot disk and retrieving it, we can boot off the network. The below walks through how to setup network boot with Memtest as an example, though once you have TFTP running you can add more to it (installers, etc).

I am using PXELINUX from the SysLinux project. There are download links on that site, and this is based on version 6.02.

We can start by building out the tftproot file system on the FreeNAS host (/mnt/storage is the root of my volume). The files below are all found within the Syslinux download.

$ ls -la /mnt/storage/tftproot/
-rw-r--r--  1 root  nobody   24040 May 27 17:26 chain.c32
-rw-r--r--  1 root  nobody  122044 May 27 17:26 ldlinux.c32
-rw-r--r--  1 root  nobody  186444 May 27 18:39 libcom32.c32
-rw-r--r--  1 root  nobody   24156 May 27 18:44 libutil.c32
-rw-r--r--  1 root  nobody    1252 May 27 18:38 localboot.c32
-rw-r--r--  1 root  nobody   10700 May 27 17:26 mboot.c32
-rw-r--r--  1 root  nobody   26568 May 27 17:26 menu.c32
-rw-r--r--  1 root  nobody   46545 Oct 13  2013 pxelinux.0
drwxr-xr-x  3 root  nobody       4 May 28 13:56 pxelinux.cfg
-rw-r--r--  1 root  nobody   27076 May 27 17:26 vesamenu.c32

Within the pxelinux.cfg directory, we have the default configuration file, and files/directories to serve over TFTP.

$ ls -la /mnt/storage/tftproot/pxelinux.cfg/
-rw-r--r--  1 root  nobody  107 May 28 15:33 default
drwxr-xr-x  2 root  nobody    3 May 27 12:52 memtest

$ ls -la /mnt/storage/tftproot/pxelinux.cfg/memtest/
-rw-r--r--  1 root  nobody  150024 Aug 23  2013 memtest

The default configuration file is rather basic:

DEFAULT menu.c32
TIMEOUT 300
ONTIMEOUT localboot.c32

LABEL Memtest
  KERNEL pxelinux.cfg/memtest/memtest

The memtest file came from their download page (be sure to grab the pre-compiled bootable binary). I did rename the .bin file to be simply memtest (based on reading http://wiki.seanmadden.net/networking/pxeboot/memtest_over_pxeboot).

The last bit of FreeNAS work is to configure TFTP within Services (directory=/mnt/storage/tftproot, allow new files=unchecked, port=69, username=nobody, umask=022, extra options=). You can then check the box for Start on boot in Services and click Start Now to start TFTP.

Now, the last thing to setup is DHCP on the MicroTik router for PXE. It did take a bit to get this working, because the MicroTik router has named configurations for Next Server and Boot File Name in its DHCP Network configuration, as well as Options (where one can specify things like code 66 for boot server and code 67 for boot file). I originally setup Options, and then added those (via DHCP Options) to the DHCP Network config, though that did not work. What did finally work was to instead specify the Next Server (192.168.88.217) and Boot File Name (pxelinux.0) on the DHCP Network configuration.

Running BIND in a FreeNAS Jail

These notes are with respect to FreeNAS 11 and Bind 9.

The first thing you will need is storage for jails. If this is already setup, you can reuse it. To create it, go to Storage and select the location you want to use, then click Create Dataset. You can name it as you wish … though jails is a handy one. All the other options are defaults (sync=inherit/standard, compression=inherit/lz4, share-type=unix, enable-atime=inherit/on, zfs-deduplicatoin=inherit/off, quotas=0/unlimited, reserved-space=0/none, read-only=inherit/off, record-size=inherit).

Next, we have to add the jail. Within Jails, click Add Jail. Again, any name will do … I chose bind. All the other options are defaults though check the IP address and netmask are what you wish to use (type=standard, ipv4-dhcp unchecked, IP aliases=, IP bridge config=, sysctls=allow.raw_sockets=true, autostart=checked, vimage=checked, NAT=unchecked). If not started, start the jail.

To install Bind, we want to get to a shell on the FreeNAS system (ssh works too).

# determine jail ID for the newly created jail
$ jls

# shell into the jail
$ jexec /bin/sh

# install dependencies
$ pkg install texinfo libedit

# install bind (for this, just took defaults when prompted)
# if you do not have /usr/ports, run portsnap fetch && portsnap extract
$ cd /usr/ports/dns/bind911
$ make install clean

To configure BIND, we edit /usr/local/etc/namedb/named.conf.

It is a good idea to create an acl for the internal network:

acl goodclients {
        192.168.0.0/16;
        localhost;
        localnets;
};

Within “options {“, there are a couple things to configure:

        recursion yes;
        allow-query { goodclients; };
        allow-recursion { goodclients; };
        allow-transfer { none; };

        listen-on { 192.168.88.81; };
        listen-on-v6 { none; };

Note: The listen configurations are which address(es) BIND listens on (this matches the jail’s IP config).

Now, we can validate the config and start the service:

$ named-checkconf
$ service named start
$ service named status

To have this run on startup in our jail, edit /etc/rc.conf (in the jail):

named_enable="YES"
named_program="/usr/local/sbin/named"

By default, resolvconf will try to update /etc/resolv.conf inside the jail which may not be what you want. You can configure a static config. Edit /etc/resolv.conf:

# resolvconf is disabled, see /etc/resolvconf.conf
nameserver 208.67.222.222
nameserver 208.67.220.220
nameserver 8.8.8.8
nameserver 8.8.4.4

Edit /etc/resolvconf.conf:

# disable resolvconf from running any subscribers:
resolvconf="NO"

That should do it. You can restart your jail and ensure /etc/resolv.conf matches expectations.

The listen-on config setup within named.conf is what you will want to configure the clients on your network to use. For example, this internal network has two BIND DNS servers (running on different FreeNAS machines), and so the DHCP configuration for this network sends down two DNS servers: 192.168.88.81 and 192.168.88.90. Specifying two enables the clients to be resilient to failure of any single BIND instance.

After setting this up, I ran DNS Benchmark. This tool is great for testing DNS configuration (from DHCP settings to DNS server performance, of your local servers as well as public ones). The conclusions it showed after running its benchmark proved the local network had optimal DNS configuration.