mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-30 21:16:03 +00:00
With strictRoute, only port 53 was kept inside the TUN. But Windows' DNS Client service sends the queries for an interface's DNS servers out through that interface, whatever the routes say, and since Windows 11 and Server 2022 it may send them over HTTPS or TLS, when that is set up for the interface (as Windows Settings does) or for the server. Those left through the physical link. On those versions, the DNS Client service may now only connect through the TUN, except for its mDNS and LLMNR. The filters recognize the service by its SID in the token of its process, as Windows Firewall's own rules for it do. Earlier versions only query port 53, and may run the service in one process with others, so they get no such filters. The port 53 rule stays, for the programs that query a resolver on the local network themselves, and for those earlier versions. Tested on Windows 11, elevated: with DoH set on Wi-Fi per adapter, per network profile or by global auto-upgrade, none of the DNS Client's connections left through Wi-Fi (WFP logged the drops by the new filter), names still resolved through the TUN, mDNS and LLMNR still went out, and other programs were unaffected, also in a real Xray run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
207 lines
6.8 KiB
Go
207 lines
6.8 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 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)
|
|
}
|
|
if err := addLeakFilters(engine, loopback, true, true); err != nil {
|
|
skipUnlessElevated(err)
|
|
}
|
|
}
|
|
|
|
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"),
|
|
}
|
|
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)
|
|
}
|
|
}
|