In our previous blog, we have seen how to install KVM-QEMU VMs on Fedora 41. After installation, we can connect to the VM via GUI (virt-viewer). However, for hardcore Linux developers, GUI might be an overkill for them. A terminal is suffice for most of development scenario. Moreover, with SSH configured, you can make use of scp to facilitate file transfer across the host and the guest. If you look into Fedora's document about VM networking support, you will know that the network of VM is behind NAT, so we need either bridging or port forwarding through iptables to access service on the guest from the host.

SSH server is the exception of this rule. If we want to SSH to the guest from the host, we need to install SSH server on the guest and use either bridging / port forwarding to make the SSH server accessible from the host side. Instead of fiddling complicated iptables rules, bridging is an easier and more straight-forward method.

However, Fedora's document is silent on the setup of bridging. Instead, it provides us with a link to the libvirt's document about bridging. Click into it, we can see the following in the "Bridged networking" section.

Important Note: Unfortunately, wireless interfaces cannot be attached to a Linux host bridge, so if your connection to the external network is via a wireless interface ("wlanX"), you will not be able to use this mode of networking for your guests.

This is a bad news for many developers like the author, who only works with laptop on wireless interfaces. There is only one option left for us: port forwarding. This can be done by manipulating iptables rules, which is error-prone, intimidating and tedious (That's why we have UFW!). The author searched through the Internet via Google, but had no luck. After being tricked by answers from ChatGPT for the whole night and sick of ancient posts on Stack Exchange. I finally found a working solution for my purpose: port forwarding through userspace (passt) connection.

Since 9.0.0 an alternate backend implementation of the user interface type can be selected by setting the interface's <backend> subelement type attribute to passt. In this case, the passt transport (https://passt.top) is used. Similar to SLIRP, passt has an internal DHCP server that provides a requesting guest with one ipv4 and one ipv6 address; it then uses userspace proxies and a separate network namespace to provide outgoing UDP/TCP/ICMP sessions, and optionally redirect incoming traffic destined for the host toward the guest instead.

When the passt backend is used, the <backend> attribute logFile can be used to tell the passt process for this interface where to write its message log, and the <source> attribute dev can tell it to use a particular host interface to derive the routes given to the guest for forwarding traffic upstream. Due to the design decisions of passt, if using SELinux, the log file is recommended to reside in the runtime directory of a user under which the passt process will run, most probably /run/user/$UID where $UID is the UID of the user, e.g. qemu. Beware that libvirt does not create this directory if it does not already exist to avoid possible, however unlikely, issues, especially since this logfile attribute is meant mostly for debugging.

Additionally, when passt is used, multiple <portForward> elements can be added to forward incoming network traffic for the host to this guest interface. Each <portForward> must have a proto attribute (set to tcp or udp), optional original address (if not specified, then all incoming sessions to any host IP for the given proto/port(s) will be forwarded to the guest), and an optional dev attribute to limit the forwarded traffic to a specific host interface.

The decision of which ports to forward is described with zero or more <range> subelements of <portForward> (if there is no <range> then all ports for the given proto/address will be forwarded). Each <range> has a start and optional end attribute. If end is omitted then a single port will be forwarded, otherwise all ports between start and end (inclusive) will be forwarded. If the port number(s) should remain unmodified as the session is forwarded, no further options are needed, but if the guest is expecting the sessions on a different port, then this should be specified with the to attribute of <range> - the port number of each forwarded session in the range will be offeset by "to - start". A <range> element can also be used to specify a range of ports that should not be forwarded. This is done by setting the range's exclude attribute to yes. This may not seem very useful, but can be when it is desirable to forward a long range of ports with the exception of some subset.

...
<devices>
  ...
  <interface type='user'>
    <backend type='passt' logFile='/run/user/$UID/passt-domain.log'/>
    <mac address="00:11:22:33:44:55"/>
    <source dev='eth0'/>
    <ip family='ipv4' address='172.17.2.4' prefix='24'/>
    <ip family='ipv6' address='2001:db8:ac10:fd01::20'/>
    <portForward proto='tcp'>
      <range start='2022' to='22'/>
    </portForward>
    <portForward proto='udp' address='1.2.3.4'>
      <range start='5000' end='5020' to='6000'/>
      <range start='5010' end='5015' exclude='yes'/>
    </portForward>
    <portForward proto='tcp' address='2001:db8:ac10:fd01::1:10' dev='eth0'>
      <range start='80'/>
      <range start='443' to='344'/>
    </portForward>
  </interface>
</devices>
...

Therefore, we only need to configure the VM's network interface to work under the passt mode and tell it the forwarding rule to realize our purpose. Shutdown the VM and edit its configuration file.

$ virsh shutdown <vm-name>  # plug in your vm-name here
$ virsh edit <vm-name>
[inside editor]
...
    <interface type='user'>
      <mac address='<your-mac-or-delete-this-line-entriely>'/>
      <portForward proto='tcp'>
        <range start='2222' to='22'/>
      </portForward>
      <model type='virtio'/>
      <backend type='passt'/>
      <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
    </interface>
...

We find the interface section and modify it. Only the bold text is the one we need to add. Save and exit the editor. Then, restart libvirtd systemd service, and launch the VM. We should connect to the SSH server running in the guest.

$ sudo systemctl restart libvirtd
$ virsh start <vm-name>
$ ncat localhost 2222
SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.11

One of the most important lessons I learned from this adventure is that don't take ChatGPT's response for granted, even if your question is about informational facts only (e.g. how to call an API, how to write configuration file). Always consult the official documents. Reading them can be overwhelming at first, but it solves the problem effectively and efficiently (compared with wasting time on trials and errors due to ChatGPT's misinformation).