Skip to content

Commit 6be43c5

Browse files
committed
vpn: on Windows force NLMTU on tun interface after creation
tun.CreateTUNWithRequestedGUID only stores the requested MTU in wireguard-go's internal forcedMTU field — it does not push the value to the Windows IP stack. As a result wintun's default NLMTU of 65535 (WINTUN_MAX_IP_PACKET_SIZE) stays active, local applications see a 65535-byte interface MTU and send oversized packets, which are then silently dropped by the size check in ReadTUNPackets. This broke streaming and P2P apps (Sunshine/Moonlight) on affected machines. Force NLMTU explicitly via winipcfg.IPInterface(family).Set() — the same path the official wireguard-windows tunnel uses. Use a short retry loop because the IP interface row briefly returns ERROR_NOT_FOUND right after adapter creation. Read NLMTU back after Set() and warn on mismatch, to surface third-party LWF filters or Windows 11 MTU regressions that can silently override the value.
1 parent cf595fc commit 6be43c5

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

vpn/iface_windows.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"fmt"
88
"net"
99
"net/netip"
10+
"time"
1011

12+
"github.com/ipfs/go-log/v2"
1113
"golang.org/x/sys/windows"
1214
"golang.zx2c4.com/wireguard/tun"
1315
"golang.zx2c4.com/wireguard/windows/elevate"
@@ -28,6 +30,8 @@ func init() {
2830
}
2931

3032
func newTUN(ifname string, mtu int, localIP net.IP, ipMask net.IPMask) (tun.Device, error) {
33+
logger := log.Logger("awl/vpn")
34+
3135
var tunDevice tun.Device
3236
err := elevate.DoAsSystem(func() error {
3337
var err error
@@ -44,6 +48,23 @@ func newTUN(ifname string, mtu int, localIP net.IP, ipMask net.IPMask) (tun.Devi
4448
nativeTunDevice := tunDevice.(*tun.NativeTun)
4549
luid := winipcfg.LUID(nativeTunDevice.LUID())
4650

51+
// Wintun registers itself in NDIS with MTU=65535 (WINTUN_MAX_IP_PACKET_SIZE).
52+
// tun.CreateTUNWithRequestedGUID only stores mtu in wireguard-go's internal
53+
// forcedMTU field — it does NOT push it to the Windows IP stack. We have to
54+
// force NLMTU ourselves via winipcfg, otherwise local apps see MTU=65535,
55+
// send oversized packets, and the size check in ReadTUNPackets silently drops them.
56+
if err := setInterfaceMTU(logger, luid, winipcfg.AddressFamily(windows.AF_INET), uint32(mtu)); err != nil {
57+
tunDevice.Close()
58+
return nil, fmt.Errorf("set IPv4 MTU on tun: %v", err)
59+
}
60+
// TODO: support ipv6. Forwarding still ignores IPv6 packets (see Device.WritePacket),
61+
// but we set the system MTU best-effort so the interface is configured correctly once
62+
// IPv6 lands. On hosts with IPv6 disabled on the interface this Set() fails — that's
63+
// expected, not fatal.
64+
if err := setInterfaceMTU(logger, luid, winipcfg.AddressFamily(windows.AF_INET6), uint32(mtu)); err != nil {
65+
logger.Warnf("set IPv6 MTU on tun (best-effort, ipv6 unused by awl): %v", err)
66+
}
67+
4768
ones, _ := ipMask.Size()
4869
netipAddr := netip.MustParseAddr(localIP.String())
4970
prefix := netip.PrefixFrom(netipAddr, ones)
@@ -66,3 +87,45 @@ func (d *Device) InterfaceName() (string, error) {
6687

6788
return guid.String(), nil
6889
}
90+
91+
// setInterfaceMTU forces NLMTU on the given address family via winipcfg
92+
// (SetIpInterfaceEntry under the hood — bypasses netsh's validation, which has
93+
// regressed on some Windows 11 builds). Right after CreateAdapter the IP
94+
// interface row may briefly return ERROR_NOT_FOUND while the stack settles, so
95+
// we retry. After a successful Set() we read NLMTU back and warn if it doesn't
96+
// match — that's the signature of a third-party LWF filter or a system MTU
97+
// override silently changing our value.
98+
func setInterfaceMTU(logger *log.ZapEventLogger, luid winipcfg.LUID, family winipcfg.AddressFamily, mtu uint32) error {
99+
const attempts = 10
100+
const delay = 100 * time.Millisecond
101+
102+
var lastErr error
103+
for i := 0; i < attempts; i++ {
104+
iface, err := luid.IPInterface(family)
105+
if err != nil {
106+
lastErr = fmt.Errorf("get IPInterface: %w", err)
107+
time.Sleep(delay)
108+
continue
109+
}
110+
iface.NLMTU = mtu
111+
if err := iface.Set(); err != nil {
112+
lastErr = fmt.Errorf("set IPInterface: %w", err)
113+
time.Sleep(delay)
114+
continue
115+
}
116+
117+
verify, err := luid.IPInterface(family)
118+
if err != nil {
119+
logger.Warnf("verify NLMTU after Set (family=%d): re-read failed: %v", family, err)
120+
return nil
121+
}
122+
if verify.NLMTU != mtu {
123+
logger.Warnf("system NLMTU=%d after setting %d (family=%d) — third-party LWF filter or Windows MTU regression likely overriding the value",
124+
verify.NLMTU, mtu, family)
125+
} else {
126+
logger.Infof("tun NLMTU set to %d (family=%d)", mtu, family)
127+
}
128+
return nil
129+
}
130+
return fmt.Errorf("after %d attempts: %w", attempts, lastErr)
131+
}

0 commit comments

Comments
 (0)