3ec5be3f51
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
567 lines
15 KiB
Go
567 lines
15 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build windows
|
|
|
|
// Package wf controls the Windows Filtering Platform to change Windows firewall rules.
|
|
package wf
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
|
|
"github.com/tailscale/wf"
|
|
"golang.org/x/sys/windows"
|
|
"tailscale.com/net/netaddr"
|
|
)
|
|
|
|
// Known addresses.
|
|
var (
|
|
linkLocalRange = netip.MustParsePrefix("fe80::/10")
|
|
linkLocalDHCPMulticast = netip.MustParseAddr("ff02::1:2")
|
|
siteLocalDHCPMulticast = netip.MustParseAddr("ff05::1:3")
|
|
linkLocalRouterMulticast = netip.MustParseAddr("ff02::2")
|
|
|
|
linkLocalMulticastIPv4Range = netip.MustParsePrefix("224.0.0.0/24")
|
|
linkLocalMulticastIPv6Range = netip.MustParsePrefix("ff02::/16")
|
|
)
|
|
|
|
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, layerID wf.LayerID, name string) string {
|
|
switch layerID {
|
|
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
|
|
|
|
permittedRoutes map[netip.Prefix][]*wf.Rule
|
|
}
|
|
|
|
// New returns a new Firewall for the provided interface ID.
|
|
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,
|
|
permittedRoutes: make(map[netip.Prefix][]*wf.Rule),
|
|
}
|
|
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)
|
|
}
|
|
|
|
if err := f.permitLoopback(weightTailscaleTraffic); err != nil {
|
|
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.
|
|
func (f *Firewall) UpdatePermittedRoutes(newRoutes []netip.Prefix) error {
|
|
var routesToAdd []netip.Prefix
|
|
routeMap := make(map[netip.Prefix]bool)
|
|
for _, r := range newRoutes {
|
|
routeMap[r] = true
|
|
if _, ok := f.permittedRoutes[r]; !ok {
|
|
routesToAdd = append(routesToAdd, r)
|
|
}
|
|
}
|
|
var routesToRemove []netip.Prefix
|
|
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
|
|
if r.Addr().Is4() {
|
|
p = protocolV4
|
|
} else {
|
|
p = protocolV6
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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...)
|
|
|
|
f.permittedRoutes[r] = rules
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
}
|
|
}
|
|
|
|
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 _, layer := range p.getLayers(d) {
|
|
r, err := f.newRule(name, w, layer, 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
|
|
|
|
var icmpConditions = func(t, c uint16, remoteAddress any) []*wf.Match {
|
|
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 {
|
|
var dhcpConditions = func(remoteAddrs ...any) []*wf.Match {
|
|
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 {
|
|
var dhcpConditions = func(remoteAddrs ...any) []*wf.Match {
|
|
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,
|
|
Op: wf.MatchTypeFlagsAllSet,
|
|
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
|
|
}
|