Skip to content

feat(wireguard): WIREGUARD_GSO option to disable TUN vnet hdr batching - #3424

Merged
qdm12 merged 4 commits into
passteque:masterfrom
postmaxin:fix/disable-gso
Aug 5, 2026
Merged

feat(wireguard): WIREGUARD_GSO option to disable TUN vnet hdr batching#3424
qdm12 merged 4 commits into
passteque:masterfrom
postmaxin:fix/disable-gso

Conversation

@postmaxin

Copy link
Copy Markdown
Contributor

Description

Since wireguard-go introduced vectorized TUN I/O, it unconditionally opens the TUN device with IFF_VNET_HDR and uses GRO/GSO batched reads and writes, with no way to opt out. Some kernels misbehave on this path: notably vendor NAS kernels such as ASUSTOR ADM (Linux "6.6.x" — the vendor scrubs the exact version string), which advertise IFF_VNET_HDR support but reject GRO-coalesced writes with EINVAL. When that happens, wireguard-go repeatedly logs

Failed to write packets to TUN device: write /dev/net/tun: invalid argument

under load and ends up retrying packets individually, wasting up to half of the available bandwidth. The breakage is undetectable at TUN creation time: the kernel accepts the flags and only fails on write.

This PR adds WIREGUARD_DISABLE_GSO (default off, no behavior change unless opted in). When on, the TUN device is created without IFF_VNET_HDR, so wireguard-go detects no vnet hdr support and falls back to simple single-packet reads and writes — the same behavior all versions had before vectorized I/O. The AmneziaWG userspace path honors the same option.

Testing

  • Running in production on an ASUSTOR NAS (the affected platform) with ProtonVPN WireGuard. In an A/B test on the same host and mirror, stock gluetun (GSO enabled) sustained ~50 Mbit/s while this build with WIREGUARD_DISABLE_GSO=on sustained ~82 Mbit/s (indicative only — VPN server assignment may differ between connections).
  • The EINVAL log spam that motivated this change (spring 2026) no longer reproduces on the current ADM kernel, suggesting ASUSTOR has since shipped the mainline fix — but vendor kernels can carry the regression indefinitely, and the same regression hit mainline LTS (~6.6.44) for a window: kernel bisection documented in Tailscale is slow: wg: Failed to write packets to TUN device: write /dev/net/tun: invalid argument tailscale/tailscale#13041, which shows the identical symptom in Tailscale's embedded wireguard-go.
  • go build ./..., golangci-lint run (v2.4.0, 0 issues) and go test ./... pass; new table-test case covers DisableGSO=true and the default-MTU fallback in buildWireguardSettings.

A note on how this was developed

This was developed and iterated with substantial help from Anthropic Claude (implementation, AGENTS.md conformance pass, and testing against a live ASUSTOR deployment), with all changes reviewed and validated by a human. Happy to walk through any part of the design.

Issue

No matching gluetun issue found (searched for the error string, GSO, vnet_hdr, ASUSTOR). Closest ecosystem reference: tailscale/tailscale#13041 — same wireguard-go error and slowdown, bisected to a mainline kernel regression in the virtio_net_hdr GSO path.

Assertions

  • I am aware that any changes to settings should be reflected in the wiki — happy to submit a wiki PR for WIREGUARD_DISABLE_GSO once this is reviewed.

🤖 Generated with Claude Code

…tching

Since wireguard-go introduced vectorized TUN I/O, it unconditionally
opens the TUN device with IFF_VNET_HDR and uses GRO/GSO batched reads
and writes, with no way to opt out. Some kernels misbehave on this
path: notably vendor NAS kernels such as ASUSTOR ADM (Linux 6.6.x),
which advertise IFF_VNET_HDR support but reject GRO-coalesced writes
with EINVAL. When that happens, wireguard-go repeatedly logs

    Failed to write packets to TUN device: write /dev/net/tun: invalid argument

under load and ends up retrying packets individually, wasting up to
half of the available bandwidth.

The same regression hit mainline LTS kernels (~6.6.44) for a window
and is documented with a kernel bisection in
tailscale/tailscale#13041 - but vendor
kernels can carry it indefinitely, and there is no way to detect the
breakage at TUN creation time since the kernel accepts the flags and
only fails on write.

Setting WIREGUARD_DISABLE_GSO=on (default off, no behavior change
unless opted in) creates the TUN device without IFF_VNET_HDR, so
wireguard-go detects no vnet hdr support and falls back to simple
single-packet reads and writes - the same behavior all versions had
before vectorized I/O. The AmneziaWG userspace path honors the same
option.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@qdm12 qdm12 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice PR 💪
I appreciate the AI+Human effort 💯
and the fully/partly human written PR description too!

What a relief in this day and age 😸

Comment thread internal/configuration/settings/wireguard.go Outdated
Comment thread internal/configuration/settings/wireguard.go Outdated
Comment thread internal/configuration/settings/wireguard.go Outdated
Comment thread internal/wireguard/settings.go Outdated
Comment thread internal/wireguard/run.go
Comment thread internal/wireguard/offload_linux.go
postmaxin and others added 2 commits August 3, 2026 22:32
- Flip WIREGUARD_DISABLE_GSO (default off) to WIREGUARD_GSO
  (default on), and DisableGSO to GSO throughout
- Warn once, suggesting WIREGUARD_GSO=off, when wireguard-go emits the
  TUN write EINVAL error characteristic of kernels with broken
  IFF_VNET_HDR support
- Wrap the tun.CreateTUNFromFile error
- List WIREGUARD_GSO=on in the Dockerfile ENV defaults

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@postmaxin
postmaxin requested a review from qdm12 August 4, 2026 02:48
qdm12
qdm12 previously approved these changes Aug 4, 2026

@qdm12 qdm12 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! 👍

@qdm12 qdm12 changed the title feat(wireguard): add WIREGUARD_DISABLE_GSO to disable TUN vnet hdr batching feat(wireguard): WIREGUARD_GSO option to disable TUN vnet hdr batching Aug 4, 2026
@qdm12

qdm12 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Sorry to be annoying on the x-compilation compatibility, can you fix:

 > [xcompile 2/2] RUN GOOS=windows go build -o /dev/null ./...:
18.40 # github.com/qdm12/gluetun/internal/amneziawg
18.40 internal/amneziawg/run.go:45:25: undefined: amneziatun.CreateTUNFromFile

Thanks!

Move the TUN creation to a Linux build-tagged createTUN function
mirroring the wireguard package, since amneziatun.CreateTUNFromFile
only exists on Linux.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@postmaxin
postmaxin requested a review from qdm12 August 4, 2026 22:19
@qdm12
qdm12 merged commit 0186f2f into passteque:master Aug 5, 2026
7 checks passed
@qdm12

qdm12 commented Aug 5, 2026

Copy link
Copy Markdown
Member

merged, thanks! 👍

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants