mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-29 22:46:58 +00:00
"dns" in autoSystemWfpBlockLeak only blocked port 53 outside the TUN, plus the DNS Client service on Windows 11 and Server 2022 and later, so other programs' DNS over TLS (TCP 853) and DNS over QUIC (UDP 853) could still leave outside the TUN. autoSystemWfpBlockLeak now takes: - "tcp/<port>", "udp/<port>": connections to that port may only go through the TUN (or come from Xray itself). - "windnsclient": Windows' DNS Client service may only connect through the TUN, except for mDNS and LLMNR. As before, this only applies from Windows 11 and Server 2022 on, where the service may use DoH or DoT to any port; earlier versions only query port 53. - "dns": stands for "udp/53", "tcp/53", "windnsclient", "tcp/853" and "udp/853". - "misconfigtun": unchanged. Values are case-insensitive, deduplicated, and anything else, such as "tcp/0" or "icmp/1", is rejected. Keeping the system's DNS inside the TUN, with "udp/53", "tcp/53" or "windnsclient", needs dns to be set, as "dns" did; ports like 853 alone do not. Each port gets its own permit-through-TUN and block filters with a protocol condition, where port 53 used to have one for both protocols, and the log lists what is blocked, e.g. "[tun] blocked outside the TUN: udp/53, tcp/53, tcp/853, udp/853, windnsclient". Tested on Windows 11 with a Wintun adapter as the outside interface: with "tcp/853", "udp/853", "tcp/7215" and "udp/7216", those are blocked outside the TUN over IPv4 and IPv6, while tcp/7216, udp/7215 and 443 are not, port 853 works through the TUN, Xray's own connections to port 853 still leave outside it, and nothing is blocked after Xray stops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
234 lines
7.7 KiB
Go
234 lines
7.7 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)
|
|
}
|
|
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun", "tcp/443"})
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestNewLeakFilters(t *testing.T) {
|
|
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
block := newLeakFilters(leaks, true, false)
|
|
want := "udp/53, tcp/53, tcp/853, udp/853, IPv6"
|
|
if dnsClientUsesAnyPort() {
|
|
want = "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv6"
|
|
}
|
|
if got := block.String(); got != want {
|
|
t.Errorf("String() = %q, want %q", got, want)
|
|
}
|
|
if !block.systemDNS() {
|
|
t.Error("systemDNS() = false with dns")
|
|
}
|
|
if block := newLeakFilters([]string{"tcp/853", "udp/853", "misconfigtun"}, true, true); block.String() != "tcp/853, udp/853" || block.systemDNS() {
|
|
t.Errorf("tcp/853, udp/853: %q, systemDNS() = %v", block.String(), block.systemDNS())
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|