Configuring a DHCP server
In the previous article the remote root filesystem was mounted by the kernel witch itself was loaded by U-boot over HTTP. Both the kernel and root filesystem are served from the development host.
However, there is still a big issue to solve: this setup works only on my home network which has the following netid: 172.22.22.0/24. Many places use the 192.168.1.0/24 netid. In the current setup, the Pi obtains an IP address from the DHCP server running on my home router. This means that uboot.scr will have to be modified when my location changes and I have to remove the SD-Card, mount it, modify and re-generate the Uboot script, unmount - not practical at all.
There are several solutions to this problem, the simplest one is to use a second dedicated network adapter and connect the Pi to it. Configure systemd-networkd to assign a static IP address to the network adapter. And in U-boot script configure the network card to use a static IP address, this way the server IP address in boot.scr can remain hardcoded. Because the netid will always be the same.
A more advanced solution is to create a separate network, with a DHCP server listening ONLY on the dedicated adapter. Going forward we might need to have access to the internet on the Pi and in most scenarios, it is also generally a good idea to use DHCP to avoid manually setting static IP addresses on network cards - So I will go with this option.
The downside of both approaches is that not everyone has access to a spare USB network adapter at all times.
The dedicated network adapter is required for this use case as one doesn't always control of the network configuration. There is good news, support for mDNS is currently being added to U-Boot. So there might be a way to avoid using a dedicated network adapter in the future.
https://github.com/u-boot/u-boot/tree/master/lib/lwip/lwip/src/apps/mdns
Configuring the dedicated network adapter
Ubuntu uses the systemd-networkd service to configure the network cards, my dedicated network has the following MAC address: 48:65:ee:1d:a7:51
It has been assigned the following name: enx4865ee1da751 - so let's configure it differently 😀
The network adapters are configured with files present in /etc/systemd/network
Let's create a .link file for the dedicated adapter
sudo vim /etc/systemd/network/15-rpi-lan.link
With the following content
[Match]
MACAddress=48:65:ee:1d:a7:51
[Link]
Name=rpilan0
MACAddress=00:11:22:33:44:55
This assigns the name rpilan0 to the network adapter and sets a custom MAC address. The setting of the custom MAC address is not mandatory, but it makes the filtering of eventual network packet captures easy - so it became a habit.
Un-plug the adapter and restart systemd-networkd
sudo systemctl restart systemd-networkd
Plug the adapter back in:
Run
ip a
and you should be able to locate the adapter.
10: rpilan0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
link/ether 00:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff permaddr 48:65:ee:1d:a7:51
The next step is to assign a static IP address to the dedicated adapter
Let's create a .network file for the adapter
sudo vim /etc/systemd/network/16-rpi-lan.network
With the following content:
[Match]
Name=rpilan0
[Network]
Address=10.1.1.1/24
Un-plug the adapter and restart systemd-networkd
sudo systemctl restart systemd-networkd
Plug the adapter and with the Pi connected and powered up so there is a signal
on the Ethernet cable
The output of
ip a
should have an entry resembling this:
10: rpilan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff permaddr 48:65:ee:1d:a7:51
inet 10.1.1.1/24 brd 10.1.1.255 scope global rpilan0
valid_lft forever preferred_lft forever
inet6 fe80::211:22ff:fe33:4455/64 scope link
valid_lft forever preferred_lft forever
Once plugged-in the adapter will always be assigned the static address 10.1.1.1
Note: The numbered prefixes are used to define the order in which the configuration files are processed by systemd-networkd, more info in the manual page systemd.network(5)
Choosing and configuring a DHCP server.
There are a multitude of DHCP servers available, systemd-networkd has a builtin DHCP server.
Simply modify the previously created file /etc/systemd/network/16-rpi-lan.networkd add 'DHCPServer=yes' to the [Network] section
Edit the Uboot script and set the serverip variable to 10.1.1.1 and everything works fine.
However, I would prefer to use an external DHCP server mainly because I want to keep all configuration centralized in one place. This makes it easy to create a docker container, a chroot or a BSD jail later on. Network configuration varies from distribution to distribution so having control of the DHCP and DNS services is important.
I've decided to go with dnsmasq because it suits the use case as it made for small networks and is easy to configure https://thekelleys.org.uk/dnsmasq/doc.html
It is mainly a DNS forwarding and caching server but also handles DHCP/BOOTP
Install it like so:
apt-get install dnsmasq
Disable the systemd unit:
sudo systemctl disable dnsmasq
A custom configuration will be provided and this also prevents it from being started by default on each boot
Note: libvirt makes use of dnsmasq and by default it listens on all interfaces
so check if there are no instances running like so:
sudo netstat -a -p | grep ^udp | grep domain
If there is no output it means that no other DNS server is running on the system
dnsmasq is configured by using command line arguments of which there are many, all explained in the manual page dnsmasq(8). A configuration file can also be used check /etc/dnsmasq.conf for examples
Here is the configuration file ~/src/rpi_boot/etc/dnsmasq.conf
interface=rpilan0
host-record=devhost,10.1.1.1
dhcp-range=10.1.1.10,10.1.1.20,12h
no-resolv
no-hosts
server=8.8.8.8
It only listens on the network adapter rpilan0, it can provide the address for the A record for devhost, the DHCP range has 10 available addresses. The DNS upstream server is google's DNS and reading /etc/resolv.conf is avoided as it contains the DNS server of the main network and so it requires to configure routing between the rpilan0 interface and the main network connection. This will be the focus of a subsequent article. The reading of /etc/hosts is also avoided for the same reason.
To test the configuration use the -d argument to prevent dnsmasq from detaching and
make it prints logs of DHCP transactions:
dnsmasq -d -C ~/src/rpi_boot/etc/dnsmasq.conf
Reboot the Pi - DHCP transactions should be logged
dnsmasq: started, version 2.90 cachesize 150
dnsmasq: compile time options: IPv6 GNU-getopt DBus no-UBus i18n IDN2 DHCP DHCPv6 no-Lua TFTP conntrack ipset no-nftset auth cryptohash DNSSEC loop-detect inotify dumpfile
dnsmasq-dhcp: DHCP, IP range 10.1.1.10 -- 10.1.1.20, lease time 12h
dnsmasq: using nameserver 8.8.8.8#53
dnsmasq: cleared cache
dnsmasq-dhcp: DHCPDISCOVER(rpilan0) b8:27:eb:7a:af:a3
dnsmasq-dhcp: DHCPOFFER(rpilan0) 10.1.1.14 b8:27:eb:7a:af:a3
dnsmasq-dhcp: DHCPREQUEST(rpilan0) 10.1.1.14 b8:27:eb:7a:af:a3
dnsmasq-dhcp: DHCPACK(rpilan0) 10.1.1.14 b8:27:eb:7a:af:a3
dnsmasq-dhcp: DHCPDISCOVER(rpilan0) b8:27:eb:7a:af:a3
dnsmasq-dhcp: DHCPOFFER(rpilan0) 10.1.1.14 b8:27:eb:7a:af:a3
dnsmasq-dhcp: DHCPREQUEST(rpilan0) 10.1.1.14 b8:27:eb:7a:af:a3
dnsmasq-dhcp: DHCPACK(rpilan0) 10.1.1.14 b8:27:eb:7a:af:a3
Make a test DNS request in another shell
dig @10.1.1.1 devhost
It should produce the following output
; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> @10.1.1.1 devhost
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42086
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;devhost. IN A
;; ANSWER SECTION:
devhost. 0 IN A 10.1.1.1
;; Query time: 0 msec
;; SERVER: 10.1.1.1#53(10.1.1.1) (UDP)
;; WHEN: Sun May 25 11:39:17 CEST 2025
;; MSG SIZE rcvd: 52
Re-configuring U-boot
Modify .config in ~/rpi_boot/dependencies/uboot/src/.config
Set CONFIG_CMD_DNS=y
Re-compile Uboot to add support for the dns monitor command
Update the U-Boot script like so:
setenv autoload 0
dhcp
dns devhost ${serverip}
wget ${kernel_addr_r} /kernel/linux5.10_rpi
booti ${kernel_addr_r} - ${fdt_addr}
Instead of using the setenv command with a hardcoded ip address, the dns command queries the ip address of the host 'devhost' and sets the environment variable used by the wget command to fetch the kernel.
Note: The kernel also needs to be re-compiled, change the root= argument to devhost in CONFIG_CMDLINE
The network infrastructure is now ready and in the next article we will focus on service management and creating scripts to automate the workflow.