Skip to content

Instantly share code, notes, and snippets.

@joshenders
Last active August 27, 2025 22:50
Show Gist options
  • Save joshenders/1baa9de07c1b7af489f14c30d4667e40 to your computer and use it in GitHub Desktop.
Save joshenders/1baa9de07c1b7af489f14c30d4667e40 to your computer and use it in GitHub Desktop.
How to improve tailscale throughput via transport layer offloading in OpenWrt 24.10

How to improve tailscale throughput via transport layer offloading in OpenWrt 24.10

Tailscale version 1.54 or later used with OpenWrt 24.10 or later (which uses kernel 6.6) enables UDP throughput improvements via transport layer offloading.

Namely, tuning two features may show improved throughput:

  • rx-udp-gro-forwarding: Enables UDP Generic Receive Offload (GRO) forwarding, which aggregates incoming UDP packets to reduce CPU overhead on receive.
  • rx-gro-list: If disabled (off), it prevents multiple flows from being aggregated simultaneously which simplifies flow handling and performance on some workloads.

Note

These changes should be applied to the physical device(s) which will actually be performing the UDP encapsulation of tailscale traffic.

  1. Install ethtool
opkg update
opkg install ethtool
  1. Apply the changes:

Note

Substitute eth1 below for the device that routes your Tailscale traffic. In most configurations, this is your WAN device.

ethtool -K eth1 rx-gro-list off
ethtool -K eth1 rx-udp-gro-forwarding on
  1. Test the changes before and after before committing them permanently with something similar to the following commands.

You want to verify:

  • Packet aggregation is working as measured by reduced packets/sec on the wire with GRO enabled (verify with tools like: ethtool -S <interface> | grep udp or netstat -su)
  • CPU usage is reduced. Lower CPU usage on the receiver compared to the same test with rx-udp-gro-forwarding turned off
  • High throughput is achieved near line rate (e.g., 1 Gbps, 10Gbps, etc) without packetloss.

Note

You will need the iperf3 package installed for this. The sender side flags below are just a suggestion. Full iperf3 usage is outside the scope of this document.

Receiver

iperf3 --server

Sender

iperf3 --client <remote_addr> --udp --bitrate 1G --length 1400 --time 10

If you're satisfied with the results and want it to persist across reboots.

  1. Create /etc/config/ethtool ether using uci or by creating the file manually. The following example will use uci:

Note

Substitute eth1 below for the device that routes your Tailscale traffic. In most configurations, this is your WAN device.

touch /etc/config/ethtool
uci set ethtool.eth1=device
uci set ethtool.eth1.rx_gro_list='off'
uci set ethtool.eth1.rx_udp_gro_forwarding='on'
uci commit
  1. Create the following file in /etc/hotplug.d/iface/90-ethtool
#!/bin/sh
# shellcheck disable=SC3043
#
# Author: Josh Enders <[email protected]>
# License: CC BY-NC 4.0
# https://gist.github.com/joshenders/1baa9de07c1b7af489f14c30d4667e40

[ "${ACTION}" = "ifup" ] || exit 0

# shellcheck source=/dev/null
. /lib/functions.sh

config_load ethtool

log_crit() { logger -t "$0" -p crit "$1"; }
log_info() { logger -t "$0" -p info "$1"; }

apply_settings() {
    local config feature ifname option value
    ifname="$1"
    config=$(uci show ethtool."${ifname}" | sed -n "s/^ethtool.${ifname}\.\([^=]*\)=.*/\1/p")

    for option in ${config}; do
        config_get value "${ifname}" "${option}"
        feature=$(echo "${option}" | tr '_' '-')
        if [ -n "${value}" ]; then
            {
                ethtool -K "${ifname}" "${feature}" "${value}" \
                && log_info "${feature} set to ${value} on ${ifname}";
            } || log_crit "Failed to set ${feature} to ${value} on ${ifname}"
        else
            log_crit "Failed to set ${feature} to ${value} on ${ifname}"
        fi
    done
}

config_foreach apply_settings device
  1. Append /etc/hotplug.d/iface/90-ethtool to /etc/sysupgrade.conf to preserve this file during upgrades.
echo '/etc/hotplug.d/iface/90-ethtool' >> /etc/sysupgrade.conf
@jonathonadler
Copy link

jonathonadler commented Aug 25, 2025

Hi Josh. Thanks for this! I think I may have found a couple of issues.

  1. The ethtool section has the interface in the wrong position:
ethtool -K rx-gro-list off eth1
ethtool -K rx-udp-gro-forwarding on eth1

I think it should be like this:

ethtool -K eth1 rx-gro-list off
ethtool -K eth1 rx-udp-gro-forwarding on

