mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-11-06 02:07:17 +00:00
https://github.com/XTLS/Xray-core/pull/7288#issuecomment-6559954865 https://github.com/XTLS/Xray-core/pull/7315#issuecomment-6582481840 https://github.com/XTLS/Xray-core/pull/7315#issuecomment-6588253141 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
704 lines
28 KiB
Go
704 lines
28 KiB
Go
//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
|
|
}
|