mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-30 04:55:42 +00:00
TUN inbound: Keep Windows' DNS Client from sending DoH/DoT outside the TUN
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
eb29a4e3de
commit
edd916b08e
+2
-2
@@ -201,14 +201,14 @@ After the start network adapter with the name you chose in the config will be cr
|
||||
When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops.
|
||||
|
||||
With `strictRoute` and `autoSystemRoutingTable` set, Xray also adds Windows Filtering Platform filters that keep two kinds of traffic of every program but Xray itself from leaving outside the TUN:
|
||||
- With `dns` set, DNS (port 53) only goes through the TUN. Windows keeps sending name queries to the DNS servers of the other interfaces as well, and a resolver on the local network (e.g. `192.168.1.1` handed out by DHCP) is reached through its more specific LAN route instead of the TUN. The `dns` servers therefore have to lie within `gateway` or `autoSystemRoutingTable` (a warning is logged otherwise), and DNS servers that should be reached directly belong in Xray's own `dns` settings.
|
||||
- With `dns` set, DNS (port 53) only goes through the TUN. Windows keeps sending name queries to the DNS servers of the other interfaces as well, out through those interfaces whatever the routes say, and other programs reach a resolver on the local network (e.g. `192.168.1.1` handed out by DHCP) through its more specific LAN route instead of the TUN. On Windows 11 and Server 2022 and later, where those queries may also go over HTTPS or TLS, Windows' DNS Client service cannot connect outside the TUN at all, except for name resolution on the local network (LLMNR, mDNS). The `dns` servers therefore have to lie within `gateway` or `autoSystemRoutingTable` (a warning is logged otherwise), and DNS servers that should be reached directly belong in Xray's own `dns` settings.
|
||||
- Unless the TUN carries IPv6, that is, has an IPv6 address in `gateway` and IPv6 routes in `autoSystemRoutingTable`, IPv6 is blocked entirely, in both directions, as it would bypass the TUN. Only loopback and what Windows itself needs on the local link (neighbor and multicast listener discovery, DHCPv6) remain allowed.
|
||||
|
||||
With the filters in place, Xray's own connections out also get past Windows Firewall's block rules (other firewalls may still block them), while connections to Xray's inbounds stay subject to them.
|
||||
|
||||
Names that Xray resolves through the system resolver, such as an outbound's server address given as a domain with the default `AsIs` domain strategy, would be looked up by Windows on Xray's behalf, and those queries would then go into the TUN too. While DNS is restricted this way and `autoOutboundsInterface` is in use (the default with `autoSystemRoutingTable`), Xray therefore resolves them itself, with its own queries to the DNS servers of the other interfaces. That bypasses Windows' DNS cache, and its name resolution on the local network (LLMNR, mDNS): a server address given as a domain is looked up again for every connection, and a DNS server that does not answer delays each lookup. Having Xray's own `dns` resolve it, through the outbound's `sockopt.domainStrategy`, avoids that. The `localhost` DNS server queries the same servers whenever `autoOutboundsInterface` is in use. Both skip the TUN's own DNS servers, unless another interface uses them as well: queried from Xray itself, they would lead back into it, or nowhere.
|
||||
|
||||
If the filters cannot be added, the TUN does not start (on Windows 10 and later; older versions only log a warning). They are removed when Xray exits. Not covered are encrypted DNS that Windows may send to the servers of other interfaces (DNS over HTTPS), and name resolution on the local network over IPv4 (LLMNR, mDNS, NetBIOS).
|
||||
If the filters cannot be added, the TUN does not start (on Windows 10 and later; older versions only log a warning). They are removed when Xray exits. Not covered is name resolution on the local network (LLMNR, mDNS, NetBIOS), except over IPv6 while that is blocked.
|
||||
|
||||
`strictRoute` (Windows only) is `false` by default, as the filters break some setups: a local DNS resolver other programs use (e.g. on `127.0.0.1:53`), the DNS of another VPN on its own interface, IPv6 on the local network while the TUN has no IPv6 address, virtual machines whose NAT resolves names on the host, or signing in to a captive portal. Without the filters, DNS may leak as described above.
|
||||
|
||||
|
||||
@@ -36,12 +36,13 @@ const (
|
||||
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
|
||||
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
|
||||
|
||||
fwpMatchEqual = 0 // FWP_MATCH_EQUAL
|
||||
fwpMatchFlagsAllSet = 6 // FWP_MATCH_FLAGS_ALL_SET
|
||||
@@ -68,8 +69,14 @@ var (
|
||||
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}
|
||||
@@ -170,10 +177,14 @@ func condition(field *windows.GUID, typ uint32, value uintptr) fwpmFilterConditi
|
||||
// - dns: DNS (port 53) may only go through the TUN. 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. The physical interface usually got an
|
||||
// on-link resolver like 192.168.1.1 from DHCP, and its LAN route is more
|
||||
// specific than the TUN's default route, so those queries would leave
|
||||
// through the physical link.
|
||||
// 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. Since Windows 11 and Server 2022, Windows may also send its
|
||||
// queries over HTTPS or TLS, so there its DNS Client service may not
|
||||
// connect outside the TUN at all, except for name resolution on the local
|
||||
// link (mDNS, LLMNR).
|
||||
// - ipv6: no IPv6 at all, in either direction, for a TUN that cannot carry
|
||||
// it, except loopback and what Windows itself needs on the local link
|
||||
// (neighbor and multicast listener discovery, DHCPv6), none of which can
|
||||
@@ -304,6 +315,47 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, dns, ipv6 bool) er
|
||||
}
|
||||
}
|
||||
|
||||
// Since Windows 11 and Server 2022 (build 20348), 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, which stay on the
|
||||
// local link (over IPv6 only while IPv6 is not blocked altogether).
|
||||
// Earlier versions only query port 53, and may run the service in one
|
||||
// process with others, which the filters would catch as well. Like
|
||||
// Windows Firewall's rules for it, they 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 _, _, build := windows.RtlGetNtVersionNumbers(); dns && build >= 20348 {
|
||||
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
|
||||
localLink bool
|
||||
}{
|
||||
{&fwpmLayerALEAuthConnectV4, true},
|
||||
{&fwpmLayerALEAuthConnectV6, !ipv6},
|
||||
} {
|
||||
if err := add(layer.key, "permit the DNS Client service through the TUN", 0, fwpActionPermit, 3, dnsClient, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPNexthopInterface)); err != nil {
|
||||
return err
|
||||
}
|
||||
if layer.localLink {
|
||||
if err := add(layer.key, "permit the DNS Client service's mDNS and LLMNR", 0, fwpActionPermit, 3, mdnsLLMNR...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := add(layer.key, "block the DNS Client service", 0, fwpActionBlock, 2, dnsClient); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ipv6 {
|
||||
// Both directions: replies to a connection accepted from outside
|
||||
// would leave through the physical link as well.
|
||||
|
||||
@@ -119,6 +119,16 @@ func TestLeakFiltersAccepted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user