TUN inbound: Only log IPv6 setup errors on Windows when DisabledComponents turns IPv6 off, with "misconfigtun" blocking IPv6 then

https://github.com/XTLS/Xray-core/pull/7341#issuecomment-6700092032

IPv6 entries are no longer left out. With IPv6 turned off by
DisabledComponents (0x10, the non-tunnel interfaces, the TUN among them,
as in 0xFF), IPv6 is set up as usual, and errors doing so, clearing its
DNS servers included, are only logged; otherwise they exit, also for a
config without IPv6 entries, which replaces the skips of #6874, #7325 and
#7332. "misconfigtun" then blocks IPv6 outside the TUN whatever the routes,
as IPv6 still works until the reboot that applies the value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
patterniha
2026-11-07 05:08:06 +03:30
co-authored by Claude Opus 5.5
parent 4bcd0c6405
commit 4296be5d05
2 changed files with 37 additions and 56 deletions
+2 -2
View File
@@ -223,7 +223,7 @@ 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. A Wintun adapter that already has that name is reused.
With IPv6 off on all interfaces by the `DisabledComponents` registry value (0x01 and 0x10 both set, as in 0xFF), the TUN starts with IPv4 only, ignoring the IPv6 entries of `gateway`, `dns` and `autoSystemRoutingTable`. As Windows applies the value at boot only, ignored IPv6 routes add `"[::/0]:misconfigtun"` to `autoSystemWfpBlockLeak`, so IPv6 cannot bypass the TUN before the reboot either. Should the adapter lack IPv6 otherwise, or refuse its IPv6 DNS servers, a config without IPv6 entries starts with IPv4 only, and IPv6 entries keep the TUN from starting.
With IPv6 turned off by the `DisabledComponents` registry value (0x10 set, as in 0xFF), errors setting up IPv6 for `gateway`, `dns` and `autoSystemRoutingTable`, clearing the IPv6 DNS servers when `dns` has none included, are only logged. Otherwise, they keep the TUN from starting.
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. Xray's own lookups through the system's DNS servers, like those of the `localhost` DNS server with `autoOutboundsInterface`, skip them, unless another interface uses them as well, as they would lead back into the TUN.
@@ -232,7 +232,7 @@ Setting `autoSystemWfpBlockLeak` to at least `["dns", "misconfigtun"]` is recomm
- `"windnsclient"`: Windows' DNS Client service, except for name resolution on the local network (LLMNR, mDNS). Since Windows 11 and Server 2022, it may query over HTTPS or TLS, to any port; earlier versions only query port 53, so there it does nothing.
- `"tcp"`, `"udp"`: that protocol, to all ports, or with `/<port>` or `/<low>-<high>` (1 to 65535) to some, e.g. `"tcp/853"` or `"udp/10-20"`. The ports alone stand for both, e.g. `"53"` or `"10-20"`.
- `"icmp"`: ICMP and ICMPv6, e.g. `ping`.
- `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, which would bypass the TUN.
- `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, which would bypass the TUN. With IPv6 turned off by `DisabledComponents`, it covers IPv6 whatever the routes, as IPv6 setup errors are then only logged, and IPv6 still works until the reboot that applies the value.
- `"all"`: everything, in both directions. Access to the local network then breaks unless it is routed into the TUN as well, and so do incoming connections to other programs, other VPNs, and virtual machines whose NAT runs on the host.
- `"allexceptlan"`: like `"all"`, but traffic to and from the local network stays allowed, e.g. for printers or casting to a TV: private, link-local, multicast and broadcast addresses (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `224.0.0.0/4`, `255.255.255.255`, `fc00::/7`, `fe80::/10`, `ff00::/8`), not global IPv6 ones. It includes `"dns"`, and the other values apply there as well. `"all"` overrides it.
+35 -54
View File
@@ -99,9 +99,9 @@ func open(name, desc string) (*wintun.Adapter, error) {
return nil, err
}
// ipv6Off tells whether DisabledComponents turns IPv6 off on all interfaces
// but loopback, the tunnel (0x01) and the native (0x10) ones, as 0xFF does.
// Windows applies it at boot only.
// ipv6Off tells whether DisabledComponents turns IPv6 off on the non-tunnel
// interfaces (0x10, as 0xFF does), the TUN among them. Windows applies it at
// boot only.
func ipv6Off() bool {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`, registry.QUERY_VALUE)
if err != nil {
@@ -109,24 +109,16 @@ func ipv6Off() bool {
}
defer key.Close()
value, _, err := key.GetIntegerValue("DisabledComponents")
return err == nil && value&0x11 == 0x11
return err == nil && value&0x10 != 0
}
func (t *WindowsTun) Start() (err error) {
// With IPv6 off on all interfaces, the TUN can have IPv4 only, so the
// IPv6 entries are left out, for one config to work either way.
off6 := ipv6Off()
var ignored6, ignoredRoute6 bool
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 if off6 {
ignored6 = true
continue
} else {
address6 = true
}
@@ -135,14 +127,8 @@ func (t *WindowsTun) Start() (err error) {
dns := make([]netip.Addr, 0, len(t.options.DNS))
for _, ip := range t.options.DNS {
addr := netip.MustParseAddr(ip)
if addr.Is6() && off6 {
ignored6 = true
continue
}
dns = append(dns, addr)
dns = append(dns, netip.MustParseAddr(ip))
}
dns6 := slices.ContainsFunc(dns, netip.Addr.Is6)
var route4, route6 bool
routesMap := make(map[winipcfg.RouteData]struct{})
@@ -155,24 +141,29 @@ func (t *WindowsTun) Start() (err error) {
if prefix.Addr().Is4() {
route4 = true
route.NextHop = netip.IPv4Unspecified()
} else if off6 {
ignored6, ignoredRoute6 = true, true
continue
} else {
route6 = true
route.NextHop = netip.IPv6Unspecified()
}
routesMap[route] = struct{}{}
}
if ignored6 {
errors.LogInfo(context.Background(), "[tun] IPv6 is off on all interfaces (DisabledComponents), so the IPv6 entries of gateway, dns and autoSystemRoutingTable are ignored")
}
routesData := make([]*winipcfg.RouteData, 0, len(routesMap))
for route := range routesMap {
r := route
routesData = append(routesData, &r)
}
// With IPv6 off by DisabledComponents, setting it up may fail, clearing
// its DNS servers too, which is then only logged.
off6 := ipv6Off()
ignored := func(family winipcfg.AddressFamily, err error) bool {
if family != windows.AF_INET6 || !off6 {
return false
}
errors.LogErrorInner(context.Background(), err, "[tun] ignored, as DisabledComponents turns IPv6 off")
return true
}
var retryTimes int
var firstErr error
startOver:
@@ -189,6 +180,9 @@ startOver:
err = t.luid.SetRoutesForFamily(family, routesData)
if err != nil {
firstErr = errors.New("unable to set routes").Base(err)
if ignored(family, firstErr) {
continue
}
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
@@ -199,6 +193,9 @@ startOver:
err = t.luid.SetIPAddressesForFamily(family, addresses)
if err != nil {
firstErr = errors.New("unable to set ips").Base(err)
if ignored(family, firstErr) {
continue
}
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
@@ -207,9 +204,7 @@ startOver:
}
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 && !dns6 {
if ignored(family, err) {
continue
}
return err
@@ -228,6 +223,9 @@ startOver:
err = ipif.Set()
if err != nil {
firstErr = errors.New("unable to set metric and MTU").Base(err)
if ignored(family, firstErr) {
continue
}
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
@@ -235,13 +233,10 @@ startOver:
}
err = t.luid.SetDNS(family, dns, nil)
if err != nil {
// Skip IPv6 the config does not ask for also when setting its DNS
// servers fails, as reported on Windows 11 24H2 with IPv6 disabled
// (Access is denied).
if family == windows.AF_INET6 && !address6 && !route6 && !dns6 {
firstErr = errors.New("unable to set DNS").Base(err)
if ignored(family, firstErr) {
continue
}
firstErr = errors.New("unable to set DNS").Base(err)
if err == windows.ERROR_NOT_FOUND {
goto startOver
}
@@ -298,38 +293,24 @@ startOver:
if err != nil {
return err
}
// DisabledComponents takes effect at the next boot only, and until then,
// IPv6 of the ignored routes would go around the TUN. So IPv6 is kept from
// leaving outside it, as by "misconfigtun".
if ignoredRoute6 {
leaks = append(leaks, "[::/0]:misconfigtun")
}
routed := route4 || route6 || ignoredRoute6
block := newLeakFilters(leaks, route4, route6)
if routed {
// With IPv6 off by DisabledComponents, IPv6 setup failures are only logged,
// and IPv6 still works until the reboot that applies it. So "misconfigtun"
// then blocks IPv6 outside the TUN whatever the routes.
block := newLeakFilters(leaks, route4, route6 && !off6)
if route4 || route6 {
dnsClient := func(l leak) bool { return l.name == "windnsclient" }
if slices.ContainsFunc(block, dnsClient) && !dnsClientUsesAnyPort() {
block = slices.DeleteFunc(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 {
if len(t.options.DNS) > 0 {
return errors.New("keeping the system's DNS inside the TUN needs IPv4 servers in dns, as IPv6 is off on all interfaces")
}
return errors.New("keeping the system's DNS inside the TUN needs dns to be set")
}
}
if blocked := block.String(); routed && blocked != "" {
if blocked := block.String(); (route4 || route6) && blocked != "" {
if t.wfp, err = blockLeaks(t.luid, block); err != nil {
// Rather no TUN than a leaking one.
without := "autoSystemWfpBlockLeak"
switch {
case len(t.options.AutoSystemWfpBlockLeak) == 0:
without = "the IPv6 entries of autoSystemRoutingTable"
case ignoredRoute6:
without += " and the IPv6 entries of autoSystemRoutingTable"
}
return errors.New("unable to block ", blocked, " outside the TUN (remove ", without, " to run without)").Base(err)
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)
covered := slices.Clone(addresses)