mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-11-04 01:17:02 +00:00
With autoSystemWfpBlockLeak, loopback now always stays allowed, so a DNS server in dns like 127.0.0.1, a local resolver, stays reachable although it lies in neither gateway nor autoSystemRoutingTable. Leave loopback servers out of the warning that queries to them cannot go through the TUN and are blocked, and say so in the README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
285 lines
9.9 KiB
Go
285 lines
9.9 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)
|
|
}
|
|
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun", "all", "tcp/443", "udp/1000-2000", "tcp", "icmp"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
block := newLeakFilters(leaks, false, false)
|
|
block.dnsClient = true // whatever the Windows version
|
|
if err := addLeakFilters(engine, loopback, block); err != nil {
|
|
skipUnlessElevated(err)
|
|
}
|
|
// The local network's addresses, with another sublayer.
|
|
if leaks, err = ExpandWfpBlockLeak([]string{"allexceptlan", "udp/1000-2000", "icmp"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
block = newLeakFilters(leaks, true, false)
|
|
block.dnsClient = true
|
|
if !block.lan {
|
|
t.Fatal("allexceptlan left the local network blocked")
|
|
}
|
|
if err := addLeakFilters(engine, loopback, block); err != nil {
|
|
skipUnlessElevated(err)
|
|
}
|
|
}
|
|
|
|
func TestNewLeakFilters(t *testing.T) {
|
|
windnsclient := ""
|
|
if dnsClientUsesAnyPort() {
|
|
windnsclient = ", windnsclient"
|
|
}
|
|
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},
|
|
} {
|
|
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{ipv4: true, ipv6: true, lan: true}).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)
|
|
}
|
|
}
|