//go:build windows package tun import ( "encoding/binary" "net/netip" "os" "runtime" "slices" "strings" "unsafe" "github.com/xtls/xray-core/common/errors" "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" ) var ( modfwpuclnt = windows.NewLazySystemDLL("fwpuclnt.dll") moddnsapi = windows.NewLazySystemDLL("dnsapi.dll") procFwpmEngineOpen0 = modfwpuclnt.NewProc("FwpmEngineOpen0") procFwpmEngineClose0 = modfwpuclnt.NewProc("FwpmEngineClose0") procFwpmTransactionBegin0 = modfwpuclnt.NewProc("FwpmTransactionBegin0") procFwpmTransactionCommit0 = modfwpuclnt.NewProc("FwpmTransactionCommit0") procFwpmTransactionAbort0 = modfwpuclnt.NewProc("FwpmTransactionAbort0") procFwpmSubLayerAdd0 = modfwpuclnt.NewProc("FwpmSubLayerAdd0") procFwpmFilterAdd0 = modfwpuclnt.NewProc("FwpmFilterAdd0") procFwpmGetAppIdFromFileName0 = modfwpuclnt.NewProc("FwpmGetAppIdFromFileName0") procFwpmFreeMemory0 = modfwpuclnt.NewProc("FwpmFreeMemory0") procDnsFlushResolverCache = moddnsapi.NewProc("DnsFlushResolverCache") ) // fwptypes.h and fwpmtypes.h const ( rpcCAuthnWinNT = 10 // RPC_C_AUTHN_WINNT fwpmSessionFlagDynamic = 1 // FWPM_SESSION_FLAG_DYNAMIC fwpmFilterFlagClearActionRight = 8 // FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT fwpUint8 = 1 // FWP_UINT8 fwpUint16 = 2 // FWP_UINT16 fwpUint32 = 3 // FWP_UINT32 fwpUint64 = 4 // FWP_UINT64 fwpByteArray16Type = 11 // FWP_BYTE_ARRAY16_TYPE fwpByteBlobType = 12 // FWP_BYTE_BLOB_TYPE fwpSecurityDescriptorType = 14 // FWP_SECURITY_DESCRIPTOR_TYPE fwpV4AddrMask = 0x100 // FWP_V4_ADDR_MASK fwpV6AddrMask = 0x101 // FWP_V6_ADDR_MASK fwpRangeType = 0x102 // FWP_RANGE_TYPE fwpMatchEqual = 0 // FWP_MATCH_EQUAL fwpMatchRange = 5 // FWP_MATCH_RANGE fwpMatchFlagsAllSet = 6 // FWP_MATCH_FLAGS_ALL_SET fwpConditionFlagIsLoopback = 1 // FWP_CONDITION_FLAG_IS_LOOPBACK fwpActionBlock = 0x1001 // FWP_ACTION_BLOCK fwpActionPermit = 0x1002 // FWP_ACTION_PERMIT ) // fwpmu.h var ( fwpmLayerALEAuthConnectV4 = windows.GUID{Data1: 0xc38d57d1, Data2: 0x05a7, Data3: 0x4c33, Data4: [8]byte{0x90, 0x4f, 0x7f, 0xbc, 0xee, 0xe6, 0x0e, 0x82}} fwpmLayerALEAuthConnectV6 = windows.GUID{Data1: 0x4a72393b, Data2: 0x319f, Data3: 0x44bc, Data4: [8]byte{0x84, 0xc3, 0xba, 0x54, 0xdc, 0xb3, 0xb6, 0xb4}} fwpmLayerALEAuthRecvAcceptV4 = windows.GUID{Data1: 0xe1cd9fe7, Data2: 0xf4b5, Data3: 0x4273, Data4: [8]byte{0x96, 0xc0, 0x59, 0x2e, 0x48, 0x7b, 0x86, 0x50}} fwpmLayerALEAuthRecvAcceptV6 = windows.GUID{Data1: 0xa3b42c97, Data2: 0x9f04, Data3: 0x4672, Data4: [8]byte{0xb8, 0x7e, 0xce, 0xe9, 0xc4, 0x83, 0x25, 0x7f}} fwpmConditionFlags = windows.GUID{Data1: 0x632ce23b, Data2: 0x5167, Data3: 0x435c, Data4: [8]byte{0x86, 0xd7, 0xe9, 0x03, 0x68, 0x4a, 0xa8, 0x0c}} fwpmConditionIPArrivalInterface = windows.GUID{Data1: 0x618a9b6d, Data2: 0x386b, Data3: 0x4136, Data4: [8]byte{0xad, 0x6e, 0xb5, 0x15, 0x87, 0xcf, 0xb1, 0xcd}} fwpmConditionIPLocalInterface = windows.GUID{Data1: 0x4cd62a49, Data2: 0x59c3, Data3: 0x4969, Data4: [8]byte{0xb7, 0xf3, 0xbd, 0xa5, 0xd3, 0x28, 0x90, 0xa4}} fwpmConditionIPLocalPort = windows.GUID{Data1: 0x0c1ba1af, Data2: 0x5765, Data3: 0x453f, Data4: [8]byte{0xaf, 0x22, 0xa8, 0xf7, 0x91, 0xac, 0x77, 0x5b}} // also FWPM_CONDITION_ICMP_TYPE fwpmConditionIPNexthopInterface = windows.GUID{Data1: 0x93ae8f5b, Data2: 0x7f6f, Data3: 0x4719, Data4: [8]byte{0x98, 0xc8, 0x14, 0xe9, 0x74, 0x29, 0xef, 0x04}} fwpmConditionIPProtocol = windows.GUID{Data1: 0x3971ef2b, Data2: 0x623e, Data3: 0x4f9a, Data4: [8]byte{0x8c, 0xb1, 0x6e, 0x79, 0xb8, 0x06, 0xb9, 0xa7}} fwpmConditionIPRemoteAddress = windows.GUID{Data1: 0xb235ae9a, Data2: 0x1d64, Data3: 0x49b8, Data4: [8]byte{0xa4, 0x4c, 0x5f, 0xf3, 0xd9, 0x09, 0x50, 0x45}} fwpmConditionIPRemotePort = windows.GUID{Data1: 0xc35a604d, Data2: 0xd22b, Data3: 0x4e1a, Data4: [8]byte{0x91, 0xb4, 0x68, 0xf6, 0x74, 0xee, 0x67, 0x4b}} // also FWPM_CONDITION_ICMP_CODE fwpmConditionALEAppID = windows.GUID{Data1: 0xd78e1e87, Data2: 0x8644, Data3: 0x4ea5, Data4: [8]byte{0x94, 0x37, 0xd8, 0x09, 0xec, 0xef, 0xc9, 0x71}} fwpmConditionALEUserID = windows.GUID{Data1: 0xaf043a0a, Data2: 0xb34d, Data3: 0x4f86, Data4: [8]byte{0x97, 0x9c, 0xc9, 0x03, 0x71, 0xaf, 0x6e, 0x66}} ) // dnsClientSID is the SID of Windows' DNS Client service, NT SERVICE\Dnscache. // Service SIDs derive from the service name, so it is the same everywhere (sc // showsid dnscache). const dnsClientSID = "S-1-5-80-859482183-879914841-863379149-1145462774-2388618682" // ff02::1:2, where DHCPv6 clients send to. A package-level variable never // moves, so conditions may refer to it through uintptr. var ipv6AllDHCPv6Servers = [16]byte{0xff, 0x02, 13: 0x01, 15: 0x02} type fwpByteBlob struct { size uint32 data *byte } // fwpValue0 is FWP_VALUE0 as well as FWP_CONDITION_VALUE0. Their union holds // a scalar of at most 32 bits, or a pointer for the larger types. type fwpValue0 struct { typ uint32 value uintptr } // fwpRange0 is FWP_RANGE0, which an FWP_RANGE_TYPE condition points to. type fwpRange0 struct { low, high fwpValue0 } // fwpV4AddrAndMask is FWP_V4_ADDR_AND_MASK, in host byte order. type fwpV4AddrAndMask struct { addr, mask uint32 } // fwpV6AddrAndMask is FWP_V6_ADDR_AND_MASK. type fwpV6AddrAndMask struct { addr [16]byte prefixLength uint8 } // lanPrefixes are what "allexceptlan" leaves to the local network: private // and link-local addresses, and multicast and broadcast, which discovery on // it uses. var lanPrefixes = []netip.Prefix{ netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("172.16.0.0/12"), netip.MustParsePrefix("192.168.0.0/16"), netip.MustParsePrefix("169.254.0.0/16"), netip.MustParsePrefix("224.0.0.0/4"), netip.MustParsePrefix("255.255.255.255/32"), netip.MustParsePrefix("fc00::/7"), netip.MustParsePrefix("fe80::/10"), netip.MustParsePrefix("ff00::/8"), } type fwpmDisplayData0 struct { name *uint16 description *uint16 } type fwpmSession0 struct { sessionKey windows.GUID displayData fwpmDisplayData0 flags uint32 txnWaitTimeoutInMSec uint32 processID uint32 sid *windows.SID username *uint16 kernelMode int32 } type fwpmSublayer0 struct { subLayerKey windows.GUID displayData fwpmDisplayData0 flags uint32 providerKey *windows.GUID providerData fwpByteBlob weight uint16 } type fwpmFilterCondition0 struct { fieldKey windows.GUID matchType uint32 conditionValue fwpValue0 } type fwpmAction0 struct { typ uint32 filterType windows.GUID } type fwpmFilter0 struct { filterKey windows.GUID displayData fwpmDisplayData0 flags uint32 providerKey *windows.GUID providerData fwpByteBlob layerKey windows.GUID subLayerKey windows.GUID weight fwpValue0 numFilterConditions uint32 filterCondition *fwpmFilterCondition0 action fwpmAction0 _ uint32 // C aligns the following union to 8 bytes, as it holds a UINT64 providerContextKey windows.GUID reserved *windows.GUID _ [8 - unsafe.Sizeof(uintptr(0))]byte // and filterId as well, also on 32-bit filterID uint64 effectiveWeight fwpValue0 } // fwpmResult converts the DWORD status the Fwpm functions return. func fwpmResult(r1, _ uintptr, _ error) error { if r1 != 0 { return windows.Errno(r1) } return nil } func utf16Ptr(s string) *uint16 { p, _ := windows.UTF16PtrFromString(s) return p } func condition(field *windows.GUID, typ uint32, value uintptr) fwpmFilterCondition0 { return fwpmFilterCondition0{ fieldKey: *field, matchType: fwpMatchEqual, conditionValue: fwpValue0{typ: typ, value: value}, } } // leakFilters are the values of autoSystemWfpBlockLeak that blockLeaks keeps // from leaving outside the TUN. type leakFilters []leak // newLeakFilters turns expanded autoSystemWfpBlockLeak values into the filters // for a TUN that the routes of route4 (IPv4) and route6 (IPv6) lead to, which // leave "misconfigtun" over the IP versions without such routes only. func newLeakFilters(leaks []string, route4, route6 bool) leakFilters { var block leakFilters for _, value := range leaks { l, ok := parseLeak(value) if !ok { continue } if l.name == "misconfigtun" { unrouted4, unrouted6 := !route4, !route6 if l.prefix.IsValid() { unrouted4, unrouted6 = unrouted4 && l.prefix.Addr().Is4(), unrouted6 && l.prefix.Addr().Is6() } switch { case !unrouted4 && !unrouted6: continue case !l.prefix.IsValid() && !unrouted4: l.prefix = allAddresses(true) case !l.prefix.IsValid() && !unrouted6: l.prefix = allAddresses(false) } } if !slices.Contains(block, l) { block = append(block, l) } } return block } // String lists what block keeps inside the TUN, or is empty for nothing. func (block leakFilters) String() string { values := make([]string, len(block)) for i, l := range block { values[i] = l.String() } return strings.Join(values, ", ") } // systemDNS tells whether block keeps the system's DNS inside the TUN, to at // least some addresses: port 53, the DNS Client service, or everything ("all"), // though not everything but the local network ("allexceptlan"), where a // resolver like the router's may be. func (block leakFilters) systemDNS() bool { return slices.ContainsFunc(block, leak.blocksDNS) } // prefixes returns the CIDRs of the values of block named name over an IP // version. func (block leakFilters) prefixes(name string, ipv6 bool) []netip.Prefix { var prefixes []netip.Prefix for _, l := range block { if prefix, ok := l.over(ipv6); ok && l.name == name { prefixes = append(prefixes, prefix) } } return prefixes } // over returns the CIDR of l over an IP version, all its addresses for l // without one, or false for l over the other IP version only. func (l leak) over(ipv6 bool) (netip.Prefix, bool) { if !l.prefix.IsValid() { return allAddresses(ipv6), true } return l.prefix, l.prefix.Addr().Is6() == ipv6 } // allAddresses is the CIDR of all addresses of an IP version. func allAddresses(ipv6 bool) netip.Prefix { if ipv6 { return netip.PrefixFrom(netip.IPv6Unspecified(), 0) } return netip.PrefixFrom(netip.IPv4Unspecified(), 0) } // lanWithin returns the parts of lanPrefixes that lie in any of prefixes: two // prefixes either share no address, or one holds the other. func lanWithin(prefixes ...netip.Prefix) []netip.Prefix { var parts []netip.Prefix for _, lan := range lanPrefixes { for _, prefix := range prefixes { if !prefix.Overlaps(lan) { continue } part := lan if prefix.Bits() > lan.Bits() { part = prefix } if !slices.Contains(parts, part) { parts = append(parts, part) } } } return parts } // dnsClientUsesAnyPort tells whether Windows' DNS Client service may send its // queries over HTTPS or TLS, and so to any port, which it does since Windows 11 // and Server 2022 (build 20348). Earlier versions only query port 53, and may // run the service in one process with others, which its filters would catch // as well. func dnsClientUsesAnyPort() bool { _, _, build := windows.RtlGetNtVersionNumbers() return build >= 20348 } // The weights of the filters of blockLeaks, the heaviest matching one of which // decides. const ( weightAllExceptLAN uint8 = iota // block "allexceptlan" weightLAN // permit its local network weightDNSClient // block "windnsclient" weightLocalLink // permit its mDNS and LLMNR on the local network weightBlock // block the other values weightPermit // permit what always stays allowed, and the TUN weightXray // permit Xray ) // blockLeaks keeps traffic from leaving through interfaces other than tun, // for every program but Xray itself, whose outbounds (DNS included) use the // other interfaces on purpose, as the values of block say, each to the // addresses of its CIDR, or to all: // // - Protocols and ports: connections of these, TCP or UDP to some or all // ports, or ICMP, may only go through the TUN, those of DNS (53), DNS // over TLS (TCP 853) and DNS over QUIC (UDP 853) with "dns". Windows // sends a name query to the DNS servers of all interfaces, not only to // those of the TUN: to the first server of each interface, then to all // of them when no answer arrives within a second or two. It sends the // queries for the servers of an interface out through that interface, // whatever the routes say, and other programs reach an on-link resolver, // like 192.168.1.1 from DHCP, through its LAN route, which is more // specific than the TUN's default route. // - "windnsclient": since Windows 11 and Server 2022, Windows may also send // its queries over HTTPS or TLS, to any port, so its DNS Client service // may not connect outside the TUN at all, except for name resolution on // the local network (mDNS, LLMNR). // - "all", and "misconfigtun" over an IP version no route of which leads to // the TUN: nothing at all outside the TUN, in either direction. The TUN // carries what is routed to it even without an address of that IP // version in gateway: Windows gives it link-local ones itself, an IPv6 // one at once, an IPv4 one from 169.254.0.0/16 after some seconds (until // then, IPv4 routed to the TUN is unreachable). // - "allexceptlan": the same, but to and from the local network. // // The other values still apply to what "allexceptlan" and "windnsclient" leave // open on the local network. Whatever is blocked, loopback, which cannot leave // the system, and what Windows itself needs on the local link stay allowed: // DHCP, and for IPv6 neighbor and multicast listener discovery. // // The filters live in a dynamic WFP session: closing the returned engine handle // with closeWFPEngine deletes them, and so does Windows when the process dies. func blockLeaks(tun winipcfg.LUID, block leakFilters) (windows.Handle, error) { engine, err := openWFPEngine() if err != nil { return 0, err } if err := fwpmResult(procFwpmTransactionBegin0.Call(uintptr(engine), 0)); err != nil { closeWFPEngine(engine) return 0, errors.New("FwpmTransactionBegin0 failed").Base(err) } err = addLeakFilters(engine, tun, block) if err == nil { if err = fwpmResult(procFwpmTransactionCommit0.Call(uintptr(engine))); err != nil { err = errors.New("FwpmTransactionCommit0 failed").Base(err) } } if err != nil { procFwpmTransactionAbort0.Call(uintptr(engine)) closeWFPEngine(engine) return 0, err } return engine, nil } func openWFPEngine() (windows.Handle, error) { if err := modfwpuclnt.Load(); err != nil { return 0, err } // txnWaitTimeoutInMSec stays 0 for BFE's default, so that a transaction // held by another program cannot hang the start forever. session := fwpmSession0{ displayData: fwpmDisplayData0{name: utf16Ptr("Xray TUN")}, flags: fwpmSessionFlagDynamic, } var engine windows.Handle if err := fwpmResult(procFwpmEngineOpen0.Call(0, rpcCAuthnWinNT, 0, uintptr(unsafe.Pointer(&session)), uintptr(unsafe.Pointer(&engine)))); err != nil { return 0, errors.New("FwpmEngineOpen0 failed").Base(err) } return engine, nil } func closeWFPEngine(engine windows.Handle) { procFwpmEngineClose0.Call(uintptr(engine)) } // addLeakFilters adds the filters of blockLeaks in a sublayer of their own. // blockLeaks runs it in a transaction, so that they take effect all at once. func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters) error { exe, err := os.Executable() if err != nil { return err } exePath, err := windows.UTF16PtrFromString(exe) if err != nil { return err } var appID *fwpByteBlob if err := fwpmResult(procFwpmGetAppIdFromFileName0.Call(uintptr(unsafe.Pointer(exePath)), uintptr(unsafe.Pointer(&appID)))); err != nil { return errors.New("FwpmGetAppIdFromFileName0 failed for ", exe).Base(err) } defer func() { procFwpmFreeMemory0.Call(uintptr(unsafe.Pointer(&appID))) }() sublayer := fwpmSublayer0{ displayData: fwpmDisplayData0{name: utf16Ptr("Xray TUN")}, weight: 0xffff, } if sublayer.subLayerKey, err = windows.GenerateGUID(); err != nil { return err } if err := fwpmResult(procFwpmSubLayerAdd0.Call(uintptr(engine), uintptr(unsafe.Pointer(&sublayer)), 0)); err != nil { return errors.New("FwpmSubLayerAdd0 failed").Base(err) } add := func(layer *windows.GUID, name string, flags, action uint32, weight uint8, conditions ...fwpmFilterCondition0) error { return addFilter(engine, &sublayer.subLayerKey, layer, "Xray TUN: "+name, flags, action, weight, conditions...) } var pinner runtime.Pinner defer pinner.Unpin() tunLUID := new(uint64) *tunLUID = uint64(tun) pinner.Pin(tunLUID) // the condition only holds it as uintptr // The heaviest matching filter of a sublayer decides. All sublayers have // their say, though, and a block in any of them beats a permit, unless // the permit is hard: it clears the action right, and then the blocks of // lower sublayers, Windows Firewall rules among them, no longer override // it, only a callout's veto does. Xray's own connections out get such a // hard permit. Connections from outside to Xray get an ordinary one, so // that firewalls keep guarding its inbounds. self := condition(&fwpmConditionALEAppID, fwpByteBlobType, uintptr(unsafe.Pointer(appID))) // A connection goes through the TUN when its local address is the TUN's, // and it also leaves, or arrives, through the TUN. The local address alone // decides by default, but with weak host sending or receiving enabled, // packets of the TUN's address can use other interfaces. (The next hop, // the interface replies would leave by, is not known for arriving ones.) onTUN := func(field *windows.GUID) fwpmFilterCondition0 { return condition(field, fwpUint64, uintptr(unsafe.Pointer(tunLUID))) } // remote matches the remote addresses in any of prefixes, all of one IP // version: conditions on the same field match when any of them does. With // all addresses among them, no condition is needed. remote := func(prefixes ...netip.Prefix) []fwpmFilterCondition0 { if slices.ContainsFunc(prefixes, func(prefix netip.Prefix) bool { return prefix.Bits() == 0 }) { return nil } var conditions []fwpmFilterCondition0 for _, prefix := range prefixes { var value uintptr typ := uint32(fwpV4AddrMask) if prefix.Addr().Is6() { addr := &fwpV6AddrAndMask{addr: prefix.Addr().As16(), prefixLength: uint8(prefix.Bits())} pinner.Pin(addr) // the condition only holds it as uintptr value, typ = uintptr(unsafe.Pointer(addr)), fwpV6AddrMask } else { a := prefix.Addr().As4() addr := &fwpV4AddrAndMask{addr: binary.BigEndian.Uint32(a[:]), mask: ^uint32(0) << (32 - prefix.Bits())} pinner.Pin(addr) // the condition only holds it as uintptr value = uintptr(unsafe.Pointer(addr)) } conditions = append(conditions, condition(&fwpmConditionIPRemoteAddress, typ, value)) } return conditions } // toPorts matches the ports of traffic, if not all of them. toPorts := func(traffic leak) []fwpmFilterCondition0 { switch { case traffic.low == 0: return nil case traffic.low == traffic.high: return []fwpmFilterCondition0{condition(&fwpmConditionIPRemotePort, fwpUint16, uintptr(traffic.low))} } ports := &fwpRange0{ low: fwpValue0{typ: fwpUint16, value: uintptr(traffic.low)}, high: fwpValue0{typ: fwpUint16, value: uintptr(traffic.high)}, } pinner.Pin(ports) // the condition only holds it as uintptr return []fwpmFilterCondition0{{ fieldKey: fwpmConditionIPRemotePort, matchType: fwpMatchRange, conditionValue: fwpValue0{typ: fwpRangeType, value: uintptr(unsafe.Pointer(ports))}, }} } // What stays allowed whatever is blocked, in both directions: replies to // a connection accepted from outside would leave through the physical // link as well. type permit struct { name string conditions []fwpmFilterCondition0 } loopback := permit{"loopback", []fwpmFilterCondition0{{ fieldKey: fwpmConditionFlags, matchType: fwpMatchFlagsAllSet, conditionValue: fwpValue0{typ: fwpUint32, value: fwpConditionFlagIsLoopback}, }}} // DHCP keeps the addresses of the other interfaces, which Xray's own // connections use. dhcp := permit{"DHCP", []fwpmFilterCondition0{ condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP), condition(&fwpmConditionIPLocalPort, fwpUint16, 68), condition(&fwpmConditionIPRemotePort, fwpUint16, 67), }} // Neighbor and multicast listener discovery, ICMPv6 130-137 and 143, // whose type and code sit where the local and remote port are. discovery := permit{"IPv6 neighbor and multicast listener discovery", []fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_ICMPV6)}} for _, typ := range []uintptr{130, 131, 132, 133, 134, 135, 136, 137, 143} { discovery.conditions = append(discovery.conditions, condition(&fwpmConditionIPLocalPort, fwpUint16, typ)) } discovery.conditions = append(discovery.conditions, condition(&fwpmConditionIPRemotePort, fwpUint16, 0)) dhcpv6 := []fwpmFilterCondition0{ condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP), condition(&fwpmConditionIPLocalPort, fwpUint16, 546), condition(&fwpmConditionIPRemotePort, fwpUint16, 547), } // The DHCPv6 client sends to the servers' multicast address, and they // answer from their own. dhcpv6Out := permit{"DHCPv6", slices.Concat(dhcpv6, []fwpmFilterCondition0{condition(&fwpmConditionIPRemoteAddress, fwpByteArray16Type, uintptr(unsafe.Pointer(&ipv6AllDHCPv6Servers)))})} dhcpv6In := permit{"DHCPv6", dhcpv6} for _, layer := range []struct { key *windows.GUID selfFlags uint32 via *windows.GUID // the interface that has to be the TUN as well ipv6 bool permits []permit }{ {&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface, false, []permit{loopback, dhcp}}, {&fwpmLayerALEAuthRecvAcceptV4, 0, &fwpmConditionIPArrivalInterface, false, []permit{loopback, dhcp}}, {&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface, true, []permit{loopback, discovery, dhcpv6Out}}, {&fwpmLayerALEAuthRecvAcceptV6, 0, &fwpmConditionIPArrivalInterface, true, []permit{loopback, discovery, dhcpv6In}}, } { throughTUN := permit{"through the TUN", []fwpmFilterCondition0{onTUN(&fwpmConditionIPLocalInterface), onTUN(layer.via)}} if err := add(layer.key, "permit Xray", layer.selfFlags, fwpActionPermit, weightXray, self); err != nil { return err } for _, p := range append(layer.permits, throughTUN) { if err := add(layer.key, "permit "+p.name, 0, fwpActionPermit, weightPermit, p.conditions...); err != nil { return err } } for _, l := range block { prefix, ok := l.over(layer.ipv6) if !ok { continue // for the other IP version only } var protocols []uintptr switch l.name { case "tcp": protocols = []uintptr{windows.IPPROTO_TCP} case "udp": protocols = []uintptr{windows.IPPROTO_UDP} case "": protocols = []uintptr{windows.IPPROTO_TCP, windows.IPPROTO_UDP} case "icmp": protocols = []uintptr{windows.IPPROTO_ICMP} if layer.ipv6 { protocols = []uintptr{windows.IPPROTO_ICMPV6} } case "all", "misconfigtun": // every protocol default: continue // "allexceptlan" and "windnsclient" have filters of their own } var matches []fwpmFilterCondition0 for _, protocol := range protocols { matches = append(matches, condition(&fwpmConditionIPProtocol, fwpUint8, protocol)) } matches = slices.Concat(matches, toPorts(l), remote(prefix)) if err := add(layer.key, "block "+l.String(), 0, fwpActionBlock, weightBlock, matches...); err != nil { return err } } if allExceptLAN := block.prefixes("allexceptlan", layer.ipv6); len(allExceptLAN) > 0 { // Lighter than the other values, which so still apply on the // local network, DNS with "allexceptlan" among them. if lan := lanWithin(allExceptLAN...); len(lan) > 0 { if err := add(layer.key, "permit the local network", 0, fwpActionPermit, weightLAN, remote(lan...)...); err != nil { return err } } if err := add(layer.key, "block allexceptlan", 0, fwpActionBlock, weightAllExceptLAN, remote(allExceptLAN...)...); err != nil { return err } } } // Since Windows 11 and Server 2022, the DNS Client service may also send // the queries for an interface's servers over HTTPS or TLS, out through // that interface and to any port. So there it may only connect through // the TUN, except for mDNS and LLMNR on the local network. Like Windows // Firewall's rules for it, the filters recognize the service by its SID, // which Windows puts in the token of its process: the security descriptor // grants that SID the right to match (FWP_ACTRL_MATCH_FILTER, CC in SDDL). if slices.ContainsFunc(block, func(l leak) bool { return l.name == "windnsclient" }) { sd, err := windows.SecurityDescriptorFromString("O:SYG:SYD:(A;;CCRC;;;" + dnsClientSID + ")") if err != nil { return err } sdBlob := &fwpByteBlob{size: sd.Length(), data: (*byte)(unsafe.Pointer(sd))} pinner.Pin(sdBlob) // the condition only holds it as uintptr dnsClient := condition(&fwpmConditionALEUserID, fwpSecurityDescriptorType, uintptr(unsafe.Pointer(sdBlob))) // Conditions on the same field match when any of them does. mdnsLLMNR := []fwpmFilterCondition0{dnsClient, condition(&fwpmConditionIPRemotePort, fwpUint16, 5353), condition(&fwpmConditionIPRemotePort, fwpUint16, 5355)} for _, layer := range []struct { key *windows.GUID ipv6 bool }{ {&fwpmLayerALEAuthConnectV4, false}, {&fwpmLayerALEAuthConnectV6, true}, } { prefixes := block.prefixes("windnsclient", layer.ipv6) if len(prefixes) == 0 { continue } if err := add(layer.key, "permit the DNS Client service's mDNS and LLMNR", 0, fwpActionPermit, weightLocalLink, slices.Concat(mdnsLLMNR, remote(lanWithin(allAddresses(layer.ipv6))...))...); err != nil { return err } if err := add(layer.key, "block the DNS Client service", 0, fwpActionBlock, weightDNSClient, slices.Concat([]fwpmFilterCondition0{dnsClient}, remote(prefixes...))...); err != nil { return err } } } return nil } func addFilter(engine windows.Handle, sublayer, layer *windows.GUID, name string, flags, action uint32, weight uint8, conditions ...fwpmFilterCondition0) error { filter := fwpmFilter0{ displayData: fwpmDisplayData0{name: utf16Ptr(name)}, flags: flags, layerKey: *layer, subLayerKey: *sublayer, weight: fwpValue0{typ: fwpUint8, value: uintptr(weight)}, numFilterConditions: uint32(len(conditions)), action: fwpmAction0{typ: action}, } if len(conditions) > 0 { filter.filterCondition = &conditions[0] } if err := fwpmResult(procFwpmFilterAdd0.Call(uintptr(engine), uintptr(unsafe.Pointer(&filter)), 0, 0)); err != nil { return errors.New("FwpmFilterAdd0 failed for ", name).Base(err) } return nil } // dnsOutsideTUN returns the servers whose queries block blocks: those outside // all of prefixes, the TUN's own subnets and routes, as queries to them cannot // go through the TUN, and inside the CIDR of a value that keeps DNS inside the // TUN, or of "misconfigtun". Loopback ones, local resolvers, are not among // them, as loopback stays allowed. func dnsOutsideTUN(servers []netip.Addr, prefixes []netip.Prefix, block leakFilters) []netip.Addr { var outside []netip.Addr for _, server := range servers { server = server.Unmap() if server.IsLoopback() || slices.ContainsFunc(prefixes, func(p netip.Prefix) bool { return p.Contains(server) }) { continue } if slices.ContainsFunc(block, func(l leak) bool { prefix, ok := l.over(server.Is6()) return ok && prefix.Contains(server) && (l.blocksDNS() || l.name == "misconfigtun") }) { outside = append(outside, server) } } return outside } // flushDNSCache drops the answers Windows cached so far, like ipconfig // /flushdns, so that names get resolved again with the current DNS setup. func flushDNSCache() error { if err := procDnsFlushResolverCache.Find(); err != nil { return err } if r, _, err := procDnsFlushResolverCache.Call(); r == 0 { return err } return nil }