Network lab with QEMU

Vincent Bernat

To ex­per­i­ment with net­work stuff, I was us­ing UML-based net­work labs. Many al­ter­na­tives ex­ist, like GNS3, Netkit, Mar­i­on­net or Cloonix. All of them are great vi­able so­lu­tions but I still pre­fer to stick to my min­i­mal home-made so­lu­tion with UML vir­tual ma­chines. Here is why:

  • I didn’t want to use disk im­ages. They take a lot of space and they have to be main­tained. They also be­come clut­tered, es­pe­cially if you try to re­use them across sev­eral labs. They are also dif­fi­cult to share.
  • I want to be able to ac­cess my home di­rec­tory. It con­tains the im­por­tant con­fig­u­ra­tion files re­lated to the lab and I can put them in the right place thanks to sym­bolic links when the lab starts. It also makes ex­chang­ing files with the lab quite easy.
  • I don’t want to boot a com­plete sys­tem. This al­lows me to be cheap on mem­ory and each vir­tual sys­tem should boot in a few seconds.

The use of UML had some drawbacks:

  • It may be buggy. For ex­am­ple, it is cur­rently not pos­si­ble to use gdb­server in­side UML with­out a patch. Some­times, the ker­nel won’t even compile.
  • It is slow.

How­ever, UML fea­tures HostFS, a filesys­tem pro­vid­ing ac­cess to any part of the host filesys­tem. This is the killer fea­ture which al­lows me to not use any vir­tual disk im­age and to get ac­cess to my home di­rec­tory right from the guest.

I dis­cov­ered re­cently that QEMU pro­vided 9P, a sim­i­lar filesys­tem on top of VirtIO, the par­avir­tu­al­ized IO framework.

Setting up the lab#

The setup of the lab is done with a sin­gle self-con­tained shell file. The lay­out is sim­i­lar to what I have done with UML. I will only high­light here the most in­ter­est­ing steps.

Booting QEMU with a minimal kernel#

My ini­tial goal was to ex­per­i­ment with Nico­las Dich­tel’s IPv6 ECMP patch. There­fore, I needed to con­fig­ure a cus­tom ker­nel. I have started from make defconfig, re­moved ev­ery­thing that was not nec­es­sary, added what I needed for my lab (mostly net­work stuff) and added the ap­pro­pri­ate op­tions for VirtIO drivers:

CONFIG_NET_9P_VIRTIO=y
CONFIG_VIRTIO_BLK=y
CONFIG_VIRTIO_NET=y
CONFIG_VIRTIO_CONSOLE=y
CONFIG_HW_RANDOM_VIRTIO=y
CONFIG_VIRTIO=y
CONFIG_VIRTIO_RING=y
CONFIG_VIRTIO_PCI=y
CONFIG_VIRTIO_BALLOON=y
CONFIG_VIRTIO_MMIO=y

No mod­ules. Grab the com­plete con­fig­u­ra­tion if you want to have a look.

From here, you can start your ker­nel with the fol­low­ing com­mand ($LINUX is the ap­pro­pri­ate bzImage):

qemu-system-x86_64 \
  -m 256m \
  -display none \
  -nodefconfig -no-user-config -nodefaults \
  \
  -chardev stdio,id=charserial0,signal=off \
  -device isa-serial,chardev=charserial0,id=serial0 \
  \
  -chardev socket,id=con0,path=$TMP/vm-$name-console.pipe,server,nowait \
  -mon chardev=con0,mode=readline,default \
  \
  -kernel $LINUX \
  -append "init=/bin/sh console=ttyS0"

Since there is no disk to boot from, the ker­nel will panic when try­ing to mount the root filesys­tem. QEMU is con­fig­ured to not dis­play video out­put (-display none). A se­rial port is de­fined and uses stdio as a back­end.1 The ker­nel is con­fig­ured to use this se­rial port as a con­sole (console=​ttyS0). A VirtIO con­sole could have been used in­stead but it seems it is not pos­si­ble to make it work early in the boot process.

The QEMU mon­i­tor is setup to lis­ten on a Unix socket. It is pos­si­ble to con­nect to it with socat UNIX:$TMP/​vm-$name-console.pipe -.

Initial ramdisk#

Up­date (2012-10)

I was ini­tially un­able to mount the host filesys­tem as the root filesys­tem for the guest di­rectly by the ker­nel. In a com­ment, Josh Triplett told me to use /dev/​root as the mount tag to solve this prob­lem. I keep us­ing an ini­trd in this post but the lab on GitHub has been up­dated to not use one.

Here is how to build a small ini­tial ramdisk:

