5 minutes
Raspbian Emulation in QEMU
Recently, I decided to reinstall my Raspberries, and while doing so, automate their configuration in Ansible. They were also long-overdue for an upgrade, as I set them up some time in 2019 when I knew shit about system administration.
I installed the current version of Raspbian and proceeded with running the Ansible playbook I prepared. Unsurprisingly, there were several bugs, and I had to rewrite and redeploy it several times. I even locked myself out of network access and had to physically plug the SD card into my computer to change some configs a few times.
The whole process of removing the SD card with pliers, plugging it in my computer, changing a config, and plugging it back, isn’t exactly efficient. A few more times and I’d fry the card. To save the SD card and my patience, I decided to continue the experiments in QEMU. Once the whole deployment works, I’ll reinstall Raspbian for the final time and configure the system on a real Raspberry.
Emulating a Raspberry Pi isn’t as straightforward as some other systems, because QEMU doesn’t support it natively. Most of the hackery below involves masquerading the Pi for a different ARM board which QEMU does support.
The guide sets up Raspbian Bullseye in QEMU and enables SSH access to the guest machine, so that Ansible can be tested.
Preparation
- Install QEMU.
$ pacman -S qemu-full qemu-emulators-full
- Download a QEMU kernel and a DTB file (containing the hardware description) from dhruvvyas90/qemu-rpi-kernel. The reason for this is that the Raspbian-bundled kernel has been compiled specifically for the Raspberry Pi board, which is not supported by QEMU (at the time of writing). This site hosts kernels cross-compiled for the ARM Versatile development board, which QEMU supports.
- Download the most-recent Raspbian-Lite image from the official website. Note that (at the time of writing) the kernels in the above link require a 32bit Raspbian.
Mount and edit the image
The image contains the boot partition and the root partition. We need to mount both of them.
- Recently, the default
pi
user with theraspberry
password was removed for security reasons. Because we are doing a headless setup, we need to define the admin user in a special file inside the boot partition. - We have to disable loading of additional shared libraries in the root partition.
To mount both partitions, run the following.
$ losetup --show --find --partscan 2022-04-04-raspios-bullseye-armhf-lite.img
/dev/loop1
$ ls /dev/loop1*
/dev/loop1 /dev/loop1p1 /dev/loop1p2
$ mkdir /mnt/{raspbian-boot,raspbian-root}
$ mount /dev/loop1p1 /mnt/raspbian-boot
$ mount /dev/loop1p2 /mnt/raspbian-root
Create a file /mnt/raspbian-boot/userconf.txt
with the following structure:
<admin-username>:<password-hash>
where the password hash can be obtained from
$ echo '<password>' | openssl passwd -6 -stdin
Next, comment out every line inside /mnt/raspbian-root/etc/ld.so.preload
.
Finally, unmount the image.
$ umount /dev/loop1p1
$ umount /dev/loop1p2
$ rmdir /mnt/{raspbian-boot,raspbian-root}
$ losetup --detach /dev/loop1
Convert the image
For efficiency, the image is converted from the raw format to a qcow2
format.
$ qemu-img convert -f raw -O qcow2 2022-04-04-raspios-bullseye-armhf-lite.img raspbian-bullseye-lite.qcow2
This allow us to quickly resize the image so that it will only grow in size when the guest OS requests so.
$ qemu-img resize raspbian-bullseye-lite.qcow2 +6G
Run QEMU
The virtual machine is started using the following command.
$ qemu-system-arm \
-no-reboot \
-machine versatilepb -cpu arm1176 -m 256 \
-kernel kernel-qemu-5.10.63-bullseye \
-dtb versatile-pb-bullseye-5.10.63.dtb \
-drive format=qcow2,file=raspbian-bullseye-lite.qcow2 \
-append "root=/dev/sda2 panic=1 rootfstype=ext4 rw" \
-nographic \
-nic user,hostfwd=tcp::5022-:22
The parameters stand for
-no-reboot
: Exit on error instead of rebooting.-machine versatilepb -cpu arm1176 -m 256
: Set the emulated machine, its CPU and the amount of memory (256MB is the maximum for versatilepb).-kernel kernel-qemu-5.10.63-bullseye
: Set the kernel.-dtb versatile-pb-bullseye-5.10.63.dtb
: Set the device tree.-drive format=qcow2,file=raspbian-bullseye-lite.qcow2
: Set the drive image.-append "root=/dev/sda2 panic=1 rootfstype=ext4 rw"
: Set the root partition and its file system type.-nographic
: Do not display the QEMU GUI, since we are running Raspbian Lite anyway.-nic user,hostfwd=tcp::5022-:22
: Forward the host port 5022 to the guest port 22.
SSH connection
Enable the SSH server in the guest machine.
$ systemctl enable --now ssh.service
It is then possible to connect from the host to the guest over SSH.
$ ssh <admin-username>@127.0.0.1 -p 5022
Setup bridged networking
By default, QEMU uses user-mode networking with a virtual DHCP server. When a virtual machine runs its DHCP client, it gets assigned an IP address and is then able to access the host machine’s network stack through masquerading done by QEMU.
This has a couple of issues, which may or may not be relevant, depending on the use case.
- Because the network emulation happens in the user space, its performance can be quite poor.
- If we want to expose additional services running on the guest machine (like we do with SSH above), we must shutdown the machine and relaunch it with additional forwarding arguments to QEMU.
A solution is to setup a bridged network. We create a network bridge between the guest’s virtual TAP interface and the host’s physical interface. The virtual machine gets assigned an IP address as if it were a physical device, and is able to communicate with other devices on the network. Because TAP interfaces are a kernel feature, the resulting network can achieve much better performance. Since the virtual machine now appears as any other device on the network, we can readily access any service it provides, without needing to setup any port forwarding.
First, we have to create a bridge on the host and bring it up.
$ ip link add name br0 type bridge
$ ip link set dev br0 up
Next, we have to assign an interface to the bridge we just created so that it
knows where to forward the received frames. For this, the interface needs to be
up. Here I am assigning the eth0
interface to the bridge.
$ ip link set dev eth0 master br0
Having set up the bridge, we now instruct QEMU to use it. Since QEMU 1.1, there
is a nifty qemu-bridge-helper
utility that sets up the TAP device
automagically. Because port forwarding is no longer needed, we can remove the
-nic user,hostfwd=tcp::5022-:22
flag. To make QEMU use the br0
bridge, we
launch it like this.
$ qemu-system-arm \
... \
-net nic -net bridge,br=br0
To later remove the bridge, we can run the following.
$ ip link set dev eth0 nomaster
$ ip link delete dev br0
1051 Words
2022-04-29 19:44