mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-11-04 01:17:02 +00:00
"allexceptlan" is "all" but for the local network: traffic to and from private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7), link-local (169.254.0.0/16, fe80::/10), multicast (224.0.0.0/4, ff00::/8) and broadcast (255.255.255.255) addresses stays allowed, so that printers, the router's settings page, or casting to a TV keep working while nothing else leaves outside the TUN. It includes "dns": a resolver on the local network, like the router's, would forward the queries outside the TUN, so DNS stays blocked there. The LAN permit sits below the other values, like the permit through the TUN, so they still apply on the local network, e.g. "tcp/445" blocks Windows file sharing with it. The DNS Client service keeps mDNS and LLMNR, to the local network's addresses only. "all" overrides it. The addresses are matched with FWP_V4_ADDR_MASK and FWP_V6_ADDR_MASK conditions on the remote address, which match when any of them does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
281 lines
9.7 KiB
Go
281 lines
9.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 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"),
|
|
}
|
|
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)
|
|
}
|
|
}
|