Files
Xray-core/proxy/tun/tun_windows.go
T
patternihaandClaude Opus 5.5 f2f292c90c TUN inbound: Support "tcp/port" and "udp/port" in autoSystemWfpBlockLeak on Windows; "dns" covers DoT and DoQ as well
"dns" in autoSystemWfpBlockLeak only blocked port 53 outside the TUN, plus
the DNS Client service on Windows 11 and Server 2022 and later, so other
programs' DNS over TLS (TCP 853) and DNS over QUIC (UDP 853) could still
leave outside the TUN.

autoSystemWfpBlockLeak now takes:

- "tcp/<port>", "udp/<port>": connections to that port may only go through
  the TUN (or come from Xray itself).
- "windnsclient": Windows' DNS Client service may only connect through the
  TUN, except for mDNS and LLMNR. As before, this only applies from Windows
  11 and Server 2022 on, where the service may use DoH or DoT to any port;
  earlier versions only query port 53.
- "dns": stands for "udp/53", "tcp/53", "windnsclient", "tcp/853" and
  "udp/853".
- "misconfigtun": unchanged.

Values are case-insensitive, deduplicated, and anything else, such as
"tcp/0" or "icmp/1", is rejected. Keeping the system's DNS inside the TUN,
with "udp/53", "tcp/53" or "windnsclient", needs dns to be set, as "dns"
did; ports like 853 alone do not. Each port gets its own permit-through-TUN
and block filters with a protocol condition, where port 53 used to have one
for both protocols, and the log lists what is blocked, e.g. "[tun] blocked
outside the TUN: udp/53, tcp/53, tcp/853, udp/853, windnsclient".

Tested on Windows 11 with a Wintun adapter as the outside interface: with
"tcp/853", "udp/853", "tcp/7215" and "udp/7216", those are blocked outside
the TUN over IPv4 and IPv6, while tcp/7216, udp/7215 and 443 are not, port
853 works through the TUN, Xray's own connections to port 853 still leave
outside it, and nothing is blocked after Xray stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:52:11 +03:30

650 lines
19 KiB
Go

