mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-01 03:18:02 +03:00
TUN inbound: Add autoSystemWfpBlockLeak on Windows (blocks "dns" and "misconfigtun" IPv4/IPv6 traffic leaks outside the TUN); Rename autoSystemDNS to autoSystemDnsToGateway on Linux (and change some behaviors) (#6853)
https://github.com/XTLS/Xray-core/pull/6853#issuecomment-5899791359 https://github.com/XTLS/Xray-core/pull/6853#issuecomment-5901287980 https://github.com/XTLS/Xray-core/pull/6853#issuecomment-5903680113 https://github.com/XTLS/Xray-core/pull/6853#issuecomment-5904123488 https://github.com/XTLS/Xray-core/pull/6853#issuecomment-5904647772 https://github.com/XTLS/Xray-core/pull/6853#issuecomment-5905047424 Fixes https://github.com/XTLS/Xray-core/issues/6454#issuecomment-5863800676 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
RPRX
co-authored by
Claude Opus 5.5
parent
0086362663
commit
1f304916bd
@@ -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()
|
||||
}
|
||||
+32
-2
@@ -5,8 +5,12 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/proxy/tun"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -20,7 +24,8 @@ type TunConfig struct {
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
AutoSystemRoutingTable []string `json:"autoSystemRoutingTable"`
|
||||
AutoOutboundsInterface *string `json:"autoOutboundsInterface"`
|
||||
AutoSystemDNS bool `json:"autoSystemDNS"`
|
||||
AutoSystemDnsToGateway bool `json:"autoSystemDnsToGateway"`
|
||||
AutoSystemWfpBlockLeak []string `json:"autoSystemWfpBlockLeak"`
|
||||
}
|
||||
|
||||
func (v *TunConfig) Build() (proto.Message, error) {
|
||||
@@ -32,7 +37,32 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
||||
DNS: v.DNS,
|
||||
UserLevel: v.UserLevel,
|
||||
AutoSystemRoutingTable: v.AutoSystemRoutingTable,
|
||||
AutoSystemDns: v.AutoSystemDNS,
|
||||
AutoSystemDnsToGateway: v.AutoSystemDnsToGateway,
|
||||
}
|
||||
for _, leak := range v.AutoSystemWfpBlockLeak {
|
||||
switch leak := strings.ToLower(leak); leak {
|
||||
case "dns", "misconfigtun":
|
||||
config.AutoSystemWfpBlockLeak = append(config.AutoSystemWfpBlockLeak, leak)
|
||||
default:
|
||||
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", leak)
|
||||
}
|
||||
}
|
||||
// Each option needs other settings on the system it takes effect on: the
|
||||
// filters go along with the routes of autoSystemRoutingTable, "dns" lets
|
||||
// DNS through the TUN only, and autoSystemDnsToGateway points the system
|
||||
// DNS at the gateway.
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if len(config.AutoSystemWfpBlockLeak) > 0 && len(v.AutoSystemRoutingTable) == 0 {
|
||||
return nil, errors.New("autoSystemWfpBlockLeak needs autoSystemRoutingTable to be set")
|
||||
}
|
||||
if slices.Contains(config.AutoSystemWfpBlockLeak, "dns") && len(v.DNS) == 0 {
|
||||
return nil, errors.New(`autoSystemWfpBlockLeak "dns" needs dns to be set`)
|
||||
}
|
||||
case "linux":
|
||||
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
|
||||
return nil, errors.New("autoSystemDnsToGateway needs gateway to be set")
|
||||
}
|
||||
}
|
||||
if v.AutoOutboundsInterface != nil {
|
||||
config.AutoOutboundsInterface = *v.AutoOutboundsInterface
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/proxy/tun"
|
||||
)
|
||||
|
||||
func TestTunConfigAutoSystem(t *testing.T) {
|
||||
creator := func() Buildable {
|
||||
return new(TunConfig)
|
||||
}
|
||||
|
||||
runMultiTestCase(t, []TestCase{
|
||||
{
|
||||
Input: `{"name": "xray0"}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500},
|
||||
},
|
||||
{
|
||||
Input: `{"name": "xray0", "gateway": ["10.0.0.1/24"], "autoSystemDnsToGateway": true}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, Gateway: []string{"10.0.0.1/24"}, AutoSystemDnsToGateway: true},
|
||||
},
|
||||
{
|
||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns", "misconfigtun"]}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"dns", "misconfigtun"}},
|
||||
},
|
||||
{
|
||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["DNS"]}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"dns"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TestTunConfigAutoSystemNeeds checks that an option is rejected without the
|
||||
// setting it needs, only on the system it takes effect on.
|
||||
func TestTunConfigAutoSystemNeeds(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
input string
|
||||
goos string // where it is rejected
|
||||
}{
|
||||
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["misconfigtun"]}`, "windows"},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["misconfigtun"]}`, ""},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns"]}`, "windows"},
|
||||
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, "linux"},
|
||||
} {
|
||||
config := new(TunConfig)
|
||||
if err := json.Unmarshal([]byte(c.input), config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := config.Build(); (err != nil) != (runtime.GOOS == c.goos) {
|
||||
t.Errorf("%s on %s: error = %v", c.input, runtime.GOOS, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
+28
-13
@@ -15,27 +15,28 @@ Plainly enabling it in the config probably will result nothing, or lock your rou
|
||||
## DETAILS
|
||||
|
||||
By default, enabling the feature will only bring the tun interface up. \
|
||||
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS uses the first IPv4 prefix from `gateway` to configure the utun point-to-point address. \
|
||||
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS and FreeBSD use the first IPv4 prefix from `gateway` for the point-to-point address. \
|
||||
Without `gateway`, the systems differ: Xray assigns no address on Linux, Windows gives the interface link-local addresses itself (an IPv6 one at once, an IPv4 one from `169.254.0.0/16` after a few seconds), and macOS and FreeBSD use `169.254.10.1/30`. \
|
||||
Windows, Linux and macOS can also apply system routes from `autoSystemRoutingTable`.
|
||||
macOS does not configure system DNS from the `dns` field, and neither does Linux by default; system DNS remains managed by the OS or distribution-specific network services. \
|
||||
For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears.
|
||||
This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \
|
||||
Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README.
|
||||
|
||||
### SYSTEM DNS ON LINUX (`autoSystemDNS`)
|
||||
### SYSTEM DNS ON LINUX (`autoSystemDnsToGateway`)
|
||||
|
||||
On Linux, setting `autoSystemDNS` to `true` lets the inbound point the system resolver at the tun interface, so name lookups resolve through Xray instead of going out over the physical link. It is off by default, and it is Linux-only.
|
||||
On Linux, setting `autoSystemDnsToGateway` to `true` lets the inbound point the system resolver at the tun interface, so name lookups resolve through Xray instead of going out over the physical link. It is off by default, and it is Linux-only.
|
||||
|
||||
It uses `resolvectl`, which means it applies only when all of these hold:
|
||||
It uses `resolvectl`, which means it only works when all of these hold. Where Xray can tell that one does not, it does not start:
|
||||
|
||||
- the system runs systemd and `resolvectl` is on `PATH`
|
||||
- `systemd-resolved` is enabled and actually managing DNS (installed but not running has no effect)
|
||||
- `systemd-resolved` is enabled and actually managing DNS (installed but not running is not enough)
|
||||
- systemd-resolved is version 240 or newer, where `default-route` exists
|
||||
- no `dns` upstream resolves through the system resolver, directly or through its own bootstrap (see below)
|
||||
|
||||
The address handed over is the first IPv4 `gateway` incremented by one (e.g. `192.168.100.1/30` -> `192.168.100.2`). It is not taken from `dns`: handing `1.1.1.1` to `resolvectl dns` would make systemd-resolved query that server directly over the physical link, which is the leak this option exists to close.
|
||||
The address handed over is the first IPv4 `gateway`, or without one the first IPv6 `gateway`, incremented by one (e.g. `192.168.100.1/30` -> `192.168.100.2`, `fc00::1/64` -> `fc00::2`). Without any `gateway`, the config is rejected. It is not taken from `dns`: handing `1.1.1.1` to `resolvectl dns` would make systemd-resolved query that server directly over the physical link, which is the leak this option exists to close.
|
||||
|
||||
Because that address has to actually answer, the takeover is checked before it happens. A query from the interface address to that address is routed through the configured rules, and host-wide DNS is only changed when the result is a DNS-capable outbound. Otherwise the option does nothing and DNS is left to the OS. In practice this means you also need a routing rule sending the interface's port 53 to a `dns` outbound, for example:
|
||||
Because that address has to actually answer, the takeover is checked before it happens. A query from the interface address to that address is routed through the configured rules, and host-wide DNS is only changed when the result is a DNS-capable outbound. Otherwise DNS is left alone and Xray does not start. In practice this means you also need a routing rule sending the interface's port 53 to a `dns` outbound, for example:
|
||||
|
||||
```json
|
||||
"routing": {
|
||||
@@ -49,19 +50,19 @@ The check is a preflight, not a proof for arbitrary rules. It sends its query fr
|
||||
|
||||
It is also a check for the dependencies it knows about, not a proof that no indirect one exists. A hostname-based upstream that bootstraps through system DNS is the case in point: `https+local://dns.google/dns-query` resolves its own hostname with `DialSystem`, so once the takeover is in place that bootstrap goes `resolved -> TUN -> DNS outbound -> bootstrap -> resolved` and the query times out. The preflight does not see it, because the dependency sits in the upstream's bootstrap rather than in the clients it inspects. Upstream resolution, bootstrap included, therefore has to stay independent of the resolver path being redirected; configuring the address instead of the hostname, or resolving the hostname beforehand, avoids it.
|
||||
|
||||
The upstream requirement in the list above matters as much as the routing rule. With no name servers configured, Core resolves through a client that forwards to the system resolver; pointing the system resolver at the TUN would then close a loop through the DNS outbound, `resolved -> TUN -> DNS outbound -> system resolver -> resolved`, and resolution stops. The takeover is refused in that case.
|
||||
The upstream requirement in the list above matters as much as the routing rule. With no name servers configured, Core resolves through a client that forwards to the system resolver; pointing the system resolver at the TUN would then close a loop through the DNS outbound, `resolved -> TUN -> DNS outbound -> system resolver -> resolved`, and resolution stops. The takeover is refused in that case, and Xray does not start.
|
||||
|
||||
The same applies to a name server pointed at `localhost`, and to a `dns` section that is present but lists no name servers. One such upstream is enough to refuse the takeover even when independent upstreams are configured alongside it: name servers are selected per domain, so a domain-specific rule can still choose the local one, and the loop then affects whichever domains reach it. The check is deliberately broader than the loop it observed, because the alternative would be to drop a name server the user configured.
|
||||
|
||||
Where it does not apply, DNS is left alone and the leak described in XTLS/Xray-core#6454 remains:
|
||||
Where it cannot apply, Xray does not start, rather than run with the leak described in XTLS/Xray-core#6454, so leave the option off there:
|
||||
|
||||
| Environment | Behaviour |
|
||||
|---|---|
|
||||
| systemd distribution with systemd-resolved enabled | applies |
|
||||
| Alpine, Void, Devuan, OpenRC-based, OpenWrt | no `resolvectl`, skipped |
|
||||
| DNS managed by dnsmasq / unbound / BIND / static `resolv.conf` | unreachable by `resolvectl`, skipped |
|
||||
| Containers without a systemd-resolved daemon | skipped |
|
||||
| systemd older than 240 | `default-route` unavailable, skipped |
|
||||
| Alpine, Void, Devuan, OpenRC-based, OpenWrt | no `resolvectl`, does not start |
|
||||
| DNS managed by dnsmasq / unbound / BIND / static `resolv.conf` | unreachable by `resolvectl`, does not start |
|
||||
| Containers without a systemd-resolved daemon | does not start |
|
||||
| systemd older than 240 | `default-route` unavailable, does not start |
|
||||
|
||||
On `Close()` the setting is reverted. It is **not** reverted if the process is killed with `SIGKILL`, since a process cannot handle that signal; run `resolvectl revert <iface>` to clean up by hand. An application that brings its own DNS endpoint is unaffected either way — this only covers the system resolver.
|
||||
|
||||
@@ -198,6 +199,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 `autoSystemWfpBlockLeak`, which needs `autoSystemRoutingTable` (the config is rejected otherwise), 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", "misconfigtun"]`:
|
||||
- `"dns"` (needs `dns`, the config is rejected otherwise): 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.
|
||||
- `"misconfigtun"`: 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.
|
||||
|
||||
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, Xray does not start. 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 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 `"misconfigtun"`, 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.
|
||||
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.
|
||||
|
||||
+16
-6
@@ -32,7 +32,8 @@ type Config struct {
|
||||
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
|
||||
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"`
|
||||
AutoSystemDnsToGateway bool `protobuf:"varint,9,opt,name=auto_system_dns_to_gateway,json=autoSystemDnsToGateway,proto3" json:"auto_system_dns_to_gateway,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
|
||||
}
|
||||
@@ -123,18 +124,25 @@ func (x *Config) GetDesc() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetAutoSystemDns() bool {
|
||||
func (x *Config) GetAutoSystemDnsToGateway() bool {
|
||||
if x != nil {
|
||||
return x.AutoSystemDns
|
||||
return x.AutoSystemDnsToGateway
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Config) GetAutoSystemWfpBlockLeak() []string {
|
||||
if x != nil {
|
||||
return x.AutoSystemWfpBlockLeak
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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\"\xfa\x02\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
|
||||
@@ -144,8 +152,10 @@ const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
|
||||
"\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" +
|
||||
"\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" +
|
||||
" \x03(\tR\x16autoSystemWfpBlockLeakBL\n" +
|
||||
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
|
||||
|
||||
var (
|
||||
|
||||
@@ -15,5 +15,6 @@ message Config {
|
||||
repeated string auto_system_routing_table = 6;
|
||||
string auto_outbounds_interface = 7;
|
||||
string desc = 8;
|
||||
bool auto_system_dns = 9;
|
||||
bool auto_system_dns_to_gateway = 9;
|
||||
repeated string auto_system_wfp_block_leak = 10;
|
||||
}
|
||||
|
||||
@@ -166,12 +166,14 @@ func (t *Handler) Start() error {
|
||||
}
|
||||
|
||||
// Platform-specific system DNS takeover, where the platform implements it.
|
||||
// Non-fatal: a failure leaves DNS management with the OS.
|
||||
// Rather no TUN than one that the system DNS bypasses.
|
||||
if c, ok := tunInterface.(interface {
|
||||
ConfigureSystemDNS(context.Context, string) error
|
||||
}); ok {
|
||||
if err := c.ConfigureSystemDNS(t.ctx, t.tag); err != nil {
|
||||
errors.LogInfoInner(t.ctx, err, "[tun] system DNS not configured")
|
||||
_ = tunStack.Close()
|
||||
_ = tunInterface.Close()
|
||||
return errors.New("unable to set the system DNS (remove autoSystemDnsToGateway to run without)").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-15
@@ -53,23 +53,29 @@ var resolvectlRunner = func(name string, args ...string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// systemDNSAddrs derives the addresses used for the system DNS takeover from the
|
||||
// first IPv4 gateway: the gateway address itself is what a query from this
|
||||
// interface appears to come from, and the next address is what the resolver is
|
||||
// pointed at. The latter belongs to the TUN and is answered inside Xray;
|
||||
// handing the configured public resolvers to resolvectl instead would leave the
|
||||
// system querying them directly over the physical link, defeating the point of
|
||||
// the TUN.
|
||||
// first IPv4 gateway, or without one, the first IPv6 gateway: the gateway
|
||||
// address itself is what a query from this interface appears to come from, and
|
||||
// the next address is what the resolver is pointed at. The latter belongs to
|
||||
// the TUN and is answered inside Xray; handing the configured public resolvers
|
||||
// to resolvectl instead would leave the system querying them directly over the
|
||||
// physical link, defeating the point of the TUN.
|
||||
func systemDNSAddrs(gateway []string) (source, dns netip.Addr, ok bool) {
|
||||
var first6 netip.Addr
|
||||
for _, address := range gateway {
|
||||
prefix, err := netip.ParsePrefix(address)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addr := prefix.Addr()
|
||||
if !addr.Is4() {
|
||||
continue
|
||||
if addr.Is4() {
|
||||
return addr, addr.Next(), true
|
||||
}
|
||||
return addr, addr.Next(), true
|
||||
if !first6.IsValid() {
|
||||
first6 = addr
|
||||
}
|
||||
}
|
||||
if first6.IsValid() {
|
||||
return first6, first6.Next(), true
|
||||
}
|
||||
return netip.Addr{}, netip.Addr{}, false
|
||||
}
|
||||
@@ -115,11 +121,11 @@ const probeSourcePort = 49152
|
||||
// Overridable for tests.
|
||||
var verifyDNSRouting = func(ctx context.Context, inboundTag, source, address string) error {
|
||||
ip, err := netip.ParseAddr(address)
|
||||
if err != nil || !ip.Is4() {
|
||||
if err != nil {
|
||||
return errors.New("invalid DNS address ", address).Base(err)
|
||||
}
|
||||
src, err := netip.ParseAddr(source)
|
||||
if err != nil || !src.Is4() {
|
||||
if err != nil || src.Is4() != ip.Is4() {
|
||||
return errors.New("invalid source address ", source).Base(err)
|
||||
}
|
||||
|
||||
@@ -182,10 +188,10 @@ var verifyDNSRouting = func(ctx context.Context, inboundTag, source, address str
|
||||
//
|
||||
// It acts only when the config opts in, and it verifies the data path first:
|
||||
// unless a query to the advertised address would actually be handled, host-wide
|
||||
// resolution is left to the OS, which is the documented default. Errors are
|
||||
// returned to the caller, which treats them as non-fatal.
|
||||
// resolution is left to the OS and an error returned. The caller does not start
|
||||
// the TUN on an error, as the system DNS would bypass it.
|
||||
func (t *LinuxTun) ConfigureSystemDNS(ctx context.Context, inboundTag string) error {
|
||||
if !t.options.AutoSystemDns {
|
||||
if !t.options.AutoSystemDnsToGateway {
|
||||
return nil
|
||||
}
|
||||
if t.systemDNSSet {
|
||||
@@ -202,7 +208,7 @@ func (t *LinuxTun) ConfigureSystemDNS(ctx context.Context, inboundTag string) er
|
||||
|
||||
source, address, ok := systemDNSAddrs(t.options.Gateway)
|
||||
if !ok {
|
||||
return errors.New("no IPv4 gateway, cannot derive a system DNS address")
|
||||
return errors.New("no gateway, cannot derive a system DNS address")
|
||||
}
|
||||
|
||||
iface := t.ifaceName()
|
||||
|
||||
@@ -191,3 +191,15 @@ func TestVerifyDNSRoutingDecisions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Without an IPv4 gateway, the takeover uses the first IPv6 one, and the probe
|
||||
// carries IPv6 addresses.
|
||||
func TestVerifyDNSRoutingIPv6(t *testing.T) {
|
||||
ctx := newRouteTestContext(t, true, udpNameServer([]byte{9, 9, 9, 9}), []*router.RoutingRule{port53Rule()})
|
||||
if err := verifyDNSRouting(ctx, routeTestInboundTag, "fc00::1", "fc00::2"); err != nil {
|
||||
t.Fatalf("expected the takeover to be accepted, got: %v", err)
|
||||
}
|
||||
if err := verifyDNSRouting(ctx, routeTestInboundTag, routeTestSource, "fc00::2"); err == nil {
|
||||
t.Fatal("expected mixed IPv4 and IPv6 addresses to be refused")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +58,9 @@ func recorder(t *testing.T, failOn string) *[][]string {
|
||||
func optedInTun() *LinuxTun {
|
||||
return &LinuxTun{
|
||||
options: &Config{
|
||||
Name: "xray_tun",
|
||||
Gateway: []string{"192.168.100.1/30"},
|
||||
AutoSystemDns: true,
|
||||
Name: "xray_tun",
|
||||
Gateway: []string{"192.168.100.1/30"},
|
||||
AutoSystemDnsToGateway: true,
|
||||
},
|
||||
tunLink: testLink("xray_tun"),
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func TestConfigureSystemDNSDisabledByDefault(t *testing.T) {
|
||||
calls := recorder(t, "")
|
||||
|
||||
t1 := optedInTun()
|
||||
t1.options.AutoSystemDns = false
|
||||
t1.options.AutoSystemDnsToGateway = false
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -103,7 +103,7 @@ func TestConfigureSystemDNSNoGateway(t *testing.T) {
|
||||
t1.options.Gateway = nil
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
|
||||
t.Fatal("expected an error when no IPv4 gateway is configured")
|
||||
t.Fatal("expected an error when no gateway is configured")
|
||||
}
|
||||
if len(*probes) != 0 {
|
||||
t.Errorf("routing probe must not run without a gateway, got %d calls", len(*probes))
|
||||
@@ -351,9 +351,18 @@ func TestSystemDNSAddrs(t *testing.T) {
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "ipv6 only",
|
||||
gateway: []string{"fc00::1/64"},
|
||||
wantOK: false,
|
||||
name: "ipv6 only",
|
||||
gateway: []string{"fc00::1/64"},
|
||||
wantSource: "fc00::1",
|
||||
wantDNS: "fc00::2",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "first ipv6 without ipv4",
|
||||
gateway: []string{"fc00::1/64", "fd00::1/64"},
|
||||
wantSource: "fc00::1",
|
||||
wantDNS: "fc00::2",
|
||||
wantOK: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+229
-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,105 @@ 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")
|
||||
}
|
||||
}
|
||||
|
||||
// With autoSystemWfpBlockLeak, once the system routes lead to the TUN,
|
||||
// keep DNS ("dns", if dns is set), 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 := t.options.AutoSystemWfpBlockLeak
|
||||
blockDNS := slices.Contains(leaks, "dns") && len(dns) > 0
|
||||
blockIPv4 := slices.Contains(leaks, "misconfigtun") && !route4
|
||||
blockIPv6 := slices.Contains(leaks, "misconfigtun") && !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 {
|
||||
on bool
|
||||
what string
|
||||
}{{blockDNS, "DNS"}, {blockIPv4, "IPv4"}, {blockIPv6, "IPv6"}} {
|
||||
if b.on {
|
||||
blocked = append(blocked, b.what)
|
||||
}
|
||||
}
|
||||
// Rather no TUN than a leaking one.
|
||||
return errors.New("unable to block ", strings.Join(blocked, " and "), " outside the TUN (remove autoSystemWfpBlockLeak to run without)").Base(err)
|
||||
}
|
||||
errors.LogInfo(context.Background(), "[tun] outside the TUN, blocked DNS: ", blockDNS, ", blocked IPv4: ", blockIPv4, ", 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 +334,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 +357,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,471 @@
|
||||
//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
|
||||
fwpSecurityDescriptorType = 14 // FWP_SECURITY_DESCRIPTOR_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}}
|
||||
fwpmConditionALEUserID = windows.GUID{Data1: 0xaf043a0a, Data2: 0xb34d, Data3: 0x4f86, Data4: [8]byte{0x97, 0x9c, 0xc9, 0x03, 0x71, 0xaf, 0x6e, 0x66}}
|
||||
)
|
||||
|
||||
// dnsClientSID is the SID of Windows' DNS Client service, NT SERVICE\Dnscache.
|
||||
// Service SIDs derive from the service name, so it is the same everywhere (sc
|
||||
// showsid dnscache).
|
||||
const dnsClientSID = "S-1-5-80-859482183-879914841-863379149-1145462774-2388618682"
|
||||
|
||||
// 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. It sends the queries for the servers of
|
||||
// an interface out through that interface, whatever the routes say, and
|
||||
// other programs reach an on-link resolver, like 192.168.1.1 from DHCP,
|
||||
// through its LAN route, which is more specific than the TUN's default
|
||||
// route. Since Windows 11 and Server 2022, Windows may also send its
|
||||
// queries over HTTPS or TLS, so there its DNS Client service may not
|
||||
// connect outside the TUN at all, except for name resolution on the local
|
||||
// link (mDNS, LLMNR).
|
||||
// - ipv4, ipv6: no IPv4, or no IPv6, at all, in either direction, for a TUN
|
||||
// that no route of it leads to, except loopback and what Windows itself
|
||||
// needs on the local link (DHCP, and for IPv6 neighbor and multicast
|
||||
// listener discovery), none of which can leave it. The TUN carries what
|
||||
// is routed to it even without an address of that IP version in gateway:
|
||||
// Windows gives it link-local ones itself, an IPv6 one at once, an IPv4
|
||||
// one from 169.254.0.0/16 after some seconds (until then, IPv4 routed to
|
||||
// the TUN is unreachable).
|
||||
//
|
||||
// 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, ipv4, 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, ipv4, 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, ipv4, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since Windows 11 and Server 2022 (build 20348), the DNS Client service
|
||||
// may also send the queries for an interface's servers over HTTPS or TLS,
|
||||
// out through that interface and to any port. So there it may only
|
||||
// connect through the TUN, except for mDNS and LLMNR, which stay on the
|
||||
// local link (over an IP version only while it is not blocked altogether).
|
||||
// Earlier versions only query port 53, and may run the service in one
|
||||
// process with others, which the filters would catch as well. Like
|
||||
// Windows Firewall's rules for it, they recognize the service by its SID,
|
||||
// which Windows puts in the token of its process: the security descriptor
|
||||
// grants that SID the right to match (FWP_ACTRL_MATCH_FILTER, CC in SDDL).
|
||||
if _, _, build := windows.RtlGetNtVersionNumbers(); dns && build >= 20348 {
|
||||
sd, err := windows.SecurityDescriptorFromString("O:SYG:SYD:(A;;CCRC;;;" + dnsClientSID + ")")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sdBlob := &fwpByteBlob{size: sd.Length(), data: (*byte)(unsafe.Pointer(sd))}
|
||||
pinner.Pin(sdBlob) // the condition only holds it as uintptr
|
||||
dnsClient := condition(&fwpmConditionALEUserID, fwpSecurityDescriptorType, uintptr(unsafe.Pointer(sdBlob)))
|
||||
// Conditions on the same field match when any of them does.
|
||||
mdnsLLMNR := []fwpmFilterCondition0{dnsClient, condition(&fwpmConditionIPRemotePort, fwpUint16, 5353), condition(&fwpmConditionIPRemotePort, fwpUint16, 5355)}
|
||||
for _, layer := range []struct {
|
||||
key *windows.GUID
|
||||
localLink bool
|
||||
}{
|
||||
{&fwpmLayerALEAuthConnectV4, !ipv4},
|
||||
{&fwpmLayerALEAuthConnectV6, !ipv6},
|
||||
} {
|
||||
if err := add(layer.key, "permit the DNS Client service through the TUN", 0, fwpActionPermit, 3, dnsClient, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPNexthopInterface)); err != nil {
|
||||
return err
|
||||
}
|
||||
if layer.localLink {
|
||||
if err := add(layer.key, "permit the DNS Client service's mDNS and LLMNR", 0, fwpActionPermit, 3, mdnsLLMNR...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := add(layer.key, "block the DNS Client service", 0, fwpActionBlock, 2, dnsClient); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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},
|
||||
}
|
||||
if ipv4 {
|
||||
// DHCP keeps the addresses of the other interfaces, which Xray's own
|
||||
// connections use.
|
||||
dhcp := []fwpmFilterCondition0{
|
||||
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
||||
condition(&fwpmConditionIPLocalPort, fwpUint16, 68),
|
||||
condition(&fwpmConditionIPRemotePort, fwpUint16, 67),
|
||||
}
|
||||
for _, layer := range []*windows.GUID{&fwpmLayerALEAuthConnectV4, &fwpmLayerALEAuthRecvAcceptV4} {
|
||||
if err := add(layer, "permit IPv4 loopback", 0, fwpActionPermit, 1, loopback); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(layer, "permit DHCP", 0, fwpActionPermit, 1, dhcp...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(layer, "block IPv4", 0, fwpActionBlock, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if ipv6 {
|
||||
// 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,206 @@
|
||||
//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, true); err != nil {
|
||||
skipUnlessElevated(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSClientSID(t *testing.T) {
|
||||
sid, _, _, err := windows.LookupSID("", `NT SERVICE\Dnscache`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sid.String() != dnsClientSID {
|
||||
t.Errorf(`NT SERVICE\Dnscache is %v, not %v`, sid, dnsClientSID)
|
||||
}
|
||||
}
|
||||
|
||||
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