# Setup initrd
setup_initrd() {
    info "Build initrd"
    DESTDIR=$TMP/initrd
    mkdir -p $DESTDIR

    # Setup busybox
    copy_exec $($WHICH busybox) /bin/busybox
    for applet in $(${DESTDIR}/bin/busybox --list); do
        ln -s busybox ${DESTDIR}/bin/${applet}
    done

    # Setup init
    cp $PROGNAME ${DESTDIR}/init

    cd "${DESTDIR}" && find . | \
       cpio --quiet -R 0:0 -o -H newc | \
       gzip > $TMP/initrd.gz
}

The copy_​exec func­tion is stolen from the initramfs-tools pack­age in De­bian. It will en­sure that the ap­pro­pri­ate li­braries are also copied. An­other so­lu­tion would have been to use a static busybox.

The setup script is copied as /init in the ini­tial ramdisk. It will de­tect it has been in­voked as such. If it were omit­ted, a shell would be spawned in­stead. Re­move the cp call if you want to ex­per­i­ment manually.

The flag -initrd al­lows QEMU to use this ini­tial ramdisk.

Root filesystem#

Let’s mount our root filesys­tem us­ing 9P. This is quite easy. First QEMU needs to be con­fig­ured to ex­port the host filesys­tem to the guest:

qemu-system-x86_64 \
  ${PREVIOUS_ARGS} \
  -fsdev local,security_model=passthrough,id=fsdev-root,path=${ROOT},readonly \
  -device virtio-9p-pci,id=fs-root,fsdev=fsdev-root,mount_tag=rootshare

${ROOT} can ei­ther be / or any di­rec­tory con­tain­ing a com­plete filesys­tem. Mount­ing it from the guest is quite easy:

mkdir -p /target/ro
mount -t 9p rootshare /target/ro -o trans=virtio,version=9p2000.u

You should find a com­plete root filesys­tem in­side /target/ro. I have used version=​9p2000.u in­stead of version=​9p2000.L be­cause the lat­ter does not al­low a pro­gram to mount() a host mount point.2

Now, you have a read-only root filesys­tem (be­cause you do not want to mess with your ex­ist­ing root filesys­tem and more­over, you did not run this lab as root, did you?). Let’s use a union filesys­tem. De­bian comes with AUFS while Ubuntu and OpenWRT have mi­grated to over­layfs. I was pre­vi­ously us­ing AUFS but got er­rors on some spe­cific cases. It is still not clear which one will end up in the ker­nel. So, let’s try overlayfs.

I didn’t find any patch­set ready to be ap­plied on top of my ker­nel tree. I was work­ing with David Miller’s net-next tree. Here is how I have ap­plied the over­layfs patch on top of it:

$ git remote add torvalds git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
$ git fetch torvalds
$ git remote add overlayfs git://git.kernel.org/pub/scm/linux/kernel/git/mszeredi/vfs.git
$ git fetch overlayfs
$ git merge-base overlayfs.v15 v3.6
4cbe5a555fa58a79b6ecbb6c531b8bab0650778d
$ git checkout -b net-next+overlayfs
$ git cherry-pick 4cbe5a555fa58a79b6ecbb6c531b8bab0650778d..overlayfs.v15

Don’t for­get to en­able CONFIG_​OVERLAYFS_FS in .config. Here is how I con­fig­ured the whole root filesystem:

info "Setup overlayfs"
mkdir /target
mkdir /target/ro
mkdir /target/rw
mkdir /target/overlay
# Version 9p2000.u allows one to access /dev, /sys and mount new
# partitions over them. This is not the case for 9p2000.L.
mount -t 9p        rootshare /target/ro      -o trans=virtio,version=9p2000.u
mount -t tmpfs     tmpfs     /target/rw      -o rw
mount -t overlayfs overlayfs /target/overlay -o lowerdir=/target/ro,upperdir=/target/rw
mount -n -t proc  proc /target/overlay/proc
mount -n -t sysfs sys  /target/overlay/sys

info "Mount home directory on /root"
mount -t 9p homeshare /target/overlay/root -o trans=virtio,version=9p2000.L,access=0,rw

info "Mount lab directory on /lab"
mkdir /target/overlay/lab
mount -t 9p labshare /target/overlay/lab -o trans=virtio,version=9p2000.L,access=0,rw

info "Chroot"
export STATE=1
cp "$PROGNAME" /target/overlay
exec chroot /target/overlay "$PROGNAME"

You have to ex­port your ${HOME} and the lab di­rec­tory from host:

qemu-system-x86_64 \
  ${PREVIOUS_ARGS} \
  -fsdev local,security_model=passthrough,id=fsdev-root,path=${ROOT},readonly \
  -device virtio-9p-pci,id=fs-root,fsdev=fsdev-root,mount_tag=rootshare \
  -fsdev local,security_model=none,id=fsdev-home,path=${HOME} \
  -device virtio-9p-pci,id=fs-home,fsdev=fsdev-home,mount_tag=homeshare \
  -fsdev local,security_model=none,id=fsdev-lab,path=$(dirname "$PROGNAME") \
  -device virtio-9p-pci,id=fs-lab,fsdev=fsdev-lab,mount_tag=labshare

