Files
Xray-core/proxy/tun/tun_windows_wfp_test.go
T
patternihaandClaude Opus 5.5 c684201cd8 TUN inbound's autoSystemWfpBlockLeak: "ipv4:" or "ipv6:" in front of any value instead of "tcpv4" and the like
"ipv4:" or "ipv6:" in front of any value limits it to that IP version,
e.g. "ipv4:udp/53", "ipv6:all", "ipv4:dns" or "ipv6:misconfigtun".
"tcpv4", "udpv6", "icmpv4" and the like are gone. Values that only differ
in the IP version become one: "ipv4:udp/53" with "ipv6:udp/53" is
"udp/53".

"all" overrides "allexceptlan" per IP version, and "all" over one IP
version needs dns, as port 53 over one does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:52:23 +03:30

291 lines
11 KiB
Go

//go:build windows
package tun
import (
"context"
go_errors "errors"
"net"
"net/netip"
"slices"
"testing"
"unsafe"
"github.com/xtls/xray-core/transport/internet"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
)
// The WFP structures are handed to fwpuclnt.dll as they are, so their layout
// has to match what MSVC produces for 64-bit and for 32-bit Windows.
func TestWFPStructLayout(t *testing.T) {
check := func(name string, got, want64, want32 []uintptr) {
t.Helper()
want := want32
if unsafe.Sizeof(uintptr(0)) == 8 {
want = want64
}
if !slices.Equal(got, want) {
t.Errorf("%s: size and offsets are %v, want %v", name, got, want)
}
}
var blob fwpByteBlob
check("FWP_BYTE_BLOB",
[]uintptr{unsafe.Sizeof(blob), unsafe.Offsetof(blob.data)},
[]uintptr{16, 8}, []uintptr{8, 4})
var value fwpValue0
check("FWP_VALUE0",
[]uintptr{unsafe.Sizeof(value), unsafe.Offsetof(value.value)},
[]uintptr{16, 8}, []uintptr{8, 4})
var ports fwpRange0
check("FWP_RANGE0",
[]uintptr{unsafe.Sizeof(ports), unsafe.Offsetof(ports.high)},
[]uintptr{32, 16}, []uintptr{16, 8})
var v4 fwpV4AddrAndMask
check("FWP_V4_ADDR_AND_MASK",
[]uintptr{unsafe.Sizeof(v4), unsafe.Offsetof(v4.mask)},
[]uintptr{8, 4}, []uintptr{8, 4})
var v6 fwpV6AddrAndMask
check("FWP_V6_ADDR_AND_MASK",
[]uintptr{unsafe.Sizeof(v6), unsafe.Offsetof(v6.prefixLength)},
[]uintptr{17, 16}, []uintptr{17, 16})
var display fwpmDisplayData0
check("FWPM_DISPLAY_DATA0",
[]uintptr{unsafe.Sizeof(display), unsafe.Offsetof(display.description)},
[]uintptr{16, 8}, []uintptr{8, 4})
var action fwpmAction0
check("FWPM_ACTION0",
[]uintptr{unsafe.Sizeof(action), unsafe.Offsetof(action.filterType)},
[]uintptr{20, 4}, []uintptr{20, 4})
var cond fwpmFilterCondition0
check("FWPM_FILTER_CONDITION0",
[]uintptr{unsafe.Sizeof(cond), unsafe.Offsetof(cond.matchType), unsafe.Offsetof(cond.conditionValue)},
[]uintptr{40, 16, 24}, []uintptr{28, 16, 20})
var session fwpmSession0
check("FWPM_SESSION0",
[]uintptr{
unsafe.Sizeof(session), unsafe.Offsetof(session.displayData), unsafe.Offsetof(session.flags),
unsafe.Offsetof(session.txnWaitTimeoutInMSec), unsafe.Offsetof(session.processID), unsafe.Offsetof(session.sid),
unsafe.Offsetof(session.username), unsafe.Offsetof(session.kernelMode),
},
[]uintptr{72, 16, 32, 36, 40, 48, 56, 64},
[]uintptr{48, 16, 24, 28, 32, 36, 40, 44})
var sublayer fwpmSublayer0
check("FWPM_SUBLAYER0",
[]uintptr{
unsafe.Sizeof(sublayer), unsafe.Offsetof(sublayer.displayData), unsafe.Offsetof(sublayer.flags),
unsafe.Offsetof(sublayer.providerKey), unsafe.Offsetof(sublayer.providerData), unsafe.Offsetof(sublayer.weight),
},
[]uintptr{72, 16, 32, 40, 48, 64},
[]uintptr{44, 16, 24, 28, 32, 40})
var filter fwpmFilter0
check("FWPM_FILTER0",
[]uintptr{
unsafe.Sizeof(filter), unsafe.Offsetof(filter.displayData), unsafe.Offsetof(filter.flags),
unsafe.Offsetof(filter.providerKey), unsafe.Offsetof(filter.providerData), unsafe.Offsetof(filter.layerKey),
unsafe.Offsetof(filter.subLayerKey), unsafe.Offsetof(filter.weight), unsafe.Offsetof(filter.numFilterConditions),
unsafe.Offsetof(filter.filterCondition), unsafe.Offsetof(filter.action), unsafe.Offsetof(filter.providerContextKey),
unsafe.Offsetof(filter.reserved), unsafe.Offsetof(filter.filterID), unsafe.Offsetof(filter.effectiveWeight),
},
[]uintptr{200, 16, 32, 40, 48, 64, 80, 96, 112, 120, 128, 152, 168, 176, 184},
[]uintptr{152, 16, 24, 28, 32, 40, 56, 72, 80, 84, 88, 112, 128, 136, 144})
}
// TestLeakFiltersAccepted has WFP validate the filters by adding them inside a
// transaction that is then aborted, which leaves the system untouched. Adding
// filters requires an elevated process.
func TestLeakFiltersAccepted(t *testing.T) {
skipUnlessElevated := func(err error) {
t.Helper()
if go_errors.Is(err, windows.ERROR_ACCESS_DENIED) {
t.Skipf("WFP filters can only be added by an elevated process: %v", err)
}
t.Fatal(err)
}
engine, err := openWFPEngine()
if err != nil {
skipUnlessElevated(err)
}
defer closeWFPEngine(engine)
if err := fwpmResult(procFwpmTransactionBegin0.Call(uintptr(engine), 0)); err != nil {
skipUnlessElevated(err)
}
defer procFwpmTransactionAbort0.Call(uintptr(engine))
// Any interface stands in for the TUN; the loopback one always exists.
loopback, err := winipcfg.LUIDFromIndex(1)
if err != nil {
t.Fatal(err)
}
// Each with a sublayer of its own.
for _, c := range []struct {
values []string
route4, route6 bool
}{
{[]string{"dns", "misconfigtun", "all", "tcp/443", "udp/1000-2000", "tcp", "icmp", "ipv4:tcp/80", "ipv6:udp/5000-6000", "ipv6:icmp"}, false, false},
// The local network's addresses.
{[]string{"allexceptlan", "udp/1000-2000", "icmp"}, true, false},
{[]string{"ipv4:allexceptlan", "ipv6:all", "ipv6:windnsclient"}, true, true},
{[]string{"ipv6:allexceptlan", "ipv4:misconfigtun"}, false, true},
} {
leaks, err := ExpandWfpBlockLeak(c.values)
if err != nil {
t.Fatal(err)
}
if err := addLeakFilters(engine, loopback, newLeakFilters(leaks, c.route4, c.route6)); err != nil {
skipUnlessElevated(err)
}
}
}
func TestNewLeakFilters(t *testing.T) {
for _, c := range []struct {
values []string
route4, route6 bool
blocked string
systemDNS bool
}{
{[]string{"dns", "misconfigtun"}, true, false, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv6", true},
{[]string{"tcp/853", "udp/853", "misconfigtun"}, true, true, "tcp/853, udp/853", false},
{[]string{"all"}, true, true, "IPv4, IPv6", true},
{[]string{"all", "misconfigtun"}, true, true, "IPv4, IPv6", true},
{[]string{"misconfigtun"}, true, false, "IPv6", false},
{[]string{"tcp/10-20", "udp", "icmp"}, true, true, "tcp/10-20, udp, icmp", true},
{[]string{"tcp/54-60", "icmp"}, true, true, "tcp/54-60, icmp", false},
{[]string{"allexceptlan"}, true, true, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv4 except LAN, IPv6 except LAN", true},
{[]string{"allexceptlan", "all"}, true, true, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv4, IPv6", true},
{[]string{"misconfigtun", "allexceptlan"}, true, false, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv4 except LAN, IPv6 except LAN", true},
// For one IP version only.
{[]string{"IPv4:TCP/10-20", "ipv6:udp", "ipv4:icmp"}, true, true, "ipv4:tcp/10-20, ipv6:udp, ipv4:icmp", true},
{[]string{"ipv6:tcp/54-60", "ipv6:icmp"}, true, true, "ipv6:tcp/54-60, ipv6:icmp", false},
{[]string{"ipv4:windnsclient"}, true, true, "ipv4:windnsclient", true},
{[]string{"ipv6:all"}, true, true, "IPv6", true},
{[]string{"ipv6:all"}, true, false, "IPv6", true},
{[]string{"ipv4:allexceptlan", "ipv6:all"}, true, true, "ipv4:udp/53, ipv4:tcp/53, ipv4:tcp/853, ipv4:udp/853, ipv4:windnsclient, IPv4 except LAN, IPv6", true},
{[]string{"allexceptlan", "ipv6:all"}, true, true, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv4 except LAN, IPv6", true},
{[]string{"ipv4:misconfigtun"}, true, false, "", false},
{[]string{"ipv6:misconfigtun"}, true, false, "IPv6", false},
{[]string{"ipv4:misconfigtun", "ipv6:allexceptlan"}, false, true, "ipv6:udp/53, ipv6:tcp/53, ipv6:tcp/853, ipv6:udp/853, ipv6:windnsclient, IPv4, IPv6 except LAN", true},
{[]string{"ipv6:windnsclient", "ipv4:windnsclient"}, true, true, "windnsclient", true},
} {
leaks, err := ExpandWfpBlockLeak(c.values)
if err != nil {
t.Fatal(err)
}
block := newLeakFilters(leaks, c.route4, c.route6)
if got := block.String(); got != c.blocked {
t.Errorf("%v: String() = %q, want %q", c.values, got, c.blocked)
}
if got := block.systemDNS(); got != c.systemDNS {
t.Errorf("%v: systemDNS() = %v", c.values, got)
}
}
// Without the port 53 values "allexceptlan" brings along, a resolver on
// the local network, like the router's, stays reachable.
if (leakFilters{all: bothIPVersions, lan: bothIPVersions}).systemDNS() {
t.Error("systemDNS() = true with the local network open")
}
}
func TestDNSClientSID(t *testing.T) {
sid, _, _, err := windows.LookupSID("", `NT SERVICE\Dnscache`)
if err != nil {
t.Fatal(err)
}
if sid.String() != dnsClientSID {
t.Errorf(`NT SERVICE\Dnscache is %v, not %v`, sid, dnsClientSID)
}
}
func TestDNSOutsideTUN(t *testing.T) {
prefixes := []netip.Prefix{
netip.MustParsePrefix("198.51.100.1/30"), // gateway, not masked
netip.MustParsePrefix("203.0.113.0/24"), // route
}
servers := []netip.Addr{
netip.MustParseAddr("198.51.100.2"),
netip.MustParseAddr("203.0.113.53"),
netip.MustParseAddr("::ffff:203.0.113.54"),
netip.MustParseAddr("8.8.8.8"),
netip.MustParseAddr("2001:db8::53"),
// Local resolvers, reachable over loopback whatever is blocked.
netip.MustParseAddr("127.0.0.1"),
netip.MustParseAddr("::ffff:127.0.0.53"),
netip.MustParseAddr("::1"),
}
want := []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("2001:db8::53")}
if got := dnsOutsideTUN(servers, prefixes); !slices.Equal(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
func TestResolveOnOwn(t *testing.T) {
internet.SkipDNSServers([]netip.Addr{netip.MustParseAddr("::ffff:203.0.113.53")})
t.Cleanup(func() { internet.SkipDNSServers(nil) })
preferGo, dial := net.DefaultResolver.PreferGo, net.DefaultResolver.Dial
saved := resolveOnOwn()
t.Cleanup(saved.restore)
if !net.DefaultResolver.PreferGo || net.DefaultResolver.Dial == nil {
t.Fatal("net.DefaultResolver is unchanged")
}
if _, err := net.DefaultResolver.Dial(context.Background(), "udp", "203.0.113.53:53"); err == nil {
t.Error("the TUN's DNS server was not skipped")
}
conn, err := net.DefaultResolver.Dial(context.Background(), "udp", "127.0.0.1:53")
if err != nil {
t.Fatal(err)
}
conn.Close()
saved.restore()
if net.DefaultResolver.PreferGo != preferGo || (net.DefaultResolver.Dial == nil) != (dial == nil) {
t.Error("net.DefaultResolver is not restored")
}
}
// TestTunOnlyDNS checks that a DNS server another interface uses as well is
// not skipped, while one of the TUN alone is.
func TestTunOnlyDNS(t *testing.T) {
adapters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeGateways)
if err != nil {
t.Fatal(err)
}
var other netip.Addr
for _, adapter := range adapters {
if adapter.OperStatus == winipcfg.IfOperStatusUp && adapter.FirstGatewayAddress != nil && adapter.FirstDNSServerAddress != nil {
other, _ = netip.AddrFromSlice(adapter.FirstDNSServerAddress.Address.IP())
other = other.Unmap()
break
}
}
if !other.IsValid() {
t.Skip("no interface with a gateway and a DNS server")
}
tunOnly := netip.MustParseAddr("203.0.113.53")
// LUID 0 is no interface, so every one counts as another.
got, err := tunOnlyDNS(0, []netip.Addr{other, tunOnly})
if err != nil {
t.Fatal(err)
}
if !slices.Equal(got, []netip.Addr{tunOnly}) {
t.Errorf("got %v, want [%v]", got, tunOnly)
}
}
func TestFlushDNSCache(t *testing.T) {
if err := flushDNSCache(); err != nil {
t.Fatal(err)
}
}