Multiple bridges for KVM virtualization

3 minute read

In a previous post, I went over how to create one brdige interface for the hosting of VMs.

Now I didn’t feel confortable having one VM exposed to the Internet in the same internal network than my other VMs.Therefore, I wanted to go for a more professionally approach: DMZ!

So, given that my host has 4 networking interfaces, I plugged another cable in it, added a DMZ VLAN with proper restrictions and searched the internet for a way to create another bridged interface for KVM, in case I needed to create more VMs in that DMZ.

And I couldn’t find any that applied to KVM hosting! Here is how I managed to pull it off.

Netplan

The first thing to do is to modify the netplan configuration file to include a new bridge called br-dmz:

network:
  renderer: networkd
  ethernets:
    eno1:
      dhcp4: false
      dhcp6: false
    eno2:
      dhcp4: false
      dhcp6: false
    eno3:
      dhcp4: true
    eno4:
      dhcp4: true
  version: 2

  bridges:
    br0:
      addresses: [10.0.20.2/24]
      routes:
        - to: default
          via: 10.0.20.1
          metric: 100
      mtu: 1500
      nameservers:
        addresses:
          - 10.0.20.1
      parameters:
        stp: true
        forward-delay: 2
      dhcp4: false
      dhcp6: false
      interfaces: [eno1]
    br-dmz:
      addresses: [10.0.21.2/24]
      mtu: 1500
      nameservers:
        addresses:
          - 10.0.25.1
      parameters:
        stp: true
        forward-delay: 2
      dhcp4: false
      dhcp6: false
      interfaces: [eno2]

The attentive reader has noticed that we don’t have a default route with br-dmz. And netplan will not allow you too! This actually makes sense because the default route is suppposed to be the complete default for all routes so another definition would just result in confusion.

The problem

So I tried this configuration, I was happy, I could reach my DMZ VM, ping the router, tried the NFS shares… but wait! Sometimes, the NFS shares would just hang, and sometimes it would work nicely. What is happening?

Well, the issue was that the packets from the DMZ were leaving via br-dmz but were sometimes, and sometimes only, come back via the br0 interface! This is probably because br0 contains the default route, which would mean that it’s the default for the out but also for the in (?).

To confirm this, I used tcpdump on both interfaces on the 2049 port and accessing an NFS share and could sometimes see packets destined to the DMZ network on the br0 interface.

The solution

The solution came from a post that I read, and basically said this: on Linux, it is possible to tag the packets when leaving so that when they come back, the kernel does force it back to the interface which is part of the tagging. Linux is so awesome, it’s so satisfying knowing you can go deeper and solve such a tricky problem!

So, first, we need to create a new routing table which will hold the tagging. Create a file /etc/iproute2/rt_tables.d/tagging_br-dmz_to_assure_return.conf and add the following:

200 dmz

So now, we can create the rules that will say: “when coming from this interface, use the 200 dmz routing table (tag) and when coming back on this interface, use the same routing table”. Also, now that we define a new routing, we can add a default route for that table only!

1user $ sudo ip rule add from 10.0.21.0/24 table dmz prio 1
2user $ sudo ip rule add to 10.0.21.0/24 table dmz prio 1
3user $ sudo ip route add default via 10.0.21.1 dev br-dmz table dmz

Making this persistent

I have not yet tested this given that I rarely restart my host (I have kernel live patching), but I have implemented it and if you see this message, this probably means that it’s working, otherwise, I would correct the following.

Create a shell script with the ip commands /usr/local/sbin/dmz_ip_commands.sh (don’t forget to chmod +x the script):

1#!/bin/bash
2ip rule add from 10.0.21.0/24 table dmz prio 1
3ip rule add to 10.0.21.0/24 table dmz prio 1
4ip route add default via 10.0.21.1 dev br-dmz table dmz

Create a systemd unit file /etc/systemd/system/dmz-multiple-bridges-ip-commands.service (don’t forget to reload the systemd daemon):

 1[Unit]
 2Description=DMZ bridge ip commands
 3After=network.target
 4
 5[Service]
 6User=root
 7ExecStart=/usr/local/sbin/dmz_ip_commands.sh
 8Type=oneshot
 9
10[Install]
11WantedBy=multi-user.target