Network#

You know what is miss­ing from our net­work lab? Net­work setup. For each LAN that I will need, I spawn a VDE switch:

# Setup a VDE switch
setup_switch() {
    info "Setup switch $1"
    screen -t "sw-$1" \
        start-stop-daemon --make-pidfile --pidfile "$TMP/switch-$1.pid" \
        --start --startas $($WHICH vde_switch) -- \
        --sock "$TMP/switch-$1.sock"
    screen -X select 0
}

To at­tach an in­ter­face to the newly cre­ated LAN, I use:

mac=$(echo $name-$net | sha1sum | \
            awk '{print "52:54:" substr($1,0,2) ":" substr($1, 2, 2) ":" substr($1, 4, 2) ":" substr($1, 6, 2)}')
qemu-system-x86_64 \
  ${PREVIOUS_ARGS} \
  -net nic,model=virtio,macaddr=$mac,vlan=$net \
  -net vde,sock=$TMP/switch-$net.sock,vlan=$net

The use of a VDE switch al­lows me to run the lab as a non-root user. It is pos­si­ble to give In­ter­net ac­cess to each VM, ei­ther by us­ing the -net user flag or us­ing slirpvde on a spe­cial switch. I pre­fer the lat­ter so­lu­tion since it will al­low the VMs to speak to each other.

Debugging#

This lab was mostly done to de­bug both the ker­nel and Quagga. Each of them can be de­bugged remotely.

Kernel debugging#

While the ker­nel fea­tures KGDB, its own de­bug­ger, com­pat­i­ble with GDB, it is eas­ier to use the re­mote GDB server built in­side QEMU.

qemu-system-x86_64 \
  ${PREVIOUS_ARGS} \
  -gdb unix:$TMP/vm-$name-gdb.pipe,server,nowait

To con­nect to the re­mote GDB server from the host, first lo­cate the vmlinux file at the root of the source tree and run GDB on it. The ker­nel has to be com­piled with CONFIG_​DEBUG_​INFO=y to get the ap­pro­pri­ate de­bug­ging sym­bols. Then, use socat with the Unix socket to at­tach to the re­mote debugger:

$ gdb vmlinux
GNU gdb (GDB) 7.4.1-debian
Reading symbols from /home/bernat/src/linux/vmlinux...done.
(gdb) target remote | socat UNIX:$TMP/vm-r1-gdb.pipe -
Remote debugging using | socat UNIX:/tmp/tmp.W36qWnrCEj/vm-r1-gdb.pipe -
native_safe_halt () at /home/bernat/src/linux/arch/x86/include/asm/irqflags.h:50
50  }
(gdb)

You can now set break­points and re­sume the ex­e­cu­tion of the kernel.

It is eas­ier to de­bug the ker­nel if op­ti­miza­tions are not en­abled. How­ever, it is not pos­si­ble to dis­able them glob­ally. You can how­ever dis­able them for some files. For ex­am­ple, to de­bug net/​ipv6/​route.c, add CFLAGS_​route.​o = -O0 to net/​ipv6/​Makefile, re­move net/​ipv6/​route.o and type make.

Userland debugging#

To de­bug a pro­gram in­side QEMU, you can just use gdb as usual. Your $HOME di­rec­tory is avail­able and it should there­fore be straight­for­ward. How­ever, if you want to per­form some re­mote de­bug­ging, that’s quite easy. Add a new se­rial port to QEMU:

qemu-system-x86_64 \
  ${PREVIOUS_ARGS} \
  -chardev socket,id=charserial1,path=$TMP/vm-$name-serial.pipe,server,nowait \
  -device isa-serial,chardev=charserial1,id=serial1

Start gdbserver in the guest:

$ libtool execute gdbserver /dev/ttyS1 zebra/zebra
Process /root/code/orange/quagga/build/zebra/.libs/lt-zebra created; pid = 800
Remote debugging using /dev/ttyS1

And from the host, you can at­tach to the re­mote process:

$ libtool execute gdb zebra/zebra
GNU gdb (GDB) 7.4.1-debian
Reading symbols from /home/bernat/code/orange/quagga/build/zebra/.libs/lt-zebra...done.
(gdb) target remote | socat UNIX:/tmp/tmp.W36qWnrCEj/vm-r1-serial.pipe -
Remote debugging using | socat UNIX:/tmp/tmp.W36qWnrCEj/vm-r1-serial.pipe -
Reading symbols from /lib64/ld-linux-x86-64.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
0x00007ffff7dddaf0 in ?? () from /lib64/ld-linux-x86-64.so.2
(gdb)

Demo#

For a demo, have a look at the fol­low­ing video: