Skip to content

fix(windows): exclude loopback from strict-route IPv6 blocking - #24

Open
XIYBHK wants to merge 1 commit into
MetaCubeX:metafrom
XIYBHK:fix/windows-ipv6-loopback
Open

XIYBHK wants to merge 1 commit into
MetaCubeX:metafrom
XIYBHK:fix/windows-ipv6-loopback

Conversation

@XIYBHK

@XIYBHK XIYBHK commented Sep 9, 2026

Copy link
Copy Markdown

When a Windows TUN uses AutoRoute and StrictRoute without an IPv6 address, its block ipv6 WFP filter also blocks connections to ::1. This breaks local services such as Unreal Engine's Zen DDC even though no traffic needs to leave the host.

Exclude loopback traffic from that address-family block using FWPM_CONDITION_FLAGS, FWP_MATCH_FLAGS_NONE_SET, and FWP_CONDITION_FLAG_IS_LOOPBACK. This narrows the existing block instead of installing a higher-priority permit: the separate DNS filters and other WFP policies still apply. No configuration option, process allowlist, filter priority, or IPv4 behavior is changed.

Microsoft documents the loopback condition at ALE_AUTH_CONNECT_V6 and the flags-none-set match type.

Reproduction

Reproduced on Windows 11 Pro 25H2, build 26200.9168, using the official rolling Alpha available on 2026-09-09:

Mihomo Meta alpha-d5f57a5 windows amd64 with go1.26.8 Tue Sep  8 10:40:03 UTC 2026
Use tags: with_gvisor

The downloaded mihomo-windows-amd64-compatible-alpha-d5f57a5.zip matched the release's SHA-256 checksum:
d35f1104d9b10707a3fe12815b0bc4b64c5d27f6ec8f12da9a26fb2b66a89ee5.

  1. Start a local HTTP server in a separate terminal: python -m http.server 18559 --bind ::1 --directory <empty-test-directory>.
  2. Verify curl.exe --noproxy "*" --max-time 3 http://[::1]:18559/ succeeds.
  3. Run the official core directly as Administrator: mihomo.exe -d <test-directory> -f repro.yaml, using the configuration below.
  4. Repeat the same curl command. The official Alpha fails with exit 7; a direct socket probe reports WSAEACCES (10013). The patched build returns HTTP 200. Stop the test core and the original connection works again.
mixed-port: 0
external-controller: 127.0.0.1:19091
mode: rule
log-level: debug
ipv6: false
dns:
  enable: false
  enhanced-mode: fake-ip
  fake-ip-range: 198.19.255.1/24
tun:
  enable: true
  device: ZenLoopbackProbe
  stack: system
  auto-route: true
  auto-detect-interface: true
  strict-route: true
  route-address:
    - 198.19.255.0/30
  dns-hijack: []
rules:
  - MATCH,DIRECT

The limited route and separate address range keep the test from taking over the default IPv4 route; the bug is in WFP filtering, not traffic routed through the test TUN. No subscription, remote proxy, DNS lookup, or UE installation is required.

Validation

The new opt-in integration test exercises the actual Windows TUN/WFP implementation. It launches the socket client from a different executable path, because the TUN owner's application ID is already exempt and testing from that same executable would produce a false positive. It verifies working loopback before filter installation and after cleanup.

Run in an elevated PowerShell on a test host, without another TUN's IPv6 block:

$env:SING_TUN_TEST_WINDOWS_WFP = '1'
go test -run '^TestWindowsStrictRouteIPv6Loopback$' -v

The test installs temporary system-wide WFP filters and an IPv4 test adapter, so it is skipped unless explicitly enabled. It closes the adapter/session and removes the copied client on completion.

Check Unpatched Patched
IPv4 TCP loopback Pass Pass
IPv6 TCP loopback WSAEACCES Pass
IPv6 UDP loopback echo Timeout Pass
Non-loopback IPv6 TCP (2001:db8::1) WSAEACCES WSAEACCES
Existing IPv4/IPv6 TCP port-53 blocks WSAEACCES WSAEACCES
HTTP over ::1, through the complete Mihomo build Fails HTTP 200

