I'm using systemd-networkd to configure network interfaces managed by libvirt for KVM (Kernel-based Virtual Machine) with Debian Bullseye on all nodes. I want to have transparent VLAN support on the virtual machines using the Linux Bridge. For the Linux Bridge this isn't supported by libvirt.

For example I have a virtual machine with three interfaces attached to the bridge with:

host ~$ virsh attach-interface guest-vm bridge br0 --config
host ~$ virsh attach-interface guest-vm bridge br0 --config
host ~$ virsh attach-interface guest-vm bridge br0 --config

Now when running the guest I will see on the bridge:

host ~$ sudo bridge link
3: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 4
30: vnet13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 100
31: vnet14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 100
32: vnet15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 100

All interfaces are successfully attached to the bridge. enp1s0 is the uplink interface on the host. Now when looking at the VLAN ids on the bridge I see:

~$ sudo bridge vlan
port              vlan-id
enp1s0            10

Only the host interface is shown with its VLAN ids.

Is there a way to also attach VLAN ids to the other interfaces vnet* of the guest so that it can use them?


I've worked on it and found a solution. it is no problem to manual add VLAN ids to the slave interfaces of a bridge, for example:

host ~$ sudo bridge vlan add vid 26 dev vnet13 pvid 26 untagged
host ~$ sudo bridge vlan add vid 30 dev vnet13
host ~$ sudo bridge vlan add vid 50 dev vnet14

host ~$ $ sudo bridge vlan
port              vlan-id
enp1s0            10
vnet13            26 PVID Egress Untagged
vnet14            50

The problem is to do it automatically on startup of the virtual machine. Fortunately libvirt supports libvirt hook scripts. I will use the hook script for qemu and do this in three steps.

Step 1: define which VLAN-ID is attached to what interface

For this we have an extra element <metadata> in Domain XML format for custom metadata. We can simply add the information to the static configuration of a domain (virtual machine):

host ~$ virsh edit guest-vm
--- snip ---
  <my:home xmlns:my="http://hoeft-online.de/libvirt">
    <my:iface pvid="26">
      <my:vlan untagged="yes">26</my:vlan>
      <my:vlan untagged="no">30</my:vlan>
      <my:vlan untagged="yes">50</my:vlan>
--- snap ---

As given by the documentation I have to use my own custom namespace <my:home xmlns:my="http://hoeft-online.de/libvirt">. Once created it is easier to work within the namespace:

host ~$ virsh metadata guest-vm http://hoeft-online.de/libvirt [--edit --key my]
  <iface pvid="26">
    <vlan untagged="yes">26</vlan>
    <vlan untagged="no">30</vlan>
    <vlan untagged="yes">50</vlan>

Step 2: get information on startup from the runtime XML-config of the domain

The hook script gets information on its standard input. This is the XML-config of the running VM. We can also get it with virsh dumpxml guest-vm when the VM is running. I use XSLT with xmlstarlet to get needed information with a xsl-stylesheet. I can test with:

host ~$ virsh dumpxml guest-vm | xmlstarlet transform /etc/libvirt/hooks/qemu.xsl | xmlstarlet format

Here is the stylesheet:

host ~$ cat /etc/libvirt/hooks/qemu.xsl
<?xml version="1.0" encoding="UTF-8"?>
<!-- This stylesheet processes the live XML configuration output from a virtual
     machine managed by libvirt. It transforms the custom metadata information
     together with attached interfaces and returns a normalized XML with VLAN
     ids attached to the interface. For further information look at the
     README file.
     Author: 2021-01-26 - Ingo Höft (Ingo@Hoeft-online.de)
     Licence: GPLv3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
     xmlns:my="http://hoeft-online.de/libvirt" exclude-result-prefixes="my">
  <xsl:output omit-xml-declaration="yes" indent="no"
       encoding="utf-8" media-type="text/xml"/>
  <xsl:strip-space elements="*"/>
  <xsl:template match="text()|@*"/>

  <xsl:template match="/domain">

  <xsl:template match='*'>
    <xsl:for-each select='interface[@type="bridge"]/target'>
      <xsl:variable name="_index" select="position()" />

      <xsl:attribute name="pvid">
          <xsl:when test="/*/metadata/my:home/my:iface[$_index]/@pvid != ''">
            <xsl:value-of select="/*/metadata/my:home/my:iface[$_index]/@pvid"/>

      <xsl:value-of select="@dev"/>

      <xsl:for-each select="/*/metadata/my:home/my:iface[$_index]/my:vlan">
          <xsl:attribute name="untagged">
              <xsl:when test="@untagged='yes'">

          <xsl:value-of select="."/>


