microsoft / microsoft/WSL

networkingMode=mirrored: mirrored host adapter MTU creates a PMTUD black hole (no ICMP Frag Needed)

Open
#41,419 8 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

emailed-logs network
Dominant language
C++
Stars
33.7k
Forks
1.8k
Avg merge
3d 17h
Merged PRs (30d)
116

Description

### Windows Version

`Microsoft Windows [Version 10.0.26200.9168]` (Windows 11 Pro, 25H2)

### WSL Version

2.6.2.0

### Are you using WSL 1 or WSL 2?

- [x] WSL 2
- [ ] WSL 1

### Kernel Version

6.6.87.2-microsoft-standard-WSL2

### Distro Version

Kali GNU/Linux Rolling

### Other Software

- **Wintun driver 0.14.0.0, provider "WireGuard LLC"** — this is the relevant component.
- A Wintun-based tunneling client that creates a virtual adapter with **MTU 9000** and installs a default route. Several tunneling clients default to an MTU well above 1500 for throughput reasons, since a TUN device is not itself bound by the physical link MTU.

Any software that creates a virtual adapter with an MTU larger than the real path MTU and installs a default route should reproduce this. Wintun is used by WireGuard, Tailscale and a number of other tunneling clients, so the configuration is not unusual.

The adapter is shown as `Tunnel` in the outputs below (renamed).

### Repro Steps

**1.** `.wslconfig`:

```ini
[wsl2]
networkingMode=mirrored
dnsTunneling=false
firewall=true
autoProxy=false

[experimental]
hostAddressLoopback=true
```

**2.** On the Windows host, start any utility that creates a Wintun adapter with MTU 9000 and installs a default route. Confirm on the host:

```powershell
> Get-NetIPInterface -InterfaceAlias 'Tunnel' -AddressFamily IPv4 | ft InterfaceAlias,NlMtu

InterfaceAlias NlMtu
-------------- -----
Tunnel 9000
```

The physical uplink (Wi-Fi) is MTU 1500, so 1500 is the real path MTU.

**3.** In WSL, confirm the MTU was mirrored in:

```
$ ip link show eth0 | head -1
2: eth0: mtu 9000 qdisc mq state UP mode DEFAULT group default qlen 1000

$ ip -4 route show | head -2
default via 198.18.0.2 dev eth0 proto kernel
default via 192.168.1.1 dev eth1 proto kernel metric 50
```

**4.** Probe the real path MTU from WSL with the DF bit set:

```
$ for s in 1372 1472 1500 2000 4000 8972; do ping -M do -s $s -c 1 -W 3 223.5.5.5; done
```

**5.** From WSL, request anything that needs a full-size segment — a TLS handshake is enough:

```
$ curl --noproxy '*' https://example.com/
```

### Expected Behavior

Either of the following would be correct:

1. WSL does not mirror in an MTU that the underlying transport cannot actually carry, **or**
2. when a packet exceeds what the transport can carry, an **ICMP type 3 code 4** ("Fragmentation Needed and DF set") is returned to the sender, so that Path MTU Discovery can lower the MSS and the connection proceeds.

PMTUD depends entirely on that ICMP message. Windows-native processes on the same host are unaffected because the Windows TCP stack has PMTU black hole detection and lowers the MSS on its own after repeated retransmits — the Linux stack inside WSL has no equivalent fallback, so it never recovers.

### Actual Behavior

Packets larger than 1500 bytes are **silently discarded**. No ICMP error comes back, PMTUD never triggers, and the sender retransmits until it times out.

PMTU probe from WSL (step 4):

```
payload=1372 (frame=1400) 1 received
payload=1472 (frame=1500) 1 received
payload=1500 (frame=1528) 100% packet loss <-- silently dropped, no ICMP
payload=2000 (frame=2028) 100% packet loss
payload=4000 (frame=4028) 100% packet loss
payload=8972 (frame=9000) 100% packet loss
```

Control — the same command over `eth1` (the mirrored physical adapter, MTU 1500) is rejected locally, which is the correct behaviour:

```
payload=1472 -> 1 received
payload=8972 -> ping: local error: Message too long, mtu=1500
```

So the kernel does the right thing when the interface MTU is honest. With `eth0` advertising 9000 it hands the packet off and the packet disappears.

Resulting application behaviour (all through the TUN default route, proxies disabled):

```
Target Result
--------------------------- ---------------------------------------------------
http://223.5.5.5/ http=404 ip=223.5.5.5 0.13s OK
http://119.29.29.29/ http=404 ip=119.29.29.29 0.30s OK
http://1.1.1.1/ http=301 ip=1.1.1.1 4.63s OK
https://223.5.5.5/ timed out FAIL
https://1.1.1.1/ timed out FAIL
https://example.com/ timed out FAIL
http://8.8.8.8/ TCP connected (ip=8.8.8.8), then
"Operation timed out ... with 0 bytes received"
```

The discriminating variable is **packet size**, not destination, geography or protocol. Small exchanges (TCP handshake, HTTP GET, a 404 body, DNS queries) all succeed; anything needing a full-size segment — a TLS certificate chain is typically 2–4 KB — is lost. `http://8.8.8.8/` is the clearest case: the handshake completes and the peer IP is recorded, then zero bytes ever arrive.

**Host-side control, same adapter, same MTU 9000:**

```powershell
> curl.exe --noproxy '*' -w '%{http_code} %{remote_ip}' https://1.1.1.1/
301 1.1.1.1
> curl.exe --noproxy '*' -w '%{http_code}' https://example.com/
200
```

Windows-native traffic through the very same TUN adapter is completely fine. Only WSL is affected.

### Diagnostic Logs

**Workaround** — lower the MTU on the host adapter to 1500. Re-probing from WSL afterwards:

```
frame=1500 1 received
frame=1528 ping: local error: Message too long, mtu=1500 <-- now rejected locally
```

The black hole is gone: oversized packets are refused by the local kernel instead of vanishing, and every HTTPS target listed above succeeds. One large page went from stalling at 47,893 bytes received to delivering 403,351 bytes.

Note that setting the MTU **inside** WSL (`ip link set dev eth0 mtu 1500`) does not stick — it reverts to 9000 once the instance is reclaimed by `vmIdleTimeout` and mirrored re-syncs the host adapter configuration. It has to be corrected on the host adapter.

**On why this may be under-reported:** the symptom is misleading. ping works, DNS resolves, plain HTTP loads, and only HTTPS hangs — it presents as a DNS or proxy problem, so it is unlikely to be filed with "MTU" anywhere in the text. There are several open mirrored-mode reports involving VPN clients (which also create virtual adapters) whose descriptions stop at "network broken" — #13724, #13426, #13293, #13197. Some of those may share this root cause.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the .wslconfig mirrored-networking setup and reproduce the MTU probes on eth0 and eth1, comparing behavior with the host Tunnel adapter at MTU 9000 and 1500. Trace the mirrored adapter MTU handling and packet/error path before choosing an implementation approach. Done means oversized packets are either constrained to the real path MTU or produce the required ICMP error, and the listed HTTPS requests succeed without lowering the host adapter MTU.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, linux
Domain
networking, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.