The integration test was first run against unmodified 0810e156517643a725978af03a48356382255d5e and failed on the IPv6 TCP/UDP loopback cases; the same test passed with this fix. End-to-end checks also used Mihomo d5f57a5e290eafd77a2b28d8db36e3bd70071398 built with Go 1.26.8 and with_gvisor, replacing only the sing-tun module with 0810e15 or 0810e15 plus this fix. Both the official binary and the locally built unpatched control reproduced the failure.

Other checks:

  • Windows amd64: go build ., go build -tags with_gvisor ., go vet ., and go test -v . passed (Go 1.24.5; normal test invocation skips the opt-in test).
  • Windows 386 and arm64: go build . cross-compilation passed; runtime testing was amd64 only.
  • Root-package tests and vet also passed with Go 1.26.8.
  • gofmt, gofumpt and git diff --check passed for the changed files.
  • go test ./... is not green on this Windows host. An unmodified parent worktree reproduces the existing Darwin-only internal-package build failures and internal/winipcfg test %w vet errors; the host's normal IPv6 block also causes existing ping tests to time out. These are outside this patch.

Test-host limitation: an existing Clash instance remained running to preserve connectivity, with only its own strict-route temporarily disabled during each isolated test and restored afterward. Its configuration was not used by the tested core. These are controlled before/during/after checks, not a clean-machine test or a comprehensive IPv6 leak audit. An external UDP send returning successfully was not treated as evidence of delivery or allowed egress.

Official Alpha: complete stdout and stderr
time="2026-09-09T20:31:59.412821300+08:00" level=info msg="Start initial configuration in progress"
time="2026-09-09T20:31:59.414595500+08:00" level=info msg="Geodata Loader mode: memconservative"
time="2026-09-09T20:31:59.416179800+08:00" level=info msg="Geosite Matcher implementation: succinct"
time="2026-09-09T20:31:59.418299100+08:00" level=info msg="Initial configuration complete, total time: 3ms"
time="2026-09-09T20:31:59.421980800+08:00" level=info msg="Sniffer is closed"
time="2026-09-09T20:31:59.421980800+08:00" level=info msg="RESTful API listening at: 127.0.0.1:19091"
time="2026-09-09T20:31:59.531393000+08:00" level=warning msg="[TUN] default interface changed by monitor, => 以太网"
time="2026-09-09T20:32:00.320300200+08:00" level=info msg="[TUN] Tun adapter listening at: ZenLoopbackProbe([198.19.255.1/30],[]), mtu: 9000, auto route: true, auto redir: false, ip stack: System"
time="2026-09-09T20:32:00.333148800+08:00" level=info msg="Start initial compatible provider default"
time="2026-09-09T20:32:00.320300200+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:60277"
time="2026-09-09T20:32:00.333753400+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:00.334760200+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:52936"
time="2026-09-09T20:32:00.353193900+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:00.353193900+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:60277"
time="2026-09-09T20:32:00.387740600+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:00.504537800+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:00.534834700+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:00.560044800+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:00.681151700+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:00.728305200+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:00.870256900+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:00.895284500+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:01.110745800+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:01.415162600+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
time="2026-09-09T20:32:02.909268800+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:60277"
time="2026-09-09T20:32:02.910011800+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57705"
Patched Mihomo: complete stdout and stderr
time="2026-09-09T20:30:59.474984100+08:00" level=info msg="Start initial configuration in progress"
time="2026-09-09T20:30:59.476598700+08:00" level=info msg="Geodata Loader mode: memconservative"
time="2026-09-09T20:30:59.478470700+08:00" level=info msg="Geosite Matcher implementation: succinct"
time="2026-09-09T20:30:59.481231600+08:00" level=info msg="Initial configuration complete, total time: 4ms"
time="2026-09-09T20:30:59.482923500+08:00" level=info msg="Sniffer is closed"
time="2026-09-09T20:30:59.484490600+08:00" level=info msg="RESTful API listening at: 127.0.0.1:19091"
time="2026-09-09T20:30:59.626867000+08:00" level=warning msg="[TUN] default interface changed by monitor, => 以太网"
time="2026-09-09T20:31:00.522264600+08:00" level=info msg="[TUN] Tun adapter listening at: ZenLoopbackProbe([198.19.255.1/30],[]), mtu: 9000, auto route: true, auto redir: false, ip stack: System"
time="2026-09-09T20:31:00.522264600+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57212"
time="2026-09-09T20:31:00.522782500+08:00" level=info msg="Start initial compatible provider default"
time="2026-09-09T20:31:00.523297100+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:50060"
time="2026-09-09T20:31:00.523827900+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:57213"
time="2026-09-09T20:31:00.524469300+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:53334"
time="2026-09-09T20:31:00.524469300+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:50108"
time="2026-09-09T20:31:00.525088400+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:63266"
time="2026-09-09T20:31:00.525088400+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:58953"
time="2026-09-09T20:31:00.566231800+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:65457"
time="2026-09-09T20:31:00.621268900+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:56992"
time="2026-09-09T20:31:00.824320300+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:56992"
time="2026-09-09T20:31:01.080938700+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:56992"
time="2026-09-09T20:31:01.462131800+08:00" level=debug msg="[DNS] hijack udp:198.19.255.2:53 from 198.19.255.1:56992"
Integration regression: before the production fix
=== RUN   TestWindowsStrictRouteIPv6Loopback
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp4-loopback
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp6-loopback
    tun_windows_test.go:121: tcp6 [::1]:54432 (want allow): exit status 1
        --- FAIL: TestWindowsWFPProbe (0.00s)
            tun_windows_test.go:166: dial tcp6 [::1]:54432: connectex: An attempt was made to access a socket in a way forbidden by its access permissions.
        FAIL
