mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-30 13:05:43 +00:00
TUN inbound: Block DNS and IPv6 leaks outside the TUN on Windows; Add strictRoute
Windows sends name queries to the DNS servers of all interfaces, and a resolver on the local network (e.g. 192.168.1.1 from DHCP) is reached through its more specific LAN route instead of the TUN, so DNS leaks past it. IPv6 bypasses a TUN that cannot carry it. With autoSystemRoutingTable set, the Windows TUN now adds Windows Filtering Platform filters, all in one transaction and in a dynamic session, so that they are removed when Xray exits, even if it crashes: - DNS (port 53) only goes through the TUN, in both directions: its local address, and the interface it leaves or arrives by, must be the TUN's. - IPv6 is blocked in both directions when the TUN has no IPv6 address or no IPv6 route, except loopback, neighbor and multicast listener discovery, and DHCPv6. - Xray's own traffic is exempt: its connections out with a hard permit, which Windows Firewall rules do not override (like sing-box's strict_route), connections to its inbounds with an ordinary one. If the filters cannot be added, the TUN does not start on Windows 10 and later (only a warning on 7/8). The new `strictRoute` option (true by default) turns them off. Also on Windows: - A warning for `dns` servers outside gateway and autoSystemRoutingTable, as queries to them cannot go through the TUN and are blocked. - While DNS is restricted and autoOutboundsInterface is in use, Xray resolves the names it would ask Windows for itself (Go's resolver on its own sockets). Those lookups and the `localhost` DNS server skip the TUN's DNS servers, unless another interface uses them too, instead of looping back into the TUN. - The DNS cache is flushed when the TUN starts and stops, and DNS registration is turned off on the TUN (through netsh before Windows 10 1809). - Close no longer panics when registering the route or interface change callbacks failed. The README's Windows section describes all of it. Tested on Windows 11, elevated, amd64 and 386: the filters, DNS arriving through a real Wintun adapter and blocked outside it, the IPv6 block, Windows Firewall rules, and a real Xray run. Windows 7/8 and Windows 10 before 1809 are untested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7780db9bbe
commit
2db099b34b
@@ -97,6 +97,9 @@ func New() *Client {
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if internet.IsSkippedDNSServer(address) {
|
||||
return nil, errors.New("skipped DNS server ", address)
|
||||
}
|
||||
return d.DialContext(ctx, network, address)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package localdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func TestSkippedDNSServers(t *testing.T) {
|
||||
internet.SkipDNSServers([]netip.Addr{netip.MustParseAddr("203.0.113.53")})
|
||||
t.Cleanup(func() { internet.SkipDNSServers(nil) })
|
||||
c := New()
|
||||
if _, err := c.r.Dial(context.Background(), "udp", "203.0.113.53:53"); err == nil {
|
||||
t.Error("a skipped DNS server was dialed")
|
||||
}
|
||||
conn, err := c.r.Dial(context.Background(), "udp", "127.0.0.1:53")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
@@ -21,6 +21,7 @@ type TunConfig struct {
|
||||
AutoSystemRoutingTable []string `json:"autoSystemRoutingTable"`
|
||||
AutoOutboundsInterface *string `json:"autoOutboundsInterface"`
|
||||
AutoSystemDNS bool `json:"autoSystemDNS"`
|
||||
StrictRoute *bool `json:"strictRoute"`
|
||||
}
|
||||
|
||||
func (v *TunConfig) Build() (proto.Message, error) {
|
||||
@@ -33,6 +34,7 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
||||
UserLevel: v.UserLevel,
|
||||
AutoSystemRoutingTable: v.AutoSystemRoutingTable,
|
||||
AutoSystemDns: v.AutoSystemDNS,
|
||||
StrictRoute: v.StrictRoute,
|
||||
}
|
||||
if v.AutoOutboundsInterface != nil {
|
||||
config.AutoOutboundsInterface = *v.AutoOutboundsInterface
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/proxy/tun"
|
||||
)
|
||||
|
||||
func TestTunConfigStrictRoute(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(TunConfig)
|
||||
}
|
||||
enabled, disabled := true, false
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{"name": "xray0"}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500},
|
||||
},
|
||||
{
|
||||
Input: `{"name": "xray0", "strictRoute": false}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, StrictRoute: &disabled},
|
||||
},
|
||||
{
|
||||
Input: `{"name": "xray0", "strictRoute": true}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, StrictRoute: &enabled},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -198,6 +198,20 @@ To make it start, wintun.dll specific for your Windows/arch must be present next
|
||||
|
||||
After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running.
|
||||
|
||||
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 `autoSystemRoutingTable` set, and unless `strictRoute` is set to `false`, 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.
|
||||
- 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).
|
||||
|
||||
`strictRoute` (Windows only, `true` by default) can be set to `false` to go without the filters, for setups they break: 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. DNS may then leak as described above.
|
||||
|
||||
You can give the adapter ip address manually, you can live Windows to give it autogenerated ip address (which take few seconds), it doesn't matter, the traffic going _through_ the interface will be forwarded into the app for proxying. \
|
||||
Minimal configuration that will work for local machine is routing passing the traffic on-link through the interface.
|
||||
You will need the interface id for that, unfortunately it is going to change with every Xray start due to implementation ambiguity between Xray and wintun driver.
|
||||
|
||||
+14
-2
@@ -33,6 +33,7 @@ type Config struct {
|
||||
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
|
||||
Desc string `protobuf:"bytes,8,opt,name=desc,proto3" json:"desc,omitempty"`
|
||||
AutoSystemDns bool `protobuf:"varint,9,opt,name=auto_system_dns,json=autoSystemDns,proto3" json:"auto_system_dns,omitempty"`
|
||||
StrictRoute *bool `protobuf:"varint,10,opt,name=strict_route,json=strictRoute,proto3,oneof" json:"strict_route,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -130,11 +131,18 @@ func (x *Config) GetAutoSystemDns() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Config) GetStrictRoute() bool {
|
||||
if x != nil && x.StrictRoute != nil {
|
||||
return *x.StrictRoute
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var File_proxy_tun_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xaa\x02\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xe3\x02\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
|
||||
@@ -145,7 +153,10 @@ const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
|
||||
"\x04desc\x18\b \x01(\tR\x04desc\x12&\n" +
|
||||
"\x0fauto_system_dns\x18\t \x01(\bR\rautoSystemDnsBL\n" +
|
||||
"\x0fauto_system_dns\x18\t \x01(\bR\rautoSystemDns\x12&\n" +
|
||||
"\fstrict_route\x18\n" +
|
||||
" \x01(\bH\x00R\vstrictRoute\x88\x01\x01B\x0f\n" +
|
||||
"\r_strict_routeBL\n" +
|
||||
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
|
||||
|
||||
var (
|
||||
@@ -177,6 +188,7 @@ func file_proxy_tun_config_proto_init() {
|
||||
if File_proxy_tun_config_proto != nil {
|
||||
return
|
||||
}
|
||||
file_proxy_tun_config_proto_msgTypes[0].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
|
||||
@@ -16,4 +16,5 @@ message Config {
|
||||
string auto_outbounds_interface = 7;
|
||||
string desc = 8;
|
||||
bool auto_system_dns = 9;
|
||||
optional bool strict_route = 10;
|
||||
}
|
||||
|
||||
+227
-2
@@ -3,17 +3,25 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
go_errors "errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wintun"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
@@ -38,6 +46,10 @@ type WindowsTun struct {
|
||||
luid winipcfg.LUID
|
||||
cbr winipcfg.ChangeCallback
|
||||
cbi winipcfg.ChangeCallback
|
||||
wfp windows.Handle
|
||||
resolver *savedResolver
|
||||
skipStop chan struct{}
|
||||
skipDone chan struct{}
|
||||
closed bool
|
||||
}
|
||||
|
||||
@@ -197,19 +209,103 @@ startOver:
|
||||
}
|
||||
}
|
||||
|
||||
// Windows lists the TUN's DNS servers among the system's ones, which Go's
|
||||
// resolver queries for Xray's own lookups past the TUN, where they lead
|
||||
// nowhere or back into Xray. Not skipped are those another interface uses
|
||||
// as well, as that could leave no server at all. As those can change at
|
||||
// any time, they are looked at again as often as Go rereads its servers.
|
||||
if len(dns) > 0 {
|
||||
skipped, err := tunOnlyDNS(t.luid, dns)
|
||||
if err != nil {
|
||||
skipped = dns
|
||||
}
|
||||
internet.SkipDNSServers(skipped)
|
||||
t.skipStop, t.skipDone = make(chan struct{}), make(chan struct{})
|
||||
go func() {
|
||||
defer close(t.skipDone)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if skipped, err := tunOnlyDNS(t.luid, dns); err == nil {
|
||||
internet.SkipDNSServers(skipped)
|
||||
}
|
||||
case <-t.skipStop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Keep Windows from registering the TUN's addresses, and the host name
|
||||
// with them, through dynamic DNS updates. Best effort.
|
||||
if address4 || address6 {
|
||||
if err := disableDNSRegistration(t.luid, dns); err != nil {
|
||||
errors.LogDebugInner(context.Background(), err, "[tun] unable to disable DNS registration")
|
||||
}
|
||||
}
|
||||
|
||||
// Once the system routes lead to the TUN, keep DNS if dns is set, and IPv6
|
||||
// if the TUN cannot carry it (no IPv6 address, or no IPv6 route to it),
|
||||
// from leaving through the other interfaces, unless strictRoute is off.
|
||||
strictRoute := t.options.StrictRoute == nil || *t.options.StrictRoute
|
||||
if blockDNS, blockIPv6 := len(dns) > 0, !address6 || !route6; strictRoute && (route4 || route6) && (blockDNS || blockIPv6) {
|
||||
if t.wfp, err = blockLeaks(t.luid, blockDNS, blockIPv6); err != nil {
|
||||
what := "DNS and IPv6"
|
||||
if !blockIPv6 {
|
||||
what = "DNS"
|
||||
} else if !blockDNS {
|
||||
what = "IPv6"
|
||||
}
|
||||
// Rather no TUN than a leaking one. Before Windows 10 the filters are
|
||||
// untested, and sing-box's broke its TUN there (SagerNet/sing-box#3659),
|
||||
// so older versions only get a warning.
|
||||
if major, _, _ := windows.RtlGetNtVersionNumbers(); major >= 10 {
|
||||
return errors.New("unable to block ", what, " outside the TUN (set strictRoute to false to run without)").Base(err)
|
||||
}
|
||||
errors.LogWarningInner(context.Background(), err, "[tun] unable to block ", what, " outside the TUN, leaks are possible")
|
||||
} else {
|
||||
errors.LogInfo(context.Background(), "[tun] outside the TUN, blocked DNS: ", blockDNS, ", blocked IPv6: ", blockIPv6)
|
||||
if blockDNS {
|
||||
covered := slices.Clone(addresses)
|
||||
for _, route := range routesData {
|
||||
covered = append(covered, route.Destination)
|
||||
}
|
||||
for _, server := range dnsOutsideTUN(dns, covered) {
|
||||
errors.LogWarning(context.Background(), "[tun] DNS server ", server, " is in neither gateway nor autoSystemRoutingTable, so queries to it cannot go through the TUN and are blocked")
|
||||
}
|
||||
// With updater, the dialer controllers bind Xray's own sockets
|
||||
// to the physical interface.
|
||||
if updater != nil {
|
||||
t.resolver = resolveOnOwn()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(dns) > 0 || route4 || route6 {
|
||||
if err := flushDNSCache(); err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "[tun] unable to flush DNS cache")
|
||||
}
|
||||
}
|
||||
|
||||
if updater != nil {
|
||||
t.cbr, err = winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.MibIPforwardRow2) {
|
||||
// Only a registered callback goes into the fields: a nil pointer in
|
||||
// them would not compare equal to nil in Close.
|
||||
cbr, err := winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.MibIPforwardRow2) {
|
||||
updater.Update()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.cbi, err = winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
|
||||
t.cbr = cbr
|
||||
cbi, err := winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
|
||||
updater.Update()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.cbi = cbi
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -236,6 +332,20 @@ func (t *WindowsTun) Close() error {
|
||||
t.luid.FlushIPAddresses(windows.AF_INET6)
|
||||
t.luid.FlushDNS(windows.AF_INET6)
|
||||
}
|
||||
if t.wfp != 0 {
|
||||
closeWFPEngine(t.wfp)
|
||||
}
|
||||
if t.resolver != nil {
|
||||
t.resolver.restore()
|
||||
}
|
||||
if t.skipStop != nil {
|
||||
close(t.skipStop)
|
||||
<-t.skipDone
|
||||
}
|
||||
internet.SkipDNSServers(nil)
|
||||
if len(t.options.DNS) > 0 || len(t.options.AutoSystemRoutingTable) > 0 {
|
||||
flushDNSCache()
|
||||
}
|
||||
if t.session != (wintun.Session{}) {
|
||||
t.session.End()
|
||||
}
|
||||
@@ -245,6 +355,121 @@ func (t *WindowsTun) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type savedResolver struct {
|
||||
preferGo bool
|
||||
dial func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// resolveOnOwn has Go resolve the names Xray would otherwise ask Windows for,
|
||||
// on Xray's own sockets, which the dialer controllers bind to the physical
|
||||
// interface, and skipping the TUN's DNS servers, as localdns does. Windows'
|
||||
// resolver runs in the DNS Client service, whose queries the DNS filter lets
|
||||
// through the TUN only, so Xray's own lookups, like of an outbound's server
|
||||
// domain, would go into Xray again and could end up waiting on themselves.
|
||||
//
|
||||
// It changes net.DefaultResolver for the whole process, which covers every
|
||||
// lookup that would reach Windows' resolver; restore undoes it.
|
||||
func resolveOnOwn() *savedResolver {
|
||||
saved := &savedResolver{net.DefaultResolver.PreferGo, net.DefaultResolver.Dial}
|
||||
dialer := &net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range internet.Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
// Go's resolver moves on to the next server right away when a dial fails.
|
||||
net.DefaultResolver.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if internet.IsSkippedDNSServer(address) {
|
||||
return nil, errors.New("skipped DNS server ", address)
|
||||
}
|
||||
return dialer.DialContext(ctx, network, address)
|
||||
}
|
||||
net.DefaultResolver.PreferGo = true
|
||||
return saved
|
||||
}
|
||||
|
||||
func (s *savedResolver) restore() {
|
||||
net.DefaultResolver.PreferGo = s.preferGo
|
||||
net.DefaultResolver.Dial = s.dial
|
||||
}
|
||||
|
||||
// tunOnlyDNS returns those of servers, the TUN's DNS servers, that Go's
|
||||
// resolver does not also get from another interface: one that is up and has
|
||||
// a gateway, as it reads them.
|
||||
func tunOnlyDNS(tun winipcfg.LUID, servers []netip.Addr) ([]netip.Addr, error) {
|
||||
adapters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeGateways)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var others []netip.Addr
|
||||
for _, adapter := range adapters {
|
||||
if adapter.LUID == tun || adapter.OperStatus != winipcfg.IfOperStatusUp || adapter.FirstGatewayAddress == nil {
|
||||
continue
|
||||
}
|
||||
for server := adapter.FirstDNSServerAddress; server != nil; server = server.Next {
|
||||
if addr, ok := netip.AddrFromSlice(server.Address.IP()); ok {
|
||||
others = append(others, addr.Unmap())
|
||||
}
|
||||
}
|
||||
}
|
||||
return slices.DeleteFunc(slices.Clone(servers), func(server netip.Addr) bool {
|
||||
return slices.Contains(others, server.Unmap())
|
||||
}), nil
|
||||
}
|
||||
|
||||
// disableDNSRegistration turns off the dynamic DNS registration of the
|
||||
// interface's addresses. dns are its DNS servers.
|
||||
func disableDNSRegistration(luid winipcfg.LUID, dns []netip.Addr) error {
|
||||
guid, err := luid.GUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = winipcfg.SetInterfaceDnsSettings(*guid, &winipcfg.DnsInterfaceSettings{
|
||||
Version: winipcfg.DnsInterfaceSettingsVersion1,
|
||||
Flags: winipcfg.DnsInterfaceSettingsFlagRegistrationEnabled,
|
||||
})
|
||||
if err == nil || !go_errors.Is(err, windows.ERROR_PROC_NOT_FOUND) {
|
||||
return err
|
||||
}
|
||||
return disableDNSRegistrationByNetsh(luid, dns)
|
||||
}
|
||||
|
||||
// disableDNSRegistrationByNetsh does it for Windows before 10 1809, which
|
||||
// lacks SetInterfaceDnsSettings. The setting is the interface's, not the
|
||||
// address family's, but netsh only applies it along with a DNS server, which
|
||||
// replaces the IPv4 ones, so they are set again afterwards.
|
||||
func disableDNSRegistrationByNetsh(luid winipcfg.LUID, dns []netip.Addr) error {
|
||||
row, err := luid.Interface()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
server := "127.0.0.1" // any will do when there is no IPv4 one
|
||||
if i := slices.IndexFunc(dns, netip.Addr.Is4); i >= 0 {
|
||||
server = dns[i].String()
|
||||
}
|
||||
err = runNetsh("interface", "ipv4", "set", "dnsservers", "name="+strconv.FormatUint(uint64(row.InterfaceIndex), 10), "source=static", "address="+server, "register=none", "validate=no")
|
||||
return errors.Combine(err, luid.SetDNS(windows.AF_INET, dns, nil))
|
||||
}
|
||||
|
||||
// runNetsh runs netsh.exe from the system directory. netsh reports some
|
||||
// failures, like a syntax error, only in its output, even with exit code 0,
|
||||
// so any output counts as a failure.
|
||||
func runNetsh(args ...string) error {
|
||||
system32, err := windows.GetSystemDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(filepath.Join(system32, "netsh.exe"), args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
output, err := cmd.CombinedOutput()
|
||||
if output = bytes.TrimSpace(output); err != nil || len(output) > 0 {
|
||||
return errors.New("netsh ", strings.Join(args, " "), ": ", string(output)).Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *WindowsTun) Name() (string, error) {
|
||||
row, err := t.luid.Interface()
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
//go:build windows
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"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
|
||||
|
||||
fwpMatchEqual = 0 // FWP_MATCH_EQUAL
|
||||
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}}
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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},
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
//
|
||||
// - 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.
|
||||
// - 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
|
||||
// leave it.
|
||||
//
|
||||
// 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, dns, ipv6 bool) (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, dns, ipv6)
|
||||
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, dns, ipv6 bool) 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)))
|
||||
dns53 := condition(&fwpmConditionIPRemotePort, fwpUint16, 53)
|
||||
// DNS 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)))
|
||||
}
|
||||
out := []fwpmFilterCondition0{dns53, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPNexthopInterface)}
|
||||
in := []fwpmFilterCondition0{dns53, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPArrivalInterface)}
|
||||
for _, layer := range []struct {
|
||||
key *windows.GUID
|
||||
selfFlags uint32
|
||||
throughTUN []fwpmFilterCondition0
|
||||
}{
|
||||
{&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, out},
|
||||
{&fwpmLayerALEAuthRecvAcceptV4, 0, in},
|
||||
{&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, out},
|
||||
{&fwpmLayerALEAuthRecvAcceptV6, 0, in},
|
||||
} {
|
||||
if err := add(layer.key, "permit Xray", layer.selfFlags, fwpActionPermit, 4, self); err != nil {
|
||||
return err
|
||||
}
|
||||
if dns {
|
||||
if err := add(layer.key, "permit DNS through the TUN", 0, fwpActionPermit, 3, layer.throughTUN...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(layer.key, "block DNS", 0, fwpActionBlock, 2, dns53); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ipv6 {
|
||||
// Both directions: replies to a connection accepted from outside
|
||||
// would leave through the physical link as well.
|
||||
loopback := fwpmFilterCondition0{
|
||||
fieldKey: fwpmConditionFlags,
|
||||
matchType: fwpMatchFlagsAllSet,
|
||||
conditionValue: fwpValue0{typ: fwpUint32, value: fwpConditionFlagIsLoopback},
|
||||
}
|
||||
// Neighbor and multicast listener discovery, ICMPv6 130-137 and 143,
|
||||
// whose type and code sit where the local and remote port are.
|
||||
discovery := []fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_ICMPV6)}
|
||||
for _, typ := range []uintptr{130, 131, 132, 133, 134, 135, 136, 137, 143} {
|
||||
discovery = append(discovery, condition(&fwpmConditionIPLocalPort, fwpUint16, typ))
|
||||
}
|
||||
discovery = append(discovery, condition(&fwpmConditionIPRemotePort, fwpUint16, 0))
|
||||
dhcpv6 := []fwpmFilterCondition0{
|
||||
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
||||
condition(&fwpmConditionIPLocalPort, fwpUint16, 546),
|
||||
condition(&fwpmConditionIPRemotePort, fwpUint16, 547),
|
||||
}
|
||||
for _, direction := range []struct {
|
||||
layer *windows.GUID
|
||||
dhcpv6 []fwpmFilterCondition0
|
||||
}{
|
||||
// The client sends to the servers' multicast address, and they
|
||||
// answer from their own.
|
||||
{&fwpmLayerALEAuthConnectV6, slices.Concat(dhcpv6, []fwpmFilterCondition0{condition(&fwpmConditionIPRemoteAddress, fwpByteArray16Type, uintptr(unsafe.Pointer(&ipv6AllDHCPv6Servers)))})},
|
||||
{&fwpmLayerALEAuthRecvAcceptV6, dhcpv6},
|
||||
} {
|
||||
if err := add(direction.layer, "permit IPv6 loopback", 0, fwpActionPermit, 1, loopback); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(direction.layer, "permit IPv6 neighbor and multicast listener discovery", 0, fwpActionPermit, 1, discovery...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(direction.layer, "permit DHCPv6", 0, fwpActionPermit, 1, direction.dhcpv6...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(direction.layer, "block IPv6", 0, fwpActionBlock, 0); 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 outside all of prefixes, the TUN's own
|
||||
// subnets and routes: queries to them cannot go through the TUN.
|
||||
func dnsOutsideTUN(servers []netip.Addr, prefixes []netip.Prefix) []netip.Addr {
|
||||
var outside []netip.Addr
|
||||
for _, server := range servers {
|
||||
server = server.Unmap()
|
||||
if !slices.ContainsFunc(prefixes, func(p netip.Prefix) bool { return p.Contains(server) }) {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//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)
|
||||
}
|
||||
if err := addLeakFilters(engine, loopback, true, true); err != nil {
|
||||
skipUnlessElevated(err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package internet
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var skippedDNSServers atomic.Pointer[[]netip.Addr]
|
||||
|
||||
// SkipDNSServers has the queries Xray sends to the system's DNS servers on its
|
||||
// own, like those of localdns, skip servers until it is called again. The DNS
|
||||
// servers of a TUN are only meant for what goes through it: queried by Xray
|
||||
// itself they lead back into it, or nowhere.
|
||||
func SkipDNSServers(servers []netip.Addr) {
|
||||
skipped := make([]netip.Addr, len(servers))
|
||||
for i, server := range servers {
|
||||
skipped[i] = server.Unmap()
|
||||
}
|
||||
skippedDNSServers.Store(&skipped)
|
||||
}
|
||||
|
||||
// IsSkippedDNSServer reports whether address, a DNS server as host:port, is to
|
||||
// be skipped, see SkipDNSServers.
|
||||
func IsSkippedDNSServer(address string) bool {
|
||||
skipped := skippedDNSServers.Load()
|
||||
if skipped == nil {
|
||||
return false
|
||||
}
|
||||
server, err := netip.ParseAddrPort(address)
|
||||
return err == nil && slices.Contains(*skipped, server.Addr().Unmap())
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package internet_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func TestSkipDNSServers(t *testing.T) {
|
||||
internet.SkipDNSServers([]netip.Addr{netip.MustParseAddr("::ffff:203.0.113.53"), netip.MustParseAddr("2001:db8::53")})
|
||||
t.Cleanup(func() { internet.SkipDNSServers(nil) })
|
||||
for address, want := range map[string]bool{
|
||||
"203.0.113.53:53": true,
|
||||
"[2001:db8::53]:53": true,
|
||||
"198.51.100.53:53": false,
|
||||
"localhost:53": false,
|
||||
} {
|
||||
if got := internet.IsSkippedDNSServer(address); got != want {
|
||||
t.Errorf("IsSkippedDNSServer(%q) = %v, want %v", address, got, want)
|
||||
}
|
||||
}
|
||||
internet.SkipDNSServers(nil)
|
||||
if internet.IsSkippedDNSServer("203.0.113.53:53") {
|
||||
t.Error("still skipped after SkipDNSServers(nil)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user