Fixing painfully slow GNS3 Cloud throughput with one ethtool command
Table of Contents
The setup
I was building a lab in GNS3 to test a switch-provisioning system: a controller VM plus some switches, all wired to the outside world through Cloud nodes bound to a physical NIC on the GNS3 host.
Everything worked, until I had to copy a ~2.5 GB SONiC image onto the controller VM. The upload crawled. My first instinct was the same as everyone’s: “it’s the network virtualization overhead”. It wasn’t — or at least, not in the way I expected.
Measuring instead of guessing
Before touching anything, I measured throughput to different destinations with a trivial dd | ssh 'cat > file' test:
| Path | Throughput |
|---|---|
| my host → GNS3 host (bare metal, physical NIC) | ~66 MB/s |
| my host → controller VM (behind a Cloud node) | ~1.5 MB/s |
| controller VM → sentinel VM (VM-to-VM, same host) | ~100 MB/s |
That table is the whole story:
- The physical host is fast.
- VM-to-VM inside the topology is fast.
- Only traffic entering a VM from outside through a Cloud node is slow.
So it is not the CPU emulation, not the disk, not the routing. Both fast and slow paths even share the same default gateway. The one thing unique to the slow path is the Cloud node bridging external frames into the VM.
Ruling out the usual suspects
The GNS3 community’s classic “slow performance” advice is about QEMU acceleration and NIC models, so I checked them all on the host:
# KVM acceleration present and used?
ls -l /dev/kvm # crw-rw---- ... /dev/kvm ✔
ps -eo args | grep qemu-system | tr ' ' '\n' | grep enable-kvm # -enable-kvm ✔
systemd-detect-virt # none (bare metal, so nested virt isn't a problem) ✔
# Guest NIC model actually virtio (not e1000)?
ethtool -i ens3 | grep driver # driver: virtio_net ✔
KVM on, virtio on, no CPU throttling, bare-metal host. The VM was fully accelerated and still slow from outside. The problem had to be the Cloud/pcap path.
The actual cause: NIC offloading vs. ubridge
A GNS3 Cloud node bound to a physical interface doesn’t use kernel bridging. It uses ubridge, which captures frames off the host NIC with pcap and injects them into the VM’s tap interface.
Modern NICs enable offloading by default:
- GRO (generic-receive-offload) and LRO (large-receive-offload) — the kernel coalesces many incoming TCP segments into one giant “super-frame” (up to 64 KB) before software sees it.
- TSO/GSO (TCP/generic-segmentation-offload) — the reverse, on transmit.
Here is the trap. With GRO enabled, ubridge’s pcap capture receives those 64 KB coalesced frames and forwards them to the VM’s tap — where they blow straight past the 1500-byte MTU and get dropped or mangled. TCP reacts the only way it can: relentless retransmission and a collapsed congestion window. The result is the ~1.5 MB/s I was seeing.
Disable the offloads and every frame stays ≤ 1500 bytes end-to-end, so nothing is dropped on the way into the VM.
The fix
One command on the GNS3 host, against the physical interface your Cloud node is bound to (mine is enp1s0f0):
sudo ethtool -K enp1s0f0 gro off gso off tso off
The effect was immediate. I watched the live rsync progress meter jump the instant I ran it:
513,376,256 20% 1.58MB/s 0:20:32 <-- offloads on
...
1,877,671,936 74% 82.94MB/s 0:00:07 <-- offloads off
From ~1.5 MB/s to ~83 MB/s — a ~50× improvement — with no topology changes. The 2.5 GB image finished in seconds instead of the projected 40+ minutes.
Diagnosing it yourself
Check the offloads on the interface your Cloud is bridged to:
sudo ethtool -k enp1s0f0 | grep -E \
'generic-receive-offload|large-receive-offload|tcp-segmentation|generic-segmentation'
If generic-receive-offload, tcp-segmentation-offload or generic-segmentation-offload show on, you are very likely hitting this. Find which physical interface a Cloud node uses via the GNS3 API:
PID=<project-id>
curl -s http://127.0.0.1:3080/v2/projects/$PID/nodes \
| python3 -c 'import sys,json
for n in json.load(sys.stdin):
if n["node_type"]=="cloud":
for p in n["properties"].get("ports_mapping",[]):
print(n["name"], "->", p.get("interface"))'
Making it persistent
ethtool -K does not survive a reboot or a NIC reset. To make it durable, drop in a tiny systemd unit on the GNS3 host:
# /etc/systemd/system/gns3-nic-offload-off.service
[Unit]
Description=Disable NIC offloading for GNS3 Cloud interfaces
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool -K enp1s0f0 gro off gso off tso off
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now gns3-nic-offload-off.service
If you bridge more than one physical interface into your topology, add an ExecStart line for each.
Takeaways
- Measure the matrix, don’t guess. Comparing host vs. VM vs. VM-to-VM immediately localised the problem to “external → Cloud”, saving me from chasing QEMU, disks and routing.
- “Network virtualization is just slow” is rarely the real answer. Here the emulated data path was perfectly capable of 100 MB/s VM-to-VM; the killer was a NIC feature interacting badly with pcap.
- GRO/GSO/TSO + pcap bridging is a known footgun. Any time userspace pcap sits between a NIC and a virtual interface with a hard MTU — GNS3 Cloud nodes, some tcpdump-in-the-middle setups, certain bridge configs — offloading can silently destroy throughput.
- One
ethtool -K … gro off gso off tso offis cheap to try and easy to revert. Make it the first thing you reach for.
Happy labbing.