diff --git a/infra/conf/tun.go b/infra/conf/tun.go index 1ad75a6e9..cc0388ae0 100644 --- a/infra/conf/tun.go +++ b/infra/conf/tun.go @@ -6,7 +6,9 @@ import ( "math/big" "net" "strconv" + "strings" + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/proxy/tun" "google.golang.org/protobuf/proto" ) @@ -21,7 +23,7 @@ type TunConfig struct { AutoSystemRoutingTable []string `json:"autoSystemRoutingTable"` AutoOutboundsInterface *string `json:"autoOutboundsInterface"` AutoSystemDnsToGateway bool `json:"autoSystemDnsToGateway"` - AutoSystemWfpBlockLeak bool `json:"autoSystemWfpBlockLeak"` + AutoSystemWfpBlockLeak []string `json:"autoSystemWfpBlockLeak"` } func (v *TunConfig) Build() (proto.Message, error) { @@ -34,7 +36,14 @@ func (v *TunConfig) Build() (proto.Message, error) { UserLevel: v.UserLevel, AutoSystemRoutingTable: v.AutoSystemRoutingTable, AutoSystemDnsToGateway: v.AutoSystemDnsToGateway, - AutoSystemWfpBlockLeak: v.AutoSystemWfpBlockLeak, + } + for _, leak := range v.AutoSystemWfpBlockLeak { + switch leak := strings.ToLower(leak); leak { + case "dns", "misconfig": + config.AutoSystemWfpBlockLeak = append(config.AutoSystemWfpBlockLeak, leak) + default: + return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", leak) + } } if v.AutoOutboundsInterface != nil { config.AutoOutboundsInterface = *v.AutoOutboundsInterface diff --git a/infra/conf/tun_test.go b/infra/conf/tun_test.go index 6e20e8468..648d4d8ce 100644 --- a/infra/conf/tun_test.go +++ b/infra/conf/tun_test.go @@ -1,6 +1,7 @@ package conf_test import ( + "encoding/json" "testing" . "github.com/xtls/xray-core/infra/conf" @@ -24,9 +25,24 @@ func TestTunConfigAutoSystem(t *testing.T) { Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemDnsToGateway: true}, }, { - Input: `{"name": "xray0", "autoSystemWfpBlockLeak": true}`, + Input: `{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "misconfig"]}`, Parser: loadJSON(creator), - Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemWfpBlockLeak: true}, + Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemWfpBlockLeak: []string{"dns", "misconfig"}}, + }, + { + Input: `{"name": "xray0", "autoSystemWfpBlockLeak": ["DNS"]}`, + Parser: loadJSON(creator), + Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemWfpBlockLeak: []string{"dns"}}, }, }) } + +func TestTunConfigAutoSystemWfpBlockLeakUnknown(t *testing.T) { + config := new(TunConfig) + if err := json.Unmarshal([]byte(`{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "ip"]}`), config); err != nil { + t.Fatal(err) + } + if _, err := config.Build(); err == nil { + t.Error("an unknown autoSystemWfpBlockLeak value was accepted") + } +} diff --git a/proxy/tun/README.md b/proxy/tun/README.md index 6aca00f82..dc3bb34b7 100644 --- a/proxy/tun/README.md +++ b/proxy/tun/README.md @@ -201,9 +201,9 @@ 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 `autoSystemWfpBlockLeak` 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, 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. -- An IP version without routes in `autoSystemRoutingTable`, IPv4 or IPv6, is blocked entirely, in both directions, as it would bypass the TUN. Only loopback and what Windows itself needs on the local link (DHCP, and for IPv6 neighbor and multicast listener discovery) remain allowed. An address of that version in `gateway` is not needed: without one, Windows gives the TUN link-local addresses itself, an IPv6 one at once and an IPv4 one from `169.254.0.0/16` after some seconds (until then, IPv4 routed to the TUN is unreachable), and what is routed to the TUN goes through it with those. +With `autoSystemWfpBlockLeak` 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, each chosen by a value in the list, e.g. `"autoSystemWfpBlockLeak": ["dns", "misconfig"]`: +- `"dns"`: 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. +- `"misconfig"`: an IP version without routes in `autoSystemRoutingTable`, IPv4 or IPv6, is blocked entirely, in both directions, as it would bypass the TUN. Only loopback and what Windows itself needs on the local link (DHCP, and for IPv6 neighbor and multicast listener discovery) remain allowed. An address of that version in `gateway` is not needed: without one, Windows gives the TUN link-local addresses itself, an IPv6 one at once and an IPv4 one from `169.254.0.0/16` after some seconds (until then, IPv4 routed to the TUN is unreachable), and what is routed to the TUN goes through it with those. 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. @@ -211,7 +211,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 is name resolution on the local network (LLMNR, mDNS, NetBIOS), except over an IP version that is blocked. -`autoSystemWfpBlockLeak` (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, IPv4 or IPv6 on the local network while no route of that version leads to the TUN, 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. +`autoSystemWfpBlockLeak` (Windows only) is empty by default, as the filters break some setups: with `"dns"`, a local DNS resolver other programs use (e.g. on `127.0.0.1:53`), the DNS of another VPN on its own interface, virtual machines whose NAT resolves names on the host, or signing in to a captive portal; with `"misconfig"`, IPv4 or IPv6 on the local network while no route of that version leads to the TUN. Without the filters, DNS may leak as described above. To keep an IP version out of the TUN on purpose while still blocking DNS leaks, use only `["dns"]`. 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 52c305aae..6a862a187 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"` AutoSystemDnsToGateway bool `protobuf:"varint,9,opt,name=auto_system_dns_to_gateway,json=autoSystemDnsToGateway,proto3" json:"auto_system_dns_to_gateway,omitempty"` - AutoSystemWfpBlockLeak bool `protobuf:"varint,10,opt,name=auto_system_wfp_block_leak,json=autoSystemWfpBlockLeak,proto3" json:"auto_system_wfp_block_leak,omitempty"` + AutoSystemWfpBlockLeak []string `protobuf:"bytes,10,rep,name=auto_system_wfp_block_leak,json=autoSystemWfpBlockLeak,proto3" json:"auto_system_wfp_block_leak,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -131,11 +131,11 @@ func (x *Config) GetAutoSystemDnsToGateway() bool { return false } -func (x *Config) GetAutoSystemWfpBlockLeak() bool { +func (x *Config) GetAutoSystemWfpBlockLeak() []string { if x != nil { return x.AutoSystemWfpBlockLeak } - return false + return nil } var File_proxy_tun_config_proto protoreflect.FileDescriptor @@ -155,7 +155,7 @@ const file_proxy_tun_config_proto_rawDesc = "" + "\x04desc\x18\b \x01(\tR\x04desc\x12:\n" + "\x1aauto_system_dns_to_gateway\x18\t \x01(\bR\x16autoSystemDnsToGateway\x12:\n" + "\x1aauto_system_wfp_block_leak\x18\n" + - " \x01(\bR\x16autoSystemWfpBlockLeakBL\n" + + " \x03(\tR\x16autoSystemWfpBlockLeakBL\n" + "\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3" var ( diff --git a/proxy/tun/config.proto b/proxy/tun/config.proto index 252f0e7c7..a6dfbfcab 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_to_gateway = 9; - bool auto_system_wfp_block_leak = 10; + repeated string auto_system_wfp_block_leak = 10; } diff --git a/proxy/tun/tun_windows.go b/proxy/tun/tun_windows.go index 70381e964..0251293be 100644 --- a/proxy/tun/tun_windows.go +++ b/proxy/tun/tun_windows.go @@ -247,11 +247,15 @@ startOver: } // With autoSystemWfpBlockLeak, once the system routes lead to the TUN, - // keep DNS if dns is set, and an IP version no route of which leads to - // the TUN, 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. - if blockDNS, blockIPv4, blockIPv6 := len(dns) > 0, !route4, !route6; t.options.AutoSystemWfpBlockLeak && (route4 || route6) && (blockDNS || blockIPv4 || blockIPv6) { + // keep DNS ("dns", if dns is set), and an IP version no route of which + // leads to the TUN ("misconfig"), 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 := t.options.AutoSystemWfpBlockLeak + blockDNS := slices.Contains(leaks, "dns") && len(dns) > 0 + blockIPv4 := slices.Contains(leaks, "misconfig") && !route4 + blockIPv6 := slices.Contains(leaks, "misconfig") && !route6 + if (route4 || route6) && (blockDNS || blockIPv4 || blockIPv6) { if t.wfp, err = blockLeaks(t.luid, blockDNS, blockIPv4, blockIPv6); err != nil { var blocked []string for _, b := range []struct { @@ -267,7 +271,7 @@ startOver: // 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 autoSystemWfpBlockLeak to false to run without)").Base(err) + return errors.New("unable to block ", what, " outside the TUN (remove autoSystemWfpBlockLeak to run without)").Base(err) } errors.LogWarningInner(context.Background(), err, "[tun] unable to block ", what, " outside the TUN, leaks are possible") } else {