Skip to main content

My Experience with libvirt

My daily driver setup has been working exceptionally well. If you missed them, you can check out the previous posts for details:

Recently, I decided to harden my setup by writing custom AppArmor profiles and nftables rules. During my research, libvirt kept popping up in tutorials. In fact, most “QEMU/KVM” guides simply assume you are using it.

Initially, I decided against using libvirt because I wasn’t a fan of its design philosophy. However, I kept hearing that it can automatically generate AppArmor profiles and firewall rules for each VM. Furthermore, this article highlighted several things that libvirt genuinely simplifies. Intrigued, I decided to dive in and get some first-hand experience.

The plan was to migrate my existing VM setup (which relies on bare QEMU scripts) to libvirt to see if it was a good fit. I specifically wanted to evaluate the parts of libvirt I was previously skeptical about:

  • Reliance on a highly privileged daemon.
  • Configuration stored in managed XML files, which makes it harder to create reusable components or templates.

The Good Parts

  • As noted in this article, while bare QEMU/KVM lacks a stable API, libvirt provides a very stable XML API.
  • CPU pinning and lifecycle management (like gracefully shutting down a VM) are as simple as just a few lines of XML. With bare QEMU, I had to write a Python daemon to parse the QMP protocol just to achieve this. libvirt also seamlessly handles a lot of edge cases during shutdowns.

The OK Parts

  • domxml-from-native didn’t work for me; this might be a Debian-specific issue. Thankfully, it wasn’t too difficult to manually recreate the XML file from my existing QEMU arguments.
  • Instead of using virsh define, I found I could use virsh create. This reads from an XML config and creates a transient VM. I love this approach because it lets me maintain control over my XML configs, making it possible to use scripts or Jinja templates to define reusable templates.
  • The highly privileged daemon is a mixed bag. Sometimes root access is required to configure network interfaces or AppArmor profiles, but I generally prefer to isolate those operations. For example, I’d rather pre-configure the network interfaces and define AppArmor profiles in systemd service files to keep the daemon rootless. That said, it’s not a dealbreaker:
    • The Arch Wiki notes that the daemon isn’t strictly required in all scenarios.
    • I had already ended up writing my own custom daemons for CPU pinning and lifecycle management anyway.
  • Networking has its quirks. libvirt can add a managed TAP device, but out of the box, it lacks NAT and network filtering.
    • It can use my existing TAP device as an unmanaged interface, but then it won’t apply network filters.
    • I couldn’t seem to manually fix the name of the TAP device, which makes writing custom nftables rules a headache.
    • Network configurations have to be defined separately (though they can be created transiently).
    • While libvirt can auto-generate network filters, any custom logic requires a separate, permanent XML config. I’d much rather just write plain nftables rules directly.
  • libvirt automatically creates AppArmor profiles for each VM, restricting the VM to only read its own disk image. The profile libvirt provides is actually quite reusable. I’ve used it in the past for server projects, even if it’s not perfect.

The Bad Parts

  • libvirt pulls in a lot of dependencies. Most annoyingly, nwfilter has a hard dependency on iptables. I’ve heard libvirt supports nftables now, so this might just be another Debian-specific quirk.
  • It wasn’t easy to assign a unique user to each VM. Setting the DAC seclabel resulted in a permission error regarding “master-key.aes”. This is likely because /var/lib/libvirt/qemu is restricted exclusively to the libvirt-qemu user.
  • I couldn’t get sound to work at all (I use ALSA). I probably just needed to add the libvirt-qemu user to the right audio groups, but it was another hurdle.

Conclusion

It is undeniable that libvirt is highly scalable and offers a rock-solid API. However, for my specific use case, it makes 80% of the setup incredibly easy while making the last 20% frustratingly difficult. To use it efficiently, you really have to commit to speaking the “libvirt language”.

I could probably iron out the remaining kinks if I spent a few more hours on it, but I don’t see the point right now. I’ll likely reconsider it in the future when I start experimenting with disposable VMs.

Comments

Popular posts from this blog

A Rocky Migration: Moving from docker-compose to Podman and gVisor

I've been running a few containers for several years. They were all running under rootless Docker with a single user. Initially, I planned to  migrate the containers to VMs , but I couldn't get a stable workflow after about two months of effort. Later,  gVisor caught my attention , and I decided to migrate to Podman with gVisor instead. The new plan is to run each container with  --userns=auto  and use Quadlet for systemd integration. This approach provides better isolation and makes writing firewall rules easier. I'm now close to migrating all my containers. Here are a couple of rough edges I'd like to share. Network Layout I compared  various networking options  and spent a few hours trying the one-interface-per-group approach before giving up. I settled on a single macvlan network and decided to use static IP addresses for my containers. To prevent a randomly assigned IP address from conflicting with a predefined one, I allocated a large IP range for my ...

GameConqueror 0.09 -- Linux Game Hacking Tool

If you are a game hacker If you've been looking for a `CheatEngine for Linux` Then you can't miss this. ============================================== GameConqueror is a game hacking tool for linux, it's written in PyGTK and uses scanmem as its backend. It's supposed to be with most useful features of CheatEngine for Linux. Currently, I've implemented almost everything about scanning, involving variant data types and scan types: Data Types: int{8/16/32/64}, float{32/64}, unknown type(int or float) and unknown width(will try each of them), byte array and string Scan Types: equal, greater, less, changed, unchanged, increased(by), decreased(by) This should be enough for most cases, so I decided to release it at the current status. ============================================= Here's how you can get it PPA (for Ubuntu users) https://launchpad.net/~coolwanglu/+archive/scanmem (I've not test it in 32bit environments or Jaunty, do please inform me if it doe...

Fix Google Security Code

Google Security Code (http://g.co/sc) is one type of 2-step verification. This is particularly useful when security keys and passkeys are not available. I have been using it in my LXC containers, until today I found out that it stopped working. It just kept saying "The code is invalid". It is easy to rule out some factors: The code works on other browsers on my laptop. The code works on other devices that are directly connected to the router. So it appears that Google also checks IP addresses besides the security code. Recently I have IPv6 enabled, so most devices that are directly connected to the router have both IPv4 and IPv6 addresses. But  I only enabled IPv4 for my LXC containers. So I guess when a code is generated by device A and used by device B, Google should be able to check that device A and device B are closely located. But in my case, IPv6 address appears on device A but not on device B, which may look suspicious. To fix the problem, I just needed to disable IPv...