=== RUN   TestWindowsStrictRouteIPv6Loopback/udp6-loopback
    tun_windows_test.go:121: udp6 [::1]:52683 (want allow): exit status 1
        --- FAIL: TestWindowsWFPProbe (2.00s)
            tun_windows_test.go:166: read udp6 [::1]:56056->[::1]:52683: i/o timeout
        FAIL
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp6-non-loopback
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp6-dns
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp4-dns
--- FAIL: TestWindowsStrictRouteIPv6Loopback (4.38s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/tcp4-loopback (0.06s)
    --- FAIL: TestWindowsStrictRouteIPv6Loopback/tcp6-loopback (0.05s)
    --- FAIL: TestWindowsStrictRouteIPv6Loopback/udp6-loopback (2.05s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/tcp6-non-loopback (0.04s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/tcp6-dns (0.04s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/tcp4-dns (0.04s)
FAIL
Integration regression: after the production fix
=== RUN   TestWindowsStrictRouteIPv6Loopback
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp4-loopback
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp6-loopback
=== RUN   TestWindowsStrictRouteIPv6Loopback/udp6-loopback
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp6-non-loopback
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp4-dns
=== RUN   TestWindowsStrictRouteIPv6Loopback/tcp6-dns
--- PASS: TestWindowsStrictRouteIPv6Loopback (2.25s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/tcp4-loopback (0.05s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/tcp6-loopback (0.05s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/udp6-loopback (0.05s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/tcp6-non-loopback (0.04s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/tcp4-dns (0.04s)
    --- PASS: TestWindowsStrictRouteIPv6Loopback/tcp6-dns (0.04s)
PASS

Match non-loopback traffic in the IPv6 address-family WFP block so
local services remain reachable without allowing external IPv6.
Keep the existing DNS filters and other WFP policies in effect.

Add opt-in Windows integration coverage with a separate client
executable to avoid the TUN owner's application-ID exemption.
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.

1 participant