mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-11-06 10:12:40 +00:00
TUN inbound's autoSystemWfpBlockLeak: "ipv4:" or "ipv6:" in front of any value instead of "tcpv4" and the like
"ipv4:" or "ipv6:" in front of any value limits it to that IP version, e.g. "ipv4:udp/53", "ipv6:all", "ipv4:dns" or "ipv6:misconfigtun". "tcpv4", "udpv6", "icmpv4" and the like are gone. Values that only differ in the IP version become one: "ipv4:udp/53" with "ipv6:udp/53" is "udp/53". "all" overrides "allexceptlan" per IP version, and "all" over one IP version needs dns, as port 53 over one does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2218fec173
commit
c684201cd8
+3
-2
@@ -228,16 +228,17 @@ When `dns` is set, those servers are applied to the adapter. Windows is kept fro
|
||||
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"`, `"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"`. Ports go from 1 to 65535, and the low end of a range is not above the high end.
|
||||
- `"icmp"`: ICMP and ICMPv6, e.g. `ping`, only go through the TUN.
|
||||
- `"tcpv4"`, `"tcpv6"`, `"udpv4"`, `"udpv6"`, `"icmpv4"`, `"icmpv6"`: the same for IPv4 or IPv6 only, with ports written the same way, e.g. `"tcpv4/853"` or `"udpv6/10-20"`. `"icmpv4"` is ICMP and `"icmpv6"` is ICMPv6.
|
||||
- `"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.
|
||||
- `"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.
|
||||
- `"allexceptlan"`: like `"all"`, but traffic to and from the local network stays allowed, e.g. for printers, the router's settings page, or casting to a TV: private addresses (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fc00::/7`), link-local ones (`169.254.0.0/16`, `fe80::/10`), multicast (`224.0.0.0/4`, `ff00::/8`) and broadcast (`255.255.255.255`). It includes `"dns"`, so name queries to a resolver on the local network, like the router's, stay blocked, and the other values apply there as well, e.g. `"tcp/445"` blocks Windows file sharing with it. Devices on the local network are only reached through such addresses, not through public ones like global IPv6 addresses. `"all"` overrides it.
|
||||
|
||||
Any value can be limited to one IP version with `"ipv4:"` or `"ipv6:"` in front, e.g. `"ipv4:udp/53"` or `"ipv6:all"`.
|
||||
|
||||
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"`, `"allexceptlan"`, `"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`, or be loopback addresses like `127.0.0.1` for a local resolver (a warning is logged otherwise), and DNS servers that should be reached directly belong in Xray's own `dns` settings.
|
||||
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). The `dns` servers then have to lie within `gateway` or `autoSystemRoutingTable`, or be loopback addresses like `127.0.0.1` for a local resolver (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.
|
||||
|
||||
|
||||
@@ -250,16 +250,17 @@ startOver:
|
||||
// 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), 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.
|
||||
// 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 {
|
||||
|
||||
@@ -207,39 +207,44 @@ 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 {
|
||||
traffic []leakTraffic // connections of these protocols and ports
|
||||
dnsClient bool // Windows' DNS Client service
|
||||
ipv4, ipv6 bool // an IP version altogether
|
||||
lan bool // except to and from the local network
|
||||
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
|
||||
var all, allExceptLAN bool
|
||||
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 = block.ipv4 || !route4, block.ipv6 || !route6
|
||||
block.unrouted |= l.versions &^ routed
|
||||
case "all":
|
||||
all = true
|
||||
block.all |= l.versions
|
||||
case "allexceptlan":
|
||||
allExceptLAN = true
|
||||
default:
|
||||
if traffic, ok := parseLeakTraffic(leak); ok {
|
||||
block.traffic = append(block.traffic, traffic)
|
||||
}
|
||||
allExceptLAN |= l.versions
|
||||
case "tcp", "udp", "icmp":
|
||||
block.traffic = append(block.traffic, l)
|
||||
}
|
||||
}
|
||||
if all || allExceptLAN {
|
||||
block.ipv4, block.ipv6 = true, true
|
||||
block.lan = !all
|
||||
}
|
||||
// "all" overrides "allexceptlan".
|
||||
block.lan = allExceptLAN &^ block.all
|
||||
block.all |= allExceptLAN
|
||||
return block
|
||||
}
|
||||
|
||||
@@ -249,26 +254,30 @@ func (block leakFilters) String() string {
|
||||
for _, traffic := range block.traffic {
|
||||
blocked = append(blocked, traffic.String())
|
||||
}
|
||||
exceptLAN := ""
|
||||
if block.lan {
|
||||
exceptLAN = " except LAN"
|
||||
if block.dnsClient != 0 {
|
||||
blocked = append(blocked, leak{versions: block.dnsClient, name: "windnsclient"}.String())
|
||||
}
|
||||
for _, b := range []struct {
|
||||
on bool
|
||||
what string
|
||||
}{{block.dnsClient, "windnsclient"}, {block.ipv4, "IPv4" + exceptLAN}, {block.ipv6, "IPv6" + exceptLAN}} {
|
||||
if b.on {
|
||||
blocked = append(blocked, b.what)
|
||||
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: port
|
||||
// 53, the DNS Client service, or both IP versions altogether, though not on
|
||||
// the local network, where a resolver like the router's may be.
|
||||
// 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 || block.ipv4 && block.ipv6 && !block.lan || slices.ContainsFunc(block.traffic, func(traffic leakTraffic) bool {
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -285,26 +294,26 @@ 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:
|
||||
//
|
||||
// - traffic: connections of these protocols, TCP and UDP to some or all
|
||||
// ports, or ICMP, over one IP version or both, 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.
|
||||
// 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 outside the TUN, in either
|
||||
// direction: for a TUN that no route of that version leads to, or for
|
||||
// everything with "all", or with "allexceptlan" (lan) except to and from
|
||||
// the local network, below the other filters, which so still apply there.
|
||||
// - 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
|
||||
@@ -438,7 +447,7 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
return conditions
|
||||
}
|
||||
// toPorts matches the ports of traffic, if not all of them.
|
||||
toPorts := func(traffic leakTraffic) []fwpmFilterCondition0 {
|
||||
toPorts := func(traffic leak) []fwpmFilterCondition0 {
|
||||
switch {
|
||||
case traffic.low == 0:
|
||||
return nil
|
||||
@@ -497,14 +506,15 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
key *windows.GUID
|
||||
selfFlags uint32
|
||||
via *windows.GUID // the interface that has to be the TUN as well
|
||||
ipv6 bool
|
||||
version ipVersions
|
||||
permits []permit
|
||||
}{
|
||||
{&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}},
|
||||
{&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
|
||||
@@ -515,16 +525,16 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
}
|
||||
}
|
||||
for _, traffic := range block.traffic {
|
||||
if traffic.version == "v4" && layer.ipv6 || traffic.version == "v6" && !layer.ipv6 {
|
||||
if traffic.versions&layer.version == 0 {
|
||||
continue // for the other IP version only
|
||||
}
|
||||
protocol := uintptr(windows.IPPROTO_UDP)
|
||||
switch {
|
||||
case traffic.protocol == "tcp":
|
||||
case traffic.name == "tcp":
|
||||
protocol = windows.IPPROTO_TCP
|
||||
case traffic.protocol == "icmp" && layer.ipv6:
|
||||
case traffic.name == "icmp" && ipv6:
|
||||
protocol = windows.IPPROTO_ICMPV6
|
||||
case traffic.protocol == "icmp":
|
||||
case traffic.name == "icmp":
|
||||
protocol = windows.IPPROTO_ICMP
|
||||
}
|
||||
matches := slices.Concat([]fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, protocol)}, toPorts(traffic))
|
||||
@@ -535,8 +545,8 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if family := "IPv4"; layer.ipv6 && block.ipv6 || !layer.ipv6 && block.ipv4 {
|
||||
if layer.ipv6 {
|
||||
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 {
|
||||
@@ -544,8 +554,8 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
}
|
||||
// Below the other values, which so still apply on the local
|
||||
// network, DNS with "allexceptlan" among them.
|
||||
if block.lan {
|
||||
if err := add(layer.key, "permit "+family+" LAN", 0, fwpActionPermit, 1, toLAN(layer.ipv6)...); err != nil {
|
||||
if block.lan&layer.version != 0 {
|
||||
if err := add(layer.key, "permit "+family+" LAN", 0, fwpActionPermit, 1, toLAN(ipv6)...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -564,7 +574,7 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
||||
// 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
|
||||
@@ -575,20 +585,23 @@ 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
|
||||
ipv6 bool
|
||||
localLink bool
|
||||
key *windows.GUID
|
||||
version ipVersions
|
||||
}{
|
||||
{&fwpmLayerALEAuthConnectV4, false, !block.ipv4 || block.lan},
|
||||
{&fwpmLayerALEAuthConnectV6, true, !block.ipv6 || block.lan},
|
||||
{&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 {
|
||||
lan := block.lan&layer.version != 0
|
||||
if (block.all|block.unrouted)&layer.version == 0 || lan {
|
||||
localLink := mdnsLLMNR
|
||||
if block.lan {
|
||||
localLink = slices.Concat(mdnsLLMNR, toLAN(layer.ipv6))
|
||||
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
|
||||
|
||||
@@ -129,52 +129,56 @@ func TestLeakFiltersAccepted(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun", "all", "tcp/443", "udp/1000-2000", "tcp", "icmp", "tcpv4/80", "udpv6/5000-6000", "icmpv6"})
|
||||
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)
|
||||
}
|
||||
// The local network's addresses, with another sublayer.
|
||||
if leaks, err = ExpandWfpBlockLeak([]string{"allexceptlan", "udp/1000-2000", "icmp"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block = newLeakFilters(leaks, true, false)
|
||||
block.dnsClient = true
|
||||
if !block.lan {
|
||||
t.Fatal("allexceptlan left the local network blocked")
|
||||
}
|
||||
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) {
|
||||
windnsclient := ""
|
||||
if dnsClientUsesAnyPort() {
|
||||
windnsclient = ", windnsclient"
|
||||
}
|
||||
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{"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{"TCPv4/10-20", "udpv6", "icmpv4"}, true, true, "tcpv4/10-20, udpv6, icmpv4", true},
|
||||
{[]string{"tcpv6/54-60", "icmpv6"}, true, true, "tcpv6/54-60, icmpv6", 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},
|
||||
{[]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 {
|
||||
@@ -190,7 +194,7 @@ func TestNewLeakFilters(t *testing.T) {
|
||||
}
|
||||
// Without the port 53 values "allexceptlan" brings along, a resolver on
|
||||
// the local network, like the router's, stays reachable.
|
||||
if (leakFilters{ipv4: true, ipv6: true, lan: true}).systemDNS() {
|
||||
if (leakFilters{all: bothIPVersions, lan: bothIPVersions}).systemDNS() {
|
||||
t.Error("systemDNS() = true with the local network open")
|
||||
}
|
||||
}
|
||||
|
||||
+95
-72
@@ -13,120 +13,143 @@ import (
|
||||
// ports, DNS over TLS, and DNS over QUIC.
|
||||
var dnsLeaks = []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}
|
||||
|
||||
// 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", and any of them with "v4" or "v6" after the
|
||||
// protocol for one IP version only, such as "tcpv4/853" or "icmpv6":
|
||||
// connections of that protocol, to those ports, may only go through the TUN.
|
||||
type leakTraffic struct {
|
||||
protocol string // "tcp", "udp" or "icmp"
|
||||
version string // "v4" or "v6" for one IP version only, empty for both
|
||||
low, high uint16 // the ports, both 0 for all
|
||||
// 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 (t leakTraffic) String() string {
|
||||
name := t.protocol + t.version
|
||||
func (l leak) String() string {
|
||||
s := l.name
|
||||
switch l.versions {
|
||||
case ipv4Version:
|
||||
s = "ipv4:" + s
|
||||
case ipv6Version:
|
||||
s = "ipv6:" + s
|
||||
}
|
||||
switch {
|
||||
case t.low == 0:
|
||||
return name
|
||||
case t.low == t.high:
|
||||
return name + "/" + strconv.Itoa(int(t.low))
|
||||
case l.low == 0:
|
||||
return s
|
||||
case l.low == l.high:
|
||||
return s + "/" + strconv.Itoa(int(l.low))
|
||||
default:
|
||||
return name + "/" + strconv.Itoa(int(t.low)) + "-" + strconv.Itoa(int(t.high))
|
||||
return s + "/" + strconv.Itoa(int(l.low)) + "-" + strconv.Itoa(int(l.high))
|
||||
}
|
||||
}
|
||||
|
||||
// covers tells whether t includes connections of protocol to port, over at
|
||||
// covers tells whether l includes connections of protocol to port, over at
|
||||
// least one IP version.
|
||||
func (t leakTraffic) covers(protocol string, port uint16) bool {
|
||||
return t.protocol == protocol && (t.low == 0 || t.low <= port && port <= t.high)
|
||||
func (l leak) covers(protocol string, port uint16) bool {
|
||||
return l.name == protocol && (l.low == 0 || l.low <= port && port <= l.high)
|
||||
}
|
||||
|
||||
// parseLeakTraffic parses a lower case "tcp", "udp" or "icmp" value, which may
|
||||
// be followed by "v4" or "v6". TCP and UDP may end in "/<port>" or
|
||||
// "/<low>-<high>", with ports from 1 to 65535 and low not above high, where
|
||||
// "/1-65535" stands for all ports, as does no port at all; ICMP has no ports.
|
||||
func parseLeakTraffic(value string) (leakTraffic, bool) {
|
||||
name, ports, hasPorts := strings.Cut(value, "/")
|
||||
t := leakTraffic{protocol: name}
|
||||
for _, version := range []string{"v4", "v6"} {
|
||||
if protocol, ok := strings.CutSuffix(name, version); ok {
|
||||
t.protocol, t.version = protocol, version
|
||||
// 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
|
||||
}
|
||||
switch t.protocol {
|
||||
name, ports, hasPorts := strings.Cut(value, "/")
|
||||
l.name = name
|
||||
switch name {
|
||||
case "tcp", "udp":
|
||||
case "icmp":
|
||||
return t, !hasPorts
|
||||
case "dns", "windnsclient", "misconfigtun", "all", "allexceptlan", "icmp":
|
||||
return l, !hasPorts
|
||||
default:
|
||||
return leakTraffic{}, false
|
||||
return leak{}, false
|
||||
}
|
||||
if !hasPorts {
|
||||
return t, true
|
||||
return l, true
|
||||
}
|
||||
lowText, highText, isRange := strings.Cut(ports, "-")
|
||||
low, err := strconv.ParseUint(lowText, 10, 16)
|
||||
if err != nil || low == 0 {
|
||||
return leakTraffic{}, false
|
||||
return leak{}, false
|
||||
}
|
||||
high := low
|
||||
if isRange {
|
||||
if high, err = strconv.ParseUint(highText, 10, 16); err != nil || high < low {
|
||||
return leakTraffic{}, false
|
||||
return leak{}, false
|
||||
}
|
||||
}
|
||||
if low != 1 || high != 65535 {
|
||||
t.low, t.high = uint16(low), uint16(high)
|
||||
l.low, l.high = uint16(low), uint16(high)
|
||||
}
|
||||
return t, true
|
||||
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, "allexceptlan" followed by that, and ports written the
|
||||
// same way, "tcp/1-65535" as "tcp" say.
|
||||
// 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", "all":
|
||||
add(value)
|
||||
case "allexceptlan":
|
||||
// The local network is no way out for DNS, which a resolver
|
||||
// there, like the router, would forward outside the TUN.
|
||||
add(value)
|
||||
for _, leak := range dnsLeaks {
|
||||
add(leak)
|
||||
}
|
||||
default:
|
||||
traffic, ok := parseLeakTraffic(value)
|
||||
if !ok {
|
||||
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", value)
|
||||
}
|
||||
add(traffic.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 (TCP or UDP port 53, or Windows' DNS Client service) inside the
|
||||
// TUN, which then needs DNS servers of its own.
|
||||
// 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 {
|
||||
if leak == "windnsclient" || leak == "all" {
|
||||
return true
|
||||
}
|
||||
traffic, ok := parseLeakTraffic(leak)
|
||||
return ok && (traffic.covers("udp", 53) || traffic.covers("tcp", 53))
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
+28
-11
@@ -25,11 +25,25 @@ func TestExpandWfpBlockLeak(t *testing.T) {
|
||||
{[]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{"TCPv4/853", "udpv6/10-20", "ICMPv4", "icmpv6"}, []string{"tcpv4/853", "udpv6/10-20", "icmpv4", "icmpv6"}, false},
|
||||
{[]string{"tcpv6/1-65535", "tcp", "tcpv4", "udpv6/53-53"}, []string{"tcpv6", "tcp", "tcpv4", "udpv6/53"}, true},
|
||||
{[]string{"udpv4/53"}, []string{"udpv4/53"}, true},
|
||||
{[]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 {
|
||||
@@ -45,9 +59,11 @@ func TestExpandWfpBlockLeak(t *testing.T) {
|
||||
}
|
||||
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",
|
||||
"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",
|
||||
"tcpv5", "tcp4", "udp6", "v4", "tcpv4v6", "ipv4", "dnsv4", "allv6", "tcpv4/0", "icmpv4/8", "icmpv6/0",
|
||||
"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)
|
||||
@@ -55,7 +71,7 @@ func TestExpandWfpBlockLeak(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeakTrafficCovers(t *testing.T) {
|
||||
func TestLeakCovers(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
value string
|
||||
protocol string
|
||||
@@ -70,16 +86,17 @@ func TestLeakTrafficCovers(t *testing.T) {
|
||||
{"tcp/10-20", "tcp", 20, true},
|
||||
{"tcp/10-20", "tcp", 21, false},
|
||||
{"tcp/10-20", "tcp", 9, false},
|
||||
{"udpv4/53", "udp", 53, true},
|
||||
{"tcpv6", "tcp", 443, true},
|
||||
{"icmpv4", "udp", 53, false},
|
||||
{"ipv4:udp/53", "udp", 53, true},
|
||||
{"ipv6:tcp", "tcp", 443, true},
|
||||
{"ipv4:icmp", "udp", 53, false},
|
||||
{"all", "udp", 53, false},
|
||||
} {
|
||||
traffic, ok := parseLeakTraffic(c.value)
|
||||
l, ok := parseLeak(c.value)
|
||||
if !ok {
|
||||
t.Errorf("%q was not parsed", c.value)
|
||||
continue
|
||||
}
|
||||
if got := traffic.covers(c.protocol, c.port); got != c.covers {
|
||||
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