//go:build windows
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"
)
//go:linkname procyield runtime.procyield
func procyield(cycles uint32)
// WindowsTun is an object that handles tun network interface on Windows
// current version is heavily stripped to do nothing more,
// then create a network interface, whose packets are read and written through a Wintun session
type WindowsTun struct {
sync.RWMutex
options *Config
adapter *wintun.Adapter
session wintun.Session
readWait windows.Handle
luid winipcfg.LUID
cbr winipcfg.ChangeCallback
cbi winipcfg.ChangeCallback
guard outboundGuard
wfp windows.Handle
resolver *savedResolver
skipStop chan struct{}
skipDone chan struct{}
closed bool
}
// WindowsTun implements Tun
var _ Tun = (*WindowsTun)(nil)
// NewTun creates a Wintun interface with the given name. Should a Wintun
// interface with the same name exist, it tried to be reused.
func NewTun(options *Config) (Tun, error) {
// instantiate wintun adapter
adapter, err := open(options.Name, options.Desc)
if err != nil {
return nil, err
}
// start the interface with ring buffer capacity of 8 MiB
session, err := adapter.StartSession(0x800000)
if err != nil {
_ = adapter.Close()
return nil, err
}
tun := &WindowsTun{
options: options,
adapter: adapter,
session: session,
readWait: session.ReadWaitEvent(),
luid: winipcfg.LUID(adapter.LUID()),
}
return tun, nil
}
func open(name, desc string) (*wintun.Adapter, error) {
// generate a deterministic GUID from the adapter name
id := md5.Sum([]byte(name))
guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
// try to open existing adapter by name
adapter, err := wintun.OpenAdapter(name)
if err == nil {
return adapter, nil
}
// try to create adapter anew
adapter, err = wintun.CreateAdapter(name, desc, guid)
if err == nil {
return adapter, nil
}
return nil, err
}
func (t *WindowsTun) Start() (err error) {
var address4, address6 bool
addresses := make([]netip.Prefix, 0, len(t.options.Gateway))
for _, cidr := range t.options.Gateway {
prefix := netip.MustParsePrefix(cidr)
if prefix.Addr().Is4() {
address4 = true
} else {
address6 = true
}
addresses = append(addresses, prefix)
}
dns := make([]netip.Addr, 0, len(t.options.DNS))
for _, ip := range t.options.DNS {
dns = append(dns, netip.MustParseAddr(ip))
}
var route4, route6 bool
routesMap := make(map[winipcfg.RouteData]struct{})
for _, cidr := range t.options.AutoSystemRoutingTable {
prefix := netip.MustParsePrefix(cidr)
route := winipcfg.RouteData{
Destination: prefix.Masked(),
Metric: 0,
}
if prefix.Addr().Is4() {
route4 = true
route.NextHop = netip.IPv4Unspecified()
} else {
route6 = true
route.NextHop = netip.IPv6Unspecified()
}
routesMap[route] = struct{}{}
}
routesData := make([]*winipcfg.RouteData, 0, len(routesMap))
for route := range routesMap {
r := route
routesData = append(routesData, &r)
}
var retryTimes int
var firstErr error
startOver:
if retryTimes > 0 {
if retryTimes > 15 {
return windows.ERROR_NOT_FOUND
}
errors.LogErrorInner(context.Background(), firstErr, "Interface configuration failed, retrying attempt ", retryTimes, "/15")
time.Sleep(time.Second)
}
retryTimes++
for _, family := range []winipcfg.AddressFamily{windows.AF_INET, windows.AF_INET6} {
if family == windows.AF_INET && route4 || family == windows.AF_INET6 && route6 {
err = t.luid.SetRoutesForFamily(family, routesData)
if err != nil {
firstErr = errors.New("unable to set routes").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
}
}
if family == windows.AF_INET && address4 || family == windows.AF_INET6 && address6 {
err = t.luid.SetIPAddressesForFamily(family, addresses)
if err != nil {
firstErr = errors.New("unable to set ips").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
}
}
ipif, err := t.luid.IPInterface(family)
if err != nil {
// With IPv6 disabled system-wide (DisabledComponents), the adapter has no
// IPv6 interface at all. Skip the family unless the config asks for it.
if err == windows.ERROR_NOT_FOUND && family == windows.AF_INET6 && !address6 && !route6 {
continue
}
return err
}
ipif.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled
ipif.DadTransmits = 0
ipif.ManagedAddressConfigurationSupported = false
ipif.OtherStatefulConfigurationSupported = false
if family == windows.AF_INET && (address4 || route4) || family == windows.AF_INET6 && (address6 || route6) {
ipif.NLMTU = t.options.MTU
}
if family == windows.AF_INET && route4 || family == windows.AF_INET6 && route6 {
ipif.UseAutomaticMetric = false
ipif.Metric = 0
}
err = ipif.Set()
if err != nil {
firstErr = errors.New("unable to set metric and MTU").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
}
err = t.luid.SetDNS(family, dns, nil)
if err != nil {
firstErr = errors.New("unable to set DNS").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
return firstErr
}
}
// 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")
}
}
// With autoSystemWfpBlockLeak, once the system routes lead to the TUN,
// keep connections to the ports given ("dns" stands for those of DNS),
// Windows' DNS Client service, and an IP version no route of which leads
// to the TUN ("misconfigtun"), from leaving through the other interfaces.
// Addresses do not matter: without one of a version in gateway, Windows
// gives the TUN a link-local one.
leaks, err := ExpandWfpBlockLeak(t.options.AutoSystemWfpBlockLeak)
if err != nil {
return err
}
block := newLeakFilters(leaks, route4, route6)
if route4 || route6 {
if slices.Contains(leaks, "windnsclient") && !block.dnsClient {
errors.LogInfo(context.Background(), "[tun] windnsclient skipped: before Windows 11 and Server 2022, the DNS Client service only queries port 53")
}
if block.systemDNS() && len(dns) == 0 {
return errors.New("keeping the system's DNS inside the TUN needs dns to be set")
}
}
if blocked := block.String(); (route4 || route6) && blocked != "" {
if t.wfp, err = blockLeaks(t.luid, block); err != nil {
// Rather no TUN than a leaking one.
return errors.New("unable to block ", blocked, " outside the TUN (remove autoSystemWfpBlockLeak to run without)").Base(err)
}
errors.LogInfo(context.Background(), "[tun] blocked outside the TUN: ", blocked)
if block.systemDNS() {
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 {
// Xray's own connections have to stay out of the IP versions routed
// to the TUN, which needs Windows to honor the binding to updater's
// interface.
if route4 {
t.guard.families = append(t.guard.families, windows.AF_INET)
}
if route6 {
t.guard.families = append(t.guard.families, windows.AF_INET6)
}
t.guard.check()
// 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()
t.guard.check()
})
if err != nil {
return err
}
t.cbr = cbr
cbi, err := winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
updater.Update()
t.guard.check()
})
if err != nil {
return err
}
t.cbi = cbi
}
return nil
}
func (t *WindowsTun) Close() error {
t.Lock()
defer t.Unlock()
if t.closed {
return nil
}
t.closed = true
if t.cbr != nil {
t.cbr.Unregister()
}
if t.cbi != nil {
t.cbi.Unregister()
}
t.guard.restore()
if t.luid != 0 {
t.luid.FlushRoutes(windows.AF_INET)
t.luid.FlushIPAddresses(windows.AF_INET)
t.luid.FlushDNS(windows.AF_INET)
t.luid.FlushRoutes(windows.AF_INET6)
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{}) {
// ReadPacket may be waiting for a packet
_ = windows.SetEvent(t.readWait)
t.session.End()
}
if t.adapter != nil {
t.adapter.Close()
}
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)
}
// A socket of the server's IP version only needs that version bound
// to the interface, which a dual-stack one would need both of.
if ap, err := netip.ParseAddrPort(address); err == nil && (network == "udp" || network == "tcp") {
if ap.Addr().Unmap().Is4() {
network += "4"
} else {
network += "6"
}
}
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 {
return "", err
}
return row.Alias(), nil
}
func (t *WindowsTun) Index() (int, error) {
row, err := t.luid.Interface()
if err != nil {
return 0, err
}
return int(row.InterfaceIndex), nil
}
// WritePacket implements Tun: the two parts are copied into the ring of Wintun
func (t *WindowsTun) WritePacket(header, payload []byte) error {
t.RLock()
defer t.RUnlock()
if t.closed {
return windows.ERROR_HANDLE_EOF
}
packet, err := t.session.AllocateSendPacket(len(header) + len(payload))
if err != nil {
return err
}
copy(packet[copy(packet, header):], payload)
t.session.SendPacket(packet)
return nil
}
// ReadPacket implements Tun: it waits until Wintun has a packet and copies it out of the ring
func (t *WindowsTun) ReadPacket(b []byte) ([]byte, error) {
for {
t.RLock()
if t.closed {
t.RUnlock()
return nil, windows.ERROR_HANDLE_EOF
}
packet, err := t.session.ReceivePacket()
if err == nil {
n := copy(b, packet)
t.session.ReleaseReceivePacket(packet)
t.RUnlock()
return b[:n], nil
}
t.RUnlock()
if !go_errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
return nil, err
}
procyield(1)
_, _ = windows.WaitForSingleObject(t.readWait, windows.INFINITE)
}
}
const (
IP_UNICAST_IF = 31
IPV6_UNICAST_IF = 31
)
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
var index [4]byte
binary.BigEndian.PutUint32(index[:], uint32(iface.Index))
var err1, err2, err3, err4 error
switch network {
case "tcp6", "udp6", "ip6":
dualStack := false
if network == "udp6" {
ipv6Only, err := windows.GetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_V6ONLY)
if err != nil {
return internet.NewBindError(err)
}
dualStack = ipv6Only == 0
}
err1 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index)
if network == "udp6" {
err2 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index)
}
// Only dual-stack UDP can send to both IPv4 and IPv6 destinations.
if !dualStack {
break
}
fallthrough
case "tcp4", "udp4", "ip4":
err3 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
if network == "udp4" || network == "udp6" {
err4 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, windows.IP_MULTICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
}
default:
panic(network + " " + address)
}
if err := errors.Combine(err1, err2, err3, err4); err != nil {
return internet.NewBindError(err)
}
return nil
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
if fixedName != "" {
return net.InterfaceByName(fixedName)
}
r, err := winipcfg.GetIPForwardTable2(windows.AF_UNSPEC)
if err != nil {
return nil, err
}
lowestMetric := ^uint32(0)
index := uint32(0)
lowestMetricWifi := ^uint32(0)
indexWifi := uint32(0)
for i := range r {
if r[i].DestinationPrefix.PrefixLength != 0 || r[i].InterfaceIndex == uint32(tunIndex) {
continue
}
ifrow, err := r[i].InterfaceLUID.Interface()
if err != nil || ifrow.OperStatus != winipcfg.IfOperStatusUp {
continue
}
iface, err := r[i].InterfaceLUID.IPInterface(windows.AF_INET)
if err != nil {
iface, err = r[i].InterfaceLUID.IPInterface(windows.AF_INET6)
if err != nil {
continue
}
}
if ifrow.Type == windows.IF_TYPE_IEEE80211 {
if r[i].Metric+iface.Metric < lowestMetricWifi {
lowestMetricWifi = r[i].Metric + iface.Metric
indexWifi = r[i].InterfaceIndex
}
continue
}
if r[i].Metric+iface.Metric < lowestMetric {
lowestMetric = r[i].Metric + iface.Metric
index = r[i].InterfaceIndex
}
}
if indexWifi != 0 {
index = indexWifi
}
return net.InterfaceByIndex(int(index))
}