From 156b77000ce9e45dad8cd57a168cb754dfb34f11 Mon Sep 17 00:00:00 2001 From: tikD1337 <199419268+tikD1337@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:28:53 +0200 Subject: [PATCH] fix(qemu): open the adapter TAP without a vnet header The TAP netdev kept QEMU's default vnet_hdr, so virtio-net, e1000e, igb and vmxnet3 guests offload TCP/UDP checksums (and TSO) to the TAP: their frames carry only the pseudo-header sum in the checksum field. A kernel bridge link finishes them, but the uBridge relay (cloud, NAT, hub, ...) reads the TAP with libpcap and forwards the frames as they are, so DNS and TCP through a cloud fail while ICMP and DHCP work. With vnet_hdr=off QEMU stops offering the offloads and the guest (or the emulated NIC) sends finished frames, as with the 2.x socket netdev. Co-Authored-By: Claude Opus 5.5 --- gns3server/compute/qemu/qemu_vm.py | 8 +++++- .../compute/qemu/test_qemu_kernel_datapath.py | 27 ++++++++++++++++++- tests/e2e/test_qemu_kernel_datapath.py | 2 +- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 214238408..1f142f158 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -2715,8 +2715,14 @@ class QemuVM(KernelDatapathMixin, BaseNode): tap = self._kernel_host_ifc(adapter_number) or self._tap_name(adapter_number) network_options.extend(["-device", f"{device_string},netdev=gns3-{adapter_number}"]) if self._tap_datapath or isinstance(nio, NIOTAP): + # vnet_hdr=off: with QEMU's default virtio-net header the + # guest NIC offloads TCP/UDP checksums and TSO to the TAP. + # A kernel bridge finishes such frames, but the uBridge relay + # (cloud, NAT, ...) copies them off the TAP as they are, so + # receivers drop them. Without the header QEMU and the guest + # send finished frames, as with the 2.x socket netdev. network_options.extend( - ["-netdev", f"tap,id=gns3-{adapter_number},ifname={tap},script=no,downscript=no"] + ["-netdev", f"tap,id=gns3-{adapter_number},ifname={tap},script=no,downscript=no,vnet_hdr=off"] ) else: # Legacy datapath (uBridge without the tap module): QEMU talks diff --git a/tests/compute/qemu/test_qemu_kernel_datapath.py b/tests/compute/qemu/test_qemu_kernel_datapath.py index a2d57592a..4c0cb1943 100644 --- a/tests/compute/qemu/test_qemu_kernel_datapath.py +++ b/tests/compute/qemu/test_qemu_kernel_datapath.py @@ -29,6 +29,7 @@ from unittest.mock import MagicMock, patch import pytest import pytest_asyncio +from gns3server.compute.nios.nio_tap import NIOTAP from gns3server.compute.qemu import Qemu from gns3server.compute.qemu.qemu_error import QemuError from gns3server.compute.qemu.qemu_vm import QemuVM @@ -143,11 +144,35 @@ async def test_network_options_use_the_tap_anchor(vm): options = await vm._network_options() - assert f"tap,id=gns3-0,ifname={TAP0},script=no,downscript=no" in options + assert f"tap,id=gns3-0,ifname={TAP0},script=no,downscript=no,vnet_hdr=off" in options assert not any(option.startswith("socket,") for option in options) assert not vm._local_udp_tunnels +@pytest.mark.asyncio +async def test_network_options_turn_off_the_tap_vnet_header(vm): + """ + Every TAP netdev turns the virtio-net header off, the legacy NIOTAP one + included. With QEMU's default header the guest NIC hands its TCP/UDP + checksums (and TSO) to the TAP, and the uBridge relay copies those + frames off the TAP unfinished: receivers behind a cloud drop them. + """ + + vm._tap_datapath = True + vm.adapters = 2 + vm.adapter_type = "virtio-net-pci" + vm._kernel_taps[(0, 0)] = TAP0 + vm._ethernet_adapters[1].add_nio(0, NIOTAP("tap-legacy")) + + options = await vm._network_options() + + netdevs = [options[i + 1] for i, option in enumerate(options) if option == "-netdev"] + assert netdevs == [ + f"tap,id=gns3-0,ifname={TAP0},script=no,downscript=no,vnet_hdr=off", + "tap,id=gns3-1,ifname=tap-legacy,script=no,downscript=no,vnet_hdr=off", + ] + + @pytest.mark.asyncio async def test_network_options_keep_the_legacy_socket_netdev(vm): """ diff --git a/tests/e2e/test_qemu_kernel_datapath.py b/tests/e2e/test_qemu_kernel_datapath.py index b3c18296b..7134349f3 100644 --- a/tests/e2e/test_qemu_kernel_datapath.py +++ b/tests/e2e/test_qemu_kernel_datapath.py @@ -11,7 +11,7 @@ binary, the real IOS CLI behind the server's telnet console), driven through the controller REST API. QEMU is the node type whose guest leg *is* the anchor: uBridge creates one persistent TAP per adapter before launch and hands it to this user, and QEMU opens it by ifname as its netdev -(``-netdev tap,ifname=...,script=no,downscript=no``) — no userspace hop, no +(``-netdev tap,ifname=...,script=no,downscript=no,vnet_hdr=off``) — no userspace hop, no port bridge, the anchor is the NIC's host side. * two QEMU nodes on one compute report ``kernel_datapath`` when the