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 <noreply@anthropic.com>
This commit is contained in:
tikD1337 2026-10-23 15:28:53 +02:00
parent 2c10782635
commit 156b77000c
3 changed files with 34 additions and 3 deletions

View File

@ -2715,8 +2715,14 @@ class QemuVM(KernelDatapathMixin, BaseNode):
tap = self._kernel_host_ifc(adapter_number) or self._tap_name(adapter_number) tap = self._kernel_host_ifc(adapter_number) or self._tap_name(adapter_number)
network_options.extend(["-device", f"{device_string},netdev=gns3-{adapter_number}"]) network_options.extend(["-device", f"{device_string},netdev=gns3-{adapter_number}"])
if self._tap_datapath or isinstance(nio, NIOTAP): 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( 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: else:
# Legacy datapath (uBridge without the tap module): QEMU talks # Legacy datapath (uBridge without the tap module): QEMU talks

View File

@ -29,6 +29,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from gns3server.compute.nios.nio_tap import NIOTAP
from gns3server.compute.qemu import Qemu from gns3server.compute.qemu import Qemu
from gns3server.compute.qemu.qemu_error import QemuError from gns3server.compute.qemu.qemu_error import QemuError
from gns3server.compute.qemu.qemu_vm import QemuVM 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() 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 any(option.startswith("socket,") for option in options)
assert not vm._local_udp_tunnels 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 @pytest.mark.asyncio
async def test_network_options_keep_the_legacy_socket_netdev(vm): async def test_network_options_keep_the_legacy_socket_netdev(vm):
""" """

View File

@ -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 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 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 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. port bridge, the anchor is the NIC's host side.
* two QEMU nodes on one compute report ``kernel_datapath`` when the * two QEMU nodes on one compute report ``kernel_datapath`` when the