Compare commits

...
Author SHA1 Message Date
patternihaandClaude Opus 5.5 468f745fc2 TUN inbound's autoSystemWfpBlockLeak: Add "all", "tcp", "udp", "icmp" and port ranges; Always allow loopback, DHCP and IPv6 neighbor discovery
autoSystemWfpBlockLeak now also takes:

- "all": everything outside the TUN, IPv4 and IPv6, in both directions, so
  nothing but Xray leaves except through the TUN.
- "tcp", "udp": all ports of that protocol, also written "tcp/0", "udp/0".
- "tcp/<low>-<high>", "udp/<low>-<high>": a range of ports, matched with
  FWP_MATCH_RANGE, as two conditions on the same field would match when
  either does.
- "icmp": ICMP and ICMPv6, e.g. ping. It takes no port: in ICMP filters the
  local port holds the type, and "icmp/0" would read as the echo reply.

Values are normalized: "tcp/0" and "tcp/1-65535" become "tcp", and
"tcp/53-53" becomes "tcp/53".

Whatever is blocked, loopback, which cannot leave the system, and what
Windows itself needs on the local link now always stay allowed: DHCP, DHCPv6,
and IPv6 neighbor and multicast listener discovery. These used to be allowed
only along with "misconfigtun", so "udp" or "icmp" would otherwise have cut
DHCP or IPv6 neighbor discovery, and "tcp" any local proxy, Xray's own
inbounds included. Loopback being allowed means that with "dns", a local
resolver on 127.0.0.1:53 now works for other programs (its own queries to
port 53 or 853 outside the TUN stay blocked), and the DNS Client service
may use it.

"all" reuses the filters of "misconfigtun", which now also let everything
through the TUN, for both IP versions whatever the routes.

