mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-11-02 16:36:58 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5cc086d6c | ||
|
|
6b45eadf45 | ||
|
|
c684201cd8 | ||
|
|
2218fec173 | ||
|
|
9ffa0f0552 | ||
|
|
2933b0c8d5 | ||
|
|
bb4cd4b6d3 | ||
|
|
468f745fc2 | ||
|
|
7c8dbcb4bb | ||
|
|
8ab161a2b0 |
@@ -26,13 +26,13 @@ require (
|
||||
golang.org/x/crypto v0.58.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/net v0.61.0
|
||||
golang.org/x/net v0.62.0
|
||||
golang.org/x/sync v0.24.0
|
||||
golang.org/x/sys v0.49.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/windows v1.1.1
|
||||
google.golang.org/grpc v1.84.0
|
||||
google.golang.org/grpc v1.85.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
h12.io/socks v1.0.3
|
||||
layeh.com/gopher-luar v1.0.11
|
||||
@@ -58,7 +58,7 @@ require (
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/text v0.43.0 // indirect
|
||||
golang.org/x/tools v0.51.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
|
||||
|
||||
@@ -109,8 +109,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
||||
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
|
||||
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
@@ -150,10 +150,10 @@ golang.zx2c4.com/wireguard/windows v1.1.1 h1:8/H97U1v1PNDNcBsMZgU3KFuND9MQdTsU2N
|
||||
golang.zx2c4.com/wireguard/windows v1.1.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
|
||||
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 h1:utmqiUzZAEINbLmRyAt1QjVkMyLQ+lvIwCpLaB1rZfw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.85.0 h1:BPBCfuVRQ1qvBhKDOq9uvQllMvO59rKsYhXpp7Dd8vw=
|
||||
google.golang.org/grpc v1.85.0/go.mod h1:0rQrOD58yjal/0EaVbGBANindixsCZNQPDV3VKtaKuQ=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 h1:rgSNvqscFZ1JgV/4wH5GOsZFSFkR2Eua9As3KIr2LlM=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2/go.mod h1:iMEtFwDlAhjDU9L5mY6U1XLwlIId/G3h+QcBHDIvrJ8=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
||||
return nil, errors.New("autoSystemWfpBlockLeak needs autoSystemRoutingTable to be set")
|
||||
}
|
||||
if tun.BlocksSystemDNS(leaks) && len(v.DNS) == 0 {
|
||||
return nil, errors.New(`autoSystemWfpBlockLeak "dns", "udp/53", "tcp/53" or "windnsclient" needs dns to be set`)
|
||||
return nil, errors.New(`autoSystemWfpBlockLeak "all", "allexceptlan", "dns", "windnsclient", or TCP or UDP port 53, needs dns to be set`)
|
||||
}
|
||||
case "linux", "freebsd":
|
||||
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
|
||||
|
||||
+34
-1
@@ -41,6 +41,26 @@ func TestTunConfigAutoSystem(t *testing.T) {
|
||||
Parser: loadJSON(creator),
|
||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"tcp/853", "udp/853"}},
|
||||
},
|
||||
{
|
||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["All", "UDP", "tcp/8000-9000", "icmp"]}`,
|
||||
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{"all", "udp", "tcp/8000-9000", "icmp"}},
|
||||
},
|
||||
{
|
||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["AllExceptLAN"]}`,
|
||||
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{"allexceptlan", "udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}},
|
||||
},
|
||||
{
|
||||
Input: `{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["IPv4:TCP/853", "ipv6:udp/10-20", "IPv6:ICMP"]}`,
|
||||
Parser: loadJSON(creator),
|
||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"ipv4:tcp/853", "ipv6:udp/10-20", "ipv6:icmp"}},
|
||||
},
|
||||
{
|
||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["ipv4:dns", "IPv6:All", "ipv6:udp/53"]}`,
|
||||
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{"udp/53", "ipv4:tcp/53", "ipv4:windnsclient", "ipv4:tcp/853", "ipv4:udp/853", "ipv6:all"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,6 +77,19 @@ func TestTunConfigAutoSystemNeeds(t *testing.T) {
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["udp/53"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["windnsclient"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["tcp/853", "udp/853"]}`, nil},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["all"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["udp"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["tcp/50-60"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["tcp/54-60", "icmp"]}`, nil},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["ipv6:udp/53"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["ipv4:dns"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["ipv6:all"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["ipv4:windnsclient"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["ipv4:tcp/853", "ipv4:icmp", "ipv6:misconfigtun"]}`, nil},
|
||||
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["ipv6:misconfigtun"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["all"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["allexceptlan"]}`, []string{"windows"}},
|
||||
{`{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["allexceptlan"]}`, nil},
|
||||
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, []string{"linux", "freebsd"}},
|
||||
} {
|
||||
config := new(TunConfig)
|
||||
@@ -70,7 +103,7 @@ func TestTunConfigAutoSystemNeeds(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTunConfigAutoSystemWfpBlockLeakUnknown(t *testing.T) {
|
||||
for _, value := range []string{"ip", "tcp/0", "icmp/1"} {
|
||||
for _, value := range []string{"ip", "tcp/0", "tcp/0-10", "icmp/1", "udp/20-10", "tcp4", "tcpv4", "ipv4", "ipv5:udp", "ipv4:icmp/1"} {
|
||||
config := new(TunConfig)
|
||||
if err := json.Unmarshal([]byte(`{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "`+value+`"]}`), config); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
+16
-11
@@ -4,7 +4,7 @@ TUN interface support bridges the gap between network layer 3 and layer 7, intro
|
||||
|
||||
This functionality is targeted to assist applications/end devices that don't have proxy support, or can't run external applications (like Smart TV's). Making it possible to run Xray proxy right on network edge devices (routers) with support to route raw network traffic. \
|
||||
Primary targets are Linux based router devices. Like most popular OpenWRT option. \
|
||||
Support for Windows, macOS, Android and iOS is also implemented (see below).
|
||||
Support for Windows, macOS, FreeBSD, Android and iOS is also implemented (see below).
|
||||
|
||||
## PLEASE READ FOLLOWING CAREFULLY
|
||||
|
||||
@@ -25,6 +25,8 @@ Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-ne
|
||||
|
||||
Packets are handled by Xray-core's own lightweight TCP/IP stack, which replaced gVisor (XTLS/Xray-core#7130).
|
||||
|
||||
On Linux, as on Android, the TUN can also be a file descriptor handed over through the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`), e.g. for lightweight or unprivileged setups.
|
||||
|
||||
With `autoSystemRoutingTable`, Xray's own connections are kept out of the TUN through `autoOutboundsInterface` (then `"auto"` by default): each of their sockets is bound to another interface, the one named there, or with `"auto"` the one of the default route (Android needs the name). Linux and Android bind with `SO_BINDTODEVICE`, Windows with `IP_UNICAST_IF` and `IPV6_UNICAST_IF`, macOS and iOS with `IP_BOUND_IF` and `IPV6_BOUND_IF`, and FreeBSD switches them to another routing table (see [FreeBSD SUPPORT](#freebsd-support)). Connections to loopback addresses, TCP and UDP, stay unbound. If a socket cannot be bound, the connection fails rather than leave unbound, where it could loop back into the TUN (XTLS/Xray-core#7173).
|
||||
|
||||
### SYSTEM DNS ON LINUX AND FREEBSD (`autoSystemDnsToGateway`)
|
||||
@@ -223,21 +225,24 @@ After the start network adapter with the name you chose in the config will be cr
|
||||
|
||||
With IPv6 disabled system-wide (the `DisabledComponents` registry value), the adapter has no IPv6, and the TUN starts with IPv4 only, unless `gateway` or `autoSystemRoutingTable` has IPv6 entries, which then keep it from starting.
|
||||
|
||||
When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops.
|
||||
When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops. Xray's own lookups through the system's DNS servers, like those of the `localhost` DNS server with `autoOutboundsInterface`, skip them, unless another interface uses them as well, as they would lead back into the TUN.
|
||||
|
||||
With `autoSystemWfpBlockLeak`, which needs `autoSystemRoutingTable` (the config is rejected otherwise), Xray also adds Windows Filtering Platform filters that keep traffic of every program but Xray itself from leaving outside the TUN, each kind chosen by a value in the list, e.g. `"autoSystemWfpBlockLeak": ["dns", "misconfigtun"]`:
|
||||
- `"tcp/<port>"`, `"udp/<port>"`: connections to that TCP or UDP port only go through the TUN, e.g. `"tcp/853"`.
|
||||
- `"windnsclient"`: Windows' DNS Client service cannot connect outside the TUN at all, except for name resolution on the local network (LLMNR, mDNS). On Windows 11 and Server 2022 and later, it may send name queries over HTTPS or TLS, to any port. Earlier versions only query port 53, so there it does nothing.
|
||||
- `"dns"`: stands for `"udp/53"`, `"tcp/53"`, `"windnsclient"`, `"tcp/853"` (DNS over TLS) and `"udp/853"` (DNS over QUIC). 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. Keeping the system's DNS inside the TUN like this, with `"dns"`, `"udp/53"`, `"tcp/53"` or `"windnsclient"`, needs `dns` (the config is rejected otherwise). The `dns` servers then 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.
|
||||
Setting `autoSystemWfpBlockLeak` to at least `["dns"]` is recommended, to block DNS leaks. It needs `autoSystemRoutingTable` (the config is rejected otherwise), and adds Windows Filtering Platform filters that let traffic of every program but Xray itself leave only through the TUN, each kind chosen by a value:
|
||||
- `"dns"`: stands for `"udp/53"`, `"tcp/53"`, `"windnsclient"`, `"tcp/853"` (DNS over TLS) and `"udp/853"` (DNS over QUIC). Windows also queries the DNS servers of the other interfaces, through those interfaces whatever the routes say, and other programs reach a resolver on the local network (e.g. `192.168.1.1` from DHCP) through its more specific route.
|
||||
- `"windnsclient"`: Windows' DNS Client service, except for name resolution on the local network (LLMNR, mDNS). Since Windows 11 and Server 2022, it may query over HTTPS or TLS, to any port; earlier versions only query port 53, so there it does nothing.
|
||||
- `"tcp"`, `"udp"`: that protocol, to all ports, or with `/<port>` or `/<low>-<high>` (1 to 65535) to some, e.g. `"tcp/853"` or `"udp/10-20"`.
|
||||
- `"icmp"`: ICMP and ICMPv6, e.g. `ping`.
|
||||
- `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, which would bypass the TUN.
|
||||
- `"all"`: everything, in both directions. Access to the local network then breaks unless it is routed into the TUN as well, and so do incoming connections to other programs, other VPNs, and virtual machines whose NAT runs on the host.
|
||||
- `"allexceptlan"`: like `"all"`, but traffic to and from the local network stays allowed, e.g. for printers or casting to a TV: private, link-local, multicast and broadcast addresses (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `224.0.0.0/4`, `255.255.255.255`, `fc00::/7`, `fe80::/10`, `ff00::/8`), not global IPv6 ones. It includes `"dns"`, and the other values apply there as well. `"all"` overrides it.
|
||||
|
||||
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.
|
||||
Any value can be limited to one IP version with `"ipv4:"` or `"ipv6:"` in front, e.g. `"ipv4:udp/53"` or `"ipv6:all"`. `"ipv4:allexceptlan"`, for instance, includes `"ipv4:dns"` only; add `"dns"` to block DNS over IPv6 as well.
|
||||
|
||||
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.
|
||||
Loopback, DHCP, DHCPv6 and IPv6 neighbor discovery always stay allowed. Xray's own connections out also get past Windows Firewall's block rules, while connections to its inbounds stay subject to them. If the filters cannot be added, Xray does not start; they are removed when it exits. Name resolution on the local network (LLMNR, mDNS, NetBIOS) is not covered, except over an IP version that is blocked entirely.
|
||||
|
||||
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.
|
||||
Keeping the system's DNS inside the TUN, with `"all"`, `"allexceptlan"`, `"dns"`, `"windnsclient"`, or anything covering TCP or UDP port 53, even over one IP version only, needs `dns` (the config is rejected otherwise), whose servers have to lie within `gateway` or `autoSystemRoutingTable`, or be loopback addresses like `127.0.0.1` (a warning is logged otherwise); DNS servers to be reached directly belong in Xray's own `dns` settings. With `autoOutboundsInterface`, names that Xray would look up through the system, e.g. an outbound's server address with the default `AsIs` domain strategy, are then resolved by Xray itself, from the DNS servers of the other interfaces, as Windows would send those queries into the TUN. That bypasses Windows' DNS cache, so such a name is looked up for every connection; resolving it with Xray's own `dns`, through the outbound's `sockopt.domainStrategy`, avoids that.
|
||||
|
||||
`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"]`.
|
||||
`autoSystemWfpBlockLeak` is empty by default, as the filters break some setups: with `"dns"`, a local DNS resolver that queries through another interface, the DNS of another VPN, virtual machines whose NAT resolves names on the host, and captive portals; with `"misconfigtun"`, IPv4 or IPv6 on the local network while not routed into the TUN.
|
||||
|
||||
`autoOutboundsInterface` (the default with `autoSystemRoutingTable`) keeps Xray's own connections out of the TUN by binding them to another interface, which Windows only honors while that interface has weak host send and forwarding off for the IP versions routed to the TUN. Otherwise, Windows sends them into the TUN, from that interface's address, and they stall. While the TUN runs, Xray therefore turns weak host send off on that interface, and on again when it stops or another interface takes over. Forwarding cannot be turned off this way, as Mobile Hotspot and Internet Connection Sharing need it, so a warning is logged while it is on. Having the hotspot share the TUN instead of that interface (Settings, Mobile hotspot, Share my internet connection from) moves forwarding to the TUN, where it does no harm, and sends the hotspot's devices through Xray as well.
|
||||
|
||||
|
||||
@@ -247,18 +247,20 @@ startOver:
|
||||
}
|
||||
|
||||
// With autoSystemWfpBlockLeak, once the system routes lead to the TUN,
|
||||
// keep connections to the ports given ("dns" stands for those of DNS),
|
||||
// Windows' DNS Client service, and an IP version no route of which leads
|
||||
// to the TUN ("misconfigtun"), from leaving through the other interfaces.
|
||||
// Addresses do not matter: without one of a version in gateway, Windows
|
||||
// gives the TUN a link-local one.
|
||||
// keep the protocols and ports given ("dns" stands for those of DNS),
|
||||
// Windows' DNS Client service, an IP version no route of which leads to
|
||||
// the TUN ("misconfigtun"), or everything ("all", or "allexceptlan" but
|
||||
// for the local network), over IPv4, IPv6 or both, from leaving through
|
||||
// the other interfaces. Addresses do not matter: without one of a version
|
||||
// in gateway, Windows gives the TUN a link-local one.
|
||||
leaks, err := ExpandWfpBlockLeak(t.options.AutoSystemWfpBlockLeak)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block := newLeakFilters(leaks, route4, route6)
|
||||
if route4 || route6 {
|
||||
if slices.Contains(leaks, "windnsclient") && !block.dnsClient {
|
||||
if block.dnsClient != 0 && !dnsClientUsesAnyPort() {
|
||||
block.dnsClient = 0
|
||||
errors.LogInfo(context.Background(), "[tun] windnsclient skipped: before Windows 11 and Server 2022, the DNS Client service only queries port 53")
|
||||
}
|
||||
if block.systemDNS() && len(dns) == 0 {
|
||||
|
||||
+264
-138
@@ -3,6 +3,7 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
@@ -37,15 +38,19 @@ const (
|
||||
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
|
||||
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
|
||||
fwpV4AddrMask = 0x100 // FWP_V4_ADDR_MASK
|
||||
fwpV6AddrMask = 0x101 // FWP_V6_ADDR_MASK
|
||||
fwpRangeType = 0x102 // FWP_RANGE_TYPE
|
||||
|
||||
fwpMatchEqual = 0 // FWP_MATCH_EQUAL
|
||||
fwpMatchRange = 5 // FWP_MATCH_RANGE
|
||||
fwpMatchFlagsAllSet = 6 // FWP_MATCH_FLAGS_ALL_SET
|
||||
|
||||
fwpConditionFlagIsLoopback = 1 // FWP_CONDITION_FLAG_IS_LOOPBACK
|
||||
@@ -94,6 +99,37 @@ type fwpValue0 struct {
|
||||
value uintptr
|
||||
}
|
||||
|
||||
// fwpRange0 is FWP_RANGE0, which an FWP_RANGE_TYPE condition points to.
|
||||
type fwpRange0 struct {
|
||||
low, high fwpValue0
|
||||
}
|
||||
|
||||
// fwpV4AddrAndMask is FWP_V4_ADDR_AND_MASK, in host byte order.
|
||||
type fwpV4AddrAndMask struct {
|
||||
addr, mask uint32
|
||||
}
|
||||
|
||||
// fwpV6AddrAndMask is FWP_V6_ADDR_AND_MASK.
|
||||
type fwpV6AddrAndMask struct {
|
||||
addr [16]byte
|
||||
prefixLength uint8
|
||||
}
|
||||
|
||||
// lanPrefixes are what "allexceptlan" leaves to the local network: private
|
||||
// and link-local addresses, and multicast and broadcast, which discovery on
|
||||
// it uses.
|
||||
var lanPrefixes = []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
netip.MustParsePrefix("172.16.0.0/12"),
|
||||
netip.MustParsePrefix("192.168.0.0/16"),
|
||||
netip.MustParsePrefix("169.254.0.0/16"),
|
||||
netip.MustParsePrefix("224.0.0.0/4"),
|
||||
netip.MustParsePrefix("255.255.255.255/32"),
|
||||
netip.MustParsePrefix("fc00::/7"),
|
||||
netip.MustParsePrefix("fe80::/10"),
|
||||
netip.MustParsePrefix("ff00::/8"),
|
||||
}
|
||||
|
||||
type fwpmDisplayData0 struct {
|
||||
name *uint16
|
||||
description *uint16
|
||||
@@ -171,52 +207,79 @@ func condition(field *windows.GUID, typ uint32, value uintptr) fwpmFilterConditi
|
||||
}
|
||||
}
|
||||
|
||||
// leakFilters is what blockLeaks keeps from leaving outside the TUN.
|
||||
// leakFilters is what blockLeaks keeps from leaving outside the TUN, each over
|
||||
// the IP versions given.
|
||||
type leakFilters struct {
|
||||
ports []leakPort // connections to these ports
|
||||
dnsClient bool // Windows' DNS Client service
|
||||
ipv4, ipv6 bool // an IP version altogether
|
||||
traffic []leak // connections of these protocols and ports
|
||||
dnsClient ipVersions // Windows' DNS Client service
|
||||
all ipVersions // everything ("all" or "allexceptlan")
|
||||
lan ipVersions // but to and from the local network ("allexceptlan")
|
||||
unrouted ipVersions // everything, as no route leads to the TUN ("misconfigtun")
|
||||
}
|
||||
|
||||
// newLeakFilters turns expanded autoSystemWfpBlockLeak values into the filters
|
||||
// for a TUN that the routes of route4 (IPv4) and route6 (IPv6) lead to.
|
||||
func newLeakFilters(leaks []string, route4, route6 bool) leakFilters {
|
||||
var block leakFilters
|
||||
for _, leak := range leaks {
|
||||
switch leak {
|
||||
var routed, allExceptLAN ipVersions
|
||||
if route4 {
|
||||
routed |= ipv4Version
|
||||
}
|
||||
if route6 {
|
||||
routed |= ipv6Version
|
||||
}
|
||||
for _, value := range leaks {
|
||||
switch l, _ := parseLeak(value); l.name {
|
||||
case "windnsclient":
|
||||
block.dnsClient = dnsClientUsesAnyPort()
|
||||
block.dnsClient |= l.versions
|
||||
case "misconfigtun":
|
||||
block.ipv4, block.ipv6 = !route4, !route6
|
||||
default:
|
||||
if port, ok := parseLeakPort(leak); ok {
|
||||
block.ports = append(block.ports, port)
|
||||
}
|
||||
block.unrouted |= l.versions &^ routed
|
||||
case "all":
|
||||
block.all |= l.versions
|
||||
case "allexceptlan":
|
||||
allExceptLAN |= l.versions
|
||||
case "tcp", "udp", "icmp":
|
||||
block.traffic = append(block.traffic, l)
|
||||
}
|
||||
}
|
||||
// "all" overrides "allexceptlan".
|
||||
block.lan = allExceptLAN &^ block.all
|
||||
block.all |= allExceptLAN
|
||||
return block
|
||||
}
|
||||
|
||||
// String lists what block keeps inside the TUN, or is empty for nothing.
|
||||
func (block leakFilters) String() string {
|
||||
var blocked []string
|
||||
for _, port := range block.ports {
|
||||
blocked = append(blocked, port.String())
|
||||
for _, traffic := range block.traffic {
|
||||
blocked = append(blocked, traffic.String())
|
||||
}
|
||||
for _, b := range []struct {
|
||||
on bool
|
||||
what string
|
||||
}{{block.dnsClient, "windnsclient"}, {block.ipv4, "IPv4"}, {block.ipv6, "IPv6"}} {
|
||||
if b.on {
|
||||
blocked = append(blocked, b.what)
|
||||
if block.dnsClient != 0 {
|
||||
blocked = append(blocked, leak{versions: block.dnsClient, name: "windnsclient"}.String())
|
||||
}
|
||||
for _, family := range []struct {
|
||||
version ipVersions
|
||||
name string
|
||||
}{{ipv4Version, "IPv4"}, {ipv6Version, "IPv6"}} {
|
||||
if (block.all|block.unrouted)&family.version == 0 {
|
||||
continue
|
||||
}
|
||||
if block.lan&family.version != 0 {
|
||||
family.name += " except LAN"
|
||||
}
|
||||
blocked = append(blocked, family.name)
|
||||
}
|
||||
return strings.Join(blocked, ", ")
|
||||
}
|
||||
|
||||
// systemDNS tells whether block keeps the system's DNS inside the TUN.
|
||||
// systemDNS tells whether block keeps the system's DNS inside the TUN, over at
|
||||
// least one IP version: port 53, the DNS Client service, or everything ("all"),
|
||||
// though not with the local network left open, where a resolver like the
|
||||
// router's may be.
|
||||
func (block leakFilters) systemDNS() bool {
|
||||
return block.dnsClient || slices.ContainsFunc(block.ports, func(port leakPort) bool { return port.port == 53 })
|
||||
return block.dnsClient != 0 || block.all&^block.lan != 0 || slices.ContainsFunc(block.traffic, func(traffic leak) bool {
|
||||
return traffic.covers("udp", 53) || traffic.covers("tcp", 53)
|
||||
})
|
||||
}
|
||||
|
||||
// dnsClientUsesAnyPort tells whether Windows' DNS Client service may send its
|
||||
@@ -231,29 +294,34 @@ func dnsClientUsesAnyPort() bool {
|
||||
|
||||
// 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:
|
||||
// other interfaces on purpose, each over the IP versions block gives:
|
||||
//
|
||||
// - ports: connections to these TCP or UDP ports may only go through the
|
||||
// TUN, those of DNS (53), DNS over TLS (TCP 853) and DNS over QUIC (UDP
|
||||
// 853) with "dns". 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.
|
||||
// - traffic: connections of these protocols, TCP and UDP to some or all
|
||||
// ports, or ICMP, may only go through the TUN, those of DNS (53), DNS
|
||||
// over TLS (TCP 853) and DNS over QUIC (UDP 853) with "dns". 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.
|
||||
// - dnsClient: since Windows 11 and Server 2022, Windows may also send its
|
||||
// queries over HTTPS or TLS, to any port, so 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).
|
||||
// - all, unrouted: nothing at all outside the TUN, in either direction:
|
||||
// with "all", or with "allexceptlan" (lan) but to and from the local
|
||||
// network, below the other filters, which so still apply there, or with
|
||||
// "misconfigtun" for an IP version no route of which leads to the TUN.
|
||||
// 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).
|
||||
//
|
||||
// Whatever is blocked, loopback, which cannot leave the system, and what
|
||||
// Windows itself needs on the local link stay allowed: DHCP, and for IPv6
|
||||
// neighbor and multicast listener discovery.
|
||||
//
|
||||
// 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.
|
||||
@@ -354,33 +422,144 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
onTUN := func(field *windows.GUID) fwpmFilterCondition0 {
|
||||
return condition(field, fwpUint64, uintptr(unsafe.Pointer(tunLUID)))
|
||||
}
|
||||
// toLAN matches the remote addresses of lanPrefixes of an IP version:
|
||||
// conditions on the same field match when any of them does.
|
||||
toLAN := func(ipv6 bool) []fwpmFilterCondition0 {
|
||||
var conditions []fwpmFilterCondition0
|
||||
for _, prefix := range lanPrefixes {
|
||||
if prefix.Addr().Is6() != ipv6 {
|
||||
continue
|
||||
}
|
||||
var value uintptr
|
||||
typ := uint32(fwpV4AddrMask)
|
||||
if ipv6 {
|
||||
addr := &fwpV6AddrAndMask{addr: prefix.Addr().As16(), prefixLength: uint8(prefix.Bits())}
|
||||
pinner.Pin(addr) // the condition only holds it as uintptr
|
||||
value, typ = uintptr(unsafe.Pointer(addr)), fwpV6AddrMask
|
||||
} else {
|
||||
a := prefix.Addr().As4()
|
||||
addr := &fwpV4AddrAndMask{addr: binary.BigEndian.Uint32(a[:]), mask: ^uint32(0) << (32 - prefix.Bits())}
|
||||
pinner.Pin(addr) // the condition only holds it as uintptr
|
||||
value = uintptr(unsafe.Pointer(addr))
|
||||
}
|
||||
conditions = append(conditions, condition(&fwpmConditionIPRemoteAddress, typ, value))
|
||||
}
|
||||
return conditions
|
||||
}
|
||||
// toPorts matches the ports of traffic, if not all of them.
|
||||
toPorts := func(traffic leak) []fwpmFilterCondition0 {
|
||||
switch {
|
||||
case traffic.low == 0:
|
||||
return nil
|
||||
case traffic.low == traffic.high:
|
||||
return []fwpmFilterCondition0{condition(&fwpmConditionIPRemotePort, fwpUint16, uintptr(traffic.low))}
|
||||
}
|
||||
ports := &fwpRange0{
|
||||
low: fwpValue0{typ: fwpUint16, value: uintptr(traffic.low)},
|
||||
high: fwpValue0{typ: fwpUint16, value: uintptr(traffic.high)},
|
||||
}
|
||||
pinner.Pin(ports) // the condition only holds it as uintptr
|
||||
return []fwpmFilterCondition0{{
|
||||
fieldKey: fwpmConditionIPRemotePort,
|
||||
matchType: fwpMatchRange,
|
||||
conditionValue: fwpValue0{typ: fwpRangeType, value: uintptr(unsafe.Pointer(ports))},
|
||||
}}
|
||||
}
|
||||
|
||||
// What stays allowed whatever is blocked, in both directions: replies to
|
||||
// a connection accepted from outside would leave through the physical
|
||||
// link as well.
|
||||
type permit struct {
|
||||
name string
|
||||
conditions []fwpmFilterCondition0
|
||||
}
|
||||
loopback := permit{"loopback", []fwpmFilterCondition0{{
|
||||
fieldKey: fwpmConditionFlags,
|
||||
matchType: fwpMatchFlagsAllSet,
|
||||
conditionValue: fwpValue0{typ: fwpUint32, value: fwpConditionFlagIsLoopback},
|
||||
}}}
|
||||
// DHCP keeps the addresses of the other interfaces, which Xray's own
|
||||
// connections use.
|
||||
dhcp := permit{"DHCP", []fwpmFilterCondition0{
|
||||
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
||||
condition(&fwpmConditionIPLocalPort, fwpUint16, 68),
|
||||
condition(&fwpmConditionIPRemotePort, fwpUint16, 67),
|
||||
}}
|
||||
// Neighbor and multicast listener discovery, ICMPv6 130-137 and 143,
|
||||
// whose type and code sit where the local and remote port are.
|
||||
discovery := permit{"IPv6 neighbor and multicast listener discovery", []fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_ICMPV6)}}
|
||||
for _, typ := range []uintptr{130, 131, 132, 133, 134, 135, 136, 137, 143} {
|
||||
discovery.conditions = append(discovery.conditions, condition(&fwpmConditionIPLocalPort, fwpUint16, typ))
|
||||
}
|
||||
discovery.conditions = append(discovery.conditions, condition(&fwpmConditionIPRemotePort, fwpUint16, 0))
|
||||
dhcpv6 := []fwpmFilterCondition0{
|
||||
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
||||
condition(&fwpmConditionIPLocalPort, fwpUint16, 546),
|
||||
condition(&fwpmConditionIPRemotePort, fwpUint16, 547),
|
||||
}
|
||||
// The DHCPv6 client sends to the servers' multicast address, and they
|
||||
// answer from their own.
|
||||
dhcpv6Out := permit{"DHCPv6", slices.Concat(dhcpv6, []fwpmFilterCondition0{condition(&fwpmConditionIPRemoteAddress, fwpByteArray16Type, uintptr(unsafe.Pointer(&ipv6AllDHCPv6Servers)))})}
|
||||
dhcpv6In := permit{"DHCPv6", dhcpv6}
|
||||
|
||||
for _, layer := range []struct {
|
||||
key *windows.GUID
|
||||
selfFlags uint32
|
||||
via *windows.GUID // the interface that has to be the TUN as well
|
||||
version ipVersions
|
||||
permits []permit
|
||||
}{
|
||||
{&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface},
|
||||
{&fwpmLayerALEAuthRecvAcceptV4, 0, &fwpmConditionIPArrivalInterface},
|
||||
{&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface},
|
||||
{&fwpmLayerALEAuthRecvAcceptV6, 0, &fwpmConditionIPArrivalInterface},
|
||||
{&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface, ipv4Version, []permit{loopback, dhcp}},
|
||||
{&fwpmLayerALEAuthRecvAcceptV4, 0, &fwpmConditionIPArrivalInterface, ipv4Version, []permit{loopback, dhcp}},
|
||||
{&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface, ipv6Version, []permit{loopback, discovery, dhcpv6Out}},
|
||||
{&fwpmLayerALEAuthRecvAcceptV6, 0, &fwpmConditionIPArrivalInterface, ipv6Version, []permit{loopback, discovery, dhcpv6In}},
|
||||
} {
|
||||
ipv6 := layer.version == ipv6Version
|
||||
throughTUN := []fwpmFilterCondition0{onTUN(&fwpmConditionIPLocalInterface), onTUN(layer.via)}
|
||||
if err := add(layer.key, "permit Xray", layer.selfFlags, fwpActionPermit, 4, self); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, port := range block.ports {
|
||||
protocol := uintptr(windows.IPPROTO_UDP)
|
||||
if port.tcp {
|
||||
protocol = windows.IPPROTO_TCP
|
||||
}
|
||||
toPort := []fwpmFilterCondition0{
|
||||
condition(&fwpmConditionIPProtocol, fwpUint8, protocol),
|
||||
condition(&fwpmConditionIPRemotePort, fwpUint16, uintptr(port.port)),
|
||||
}
|
||||
throughTUN := slices.Concat(toPort, []fwpmFilterCondition0{onTUN(&fwpmConditionIPLocalInterface), onTUN(layer.via)})
|
||||
if err := add(layer.key, "permit "+port.String()+" through the TUN", 0, fwpActionPermit, 3, throughTUN...); err != nil {
|
||||
for _, p := range layer.permits {
|
||||
if err := add(layer.key, "permit "+p.name, 0, fwpActionPermit, 3, p.conditions...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(layer.key, "block "+port.String(), 0, fwpActionBlock, 2, toPort...); err != nil {
|
||||
}
|
||||
for _, traffic := range block.traffic {
|
||||
if traffic.versions&layer.version == 0 {
|
||||
continue // for the other IP version only
|
||||
}
|
||||
protocol := uintptr(windows.IPPROTO_UDP)
|
||||
switch {
|
||||
case traffic.name == "tcp":
|
||||
protocol = windows.IPPROTO_TCP
|
||||
case traffic.name == "icmp" && ipv6:
|
||||
protocol = windows.IPPROTO_ICMPV6
|
||||
case traffic.name == "icmp":
|
||||
protocol = windows.IPPROTO_ICMP
|
||||
}
|
||||
matches := slices.Concat([]fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, protocol)}, toPorts(traffic))
|
||||
if err := add(layer.key, "permit "+traffic.String()+" through the TUN", 0, fwpActionPermit, 3, slices.Concat(matches, throughTUN)...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(layer.key, "block "+traffic.String(), 0, fwpActionBlock, 2, matches...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if family := "IPv4"; (block.all|block.unrouted)&layer.version != 0 {
|
||||
if ipv6 {
|
||||
family = "IPv6"
|
||||
}
|
||||
if err := add(layer.key, "permit "+family+" through the TUN", 0, fwpActionPermit, 1, throughTUN...); err != nil {
|
||||
return err
|
||||
}
|
||||
// Below the other values, which so still apply on the local
|
||||
// network, DNS with "allexceptlan" among them.
|
||||
if block.lan&layer.version != 0 {
|
||||
if err := add(layer.key, "permit "+family+" LAN", 0, fwpActionPermit, 1, toLAN(ipv6)...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := add(layer.key, "block "+family, 0, fwpActionBlock, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -390,11 +569,12 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
// 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). Like Windows
|
||||
// an IP version only while it is not blocked altogether, or with
|
||||
// "allexceptlan" to the local network's addresses). Like Windows
|
||||
// Firewall's rules for it, the filters 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 block.dnsClient {
|
||||
if block.dnsClient != 0 {
|
||||
sd, err := windows.SecurityDescriptorFromString("O:SYG:SYD:(A;;CCRC;;;" + dnsClientSID + ")")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -405,17 +585,25 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
// 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
|
||||
key *windows.GUID
|
||||
version ipVersions
|
||||
}{
|
||||
{&fwpmLayerALEAuthConnectV4, !block.ipv4},
|
||||
{&fwpmLayerALEAuthConnectV6, !block.ipv6},
|
||||
{&fwpmLayerALEAuthConnectV4, ipv4Version},
|
||||
{&fwpmLayerALEAuthConnectV6, ipv6Version},
|
||||
} {
|
||||
if block.dnsClient&layer.version == 0 {
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
lan := block.lan&layer.version != 0
|
||||
if (block.all|block.unrouted)&layer.version == 0 || lan {
|
||||
localLink := mdnsLLMNR
|
||||
if lan {
|
||||
localLink = slices.Concat(mdnsLLMNR, toLAN(layer.version == ipv6Version))
|
||||
}
|
||||
if err := add(layer.key, "permit the DNS Client service's mDNS and LLMNR", 0, fwpActionPermit, 3, localLink...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -424,70 +612,6 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 block.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 block.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
|
||||
}
|
||||
|
||||
@@ -510,13 +634,15 @@ func addFilter(engine windows.Handle, sublayer, layer *windows.GUID, name string
|
||||
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.
|
||||
// dnsOutsideTUN returns the servers whose queries the filters block: those
|
||||
// outside all of prefixes, the TUN's own subnets and routes, as queries to
|
||||
// them cannot go through the TUN. Loopback ones, local resolvers, are not
|
||||
// among them, as loopback stays allowed.
|
||||
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) }) {
|
||||
if !server.IsLoopback() && !slices.ContainsFunc(prefixes, func(p netip.Prefix) bool { return p.Contains(server) }) {
|
||||
outside = append(outside, server)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,21 @@ func TestWFPStructLayout(t *testing.T) {
|
||||
[]uintptr{unsafe.Sizeof(value), unsafe.Offsetof(value.value)},
|
||||
[]uintptr{16, 8}, []uintptr{8, 4})
|
||||
|
||||
var ports fwpRange0
|
||||
check("FWP_RANGE0",
|
||||
[]uintptr{unsafe.Sizeof(ports), unsafe.Offsetof(ports.high)},
|
||||
[]uintptr{32, 16}, []uintptr{16, 8})
|
||||
|
||||
var v4 fwpV4AddrAndMask
|
||||
check("FWP_V4_ADDR_AND_MASK",
|
||||
[]uintptr{unsafe.Sizeof(v4), unsafe.Offsetof(v4.mask)},
|
||||
[]uintptr{8, 4}, []uintptr{8, 4})
|
||||
|
||||
var v6 fwpV6AddrAndMask
|
||||
check("FWP_V6_ADDR_AND_MASK",
|
||||
[]uintptr{unsafe.Sizeof(v6), unsafe.Offsetof(v6.prefixLength)},
|
||||
[]uintptr{17, 16}, []uintptr{17, 16})
|
||||
|
||||
var display fwpmDisplayData0
|
||||
check("FWPM_DISPLAY_DATA0",
|
||||
[]uintptr{unsafe.Sizeof(display), unsafe.Offsetof(display.description)},
|
||||
@@ -114,35 +129,73 @@ func TestLeakFiltersAccepted(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun", "tcp/443"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block := newLeakFilters(leaks, false, false)
|
||||
block.dnsClient = true // whatever the Windows version
|
||||
if err := addLeakFilters(engine, loopback, block); err != nil {
|
||||
skipUnlessElevated(err)
|
||||
// Each with a sublayer of its own.
|
||||
for _, c := range []struct {
|
||||
values []string
|
||||
route4, route6 bool
|
||||
}{
|
||||
{[]string{"dns", "misconfigtun", "all", "tcp/443", "udp/1000-2000", "tcp", "icmp", "ipv4:tcp/80", "ipv6:udp/5000-6000", "ipv6:icmp"}, false, false},
|
||||
// The local network's addresses.
|
||||
{[]string{"allexceptlan", "udp/1000-2000", "icmp"}, true, false},
|
||||
{[]string{"ipv4:allexceptlan", "ipv6:all", "ipv6:windnsclient"}, true, true},
|
||||
{[]string{"ipv6:allexceptlan", "ipv4:misconfigtun"}, false, true},
|
||||
} {
|
||||
leaks, err := ExpandWfpBlockLeak(c.values)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := addLeakFilters(engine, loopback, newLeakFilters(leaks, c.route4, c.route6)); err != nil {
|
||||
skipUnlessElevated(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLeakFilters(t *testing.T) {
|
||||
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
for _, c := range []struct {
|
||||
values []string
|
||||
route4, route6 bool
|
||||
blocked string
|
||||
systemDNS bool
|
||||
}{
|
||||
{[]string{"dns", "misconfigtun"}, true, false, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv6", true},
|
||||
{[]string{"tcp/853", "udp/853", "misconfigtun"}, true, true, "tcp/853, udp/853", false},
|
||||
{[]string{"all"}, true, true, "IPv4, IPv6", true},
|
||||
{[]string{"all", "misconfigtun"}, true, true, "IPv4, IPv6", true},
|
||||
{[]string{"misconfigtun"}, true, false, "IPv6", false},
|
||||
{[]string{"tcp/10-20", "udp", "icmp"}, true, true, "tcp/10-20, udp, icmp", true},
|
||||
{[]string{"tcp/54-60", "icmp"}, true, true, "tcp/54-60, icmp", false},
|
||||
{[]string{"allexceptlan"}, true, true, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv4 except LAN, IPv6 except LAN", true},
|
||||
{[]string{"allexceptlan", "all"}, true, true, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv4, IPv6", true},
|
||||
{[]string{"misconfigtun", "allexceptlan"}, true, false, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv4 except LAN, IPv6 except LAN", true},
|
||||
// For one IP version only.
|
||||
{[]string{"IPv4:TCP/10-20", "ipv6:udp", "ipv4:icmp"}, true, true, "ipv4:tcp/10-20, ipv6:udp, ipv4:icmp", true},
|
||||
{[]string{"ipv6:tcp/54-60", "ipv6:icmp"}, true, true, "ipv6:tcp/54-60, ipv6:icmp", false},
|
||||
{[]string{"ipv4:windnsclient"}, true, true, "ipv4:windnsclient", true},
|
||||
{[]string{"ipv6:all"}, true, true, "IPv6", true},
|
||||
{[]string{"ipv6:all"}, true, false, "IPv6", true},
|
||||
{[]string{"ipv4:allexceptlan", "ipv6:all"}, true, true, "ipv4:udp/53, ipv4:tcp/53, ipv4:tcp/853, ipv4:udp/853, ipv4:windnsclient, IPv4 except LAN, IPv6", true},
|
||||
{[]string{"allexceptlan", "ipv6:all"}, true, true, "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv4 except LAN, IPv6", true},
|
||||
{[]string{"ipv4:misconfigtun"}, true, false, "", false},
|
||||
{[]string{"ipv6:misconfigtun"}, true, false, "IPv6", false},
|
||||
{[]string{"ipv4:misconfigtun", "ipv6:allexceptlan"}, false, true, "ipv6:udp/53, ipv6:tcp/53, ipv6:tcp/853, ipv6:udp/853, ipv6:windnsclient, IPv4, IPv6 except LAN", true},
|
||||
{[]string{"ipv6:windnsclient", "ipv4:windnsclient"}, true, true, "windnsclient", true},
|
||||
} {
|
||||
leaks, err := ExpandWfpBlockLeak(c.values)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block := newLeakFilters(leaks, c.route4, c.route6)
|
||||
if got := block.String(); got != c.blocked {
|
||||
t.Errorf("%v: String() = %q, want %q", c.values, got, c.blocked)
|
||||
}
|
||||
if got := block.systemDNS(); got != c.systemDNS {
|
||||
t.Errorf("%v: systemDNS() = %v", c.values, got)
|
||||
}
|
||||
}
|
||||
block := newLeakFilters(leaks, true, false)
|
||||
want := "udp/53, tcp/53, tcp/853, udp/853, IPv6"
|
||||
if dnsClientUsesAnyPort() {
|
||||
want = "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv6"
|
||||
}
|
||||
if got := block.String(); got != want {
|
||||
t.Errorf("String() = %q, want %q", got, want)
|
||||
}
|
||||
if !block.systemDNS() {
|
||||
t.Error("systemDNS() = false with dns")
|
||||
}
|
||||
if block := newLeakFilters([]string{"tcp/853", "udp/853", "misconfigtun"}, true, true); block.String() != "tcp/853, udp/853" || block.systemDNS() {
|
||||
t.Errorf("tcp/853, udp/853: %q, systemDNS() = %v", block.String(), block.systemDNS())
|
||||
// Without the port 53 values "allexceptlan" brings along, a resolver on
|
||||
// the local network, like the router's, stays reachable.
|
||||
if (leakFilters{all: bothIPVersions, lan: bothIPVersions}).systemDNS() {
|
||||
t.Error("systemDNS() = true with the local network open")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +220,10 @@ func TestDNSOutsideTUN(t *testing.T) {
|
||||
netip.MustParseAddr("::ffff:203.0.113.54"),
|
||||
netip.MustParseAddr("8.8.8.8"),
|
||||
netip.MustParseAddr("2001:db8::53"),
|
||||
// Local resolvers, reachable over loopback whatever is blocked.
|
||||
netip.MustParseAddr("127.0.0.1"),
|
||||
netip.MustParseAddr("::ffff:127.0.0.53"),
|
||||
netip.MustParseAddr("::1"),
|
||||
}
|
||||
want := []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("2001:db8::53")}
|
||||
if got := dnsOutsideTUN(servers, prefixes); !slices.Equal(got, want) {
|
||||
|
||||
+117
-41
@@ -13,67 +13,143 @@ import (
|
||||
// ports, DNS over TLS, and DNS over QUIC.
|
||||
var dnsLeaks = []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}
|
||||
|
||||
// leakPort is a "tcp/<port>" or "udp/<port>" value of autoSystemWfpBlockLeak:
|
||||
// connections to that port may only go through the TUN.
|
||||
type leakPort struct {
|
||||
tcp bool
|
||||
port uint16
|
||||
// ipVersions is a set of IP versions.
|
||||
type ipVersions uint8
|
||||
|
||||
const (
|
||||
ipv4Version ipVersions = 1 << iota
|
||||
ipv6Version
|
||||
bothIPVersions = ipv4Version | ipv6Version
|
||||
)
|
||||
|
||||
// leak is a value of autoSystemWfpBlockLeak: "dns", "windnsclient",
|
||||
// "misconfigtun", "all", "allexceptlan", "icmp", or "tcp" or "udp", the latter
|
||||
// two with "/<port>" or "/<low>-<high>" for some ports only, such as "tcp/853"
|
||||
// or "udp/10-20", and any of them after "ipv4:" or "ipv6:" for one IP version
|
||||
// only, such as "ipv4:udp/53" or "ipv6:all".
|
||||
type leak struct {
|
||||
versions ipVersions
|
||||
name string
|
||||
low, high uint16 // the ports of "tcp" and "udp", both 0 for all
|
||||
}
|
||||
|
||||
func (p leakPort) String() string {
|
||||
if p.tcp {
|
||||
return "tcp/" + strconv.Itoa(int(p.port))
|
||||
func (l leak) String() string {
|
||||
s := l.name
|
||||
switch l.versions {
|
||||
case ipv4Version:
|
||||
s = "ipv4:" + s
|
||||
case ipv6Version:
|
||||
s = "ipv6:" + s
|
||||
}
|
||||
switch {
|
||||
case l.low == 0:
|
||||
return s
|
||||
case l.low == l.high:
|
||||
return s + "/" + strconv.Itoa(int(l.low))
|
||||
default:
|
||||
return s + "/" + strconv.Itoa(int(l.low)) + "-" + strconv.Itoa(int(l.high))
|
||||
}
|
||||
return "udp/" + strconv.Itoa(int(p.port))
|
||||
}
|
||||
|
||||
// parseLeakPort parses a lower case "tcp/<port>" or "udp/<port>" value.
|
||||
func parseLeakPort(value string) (leakPort, bool) {
|
||||
protocol, port, ok := strings.Cut(value, "/")
|
||||
if !ok || protocol != "tcp" && protocol != "udp" {
|
||||
return leakPort{}, false
|
||||
// covers tells whether l includes connections of protocol to port, over at
|
||||
// least one IP version.
|
||||
func (l leak) covers(protocol string, port uint16) bool {
|
||||
return l.name == protocol && (l.low == 0 || l.low <= port && port <= l.high)
|
||||
}
|
||||
|
||||
// parseLeak parses a lower case value of autoSystemWfpBlockLeak. Ports go from
|
||||
// 1 to 65535, low not above high, and "/1-65535" stands for all of them, as
|
||||
// does no port at all.
|
||||
func parseLeak(value string) (leak, bool) {
|
||||
l := leak{versions: bothIPVersions}
|
||||
if version, rest, ok := strings.Cut(value, ":"); ok {
|
||||
switch version {
|
||||
case "ipv4":
|
||||
l.versions = ipv4Version
|
||||
case "ipv6":
|
||||
l.versions = ipv6Version
|
||||
default:
|
||||
return leak{}, false
|
||||
}
|
||||
value = rest
|
||||
}
|
||||
n, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil || n == 0 {
|
||||
return leakPort{}, false
|
||||
name, ports, hasPorts := strings.Cut(value, "/")
|
||||
l.name = name
|
||||
switch name {
|
||||
case "tcp", "udp":
|
||||
case "dns", "windnsclient", "misconfigtun", "all", "allexceptlan", "icmp":
|
||||
return l, !hasPorts
|
||||
default:
|
||||
return leak{}, false
|
||||
}
|
||||
return leakPort{tcp: protocol == "tcp", port: uint16(n)}, true
|
||||
if !hasPorts {
|
||||
return l, true
|
||||
}
|
||||
lowText, highText, isRange := strings.Cut(ports, "-")
|
||||
low, err := strconv.ParseUint(lowText, 10, 16)
|
||||
if err != nil || low == 0 {
|
||||
return leak{}, false
|
||||
}
|
||||
high := low
|
||||
if isRange {
|
||||
if high, err = strconv.ParseUint(highText, 10, 16); err != nil || high < low {
|
||||
return leak{}, false
|
||||
}
|
||||
}
|
||||
if low != 1 || high != 65535 {
|
||||
l.low, l.high = uint16(low), uint16(high)
|
||||
}
|
||||
return l, true
|
||||
}
|
||||
|
||||
// ExpandWfpBlockLeak checks the values of autoSystemWfpBlockLeak and returns
|
||||
// them in lower case, in order and without duplicates, with "dns" replaced by
|
||||
// what it stands for.
|
||||
// them in lower case and in order, with "dns" replaced by what it stands for,
|
||||
// "allexceptlan" followed by that, and ports written the same way, "tcp/1-65535"
|
||||
// as "tcp" say. Values that only differ in the IP versions become one for
|
||||
// both: "ipv4:udp/53" and "ipv6:udp/53" turn into "udp/53", as does "udp/53"
|
||||
// with "ipv4:udp/53".
|
||||
func ExpandWfpBlockLeak(values []string) ([]string, error) {
|
||||
var leaks []string
|
||||
add := func(leak string) {
|
||||
if !slices.Contains(leaks, leak) {
|
||||
leaks = append(leaks, leak)
|
||||
var leaks []leak
|
||||
add := func(l leak) {
|
||||
for i, added := range leaks {
|
||||
if added.versions = l.versions; added == l {
|
||||
leaks[i].versions |= l.versions
|
||||
return
|
||||
}
|
||||
}
|
||||
leaks = append(leaks, l)
|
||||
}
|
||||
for _, value := range values {
|
||||
switch value = strings.ToLower(value); value {
|
||||
case "dns":
|
||||
for _, leak := range dnsLeaks {
|
||||
add(leak)
|
||||
l, ok := parseLeak(strings.ToLower(value))
|
||||
if !ok {
|
||||
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", value)
|
||||
}
|
||||
if l.name != "dns" {
|
||||
add(l)
|
||||
}
|
||||
// The local network is no way out for DNS with "allexceptlan", as a
|
||||
// resolver there, like the router, would forward outside the TUN.
|
||||
if l.name == "dns" || l.name == "allexceptlan" {
|
||||
for _, dnsLeak := range dnsLeaks {
|
||||
dns, _ := parseLeak(dnsLeak)
|
||||
dns.versions = l.versions
|
||||
add(dns)
|
||||
}
|
||||
case "windnsclient", "misconfigtun":
|
||||
add(value)
|
||||
default:
|
||||
port, ok := parseLeakPort(value)
|
||||
if !ok {
|
||||
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", value)
|
||||
}
|
||||
add(port.String())
|
||||
}
|
||||
}
|
||||
return leaks, nil
|
||||
var expanded []string
|
||||
for _, l := range leaks {
|
||||
expanded = append(expanded, l.String())
|
||||
}
|
||||
return expanded, nil
|
||||
}
|
||||
|
||||
// BlocksSystemDNS tells whether expanded autoSystemWfpBlockLeak values keep the
|
||||
// system's DNS (port 53, or Windows' DNS Client service) inside the TUN, which
|
||||
// then needs DNS servers of its own.
|
||||
// system's DNS (TCP or UDP port 53, or Windows' DNS Client service) inside the
|
||||
// TUN, over at least one IP version, which then needs DNS servers of its own.
|
||||
func BlocksSystemDNS(leaks []string) bool {
|
||||
return slices.ContainsFunc(leaks, func(leak string) bool {
|
||||
return leak == "udp/53" || leak == "tcp/53" || leak == "windnsclient"
|
||||
return slices.ContainsFunc(leaks, func(value string) bool {
|
||||
l, _ := parseLeak(value)
|
||||
return l.name == "windnsclient" || l.name == "all" || l.covers("udp", 53) || l.covers("tcp", 53)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,31 @@ func TestExpandWfpBlockLeak(t *testing.T) {
|
||||
{[]string{"udp/65535", "tcp/1"}, []string{"udp/65535", "tcp/1"}, false},
|
||||
{[]string{"tcp/53"}, []string{"tcp/53"}, true},
|
||||
{[]string{"windnsclient"}, []string{"windnsclient"}, true},
|
||||
{[]string{"ALL"}, []string{"all"}, true},
|
||||
{[]string{"tcp", "TCP", "UDP"}, []string{"tcp", "udp"}, true},
|
||||
{[]string{"tcp/1-65535", "udp/1-65535"}, []string{"tcp", "udp"}, true},
|
||||
{[]string{"udp/10-20", "udp/53-53", "tcp/50-60"}, []string{"udp/10-20", "udp/53", "tcp/50-60"}, true},
|
||||
{[]string{"tcp/54-60", "udp/1-52"}, []string{"tcp/54-60", "udp/1-52"}, false},
|
||||
{[]string{"ICMP", "icmp"}, []string{"icmp"}, false},
|
||||
{[]string{"AllExceptLAN"}, []string{"allexceptlan", "udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}, true},
|
||||
{[]string{"dns", "allexceptlan", "all"}, []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853", "allexceptlan", "all"}, true},
|
||||
// For one IP version only.
|
||||
{[]string{"IPv4:TCP/853", "ipv6:udp/10-20", "IPV4:icmp"}, []string{"ipv4:tcp/853", "ipv6:udp/10-20", "ipv4:icmp"}, false},
|
||||
{[]string{"ipv4:udp/53"}, []string{"ipv4:udp/53"}, true},
|
||||
{[]string{"ipv6:tcp/53-53"}, []string{"ipv6:tcp/53"}, true},
|
||||
{[]string{"ipv4:dns"}, []string{"ipv4:udp/53", "ipv4:tcp/53", "ipv4:windnsclient", "ipv4:tcp/853", "ipv4:udp/853"}, true},
|
||||
{[]string{"ipv6:all"}, []string{"ipv6:all"}, true},
|
||||
{[]string{"ipv4:windnsclient"}, []string{"ipv4:windnsclient"}, true},
|
||||
{[]string{"ipv6:misconfigtun"}, []string{"ipv6:misconfigtun"}, false},
|
||||
{[]string{"ipv4:allexceptlan", "ipv6:all"}, []string{"ipv4:allexceptlan", "ipv4:udp/53", "ipv4:tcp/53", "ipv4:windnsclient", "ipv4:tcp/853", "ipv4:udp/853", "ipv6:all"}, true},
|
||||
// Values that only differ in the IP versions become one.
|
||||
{[]string{"ipv4:udp/53", "tcp", "ipv6:udp/53"}, []string{"udp/53", "tcp"}, true},
|
||||
{[]string{"ipv6:tcp/1-65535", "ipv4:tcp"}, []string{"tcp"}, true},
|
||||
{[]string{"ipv4:icmp", "icmp"}, []string{"icmp"}, false},
|
||||
{[]string{"ipv4:tcp/853", "ipv6:tcp/853-853", "ipv6:tcp/854"}, []string{"tcp/853", "ipv6:tcp/854"}, false},
|
||||
{[]string{"dns", "ipv4:dns"}, []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}, true},
|
||||
{[]string{"ipv4:dns", "ipv6:udp/53"}, []string{"udp/53", "ipv4:tcp/53", "ipv4:windnsclient", "ipv4:tcp/853", "ipv4:udp/853"}, true},
|
||||
{[]string{"ipv4:misconfigtun", "ipv6:misconfigtun", "ipv6:all", "ipv4:all"}, []string{"misconfigtun", "all"}, true},
|
||||
} {
|
||||
leaks, err := ExpandWfpBlockLeak(c.values)
|
||||
if err != nil {
|
||||
@@ -32,9 +57,47 @@ func TestExpandWfpBlockLeak(t *testing.T) {
|
||||
t.Errorf("%v: BlocksSystemDNS() = %v", c.values, got)
|
||||
}
|
||||
}
|
||||
for _, value := range []string{"", "ip", "tcp", "tcp/", "tcp/0", "udp/65536", "tcp/-1", "tcp/+53", "tcp/53/1", "udp/abc", "icmp/1", "tcp:53", " dns"} {
|
||||
for _, value := range []string{
|
||||
"", "ip", "tcp/", "udp/65536", "tcp/-1", "tcp/+53", "tcp/53/1", "udp/abc", "icmp/0", "icmp/8", "tcp:53", " dns",
|
||||
"tcp/0-10", "tcp/20-10", "tcp/5-", "tcp/-5", "tcp/1-2-3", "tcp/1-65536", "udp/ 53", "all/0", "dns/53", "windnsclient/53",
|
||||
"tcp/0", "udp/0", "tcp/00", "udp/0-0", "udp/65535-1",
|
||||
"ipv4", "ipv6", "ipv4:", ":udp", "ipv5:udp", "ip:all", "v4:udp", "ipv4/udp", "ipv4 :udp", "ipv4: udp", "ipv4:ipv6:udp",
|
||||
"ipv4:ipv4", "ipv4:tcp/0", "ipv6:icmp/8", "ipv4:dns/53", "ipv6:udp/20-10", "ipv4+ipv6:udp",
|
||||
"tcpv4", "udpv6", "icmpv4", "tcpv4/853", "tcp4", "udp6", "dnsv4", "allv6",
|
||||
} {
|
||||
if _, err := ExpandWfpBlockLeak([]string{value}); err == nil {
|
||||
t.Errorf("%q was accepted", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeakCovers(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
value string
|
||||
protocol string
|
||||
port uint16
|
||||
covers bool
|
||||
}{
|
||||
{"tcp", "tcp", 1, true},
|
||||
{"tcp", "udp", 53, false},
|
||||
{"udp/53", "udp", 53, true},
|
||||
{"udp/53", "udp", 54, false},
|
||||
{"tcp/10-20", "tcp", 10, true},
|
||||
{"tcp/10-20", "tcp", 20, true},
|
||||
{"tcp/10-20", "tcp", 21, false},
|
||||
{"tcp/10-20", "tcp", 9, false},
|
||||
{"ipv4:udp/53", "udp", 53, true},
|
||||
{"ipv6:tcp", "tcp", 443, true},
|
||||
{"ipv4:icmp", "udp", 53, false},
|
||||
{"all", "udp", 53, false},
|
||||
} {
|
||||
l, ok := parseLeak(c.value)
|
||||
if !ok {
|
||||
t.Errorf("%q was not parsed", c.value)
|
||||
continue
|
||||
}
|
||||
if got := l.covers(c.protocol, c.port); got != c.covers {
|
||||
t.Errorf("%q covers %s/%d = %v", c.value, c.protocol, c.port, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user