<!-- vim: set sts=2 sw=2 et autoindent nowrap: -->

Step 3: set VLAN-ID to the dynamic virtual network interface vnet*

With information from the stylesheet we can now setup the network interface with a hook script. Make it executable.

harley$ cat /etc/libvirt/hooks/qemu
# /etc/libvirt/hooks/qemu
# Docs: https://www.libvirt.org/hooks.html

# Author: 2021-01-26 - Ingo Höft (Ingo@Hoeft-online.de)
# Licence: GPLv3 (https://www.gnu.org/licenses/gpl-3.0.en.html)

# If you save a modified hook script then do 'sudo systemctl restart libvirtd'.

# This script adds VLAN support to interfaces of libvirt guests on start up.
# For more details look at the README file.
# Most work is done with the powerful XML transformation of the XML
# configuration of the guest on stdin with qemu.xsl to get a normalized
# meta information into $META, for example like this
# (we need it to understand the script):
# <?xml version="1.0"?>
# <meta>
#   <iface pvid="26">vnet1
#     <vlan untagged="yes">26</vlan>
#     <vlan untagged="no">50</vlan>
#     <vlan untagged="no">30</vlan>
#   </iface>
#   <iface pvid="30">vnet2
#     <vlan untagged="yes">30</vlan>
#     <vlan untagged="no">50</vlan>
#   </iface>
#   <iface pvid="0">vnet3</iface>
# </meta>

# for DEBUG uncomment/comment next three lines
#exec 0< start-vdeb11-base02.xml  # for DEBUG: read testfile to stdin
# and call it with ./qemu "dummy-vm" "start" "begin" "-"


#GUEST=$1       # name of guest being started

#echo 'DEBUG: entering qemu.hook' >&2
case "$OPERATION" in
        if [[ "$SUB_OPERATION" != "begin" ]] || [[ "$EXTRA_PARM" != "-" ]]; then
            echo "Error: Unhandled parameter \$3='$SUB_OPERATION' or \$4='$EXTRA_PARM' to $0 \$1 \$2 \$3 \$4" >&2
            exit 1
        if [[ ! -x "$XMLPROG" ]]; then
            echo "Error: $XMLPROG is not executable" >&2
            exit 1

        #cat - >/var/log/libvirt/start-"$1".xml; exit 1   # get live xml for DEBUG
        META=$("$XMLPROG" tr "$XSLFILE" -)
        #echo "DEBUG: using hook start with $META" >&2

        # loop through interfaces
        while true; do
            IFACE=$(echo "$META" | "$XMLPROG" sel -t -c "/meta/iface[$IFACE_COUNT]/text()")
            if [[ -z "$IFACE" ]]; then
                # finished, no more interfaces available
                exit 0

            "$BRIDGE" link set dev "$IFACE" flood off

            # loop through vlans on one interface
            while true; do
                VLAN=$(echo "$META" | "$XMLPROG" sel -t -v "/meta/iface[$IFACE_COUNT]/vlan[$VLAN_COUNT]/text()")
                if [[ -z "$VLAN" ]]; then
                    # finished, no more vlans available, process next interface
                    UNTAGGED=$(echo "$META" | "$XMLPROG" sel -t -v "/meta/iface[$IFACE_COUNT]/vlan[$VLAN_COUNT]/@untagged")
                    if [[ "$UNTAGGED" == "yes" ]]; then
                        "$BRIDGE" vlan add vid "$VLAN" dev "$IFACE" pvid "$VLAN" untagged
                        "$BRIDGE" vlan add vid "$VLAN" dev "$IFACE"
        echo "Error: qemu hook called with unexpected options $*" >&2
        exit 1

# vim: set sts=4 sw=4 et autoindent nowrap:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.