On other TUN options: keeping the system's DNS inside the TUN, with "all",
"dns", "windnsclient", or anything covering TCP or UDP port 53 ("udp",
"tcp/50-60", ...), needs dns, as Windows can no longer reach the DNS
servers of the other interfaces; the config is rejected otherwise. Like the
other values, the new ones need autoSystemRoutingTable. The filters do not
need autoOutboundsInterface, as Xray is allowed by its app ID; it still
keeps Xray's own connections out of the TUN's routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:13:10 +03:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7c8dbcb4bb Bump google.golang.org/grpc from 1.84.0 to 1.85.0 (#7311)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.84.0 to 1.85.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.84.0...v1.85.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.85.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 02:03:49 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8ab161a2b0 Bump golang.org/x/net from 0.61.0 to 0.62.0 (#7312)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.61.0 to 0.62.0.
- [Commits](https://github.com/golang/net/compare/v0.61.0...v0.62.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.62.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 02:03:35 +00:00
10 changed files with 306 additions and 176 deletions
+3 -3
View File
@@ -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
+6 -6
View File
@@ -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
View File
@@ -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", "dns", "windnsclient", or TCP or UDP port 53, needs dns to be set`)
}
case "linux", "freebsd":
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
+11 -1
View File
@@ -41,6 +41,11 @@ 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/0", "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"}},
},
})
}
@@ -57,6 +62,11 @@ 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", "autoSystemWfpBlockLeak": ["all"]}`, []string{"windows"}},
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, []string{"linux", "freebsd"}},
} {
config := new(TunConfig)
@@ -70,7 +80,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-10", "icmp/1", "udp/20-10"} {
config := new(TunConfig)
if err := json.Unmarshal([]byte(`{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "`+value+`"]}`), config); err != nil {
t.Fatal(err)
+10 -4
View File
@@ -226,10 +226,16 @@ With IPv6 disabled system-wide (the `DisabledComponents` registry value), the ad
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 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"`.
- `"tcp"`, `"udp"`: connections of that protocol only go through the TUN, to all ports, or with `/<port>` or `/<low>-<high>` to some, e.g. `"tcp/853"` or `"udp/10-20"`. `"tcp/0"` stands for all ports, like `"tcp"`.
- `"icmp"`: ICMP and ICMPv6, e.g. `ping`, only go through the TUN.
- `"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.
- `"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.
- `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, IPv4 or IPv6, is blocked entirely outside the TUN, in both directions, as it would bypass the TUN. 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.
- `"all"`: everything outside the TUN, IPv4 and IPv6, in both directions, so nothing but Xray leaves except through the TUN. Windows' own route to the local network is more specific than the TUN's, so access to the local network breaks unless it is routed into the TUN as well, and so do incoming connections to other programs, name resolution on the local network, other VPNs, and virtual machines whose NAT runs as a program on the host.
Whatever is blocked, loopback, which cannot leave the system, and what Windows itself needs on the local link stay allowed: DHCP, DHCPv6, and IPv6 neighbor and multicast listener discovery.
Keeping the system's DNS inside the TUN, with `"all"`, `"dns"`, `"windnsclient"`, or anything covering TCP or UDP port 53, 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.
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.
@@ -237,7 +243,7 @@ Names that Xray resolves through the system resolver, such as an outbound's serv
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"]`.
`autoSystemWfpBlockLeak` (Windows only) is empty by default, as the filters break some setups: with `"dns"`, a local DNS resolver that sends its own queries to port 53 or 853 through another interface, 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; with `"all"`, what is listed above. 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"]`.
`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.
+5 -5
View File
@@ -247,11 +247,11 @@ 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"), 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
+141 -114
View File
@@ -37,15 +37,17 @@ 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
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 +96,11 @@ type fwpValue0 struct {
value uintptr
}
// fwpRange0 is FWP_RANGE0, which an FWP_RANGE_TYPE condition points to.
type fwpRange0 struct {
low, high fwpValue0
}
type fwpmDisplayData0 struct {
name *uint16
description *uint16
@@ -173,9 +180,9 @@ func condition(field *windows.GUID, typ uint32, value uintptr) fwpmFilterConditi
// leakFilters is what blockLeaks keeps from leaving outside the TUN.
type leakFilters struct {
ports []leakPort // connections to these ports
dnsClient bool // Windows' DNS Client service
ipv4, ipv6 bool // an IP version altogether
traffic []leakTraffic // connections of these protocols and ports
dnsClient bool // Windows' DNS Client service
ipv4, ipv6 bool // an IP version altogether
}
// newLeakFilters turns expanded autoSystemWfpBlockLeak values into the filters
@@ -187,10 +194,12 @@ func newLeakFilters(leaks []string, route4, route6 bool) leakFilters {
case "windnsclient":
block.dnsClient = dnsClientUsesAnyPort()
case "misconfigtun":
block.ipv4, block.ipv6 = !route4, !route6
block.ipv4, block.ipv6 = block.ipv4 || !route4, block.ipv6 || !route6
case "all":
block.ipv4, block.ipv6 = true, true
default:
if port, ok := parseLeakPort(leak); ok {
block.ports = append(block.ports, port)
if traffic, ok := parseLeakTraffic(leak); ok {
block.traffic = append(block.traffic, traffic)
}
}
}
@@ -200,8 +209,8 @@ func newLeakFilters(leaks []string, route4, route6 bool) leakFilters {
// 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
@@ -214,9 +223,12 @@ func (block leakFilters) String() string {
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: port
// 53, the DNS Client service, or both IP versions altogether.
func (block leakFilters) systemDNS() bool {
return block.dnsClient || slices.ContainsFunc(block.ports, func(port leakPort) bool { return port.port == 53 })
return block.dnsClient || block.ipv4 && block.ipv6 || slices.ContainsFunc(block.traffic, func(traffic leakTraffic) bool {
return traffic.covers("udp", 53) || traffic.covers("tcp", 53)
})
}
// dnsClientUsesAnyPort tells whether Windows' DNS Client service may send its
@@ -233,27 +245,30 @@ func dnsClientUsesAnyPort() bool {
// for every program but Xray itself, whose outbounds (DNS included) use the
// other interfaces on purpose:
//
// - 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).
// - ipv4, ipv6: no IPv4, or no IPv6, at all outside the TUN, in either
// direction: for a TUN that no route of that version leads to, or for
// everything with "all". 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 +369,109 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
onTUN := func(field *windows.GUID) fwpmFilterCondition0 {
return condition(field, fwpUint64, uintptr(unsafe.Pointer(tunLUID)))
}
// toPorts matches the ports of traffic, if not all of them.
toPorts := func(traffic leakTraffic) []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
ipv6 bool
permits []permit
}{
{&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface},
{&fwpmLayerALEAuthRecvAcceptV4, 0, &fwpmConditionIPArrivalInterface},
{&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface},
{&fwpmLayerALEAuthRecvAcceptV6, 0, &fwpmConditionIPArrivalInterface},
{&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface, false, []permit{loopback, dhcp}},
{&fwpmLayerALEAuthRecvAcceptV4, 0, &fwpmConditionIPArrivalInterface, false, []permit{loopback, dhcp}},
{&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface, true, []permit{loopback, discovery, dhcpv6Out}},
{&fwpmLayerALEAuthRecvAcceptV6, 0, &fwpmConditionIPArrivalInterface, true, []permit{loopback, discovery, dhcpv6In}},
} {
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 {
protocol := uintptr(windows.IPPROTO_UDP)
switch {
case traffic.protocol == "tcp":
protocol = windows.IPPROTO_TCP
case traffic.protocol == "icmp" && layer.ipv6:
protocol = windows.IPPROTO_ICMPV6
case traffic.protocol == "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"; layer.ipv6 && block.ipv6 || !layer.ipv6 && block.ipv4 {
if layer.ipv6 {
family = "IPv6"
}
if err := add(layer.key, "permit "+family+" through the TUN", 0, fwpActionPermit, 1, throughTUN...); err != nil {
return err
}
if err := add(layer.key, "block "+family, 0, fwpActionBlock, 0); err != nil {
return err
}
}
@@ -424,70 +515,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
}
+33 -16
View File
@@ -40,6 +40,11 @@ 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 display fwpmDisplayData0
check("FWPM_DISPLAY_DATA0",
[]uintptr{unsafe.Sizeof(display), unsafe.Offsetof(display.description)},
@@ -114,7 +119,7 @@ func TestLeakFiltersAccepted(t *testing.T) {
if err != nil {
t.Fatal(err)
}
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun", "tcp/443"})
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun", "all", "tcp/443", "udp/1000-2000", "tcp", "icmp"})
if err != nil {
t.Fatal(err)
}
@@ -126,23 +131,35 @@ func TestLeakFiltersAccepted(t *testing.T) {
}
func TestNewLeakFilters(t *testing.T) {
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun"})
if err != nil {
t.Fatal(err)
}
block := newLeakFilters(leaks, true, false)
want := "udp/53, tcp/53, tcp/853, udp/853, IPv6"
windnsclient := ""
if dnsClientUsesAnyPort() {
want = "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv6"
windnsclient = ", windnsclient"
}
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())
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},
} {
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)
}
}
}
+59 -25
View File
@@ -13,36 +13,66 @@ 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
// leakTraffic is a "tcp", "udp" or "icmp" value of autoSystemWfpBlockLeak, the
// former two with "/<port>" or "/<low>-<high>" for some ports only, such as
// "tcp/853" or "udp/10-20": connections of that protocol, to those ports, may
// only go through the TUN.
type leakTraffic struct {
protocol string // "tcp", "udp" or "icmp"
low, high uint16 // the ports, both 0 for all
}
func (p leakPort) String() string {
if p.tcp {
return "tcp/" + strconv.Itoa(int(p.port))
func (t leakTraffic) String() string {
switch {
case t.low == 0:
return t.protocol
case t.low == t.high:
return t.protocol + "/" + strconv.Itoa(int(t.low))
default:
return t.protocol + "/" + strconv.Itoa(int(t.low)) + "-" + strconv.Itoa(int(t.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 t includes connections of protocol to port.
func (t leakTraffic) covers(protocol string, port uint16) bool {
return t.protocol == protocol && (t.low == 0 || t.low <= port && port <= t.high)
}
// parseLeakTraffic parses a lower case "tcp", "udp" or "icmp" value. The former
// two may end in "/<port>" or "/<low>-<high>", where "/0" stands for all ports,
// as do "/1-65535" and no port at all; ICMP has no ports.
func parseLeakTraffic(value string) (leakTraffic, bool) {
protocol, ports, hasPorts := strings.Cut(value, "/")
switch protocol {
case "tcp", "udp":
case "icmp":
return leakTraffic{protocol: protocol}, !hasPorts
default:
return leakTraffic{}, false
}
n, err := strconv.ParseUint(port, 10, 16)
if err != nil || n == 0 {
return leakPort{}, false
if !hasPorts {
return leakTraffic{protocol: protocol}, true
}
return leakPort{tcp: protocol == "tcp", port: uint16(n)}, true
lowText, highText, isRange := strings.Cut(ports, "-")
low, err := strconv.ParseUint(lowText, 10, 16)
if err != nil {
return leakTraffic{}, false
}
high := low
if isRange {
if high, err = strconv.ParseUint(highText, 10, 16); err != nil || low == 0 || high < low {
return leakTraffic{}, false
}
}
if low == 0 || low == 1 && high == 65535 {
return leakTraffic{protocol: protocol}, true
}
return leakTraffic{protocol: protocol, low: uint16(low), high: uint16(high)}, 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.
// what it stands for, and ports written the same way, "tcp/0" as "tcp" say.
func ExpandWfpBlockLeak(values []string) ([]string, error) {
var leaks []string
add := func(leak string) {
@@ -56,24 +86,28 @@ func ExpandWfpBlockLeak(values []string) ([]string, error) {
for _, leak := range dnsLeaks {
add(leak)
}
case "windnsclient", "misconfigtun":
case "windnsclient", "misconfigtun", "all":
add(value)
default:
port, ok := parseLeakPort(value)
traffic, ok := parseLeakTraffic(value)
if !ok {
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", value)
}
add(port.String())
add(traffic.String())
}
}
return leaks, 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, 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"
if leak == "windnsclient" || leak == "all" {
return true
}
traffic, ok := parseLeakTraffic(leak)
return ok && (traffic.covers("udp", 53) || traffic.covers("tcp", 53))
})
}
+37 -1
View File
@@ -19,6 +19,12 @@ 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/0", "udp/0", "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},
} {
leaks, err := ExpandWfpBlockLeak(c.values)
if err != nil {
@@ -32,9 +38,39 @@ 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",
} {
if _, err := ExpandWfpBlockLeak([]string{value}); err == nil {
t.Errorf("%q was accepted", value)
}
}
}
func TestLeakTrafficCovers(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},
} {
traffic, ok := parseLeakTraffic(c.value)
if !ok {
t.Errorf("%q was not parsed", c.value)
continue
}
if got := traffic.covers(c.protocol, c.port); got != c.covers {
t.Errorf("%q covers %s/%d = %v", c.value, c.protocol, c.port, got)
}
}
}