From eb29a4e3de94935b94d583a5d820e65e236c5ce0 Mon Sep 17 00:00:00 2001 From: patterniha <71074308+patterniha@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:51:17 +0330 Subject: [PATCH] TUN inbound: Make `strictRoute` false by default Like sing-box's strict_route, strictRoute is now false by default, so the Windows Filtering Platform filters are only added when it is set to true (together with autoSystemRoutingTable). With unset meaning false, strict_route becomes a plain bool field. Co-Authored-By: Claude Opus 5.5 --- infra/conf/tun.go | 2 +- infra/conf/tun_test.go | 8 +------- proxy/tun/README.md | 4 ++-- proxy/tun/config.pb.go | 14 ++++++-------- proxy/tun/config.proto | 2 +- proxy/tun/tun_windows.go | 9 ++++----- 6 files changed, 15 insertions(+), 24 deletions(-) diff --git a/infra/conf/tun.go b/infra/conf/tun.go index 08463d3e0..0df1bc6b9 100644 --- a/infra/conf/tun.go +++ b/infra/conf/tun.go @@ -21,7 +21,7 @@ type TunConfig struct { AutoSystemRoutingTable []string `json:"autoSystemRoutingTable"` AutoOutboundsInterface *string `json:"autoOutboundsInterface"` AutoSystemDNS bool `json:"autoSystemDNS"` - StrictRoute *bool `json:"strictRoute"` + StrictRoute bool `json:"strictRoute"` } func (v *TunConfig) Build() (proto.Message, error) { diff --git a/infra/conf/tun_test.go b/infra/conf/tun_test.go index c336f7f88..5ca7e27e4 100644 --- a/infra/conf/tun_test.go +++ b/infra/conf/tun_test.go @@ -11,7 +11,6 @@ func TestTunConfigStrictRoute(t *testing.T) { creator := func() Buildable { return new(TunConfig) } - enabled, disabled := true, false runMultiTestCase(t, []TestCase{ { @@ -19,15 +18,10 @@ func TestTunConfigStrictRoute(t *testing.T) { 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}, + Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, StrictRoute: true}, }, }) } diff --git a/proxy/tun/README.md b/proxy/tun/README.md index c1758cdbc..4c5b46592 100644 --- a/proxy/tun/README.md +++ b/proxy/tun/README.md @@ -200,7 +200,7 @@ 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 `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 `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. - 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. @@ -210,7 +210,7 @@ Names that Xray resolves through the system resolver, such as an outbound's serv 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. +`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. 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. diff --git a/proxy/tun/config.pb.go b/proxy/tun/config.pb.go index 3578d7ad4..5c1c2b9c6 100644 --- a/proxy/tun/config.pb.go +++ b/proxy/tun/config.pb.go @@ -33,7 +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"` + StrictRoute bool `protobuf:"varint,10,opt,name=strict_route,json=strictRoute,proto3" json:"strict_route,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -132,8 +132,8 @@ func (x *Config) GetAutoSystemDns() bool { } func (x *Config) GetStrictRoute() bool { - if x != nil && x.StrictRoute != nil { - return *x.StrictRoute + if x != nil { + return x.StrictRoute } return false } @@ -142,7 +142,7 @@ var File_proxy_tun_config_proto protoreflect.FileDescriptor const file_proxy_tun_config_proto_rawDesc = "" + "\n" + - "\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xe3\x02\n" + + "\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xcd\x02\n" + "\x06Config\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + "\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" + @@ -153,10 +153,9 @@ 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\rautoSystemDns\x12&\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" + + " \x01(\bR\vstrictRouteBL\n" + "\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3" var ( @@ -188,7 +187,6 @@ 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{ diff --git a/proxy/tun/config.proto b/proxy/tun/config.proto index 69052fd92..f2b9631fd 100644 --- a/proxy/tun/config.proto +++ b/proxy/tun/config.proto @@ -16,5 +16,5 @@ message Config { string auto_outbounds_interface = 7; string desc = 8; bool auto_system_dns = 9; - optional bool strict_route = 10; + bool strict_route = 10; } diff --git a/proxy/tun/tun_windows.go b/proxy/tun/tun_windows.go index 71b40b121..d37753c45 100644 --- a/proxy/tun/tun_windows.go +++ b/proxy/tun/tun_windows.go @@ -246,11 +246,10 @@ startOver: } } - // 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) { + // With strictRoute, 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. + if blockDNS, blockIPv6 := len(dns) > 0, !address6 || !route6; t.options.StrictRoute && (route4 || route6) && (blockDNS || blockIPv6) { if t.wfp, err = blockLeaks(t.luid, blockDNS, blockIPv6); err != nil { what := "DNS and IPv6" if !blockIPv6 {