Why I Worked on This
I run a 10Gbps homelab network with multiple Proxmox nodes, containers, and VMs. Tailscale ties everything together—remote access, subnet routing between VLANs, and secure connections from outside. It’s been reliable, but I noticed something wrong when I started pushing real traffic through a subnet router.
Transferring large files between nodes over Tailscale was painfully slow. I’d see 200-300 Mbps when I expected closer to gigabit speeds, at least. The CPU wasn’t maxed out. The network interfaces were 10G. Something else was bottlenecking.
After digging into packet captures and kernel stats, I found two culprits: MTU mismatches causing fragmentation, and default kernel parameters that weren’t tuned for high-throughput UDP forwarding. Fixing both made a massive difference.
My Real Setup
Here’s what I was working with:
- Proxmox VE 8.x nodes running Linux kernel 6.8
- Tailscale 1.76.x installed on a dedicated LXC container acting as a subnet router
- 10Gbps fiber between nodes (Mellanox ConnectX-3 NICs)
- Multiple VLANs routed through the Tailscale subnet router
- Direct Tailscale connections between nodes (no DERP relay)
The subnet router was an unprivileged LXC container with 4 vCPUs and 4GB RAM. It advertised routes for my internal networks and handled all WireGuard encapsulation for devices connecting from outside.
The MTU Problem
Tailscale defaults to an MTU of 1280. This is conservative and works everywhere, but it’s not optimal for local networks with standard 1500-byte MTU paths. When packets hit the Tailscale interface, they get fragmented if they’re larger than 1280 bytes. Fragmentation adds overhead and slows everything down.
I confirmed this by running:
ip link show tailscale0
The MTU was 1280. My physical interface (eth0) was 1500. Every packet over 1280 bytes was being split, forwarded, and reassembled on the other side. That’s a lot of wasted cycles.
I manually set the MTU to 1500:
sudo ip link set dev tailscale0 mtu 1500
Speeds improved immediately—700-800 Mbps instead of 200-300. But the setting didn’t survive a reboot. Tailscale recreates the interface on startup, and it resets to 1280 every time.
Making MTU Changes Persistent
I needed a way to apply the MTU change automatically whenever the Tailscale interface came up. I used a udev rule to trigger a script:
sudo nano /etc/udev/rules.d/99-tailscale-mtu.rules
I added:
ACTION=="add", SUBSYSTEM=="net", KERNEL=="tailscale0", RUN+="/usr/local/bin/set-tailscale-mtu.sh"
Then I created the script:
sudo nano /usr/local/bin/set-tailscale-mtu.sh
Contents:
#!/bin/bash
ip link set dev tailscale0 mtu 1500
Made it executable:
sudo chmod +x /usr/local/bin/set-tailscale-mtu.sh
Reloaded udev rules:
sudo udevadm control --reload-rules
sudo udevadm trigger
After a reboot, the MTU stuck at 1500. This worked across all my Tailscale nodes.
The Kernel Parameter Problem
Even with the MTU fixed, speeds were still lower than expected—around 800 Mbps instead of multi-gigabit. I checked ethtool settings and found that UDP GRO (Generic Receive Offload) forwarding wasn’t enabled.
Tailscale uses WireGuard, which runs over UDP. By default, Linux doesn’t optimize UDP forwarding the same way it does for TCP. For high-speed subnet routing, you need to enable specific offload features.
I found the relevant settings in Tailscale’s documentation and kernel docs. The key parameters are:
rx-udp-gro-forwarding: Allows the NIC to aggregate UDP packets before the kernel processes themrx-gro-list: A different aggregation method that can conflict with GRO forwarding
I ran:
NETDEV=$(ip -o route get 8.8.8.8 | cut -f 5 -d " ")
sudo ethtool -K $NETDEV rx-udp-gro-forwarding on rx-gro-list off
This identified my primary network interface and enabled the right offloads. Speeds jumped to 2-3 Gbps immediately.
Making Kernel Changes Persistent
Like the MTU fix, ethtool changes don’t survive reboots by default. I needed to apply them automatically on boot.
I checked if my system used networkd-dispatcher:
systemctl is-enabled networkd-dispatcher
It did. So I created a script that runs when the network comes up:
printf '#!/bin/sh
ethtool -K %s rx-udp-gro-forwarding on rx-gro-list off
' "$(ip -o route get 8.8.8.8 | cut -f 5 -d " ")" | sudo tee /etc/networkd-dispatcher/routable.d/50-tailscale
Made it executable:
sudo chmod 755 /etc/networkd-dispatcher/routable.d/50-tailscale
Tested it:
sudo /etc/networkd-dispatcher/routable.d/50-tailscale
test $? -eq 0 || echo 'An error occurred.'
No errors. Rebooted and confirmed the settings were applied automatically.
What Worked
After both fixes, my subnet router was pushing 4-5 Gbps sustained throughput over Tailscale. That’s close to what I’d expect given WireGuard’s encryption overhead and the fact that this was a single TCP stream.
Key improvements:
- MTU set to 1500 eliminated fragmentation
- UDP GRO forwarding reduced CPU load by ~30%
- Persistent configuration meant no manual intervention after reboots
I tested this across multiple nodes and containers. The setup was stable. No packet loss, no weird latency spikes.
What Didn’t Work
I tried setting the MTU higher than 1500 (jumbo frames) since my physical network supports 9000-byte MTU. This broke everything. Tailscale couldn’t establish stable connections, and I saw constant packet loss.
The issue is that Tailscale’s control plane and DERP relays don’t support jumbo frames. Even if your local network does, the path between nodes might not. Stick with 1500 unless you’re absolutely certain every hop supports larger MTUs.
I also tried disabling rx-gro-list without enabling rx-udp-gro-forwarding. Performance got worse. Both settings need to be configured together.
Finally, I tested this on an older kernel (5.15). The UDP GRO forwarding feature wasn’t available, and I couldn’t get the same performance gains. Kernel 6.2+ is required for these optimizations to work.
Key Takeaways
- Tailscale’s default MTU (1280) is safe but slow for local networks. Increasing it to 1500 helps if your network supports it.
- Kernel 6.2+ with UDP GRO forwarding makes a huge difference for subnet routers handling high throughput.
- Both MTU and kernel parameter changes need to be made persistent, or they’ll reset on reboot.
- Jumbo frames (MTU > 1500) don’t work reliably with Tailscale unless your entire path supports them, which is rare.
- Testing is critical. What works on one network might break another. Measure before and after.
These changes turned my Tailscale subnet router from a bottleneck into something that actually uses my 10Gbps network. The setup is stable, repeatable, and doesn’t require ongoing maintenance.