Solved: this works: -net nic,model=virtio -net user

My goal

I want to run a Debian cloud image with qemu-system-x86_64 so that the guest operating system has network access using user mode networking.

Context

I want to use this as part of my CI system (Ambient). I would prefer to not use TUN/TAP networking, or to set up a bridge on the host. I'm aware that user mode networking with QEMU is constrained and limited, and I'm OK with that.

I will explore the other options if user mode networking proves to be impossible.

What I've done

I've attached the script I've been experimenting with. To run, give it two arguments: the URL to the cloud image published by Debian, and the local filename where to store that.

You'll need the following installed:

  • wget
  • qemu-img
  • qemu-system-x86_64
  • genisoimage

You can run the script as an unprivileged user. If will run faster if you can use the Linux kernel kvm module, but it isn't required. On my laptop the device takes about three minutes to run, assuming the image has been downloaded already.

What the script does is set up cloud-init to run ip a, and then run a VM with the cloud image and the cloud-init configuration, with two virtual serial ports directed to files console.log and run.log. The ip command output goes to the second one.

What I want is for there to be a virtual Ethernet device. I've not been able to make that happens. The ip output in run.log, for me, is:

xyzzy from bootcmd to ttyS1
xyzzy from runcmd to ttyS1
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute 
       valid_lft forever preferred_lft forever

This lists only the lo or loopback network interface. I'd like that to list eth0 or another device via which the guest operating system can connect to the public Internet. For now, I'd be happy just to have the device: once I have that, I can tackle the problem of getting to have an IP address and outgoing Internet connectivity.

(The xyzzy lines are just for debugging. You can ignore them.)

My specific request

How should I change the script so that the ip output lists the network device I want?

I've tried many different variations of network related QEMU options, but nothing seems to give me what I want. I'm sure I'm doing something wrong, and I'm hoping it's obvious to someone else.

Can you help me?

You can respond to by email (liw@liw.fi), or on this fediverse thread, or in any other way you can reach me.

The script

#!/bin/bash
#
# Usage: scripts/qemu.sh https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2 /tmp/debian12.qcow2

set -euo pipefail

# I use the generic Debian 12 image from
# https://cloud.debian.org/images/cloud/bookworm/latest/
url="$1"
image="$2"
if [ ! -e "$image" ]; then
    wget -c "$url" -O "$image"
    chmod a-w "$image"
fi

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

# Make a copy-on-write image, on top of the base image. This allows us
# to modify things in the file system inside the running VM, without
# affecting the base image.
qemu-img create -b "$image" -F qcow2 -f qcow2 "$tmp/vm.qcow2"

# Create a cloud-init local data source: an ISO image of a specific
# form. The user-data runcmd writes to the run log (ttyS1) what
# network interfaces are known, with the ip command.
mkdir "$tmp/cloud-init"
cat <<EOF >"$tmp/cloud-init/user-data"
#cloud-config
bootcmd:
- echo xyzzy from bootcmd to ttyS0 > /dev/ttyS0
- echo xyzzy from bootcmd to ttyS1 > /dev/ttyS1
runcmd:
- echo xyzzy from runcmd to ttyS0 > /dev/ttyS0
- echo xyzzy from runcmd to ttyS1 > /dev/ttyS1
- ip a > /dev/ttyS1
- poweroff
EOF

cat <<EOF >"$tmp/cloud-init/meta-data"
hostname: ambient
EOF

cat <<EOF >"$tmp/cloud-init/network-config"
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: true
EOF

genisoimage -quiet -volid CIDATA -joliet -rock -output "$tmp/cloud-init.iso" "$tmp/cloud-init"

# Run the VM.
qemu-system-x86_64 \
    -m 2048 \
    -display none \
    -serial file:console.log \
    -serial file:run.log \
    -drive format=qcow2,if=virtio,file="$tmp/vm.qcow2" \
    -cdrom "$tmp/cloud-init.iso" \
    -nic user