As per the tailscale kb (https://tailscale.com/kb/1320/performance-best-practices#ethtool-configuration), this can be automated too.

NETDEV=$(ip -o route get 8.8.8.8 | cut -f 5 -d " ")
ethtool -K $NETDEV rx-udp-gro-forwarding on rx-gro-list off
  1. My interface name is "pppoe-wan", containing a dash/hyphen. UCI doesn't like this! Is it possible to change the hotplug?
root@Router:~# uci commit
uci: Parse error (invalid character in name field) at line 2, byte 26

@joshenders
Copy link
Author

joshenders commented Aug 26, 2025

  1. The ethtool section has the interface in the wrong position:
ethtool -K rx-gro-list off eth1
ethtool -K rx-udp-gro-forwarding on eth1

I think it should be like this:

ethtool -K eth1 rx-gro-list off
ethtool -K eth1 rx-udp-gro-forwarding on

Good catch. I've updated the gist. Thank you for pointing that out!

As per the tailscale kb (https://tailscale.com/kb/1320/performance-best-practices#ethtool-configuration), this can be automated too.

NETDEV=$(ip -o route get 8.8.8.8 | cut -f 5 -d " ")
ethtool -K $NETDEV rx-udp-gro-forwarding on rx-gro-list off

When you say "automated" do you mean one could use the server's default route to automatically determine which interface to apply the ethtool settings to? If so, I did notice this but I think it actually might be best to allow users to explicitly specify the interface to apply the settings. Intuition tells me that this may actually pick the wrong interface (multi-wan, etc) for exotic setups.

  1. My interface name is "pppoe-wan", containing a dash/hyphen. UCI doesn't like this! Is it possible to change the hotplug?
root@Router:~# uci commit
uci: Parse error (invalid character in name field) at line 2, byte 26

Question: Is the ppoe-wan interface actually the interface name in Linux (e.g as seen in the output of ip a s) or is it in the interface name defined in /etc/config/network?

If it's the name defined in /etc/config/network, use the option device <iface> value instead. As far as I know, udev on OpenWrt doesn't support renaming interfaces and UCI keys do not allow hypens in the name, so I'd have to modify the hotplug drop-in.

Happy to do that if the device name actually contains hyphens, that'd definitely be a bug!

@jonathonadler
Copy link

When you say "automated" do you mean one could use the server's default route to automatically determine which interface apply the ethtool settings to? If so, I did notice this but I think it actually might be best to allow users to explicitly specify the interface to apply the settings. Intuition tells me that this may actually pick the wrong interface (multi-wan, etc) for exotic setups.

Yes, thats what I meant. Fair enough!

Question: Is the ppoe-wan interface actually the interface name in Linux (e.g as seen in the output of ip a s) or is it in the interface name defined in /etc/config/network?

Yes, this is the interface name.

$ ip a s
...snip...
17: pppoe-wan: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 qdisc fq_codel state UNKNOWN group default qlen 3
    link/ppp
    inet <REDACT> peer <REDACT>/32 scope global pppoe-wan
       valid_lft forever preferred_lft forever
19: tailscale0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none
    inet <REDACT>/32 scope global tailscale0
       valid_lft forever preferred_lft forever
    inet6 <REDACT>/128 scope global
       valid_lft forever preferred_lft forever
    inet6 <REDACT>/64 scope link stable-privacy proto kernel_ll
       valid_lft forever preferred_lft forever
...snip...

Here is /etc/config/network section.

config interface 'wan'
	option proto 'pppoe'
	option password '<REDACT>'
	option username '<REDACT>'
	option ipv6 '0'
	option device 'eth0'
	option delegate '0'

You gave me an idea! I checked https://github.com/openwrt/openwrt/blob/master/package/network/services/ppp/files/ppp.sh. The default iface name will have a dash:

[ -n "$pppname" ] || pppname="${proto:-ppp}-$config"

But there is a parameter I wasn't aware of called pppname. After adding it to network config, the name is now good! Thanks for your support.

config interface 'wan'
	option proto 'pppoe'
	option password '<REDACT>'
	option username '<REDACT>'
	option ipv6 '0'
	option device 'eth0'
	option delegate '0'
	option pppname 'wan'
root@Router:~# /etc/init.d/network restart
root@Router:~# ip -o route get 8.8.8.8 | cut -f 5 -d " "
wan

@joshenders
Copy link
Author

joshenders commented Aug 27, 2025

In your /etc/config/network, the wan interface is using the eth0 device:

config device 'eth0'

I don’t have an easy way to test this but I’m somewhat skeptical the hotplug drop-in will ever run if you define:

uci set ethtool.wan=device

Are there ifup hotplug events for those? Is the wan interface in Linux just an alias of the parent device and does ethtool reference or resolve that the settings need to be applied to the parent interface? Honestly, I’m not sure.

Have you tested rebooting and verifying the ethtool settings are being properly set?

In your case, (imho) irregardless of all the interface aliasing sugar that OpenWrt does to simplify configuration, the ethtool settings should probably be applied to the eth0 interface as that’s the actual physical device of your ppoe connection.

That’s being said, your feedback is really helpful. If there could ever be a - in the device name, my drop-in will fail as it did for you. I’ll go ahead and add a utility function to transform the device name into a uci-safe value. There may even be an existing helper function in /lib/functions.sh.

@jonathonadler
Copy link

No worries.

Have you tested rebooting and verifying the ethtool settings are being properly set?

Yep. It does seem to be working properly. :) [I haven't performed the iperf3 test yet though]

root@Router:~# ip a s
...snip...
14: wan: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 qdisc fq_codel state UNKNOWN group default qlen 3
    link/ppp
    inet <REDACTED> peer <REDACTED>/32 scope global wan
       valid_lft forever preferred_lft forever
...snip...

root@Router:~# cat /etc/config/ethtool
config device 'wan'
	option rx_gro_list 'off'
	option rx_udp_gro_forwarding 'on'

root@Router:~# logread
...snip...
Wed Aug 27 08:50:34 2025 user.notice firewall: Reloading firewall due to ifup of wan (wan)
Wed Aug 27 08:50:35 2025 user.info /sbin/hotplug-call: rx-gro-list set to off on wan
Wed Aug 27 08:50:35 2025 user.info /sbin/hotplug-call: rx-udp-gro-forwarding set to on on wan
...snip...

@jonathonadler
Copy link

P.S. You are probably correct about applying ethtool to eth0 interface rather than pppoe connection.

@joshenders
Copy link
Author

Could you show me the ethtool output for those flags on your eth0 interface?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment