Summary
On OpenWrt 25.12.5 (mediatek/filogic, aarch64_cortex-a53), the Linux TUN GRO capability probe fails with EIO, causing sing-tun to disable both TCP and UDP GRO:
WARN inbound/tun[tun-in]: disabled TUN TCP & UDP GRO due to GRO probe error: batch write: write tun: input/output error
Normal connectivity still works, but sing-tun falls back without TUN TCP/UDP GRO, which may increase CPU usage or reduce throughput under load.
Environment
DISTRIB_ID='OpenWrt'
DISTRIB_RELEASE='25.12.5'
DISTRIB_REVISION='r33051-f5dae5ece4'
DISTRIB_TARGET='mediatek/filogic'
DISTRIB_ARCH='aarch64_cortex-a53'
DISTRIB_DESCRIPTION='OpenWrt 25.12.5 r33051-f5dae5ece4'
DISTRIB_TAINTS=''
sing-box version 1.14.0-alpha.43
Environment: go1.25.11 linux/arm64
Tags: with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_usbip,badlinkname,tfogo_checklinkname0,with_musl
Revision: f025e6f163a2f438c8dad2402fb723c255521ce5
CGO: enabled
The warning was observed with a TUN inbound and a Tailscale endpoint. No explicit stack value was configured.
TUN features reported by the kernel
ethtool -k tun0 reports the relevant offload features as enabled:
tx-checksumming: on
tx-checksum-ip-generic: on
scatter-gather: on
tcp-segmentation-offload: on
tx-tcp-segmentation: on
tx-tcp6-segmentation: on
generic-segmentation-offload: on
generic-receive-offload: on
tx-udp-segmentation: on
rx-gro-list: on
This suggests that the kernel advertises support for the TUN offload path, but the actual probe write fails.
Suspected cause
The current Linux startup order appears to run the GRO probe before bringing the TUN link up:
NativeTun.Start() calls probeTCPGRO() when vnetHdr is enabled.
- Only afterwards does it call
t.start().
t.start() is where netlink.LinkSetUp(tunLink) is performed.
Relevant code:
Tailscale also disables GRO only when the probe returns EINVAL, while sing-tun currently disables it for any error, including EIO.
On this OpenWrt system, writing the probe packet while the link is still down may be the reason for:
write tun: input/output error
This is a hypothesis based on the startup order and kernel feature output; it has not yet been confirmed with a patched build.
Expected behavior
If the kernel and TUN device support the required offloads, the probe should pass and TCP/UDP GRO should remain enabled.
If the probe is performed before the interface is ready, a transient readiness error should not necessarily be treated as proof that GRO is unsupported.
Possible fix
Run probeTCPGRO() only after the TUN interface has been brought up, similar to Tailscale's documented requirement.
Alternatively or additionally, consider distinguishing unsupported-feature errors such as EINVAL from transient or ordering-related errors such as EIO before permanently disabling TCP and UDP GRO for the process lifetime.
I can test a patch that moves the probe after LinkSetUp if helpful.
Summary
On OpenWrt 25.12.5 (
mediatek/filogic,aarch64_cortex-a53), the Linux TUN GRO capability probe fails withEIO, causing sing-tun to disable both TCP and UDP GRO:Normal connectivity still works, but sing-tun falls back without TUN TCP/UDP GRO, which may increase CPU usage or reduce throughput under load.
Environment
The warning was observed with a TUN inbound and a Tailscale endpoint. No explicit
stackvalue was configured.TUN features reported by the kernel
ethtool -k tun0reports the relevant offload features as enabled:This suggests that the kernel advertises support for the TUN offload path, but the actual probe write fails.
Suspected cause
The current Linux startup order appears to run the GRO probe before bringing the TUN link up:
NativeTun.Start()callsprobeTCPGRO()whenvnetHdris enabled.t.start().t.start()is wherenetlink.LinkSetUp(tunLink)is performed.Relevant code:
NativeTun.Start(): https://github.com/SagerNet/sing-tun/blob/main/tun_linux.gohttps://github.com/tailscale/tailscale/blob/main/net/tstun/wrap_linux.go
Tailscale also disables GRO only when the probe returns
EINVAL, while sing-tun currently disables it for any error, includingEIO.On this OpenWrt system, writing the probe packet while the link is still down may be the reason for:
This is a hypothesis based on the startup order and kernel feature output; it has not yet been confirmed with a patched build.
Expected behavior
If the kernel and TUN device support the required offloads, the probe should pass and TCP/UDP GRO should remain enabled.
If the probe is performed before the interface is ready, a transient readiness error should not necessarily be treated as proof that GRO is unsupported.
Possible fix
Run
probeTCPGRO()only after the TUN interface has been brought up, similar to Tailscale's documented requirement.Alternatively or additionally, consider distinguishing unsupported-feature errors such as
EINVALfrom transient or ordering-related errors such asEIObefore permanently disabling TCP and UDP GRO for the process lifetime.I can test a patch that moves the probe after
LinkSetUpif helpful.