2023-01-27 21:37:20 +00:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2021-05-10 16:31:58 +00:00
|
|
|
|
2021-12-15 16:42:25 +00:00
|
|
|
//go:build windows
|
2021-05-10 16:31:58 +00:00
|
|
|
|
2024-07-10 15:45:46 +00:00
|
|
|
// Package wf controls the Windows Filtering Platform to change Windows firewall rules.
|
2021-05-10 16:31:58 +00:00
|
|
|
package wf
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2022-07-26 03:55:44 +00:00
|
|
|
"net/netip"
|
2021-05-10 16:31:58 +00:00
|
|
|
"os"
|
|
|
|
|
2024-02-14 03:12:03 +00:00
|
|
|
"github.com/tailscale/wf"
|
2021-05-10 16:31:58 +00:00
|
|
|
"golang.org/x/sys/windows"
|
2022-07-25 03:08:42 +00:00
|
|
|
"tailscale.com/net/netaddr"
|
2021-05-10 16:31:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Known addresses.
|
|
|
|
var (
|
2022-07-26 03:55:44 +00:00
|
|
|
linkLocalRange = netip.MustParsePrefix("ff80::/10")
|
|
|
|
linkLocalDHCPMulticast = netip.MustParseAddr("ff02::1:2")
|
|
|
|
siteLocalDHCPMulticast = netip.MustParseAddr("ff05::1:3")
|
|
|
|
linkLocalRouterMulticast = netip.MustParseAddr("ff02::2")
|
wf/firewall: allow link-local multicast for permitted local routes when the killswitch is on on Windows
When an Exit Node is used, we create a WFP rule to block all inbound and outbound traffic,
along with several rules to permit specific types of traffic. Notably, we allow all inbound and
outbound traffic to and from LocalRoutes specified in wgengine/router.Config. The list of allowed
routes always includes routes for internal interfaces, such as loopback and virtual Hyper-V/WSL2
interfaces, and may also include LAN routes if the "Allow local network access" option is enabled.
However, these permitting rules do not allow link-local multicast on the corresponding interfaces.
This results in broken mDNS/LLMNR, and potentially other similar issues, whenever an exit node is used.
In this PR, we update (*wf.Firewall).UpdatePermittedRoutes() to create rules allowing outbound and
inbound link-local multicast traffic to and from the permitted IP ranges, partially resolving the mDNS/LLMNR
and *.local name resolution issue.
Since Windows does not attempt to send mDNS/LLMNR queries if a catch-all NRPT rule is present,
it is still necessary to disable the creation of that rule using the disable-local-dns-override-via-nrpt nodeAttr.
Updates #13571
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-02 22:34:21 +00:00
|
|
|
|
|
|
|
linkLocalMulticastIPv4Range = netip.MustParsePrefix("224.0.0.0/24")
|
|
|
|
linkLocalMulticastIPv6Range = netip.MustParsePrefix("ff02::/16")
|
2021-05-10 16:31:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type direction int
|
|
|
|
|
|
|
|
const (
|
|
|
|
directionInbound direction = iota
|
|
|
|
directionOutbound
|
|
|
|
directionBoth
|
|
|
|
)
|
|
|
|
|
|
|
|
type protocol int
|
|
|
|
|
|
|
|
const (
|
|
|
|
protocolV4 protocol = iota
|
|
|
|
protocolV6
|
|
|
|
protocolAll
|
|
|
|
)
|
|
|
|
|
|
|
|
// getLayers returns the wf.LayerIDs where the rules should be added based
|
|
|
|
// on the protocol and direction.
|
|
|
|
func (p protocol) getLayers(d direction) []wf.LayerID {
|
|
|
|
var layers []wf.LayerID
|
|
|
|
if p == protocolAll || p == protocolV4 {
|
|
|
|
if d == directionBoth || d == directionInbound {
|
|
|
|
layers = append(layers, wf.LayerALEAuthRecvAcceptV4)
|
|
|
|
}
|
|
|
|
if d == directionBoth || d == directionOutbound {
|
|
|
|
layers = append(layers, wf.LayerALEAuthConnectV4)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if p == protocolAll || p == protocolV6 {
|
|
|
|
if d == directionBoth || d == directionInbound {
|
|
|
|
layers = append(layers, wf.LayerALEAuthRecvAcceptV6)
|
|
|
|
}
|
|
|
|
if d == directionBoth || d == directionOutbound {
|
|
|
|
layers = append(layers, wf.LayerALEAuthConnectV6)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return layers
|
|
|
|
}
|
|
|
|
|
|
|
|
func ruleName(action wf.Action, l wf.LayerID, name string) string {
|
|
|
|
switch l {
|
|
|
|
case wf.LayerALEAuthConnectV4:
|
|
|
|
return fmt.Sprintf("%s outbound %s (IPv4)", action, name)
|
|
|
|
case wf.LayerALEAuthConnectV6:
|
|
|
|
return fmt.Sprintf("%s outbound %s (IPv6)", action, name)
|
|
|
|
case wf.LayerALEAuthRecvAcceptV4:
|
|
|
|
return fmt.Sprintf("%s inbound %s (IPv4)", action, name)
|
|
|
|
case wf.LayerALEAuthRecvAcceptV6:
|
|
|
|
return fmt.Sprintf("%s inbound %s (IPv6)", action, name)
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// Firewall uses the Windows Filtering Platform to implement a network firewall.
|
|
|
|
type Firewall struct {
|
|
|
|
luid uint64
|
|
|
|
providerID wf.ProviderID
|
|
|
|
sublayerID wf.SublayerID
|
|
|
|
session *wf.Session
|
|
|
|
|
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
|
|
|
permittedRoutes map[netip.Prefix][]*wf.Rule
|
2021-05-10 16:31:58 +00:00
|
|
|
}
|
|
|
|
|
2022-09-25 18:29:55 +00:00
|
|
|
// New returns a new Firewall for the provided interface ID.
|
2021-05-10 16:31:58 +00:00
|
|
|
func New(luid uint64) (*Firewall, error) {
|
|
|
|
session, err := wf.New(&wf.Options{
|
|
|
|
Name: "Tailscale firewall",
|
|
|
|
Dynamic: true,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
wguid, err := windows.GenerateGUID()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
providerID := wf.ProviderID(wguid)
|
|
|
|
if err := session.AddProvider(&wf.Provider{
|
|
|
|
ID: providerID,
|
|
|
|
Name: "Tailscale provider",
|
|
|
|
}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
wguid, err = windows.GenerateGUID()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
sublayerID := wf.SublayerID(wguid)
|
|
|
|
if err := session.AddSublayer(&wf.Sublayer{
|
|
|
|
ID: sublayerID,
|
|
|
|
Name: "Tailscale permissive and blocking filters",
|
|
|
|
Weight: 0,
|
|
|
|
}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
f := &Firewall{
|
|
|
|
luid: luid,
|
|
|
|
session: session,
|
|
|
|
providerID: providerID,
|
|
|
|
sublayerID: sublayerID,
|
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
|
|
|
permittedRoutes: make(map[netip.Prefix][]*wf.Rule),
|
2021-05-10 16:31:58 +00:00
|
|
|
}
|
|
|
|
if err := f.enable(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type weight uint64
|
|
|
|
|
|
|
|
const (
|
|
|
|
weightTailscaleTraffic weight = 15
|
|
|
|
weightKnownTraffic weight = 12
|
|
|
|
weightCatchAll weight = 0
|
|
|
|
)
|
|
|
|
|
|
|
|
func (f *Firewall) enable() error {
|
|
|
|
if err := f.permitTailscaleService(weightTailscaleTraffic); err != nil {
|
|
|
|
return fmt.Errorf("permitTailscaleService failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := f.permitTunInterface(weightTailscaleTraffic); err != nil {
|
|
|
|
return fmt.Errorf("permitTunInterface failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := f.permitDNS(weightTailscaleTraffic); err != nil {
|
|
|
|
return fmt.Errorf("permitDNS failed: %w", err)
|
|
|
|
}
|
|
|
|
|
2021-06-16 06:35:36 +00:00
|
|
|
if err := f.permitLoopback(weightTailscaleTraffic); err != nil {
|
2021-05-10 16:31:58 +00:00
|
|
|
return fmt.Errorf("permitLoopback failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := f.permitDHCPv4(weightKnownTraffic); err != nil {
|
|
|
|
return fmt.Errorf("permitDHCPv4 failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := f.permitDHCPv6(weightKnownTraffic); err != nil {
|
|
|
|
return fmt.Errorf("permitDHCPv6 failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := f.permitNDP(weightKnownTraffic); err != nil {
|
|
|
|
return fmt.Errorf("permitNDP failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
/* TODO: actually evaluate if this does anything and if we need this. It's layer 2; our other rules are layer 3.
|
|
|
|
* In other words, if somebody complains, try enabling it. For now, keep it off.
|
|
|
|
* TODO(maisem): implement this.
|
|
|
|
err = permitHyperV(session, baseObjects, weightKnownTraffic)
|
|
|
|
if err != nil {
|
|
|
|
return wrapErr(err)
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
if err := f.blockAll(weightCatchAll); err != nil {
|
|
|
|
return fmt.Errorf("blockAll failed: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdatedPermittedRoutes adds rules to allow incoming and outgoing connections
|
|
|
|
// from the provided prefixes. It will also remove rules for routes that were
|
|
|
|
// previously added but have been removed.
|
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
|
|
|
func (f *Firewall) UpdatePermittedRoutes(newRoutes []netip.Prefix) error {
|
|
|
|
var routesToAdd []netip.Prefix
|
|
|
|
routeMap := make(map[netip.Prefix]bool)
|
2021-05-10 16:31:58 +00:00
|
|
|
for _, r := range newRoutes {
|
|
|
|
routeMap[r] = true
|
|
|
|
if _, ok := f.permittedRoutes[r]; !ok {
|
|
|
|
routesToAdd = append(routesToAdd, r)
|
|
|
|
}
|
|
|
|
}
|
all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 04:14:09 +00:00
|
|
|
var routesToRemove []netip.Prefix
|
2021-05-10 16:31:58 +00:00
|
|
|
for r := range f.permittedRoutes {
|
|
|
|
if !routeMap[r] {
|
|
|
|
routesToRemove = append(routesToRemove, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, r := range routesToRemove {
|
|
|
|
for _, rule := range f.permittedRoutes[r] {
|
|
|
|
if err := f.session.DeleteRule(rule.ID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
delete(f.permittedRoutes, r)
|
|
|
|
}
|
|
|
|
for _, r := range routesToAdd {
|
|
|
|
conditions := []*wf.Match{
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPRemoteAddress,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: r,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
var p protocol
|
2022-07-25 03:08:42 +00:00
|
|
|
if r.Addr().Is4() {
|
2021-05-10 16:31:58 +00:00
|
|
|
p = protocolV4
|
|
|
|
} else {
|
|
|
|
p = protocolV6
|
|
|
|
}
|
wf/firewall: allow link-local multicast for permitted local routes when the killswitch is on on Windows
When an Exit Node is used, we create a WFP rule to block all inbound and outbound traffic,
along with several rules to permit specific types of traffic. Notably, we allow all inbound and
outbound traffic to and from LocalRoutes specified in wgengine/router.Config. The list of allowed
routes always includes routes for internal interfaces, such as loopback and virtual Hyper-V/WSL2
interfaces, and may also include LAN routes if the "Allow local network access" option is enabled.
However, these permitting rules do not allow link-local multicast on the corresponding interfaces.
This results in broken mDNS/LLMNR, and potentially other similar issues, whenever an exit node is used.
In this PR, we update (*wf.Firewall).UpdatePermittedRoutes() to create rules allowing outbound and
inbound link-local multicast traffic to and from the permitted IP ranges, partially resolving the mDNS/LLMNR
and *.local name resolution issue.
Since Windows does not attempt to send mDNS/LLMNR queries if a catch-all NRPT rule is present,
it is still necessary to disable the creation of that rule using the disable-local-dns-override-via-nrpt nodeAttr.
Updates #13571
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-02 22:34:21 +00:00
|
|
|
name := "local route - " + r.String()
|
|
|
|
rules, err := f.addRules(name, weightKnownTraffic, conditions, wf.ActionPermit, p, directionBoth)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
name = "link-local multicast - " + r.String()
|
|
|
|
conditions = matchLinkLocalMulticast(r, false)
|
|
|
|
multicastRules, err := f.addRules(name, weightKnownTraffic, conditions, wf.ActionPermit, p, directionOutbound)
|
2021-05-10 16:31:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
wf/firewall: allow link-local multicast for permitted local routes when the killswitch is on on Windows
When an Exit Node is used, we create a WFP rule to block all inbound and outbound traffic,
along with several rules to permit specific types of traffic. Notably, we allow all inbound and
outbound traffic to and from LocalRoutes specified in wgengine/router.Config. The list of allowed
routes always includes routes for internal interfaces, such as loopback and virtual Hyper-V/WSL2
interfaces, and may also include LAN routes if the "Allow local network access" option is enabled.
However, these permitting rules do not allow link-local multicast on the corresponding interfaces.
This results in broken mDNS/LLMNR, and potentially other similar issues, whenever an exit node is used.
In this PR, we update (*wf.Firewall).UpdatePermittedRoutes() to create rules allowing outbound and
inbound link-local multicast traffic to and from the permitted IP ranges, partially resolving the mDNS/LLMNR
and *.local name resolution issue.
Since Windows does not attempt to send mDNS/LLMNR queries if a catch-all NRPT rule is present,
it is still necessary to disable the creation of that rule using the disable-local-dns-override-via-nrpt nodeAttr.
Updates #13571
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-02 22:34:21 +00:00
|
|
|
rules = append(rules, multicastRules...)
|
|
|
|
|
|
|
|
conditions = matchLinkLocalMulticast(r, true)
|
|
|
|
multicastRules, err = f.addRules(name, weightKnownTraffic, conditions, wf.ActionPermit, p, directionInbound)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
rules = append(rules, multicastRules...)
|
|
|
|
|
2021-05-10 16:31:58 +00:00
|
|
|
f.permittedRoutes[r] = rules
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
wf/firewall: allow link-local multicast for permitted local routes when the killswitch is on on Windows
When an Exit Node is used, we create a WFP rule to block all inbound and outbound traffic,
along with several rules to permit specific types of traffic. Notably, we allow all inbound and
outbound traffic to and from LocalRoutes specified in wgengine/router.Config. The list of allowed
routes always includes routes for internal interfaces, such as loopback and virtual Hyper-V/WSL2
interfaces, and may also include LAN routes if the "Allow local network access" option is enabled.
However, these permitting rules do not allow link-local multicast on the corresponding interfaces.
This results in broken mDNS/LLMNR, and potentially other similar issues, whenever an exit node is used.
In this PR, we update (*wf.Firewall).UpdatePermittedRoutes() to create rules allowing outbound and
inbound link-local multicast traffic to and from the permitted IP ranges, partially resolving the mDNS/LLMNR
and *.local name resolution issue.
Since Windows does not attempt to send mDNS/LLMNR queries if a catch-all NRPT rule is present,
it is still necessary to disable the creation of that rule using the disable-local-dns-override-via-nrpt nodeAttr.
Updates #13571
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-02 22:34:21 +00:00
|
|
|
// matchLinkLocalMulticast returns a list of conditions that match
|
|
|
|
// outbound or inbound link-local multicast traffic to or from the
|
|
|
|
// specified network.
|
|
|
|
func matchLinkLocalMulticast(pfx netip.Prefix, inbound bool) []*wf.Match {
|
|
|
|
var linkLocalMulticastRange netip.Prefix
|
|
|
|
if pfx.Addr().Is4() {
|
|
|
|
linkLocalMulticastRange = linkLocalMulticastIPv4Range
|
|
|
|
} else {
|
|
|
|
linkLocalMulticastRange = linkLocalMulticastIPv6Range
|
|
|
|
}
|
|
|
|
var localAddr, remoteAddr netip.Prefix
|
|
|
|
if inbound {
|
|
|
|
localAddr, remoteAddr = linkLocalMulticastRange, pfx
|
|
|
|
} else {
|
|
|
|
localAddr, remoteAddr = pfx, linkLocalMulticastRange
|
|
|
|
}
|
|
|
|
return []*wf.Match{
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPProtocol,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: wf.IPProtoUDP,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPLocalAddress,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: localAddr,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPRemoteAddress,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: remoteAddr,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-10 16:31:58 +00:00
|
|
|
func (f *Firewall) newRule(name string, w weight, layer wf.LayerID, conditions []*wf.Match, action wf.Action) (*wf.Rule, error) {
|
|
|
|
id, err := windows.GenerateGUID()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &wf.Rule{
|
|
|
|
Name: ruleName(action, layer, name),
|
|
|
|
ID: wf.RuleID(id),
|
|
|
|
Provider: f.providerID,
|
|
|
|
Sublayer: f.sublayerID,
|
|
|
|
Layer: layer,
|
|
|
|
Weight: uint64(w),
|
|
|
|
Conditions: conditions,
|
|
|
|
Action: action,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Firewall) addRules(name string, w weight, conditions []*wf.Match, action wf.Action, p protocol, d direction) ([]*wf.Rule, error) {
|
|
|
|
var rules []*wf.Rule
|
|
|
|
for _, l := range p.getLayers(d) {
|
|
|
|
r, err := f.newRule(name, w, l, conditions, action)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := f.session.AddRule(r); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
rules = append(rules, r)
|
|
|
|
}
|
|
|
|
return rules, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Firewall) blockAll(w weight) error {
|
|
|
|
_, err := f.addRules("all", w, nil, wf.ActionBlock, protocolAll, directionBoth)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Firewall) permitNDP(w weight) error {
|
|
|
|
// These are aliased according to:
|
|
|
|
// https://social.msdn.microsoft.com/Forums/azure/en-US/eb2aa3cd-5f1c-4461-af86-61e7d43ccc23/filtering-icmp-by-type-code?forum=wfp
|
|
|
|
fieldICMPType := wf.FieldIPLocalPort
|
|
|
|
fieldICMPCode := wf.FieldIPRemotePort
|
|
|
|
|
2022-03-16 23:27:57 +00:00
|
|
|
var icmpConditions = func(t, c uint16, remoteAddress any) []*wf.Match {
|
2021-05-10 16:31:58 +00:00
|
|
|
conditions := []*wf.Match{
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPProtocol,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: wf.IPProtoICMPV6,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: fieldICMPType,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: t,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: fieldICMPCode,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: c,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
if remoteAddress != nil {
|
|
|
|
conditions = append(conditions, &wf.Match{
|
|
|
|
Field: wf.FieldIPRemoteAddress,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: linkLocalRouterMulticast,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return conditions
|
|
|
|
}
|
|
|
|
/* TODO: actually handle the hop limit somehow! The rules should vaguely be:
|
|
|
|
* - icmpv6 133: must be outgoing, dst must be FF02::2/128, hop limit must be 255
|
|
|
|
* - icmpv6 134: must be incoming, src must be FE80::/10, hop limit must be 255
|
|
|
|
* - icmpv6 135: either incoming or outgoing, hop limit must be 255
|
|
|
|
* - icmpv6 136: either incoming or outgoing, hop limit must be 255
|
|
|
|
* - icmpv6 137: must be incoming, src must be FE80::/10, hop limit must be 255
|
|
|
|
*/
|
|
|
|
|
|
|
|
//
|
|
|
|
// Router Solicitation Message
|
|
|
|
// ICMP type 133, code 0. Outgoing.
|
|
|
|
//
|
|
|
|
conditions := icmpConditions(133, 0, linkLocalRouterMulticast)
|
|
|
|
if _, err := f.addRules("NDP type 133", w, conditions, wf.ActionPermit, protocolV6, directionOutbound); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Router Advertisement Message
|
|
|
|
// ICMP type 134, code 0. Incoming.
|
|
|
|
//
|
|
|
|
conditions = icmpConditions(134, 0, linkLocalRange)
|
|
|
|
if _, err := f.addRules("NDP type 134", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Neighbor Solicitation Message
|
|
|
|
// ICMP type 135, code 0. Bi-directional.
|
|
|
|
//
|
|
|
|
conditions = icmpConditions(135, 0, nil)
|
|
|
|
if _, err := f.addRules("NDP type 135", w, conditions, wf.ActionPermit, protocolV6, directionBoth); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Neighbor Advertisement Message
|
|
|
|
// ICMP type 136, code 0. Bi-directional.
|
|
|
|
//
|
|
|
|
conditions = icmpConditions(136, 0, nil)
|
|
|
|
if _, err := f.addRules("NDP type 136", w, conditions, wf.ActionPermit, protocolV6, directionBoth); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Redirect Message
|
|
|
|
// ICMP type 137, code 0. Incoming.
|
|
|
|
//
|
|
|
|
conditions = icmpConditions(137, 0, linkLocalRange)
|
|
|
|
if _, err := f.addRules("NDP type 137", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Firewall) permitDHCPv6(w weight) error {
|
2022-03-16 23:27:57 +00:00
|
|
|
var dhcpConditions = func(remoteAddrs ...any) []*wf.Match {
|
2021-05-10 16:31:58 +00:00
|
|
|
conditions := []*wf.Match{
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPProtocol,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: wf.IPProtoUDP,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPLocalAddress,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: linkLocalRange,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPLocalPort,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: uint16(546),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPRemotePort,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: uint16(547),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, a := range remoteAddrs {
|
|
|
|
conditions = append(conditions, &wf.Match{
|
|
|
|
Field: wf.FieldIPRemoteAddress,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: a,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return conditions
|
|
|
|
}
|
|
|
|
conditions := dhcpConditions(linkLocalDHCPMulticast, siteLocalDHCPMulticast)
|
|
|
|
if _, err := f.addRules("DHCP request", w, conditions, wf.ActionPermit, protocolV6, directionOutbound); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
conditions = dhcpConditions(linkLocalRange)
|
|
|
|
if _, err := f.addRules("DHCP response", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Firewall) permitDHCPv4(w weight) error {
|
2022-03-16 23:27:57 +00:00
|
|
|
var dhcpConditions = func(remoteAddrs ...any) []*wf.Match {
|
2021-05-10 16:31:58 +00:00
|
|
|
conditions := []*wf.Match{
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPProtocol,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: wf.IPProtoUDP,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPLocalPort,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: uint16(68),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPRemotePort,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: uint16(67),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, a := range remoteAddrs {
|
|
|
|
conditions = append(conditions, &wf.Match{
|
|
|
|
Field: wf.FieldIPRemoteAddress,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: a,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return conditions
|
|
|
|
}
|
|
|
|
conditions := dhcpConditions(netaddr.IPv4(255, 255, 255, 255))
|
|
|
|
if _, err := f.addRules("DHCP request", w, conditions, wf.ActionPermit, protocolV4, directionOutbound); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
conditions = dhcpConditions()
|
|
|
|
if _, err := f.addRules("DHCP response", w, conditions, wf.ActionPermit, protocolV4, directionInbound); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Firewall) permitTunInterface(w weight) error {
|
|
|
|
condition := []*wf.Match{
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPLocalInterface,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: f.luid,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
_, err := f.addRules("on TUN", w, condition, wf.ActionPermit, protocolAll, directionBoth)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Firewall) permitLoopback(w weight) error {
|
|
|
|
condition := []*wf.Match{
|
|
|
|
{
|
|
|
|
Field: wf.FieldFlags,
|
2021-06-16 06:35:36 +00:00
|
|
|
Op: wf.MatchTypeFlagsAllSet,
|
2021-05-10 16:31:58 +00:00
|
|
|
Value: wf.ConditionFlagIsLoopback,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
_, err := f.addRules("on loopback", w, condition, wf.ActionPermit, protocolAll, directionBoth)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Firewall) permitDNS(w weight) error {
|
|
|
|
conditions := []*wf.Match{
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPRemotePort,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: uint16(53),
|
|
|
|
},
|
|
|
|
// Repeat the condition type for logical OR.
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPProtocol,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: wf.IPProtoUDP,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Field: wf.FieldIPProtocol,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: wf.IPProtoTCP,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
_, err := f.addRules("DNS", w, conditions, wf.ActionPermit, protocolAll, directionBoth)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Firewall) permitTailscaleService(w weight) error {
|
|
|
|
currentFile, err := os.Executable()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
appID, err := wf.AppID(currentFile)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not get app id for %q: %w", currentFile, err)
|
|
|
|
}
|
|
|
|
conditions := []*wf.Match{
|
|
|
|
{
|
|
|
|
Field: wf.FieldALEAppID,
|
|
|
|
Op: wf.MatchTypeEqual,
|
|
|
|
Value: appID,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
_, err = f.addRules("unrestricted traffic for Tailscale service", w, conditions, wf.ActionPermit, protocolAll, directionBoth)
|
|
|
|
return err
|
|
|
|
}
|