Network setup for KVM guests

Host

Bridge setup

Install 'bridge-utils' :

sudo apt-get install bridge-utils                                                                                                  16h 7m

Add bridge device :

# 'br0' can be watherver you like.
brctl addbr br0

Edit '/etc/network/interfaces' to add a bridge device and configure it on startup :

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
# Change eth0 to refelct your actual if name
allow-hotplug eth0
# Disable DHCP on physical device and use it on br0 instead
iface eth0 inet manual

 # Bridge setup
iface br0 inet dhcp
  bridge_stp off
  bridge_maxwait 0
  bridge_fd      0
  # Add physical interface and all vnetX virtual interfaces (in 0-99 range)
  bridge_ports eth0 regex vnet[0-9]?[0-9]
  # You can set the bridge device MAC address with
  #bridge_hw XX:XX:XX:XX:XX:XX

If your DHCP server is configured to deliver fixed IP adresses based on MAC, replace eth0's MAC with br0's on your router in order to give it the same local IP.

Virsh : use bridge for network

Create an xml file with the following content : nano /tmp/br0.xml

<network>
  <name>br0</name>
  <forward mode="bridge"/>
  <bridge name="br0" />
</network>

Then set it as network :

virsh net-define /tmp/br0.xml

and start/autostart it :

virsh net-start br0
virsh net-autostart br0

You can also use 'virt-manager' and set your network via the XML editor.

(Optional) Add a route to your public IP range

# Edit to reflect your actual IP range and bridge device name
sudo ip route add 0.0.0.0/32 dev br0

See Configure a local and public IP below for the matching configuration on the guest.

Guest

(Optional) Basic nftables ruleset

The following ruleset accepts SSH and HTTP(s) by default.

In your '/etc/nftables.conf':

#!/usr/sbin/nft -f

flush ruleset                                                                    

table inet firewall {

    chain inbound_ipv4 {
        # accepting ping (icmp-echo-request) for diagnostic purposes.
        # However, it also lets probes discover this host is alive.
        # This sample accepts them within a certain rate limit:
        #
        # icmp type echo-request limit rate 5/second accept      
    }

    chain inbound_ipv6 {                                                         
        # accept neighbour discovery otherwise connectivity breaks
        #
        icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept

        # accepting ping (icmpv6-echo-request) for diagnostic purposes.
        # However, it also lets probes discover this host is alive.
        # This sample accepts them within a certain rate limit:
        #
        # icmpv6 type echo-request limit rate 5/second accept
    }

    chain inbound {                                                              

        # By default, drop all traffic unless it meets a filter
        # criteria specified by the rules that follow below.
        type filter hook input priority 0; policy drop;

        # Allow traffic from established and related packets, drop invalid
        ct state vmap { established : accept, related : accept, invalid : drop } 

        # Allow loopback traffic.
        iifname lo accept

        # Jump to chain according to layer 3 protocol using a verdict map
        meta protocol vmap { ip : jump inbound_ipv4, ip6 : jump inbound_ipv6 }

        # Allow SSH on port TCP/22 and allow HTTP(S) TCP/80 and TCP/443
        # for IPv4 and IPv6.
        tcp dport { 22, 80, 443} accept

        # Uncomment to enable logging of denied inbound traffic
        # log prefix "[nftables] Inbound Denied: " counter drop
    }                                                                            

    chain forward {                                                              
        # Drop everything (assumes this device is not a router)                  
        type filter hook forward priority 0; policy drop;                        
    }                                                                            

    # no need to define output chain, default policy is accept if undefined.
}

source:Simple ruleset for a server

(Optional) Configure a local and public IP

Setup a guest with two network interfaces ; one for local IP and one for public IP.
You should get two network interfaces in the host, e.g ; enp1s0 and enp2s0.

In '/etc/network/interfaces' :

auto lo
iface lo inet loopback

# Use DHCP for enp1s0 to get a LAN IP
auto enp1s0
iface enp1s0 inet dhcp

# Set static public IP on enp2s0
auto enp2s0
iface enp2s0 inet static
  # Edit to fit your range
  address 0.0.0.1
  broadcast 0.0.0.255
  netmask 255.255.255.255

Then reboot the guest to apply these settings.

Sources

https://wiki.debian.org/KVM#Setting_up_bridge_networking
https://wiki.debian.org/BridgeNetworkConnections#Configuring_bridgingin.2Fetc.2Fnetwork.2Finterfaces
https://wiki.debian.org/QEMU#Networking
https://manpages.debian.org/bookworm/bridge-utils/bridge-utils-interfaces.5.en.html
https://wiki.libvirt.org/VirtualNetworking.html

https://serverfault.com/questions/536114/libvirtd-getting-vps-to-use-existing-bridge
https://unix.stackexchange.com/questions/245628/configure-public-ip-addresses-in-kvm-vms
https://serverfault.com/questions/846834/how-to-assign-public-ip-to-kvm-virtual-machine-from-29-subnet#846986
https://serverfault.com/questions/461070/assign-public-and-private-ip-addresses-for-a-kvm-guest