Repository navigation
Mirrored networking: IPv6 neighbor for a Windows TUN next hop goes STALE → FAILED with no outgoing NS counted, breaking subsequent TCP connections #42106
Description
Windows Version
10.0.26300.9711
WSL Version
3.0.3.0
Are you using WSL 1, WSL 2, or WSLC?
- WSL 2
- WSL 1
- WSLC
Kernel Version
6.18.54.1-microsoft-standard-WSL2
Distro Version
Ubuntu 24.04.5 LTS
Other Software
-
Primary controlled reproduction: official standalone Mihomo executable, not launched through a GUI:
Mihomo Meta alpha-ca8d7c4 windows amd64 with go1.26.10 Wed Oct 28 11:30:06 UTC 2026 Use tags: with_gvisor -
TUN stack: gVisor. MTU: 1500.
-
Clash Verge Rev (CVR) 2.5.9+autobuild.1028.3953e12 with Mihomo v1.19.33 is installed. During the standalone tests its TUN was disabled and only its HTTP proxy at 127.0.0.1:7897 was kept for unrelated work connections. Tailscale was stopped and its adapter disabled. Adapter snapshots before and after each standalone trial showed only the test TUN active.
-
The sysctl experiment and the diagnostic log collection described later were run in the existing CVR TUN environment instead; this is marked where relevant.
-
A Windows Node.js HTTP server listened only on 127.0.0.1:18080. The test request was routed DIRECT to it and did not depend on any remote server.
Repro Steps
Summary
In mirrored mode, a Windows layer-3 TUN appears in WSL with address fd42:feed:1::1/126 and next hop fd42:feed:1::2. After the neighbor entry naturally becomes STALE, using it moves the entry through DELAY -> PROBE -> FAILED in about 8.2–8.4 seconds. While FAILED, route lookup falls back to the physical mirrored interface and TCP connections that should go through the TUN time out, even to a service hosted only on the same Windows machine. Pinging the gateway again restores REACHABLE and connectivity.
In separate trials in the existing CVR environment (not the standalone single-TUN setup), the key observation was that between the trigger and FAILED, both the network-namespace and per-interface Icmp6OutNeighborSolicits counters showed a delta of zero, and an AF_PACKET capture on the TUN-facing interface recorded no NS during the PROBE stage. The timing is consistent with 5 seconds of delay plus three 1-second probe intervals. The final CVR diagnostic snapshot records delay_first_probe_time=5 and retrans_time_ms=1000; these settings were not separately sampled in the original standalone trials. No NS was accounted as sent during the measured CVR intervals.
This is an intermittent failure, not a continuously broken connection, but it can be triggered reliably with the steps below. I have not established that WSL is the faulty component. The TUN application, its driver, Windows networking, and WSL mirrored networking all remain possible locations, and I am asking for help locating the failure.
1. WSL configuration
[wsl2]
networkingMode=mirrored
dnsTunneling=true
firewall=true
autoProxy=true2. Start the local HTTP service on Windows
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('mihomo-local-probe-ok\n');
}).listen(18080, '127.0.0.1');Run it with Node.js and leave it running during the test.
3. Start the standalone Windows TUN application
Tested Mihomo configuration
mode: rule
log-level: debug
ipv6: true
mixed-port: 17998
external-controller: 127.0.0.1:19097
hosts:
probe.test: 127.0.0.1
dns:
enable: true
listen: 127.0.0.1:15353
ipv6: true
enhanced-mode: fake-ip
fake-ip-range: 198.19.0.1/16
fake-ip-range6: 2001:2:feed::1/64
use-hosts: false
nameserver: [system]
tun:
enable: true
device: MihomoProbe
stack: gvisor
auto-route: true
auto-detect-interface: true
dns-mode: disabled
dns-hijack: []
strict-route: false
mtu: 1500
inet6-address: [fd42:feed:1::1/126]
route-address: [0.0.0.0/0, '2001:2:feed::/64']
proxies:
- name: CVR
type: http
server: 127.0.0.1
port: 7897
rules:
- DOMAIN,probe.test,DIRECT
- MATCH,CVRThe IPv4 default route and the CVR HTTP outbound keep unrelated work connections usable. The test request matches probe.test,DIRECT and reaches the local HTTP server. These supporting settings have not been removed and retested, so this is a small tested configuration rather than a proven minimal one.
From an elevated Windows terminal:
.\mihomo-windows-amd64.exe -d <test-directory> -f <test-directory>\config.yamlFor the single-active-TUN setup, disable any other TUN (CVR's TUN, Tailscale, etc.). In our setup, CVR's physical outbound interface was temporarily bound to WLAN to prevent a forwarding loop.
4. Establish the fake IPv6 mapping
From Windows:
const dns = require('dns');
const resolver = new dns.Resolver();
resolver.setServers(['127.0.0.1:15353']);
resolver.resolve6('probe.test', (err, addresses) => console.log(err || addresses));This returned 2001:2:feed::4. Substitute the actual address below if different.
5. Identify the WSL interface and validate the baseline
ip -6 addr
ip -6 routeFind the interface with fd42:feed:1::1/126 (it was eth8 in this run; interface numbers change between runs).
ping -6 -c 1 -W 2 fd42:feed:1::2
curl -g -6 --noproxy '*' --connect-timeout 5 --max-time 7 \
'http://[2001:2:feed::4]:18080/'
ip -s -6 neigh show to fd42:feed:1::2 dev eth8The request must return HTTP 200 with body mihomo-local-probe-ok. Restore this baseline before each trial.
6. Wait for natural STALE, then use the neighbor
Passively poll until the entry naturally becomes STALE, without changing its state or NUD settings (we allowed up to 120 seconds; a fixed sleep is not a substitute for checking):
ip -s -6 neigh show to fd42:feed:1::2 dev eth8Then run one trigger per trial:
# A: ping the next-hop gateway once
ping -6 -c 1 -W 2 fd42:feed:1::2
# B: make a TCP request directly, without a preceding ping
curl -g -6 --noproxy '*' --connect-timeout 5 --max-time 7 \
'http://[2001:2:feed::4]:18080/'
# C: ping the fake IPv6 destination once
ping -6 -c 1 -W 2 2001:2:feed::4Observe the neighbor for at least 16 seconds after the trigger (we sampled about every 0.2 seconds):
ip -6 neigh show to fd42:feed:1::2 dev eth87. Check the route and a subsequent TCP request
ip -6 route get 2001:2:feed::4
curl -g -6 --noproxy '*' --connect-timeout 5 --max-time 7 \
'http://[2001:2:feed::4]:18080/'8. Recover
ping -6 -c 1 -W 2 fd42:feed:1::2
ip -6 neigh show to fd42:feed:1::2 dev eth8
curl -g -6 --noproxy '*' --connect-timeout 5 --max-time 7 \
'http://[2001:2:feed::4]:18080/'Each trigger was tested twice, each time from a healthy baseline and a natural STALE.
Expected Behavior
With the TUN application and local server still running, using a STALE next-hop entry should lead to probes that keep the neighbor reachable, and TCP connections through the TUN should continue to work.
If the next hop does not answer the probes, I would appreciate help identifying whether the probes or responses are lost or suppressed in the TUN application, the driver, the Windows path, or the WSL mirrored path.
Actual Behavior
Single-active-TUN, local HTTP experiment
All six trials met the natural STALE precondition and produced DELAY -> PROBE -> FAILED:
| Trial | Trigger after natural STALE | Time to FAILED | TCP result |
|---|---|---|---|
| 1 | Ping gateway once | 8.16 s | Subsequent HTTP 000 / curl 28; HTTP 200 after recovery |
| 2 | Direct TCP HTTP request | 8.21 s | First HTTP 200, subsequent HTTP 000 / curl 28; HTTP 200 after recovery |
| 3 | Ping fake destination once | 8.44 s | Subsequent HTTP 000 / curl 28; HTTP 200 after recovery |
| 4 | Ping gateway once | 8.29 s | Subsequent HTTP 000 / curl 28; HTTP 200 after recovery |
| 5 | Direct TCP HTTP request | 8.25 s | First HTTP 200, subsequent HTTP 000 / curl 28; HTTP 200 after recovery |
| 6 | Ping fake destination once | 8.43 s | Subsequent HTTP 000 / curl 28; HTTP 200 after recovery |
While FAILED, route lookup selected the physical mirrored interface instead of the TUN, and the request to the fake destination timed out. Pinging the gateway again restored REACHABLE and HTTP 200 without restarting Windows, WSL, or the TUN application.
So this is not specific to pinging a fake destination (pinging the gateway itself triggers it), and a successful initial TCP request does not prevent the later failure.
Representative output (physical addresses and MACs anonymized):
Before trigger:
fd42:feed:1::2 lladdr <gateway-mac> STALE
After use:
fd42:feed:1::2 lladdr <gateway-mac> DELAY
fd42:feed:1::2 lladdr <gateway-mac> PROBE
fd42:feed:1::2 FAILED
Route after failure:
2001:2:feed::4 from :: via <physical-link-local-gateway> dev eth7
proto kernel src <physical-public-IPv6> metric 301 pref medium
Subsequent HTTP:
curl: (28) Failed to connect to 2001:2:feed::4 port 18080 after 5000 ms
http=000
After pinging fd42:feed:1::2 again:
fd42:feed:1::2 lladdr <gateway-mac> REACHABLE
http=200
NDP packet capture
A root AF_PACKET capture bound to the WSL TUN-facing interface parsed outgoing and incoming ICMPv6 NS/NA. During PROBE it recorded no NS. During recovery it recorded a normal multicast exchange:
out NS, type 135:
source = fd42:feed:1::1
destination = ff02::1:ff00:2
target = fd42:feed:1::2
hop limit = 255
in NA, type 136:
source = fd42:feed:1::2
destination = fd42:feed:1::1
target = fd42:feed:1::2
hop limit = 255
NA flags = 0x60
So the gateway does answer multicast NS when one is sent. This does not show that unicast NS was sent and left unanswered: the expected PROBE-stage probes simply did not appear at this capture point.
Windows PktMon (all components, filtered to ICMPv6 involving fd42:feed:1::/126) recorded no matching events at all, including the recovery packets visible inside WSL, so it gave no useful Windows-side visibility.
Kernel NDP counters (CVR environment)
This trial used the existing CVR/Mihomo TUN, not the standalone setup. The TUN-facing interface was eth7 (physical mirrored interface eth6), with gateway fdfe:dcba:9876::2. This was not a single-TUN control: Tailscale remained enabled, and its adapter was recorded as Up in the final diagnostic capture. NUD settings were unchanged (ucast_solicit=3, mcast_resolicit=0, app_solicit=0).
After HTTPS 200, natural STALE, and one gateway ping, the neighbor reached FAILED after about 8.34 seconds. Snapshots were taken just before the trigger and upon detecting FAILED, before any recovery traffic:
| Scope | Counter | Before | At FAILED | Delta |
|---|---|---|---|---|
| Network namespace | Icmp6OutNeighborSolicits |
3 | 3 | 0 |
| Network namespace | Icmp6InNeighborAdvertisements |
6 | 6 | 0 |
| TUN interface eth7 | Icmp6OutNeighborSolicits |
2 | 2 | 0 |
| TUN interface eth7 | Icmp6InNeighborAdvertisements |
5 | 5 | 0 |
Namespace values came from /proc/net/snmp6 and matched nstat -az; interface values came from /proc/net/dev_snmp6/eth7. The same zero deltas were observed again in the trial captured by the diagnostic logs (FAILED after 8.32 seconds).
In the WSL 6.18 kernel source, ndisc_send_skb, the ICMPv6 output counters are incremented when the local output hook/path returns zero. This is an accounting point, not proof of delivery to the next hop. A zero delta does not distinguish no attempted probe from failure before that accounting point; I have not traced those code paths. The counters also say nothing about frames elsewhere in Windows/Hyper-V.
Complete IPv6 route table at FAILED (CVR environment; physical addresses anonymized)
Command: ip -6 route show table all
<physical-public-prefix-1>/64 dev eth6 proto kernel metric 301 pref medium
fdfe:dcba:9876::2 dev eth7 proto kernel metric 1 pref medium
fdfe:dcba:9876::/126 dev eth7 proto kernel metric 256 pref medium
<link-local-address-2> dev eth6 proto kernel metric 301 pref medium
fe80::/64 dev eth6 proto kernel metric 301 pref medium
default via fdfe:dcba:9876::2 dev eth7 proto kernel metric 1 onlink pref medium
default via <link-local-address-2> dev eth6 proto kernel metric 301 onlink pref medium
local ::1 dev lo table local proto kernel metric 0 pref medium
local <physical-public-address-3> dev eth6 table local proto kernel metric 0 pref medium
local <physical-public-address-4> dev eth6 table local proto kernel metric 0 pref medium
local fdfe:dcba:9876::1 dev eth7 table local proto kernel metric 0 pref medium
local <link-local-address-5> dev eth6 table local proto kernel metric 0 pref medium
local <link-local-address-6> dev eth1 table local proto kernel metric 0 pref medium
multicast ff00::/8 dev eth1 table local proto kernel metric 256 pref medium
multicast ff00::/8 dev loopback0 table local proto kernel metric 256 pref medium
multicast ff00::/8 dev eth6 table local proto kernel metric 256 pref medium
multicast ff00::/8 dev eth7 table local proto kernel metric 256 pref medium
Both the TUN default route (metric 1) and the physical default route (metric 301) remained present; the route itself did not disappear when the neighbor became FAILED.
NUD sysctl A → B → A experiment (CVR environment, Tailscale active)
Interface eth0, address fdfe:dcba:9876::1, next hop fdfe:dcba:9876::2. The HTTPS target was a fixed fake IPv6 for www.google.com, using curl --resolve to keep the hostname/SNI.
Original values (from procfs): ucast_solicit=3, mcast_resolicit=0, app_solicit=0. Phase B temporarily changed only:
sudo /usr/sbin/sysctl -w net.ipv6.neigh.eth0.ucast_solicit=0
sudo /usr/sbin/sysctl -w net.ipv6.neigh.eth0.mcast_resolicit=3Values were verified via /proc/sys/net/ipv6/neigh/eth0/ and restored afterwards with an independent timed rollback.
Each trial waited for natural STALE, pinged the gateway once, observed for 20 seconds, then tested HTTPS:
| Phase/trial | FAILED during observation? | Time to FAILED | State at end of observation | Subsequent HTTPS |
|---|---|---|---|---|
| Original A1 | Yes | 8.20 s | FAILED | curl 28; 200 after explicit recovery |
| Original A2 | No | — | REACHABLE | 200 |
| Multicast B1 | Yes | 8.15 s | FAILED | Re-resolution during the request, then 200 |
| Multicast B2 | Yes | 8.37 s | FAILED | Re-resolution during the request, then 200 |
| Restored A1 | Yes | 8.32 s | Recovered to REACHABLE | 200 |
| Restored A2 | Yes | 8.31 s | FAILED | curl 28; 200 after explicit recovery |
Switching to multicast reprobes did not prevent FAILED. In both B trials the capture again recorded no NS/NA during the observation window; a multicast NS and a responding NA appeared only during the following HTTPS re-resolution. A final HTTP success alone therefore does not show that no failure occurred.
Elapsed times above use monotonic timestamps. Wall-clock jumps occurred during logging, so epoch timestamps should not be used for precise cross-endpoint timing.
Tests not yet completed
- A comparison with a non-Mihomo Wintun application. Official sing-box 1.14.4 was tried, but all three setup attempts failed an unrelated work-connection health check and were rolled back before a valid trial, so they are neither a reproduction nor a negative control.
- A comparison with the official WireGuard client.
- WSL NAT mode versus mirrored mode.
- Turning off
dnsTunneling,autoProxy, orfirewall. - Recording
delay_first_probe_time/retrans_time, and outbound drop/error counters during PROBE. - A Windows/WSL reboot to clear historical HNS state before testing.
Diagnostic Logs
A networking log archive was collected on October 29, 2026 with the official collect-wsl-logs.ps1 -LogProfile networking and the unmodified WSL-Networking profile, with tcpdump and iptables installed. It includes tcpdump output, Windows/Linux network state, the WSL-Networking ETL, a PktMon ETL, and WFP diagnostics. Two automation adaptations were made: the stop-key wait was replaced by the bounded reproduction script, and the startup retry that would cancel another WPR session was disabled. RestartWslReproMode and process dumps were not enabled. WSLg log collection timed out.
The archive covers a valid natural STALE → gateway ping → FAILED trial (FAILED after 8.32 seconds, zero NS/NA counter deltas), followed by a successful recovery ping and HTTPS request. The reproduction script ran from 15:16:54.699 to 15:17:24.509 on October 29, 2026 (UTC+08:00). Log collection started at 15:16:28; these are wall-clock references for locating events, not the basis of the elapsed-time measurements.
- File:
WslLogs-2026-10-29_15-16-28.tar.gz(183,130,378 bytes) - SHA-256:
1128b3475a394838d84c2f4ab32b6294d08983061b52ba78549d35f670aceae0
The complete diagnostic archive has now been shared privately with wsl-gh-logs@microsoft.com via restricted Google Drive links sent by email.
Request for investigation
Could the WSL networking team help identify an effective trace point for IPv6 neighbor probes on the mirrored TUN path, and determine whether this behavior belongs to WSL/Windows or to the TUN application/driver?
The strongest evidence is the six repeated failures with a single active TUN and a local HTTP endpoint, together with the separate CVR-environment observations of zero outgoing NS counted during the STALE-to-FAILED interval. This does not yet justify assigning the root cause to WSL.
Related reports
I searched the tracker for combinations of mirrored, IPv6, neighbor, NDP, TUN, VPN, and FAILED. These look relevant but are not established duplicates:
- #42013 — Fortinet VPN-V IP-media adapter: ARP unresolved and TCP SYN dropped by Hyper-V: IPv4 ARP with an observed Unauthorized MAC drop, whereas this report concerns IPv6 NUD and has no identified Windows-side drop reason.
- #41443 — Reply from TUN-based VPN adapter dropped before reaching WSL vNIC: also a mirrored layer-3 VPN adapter, but a different client and packet path.
Microsoft previously documented a mirrored/VPN failure associated with KB5067036 and listed its fix in KB5074109 (January 13, 2026). This is context for a possibly related class of problems, not evidence that this is the same regression; how build 26300.9711 relates to that fix is unknown.
Activity
github-actions commentedon Oct 29, 2026
Logs are required for review from WSL team
If this a feature request, please reply with '/feature'. If this is a question, reply with '/question'.
Otherwise, please attach logs by following the instructions below, your issue will not be reviewed unless they are added. These logs will help us understand what is going on in your machine.
How to collect WSL logs
Download and execute collect-wsl-logs.ps1 in an administrative powershell prompt:
The script will output the path of the log file once done.
If this is a networking issue, please use
.\collect-wsl-logs.ps1 -LogProfile networkinginstead, following the instructions in Collect WSL logs for networking issuesOnce completed please upload the output files to this GitHub issue.
See Collect WSL logs (recommended method).
If you choose to email these logs instead of attaching them to the bug, please send them to wsl-gh-logs@microsoft.com with the GitHub issue number in the subject, and include a link to your GitHub issue comment in the message body, and reply with '/emailed-logs'.
taixinguo commentedon Oct 29, 2026
A complete networking diagnostic archive has been collected for this report:
WslLogs-2026-10-29_15-16-28.tar.gz(183,130,378 bytes; SHA-2561128b3475a394838d84c2f4ab32b6294d08983061b52ba78549d35f670aceae0). It covers the natural STALE → gateway ping → FAILED reproduction described above, followed by recovery.Because the archive contains unanonymized network and system identifiers, I am arranging private delivery to the official log mailbox. Download links will not be posted publicly. I will confirm here once sharing and email delivery have actually been completed.
taixinguo commentedon Oct 29, 2026
/emailed-logs
The private download links for the complete networking diagnostic archive have now been emailed to wsl-gh-logs@microsoft.com with subject
Diagnostic logs for microsoft/WSL#42106, referencing the diagnostic comment above.Archive:
WslLogs-2026-10-29_15-16-28.tar.gz(183,130,378 bytes; SHA-2561128b3475a394838d84c2f4ab32b6294d08983061b52ba78549d35f670aceae0). It is provided as two raw byte chunks with reconstruction instructions and the original checksum. Both files are restricted and read access has been granted to the official log mailbox; download links are not posted publicly.github-actions commentedon Oct 29, 2026
Diagnostic information