diff --git a/cmd/tailscale/cli/cli_test.go b/cmd/tailscale/cli/cli_test.go index f8372d642..fecdb76b2 100644 --- a/cmd/tailscale/cli/cli_test.go +++ b/cmd/tailscale/cli/cli_test.go @@ -506,7 +506,7 @@ func TestPrefsFromUpArgs(t *testing.T) { args: upArgsT{ exitNodeIP: "foo", }, - wantErr: `invalid IP address "foo" for --exit-node: unable to parse IP`, + wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`, }, { name: "error_exit_node_allow_lan_without_exit_node", diff --git a/go.mod b/go.mod index 79903628e..9de4ccb22 100644 --- a/go.mod +++ b/go.mod @@ -33,16 +33,17 @@ require ( golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 golang.org/x/net v0.0.0-20210510120150-4163338589ed golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210423082822-04245dca01da + golang.org/x/sys v0.0.0-20210510120138-977fb7262007 golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba - golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 + golang.org/x/tools v0.1.0 golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 gopkg.in/yaml.v2 v2.2.8 // indirect honnef.co/go/tools v0.1.0 - inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 + inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 inet.af/peercred v0.0.0-20210302202138-56e694897155 + inet.af/wf v0.0.0-20210424212123-eaa011a774a4 rsc.io/goversion v1.2.0 ) diff --git a/go.sum b/go.sum index 69d00735c..2f79bbcc3 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/frankban/quicktest v1.12.1 h1:P6vQcHwZYgVGIpUzKB5DXzkEeYJppJOStPLuh9aB89c= @@ -106,6 +107,7 @@ github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwp github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY= github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk= +github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -197,14 +199,17 @@ golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs= @@ -220,8 +225,9 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -243,11 +249,14 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c= honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM= -inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1OvFOrW9SOtvgnzqUZX4= inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4= +inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d h1:9tuJMxDV7THGfXWirKBD/v9rbsBC21bHd2eEYsYuIek= +inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls= inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 h1:DNtszwGa6w76qlIr+PbPEnlBJdiRV8SaxeigOy0q1gg= inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22/go.mod h1:GVx+5OZtbG4TVOW5ilmyRZAZXr1cNwfqUEkTOtWK0PM= inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE= inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU= +inet.af/wf v0.0.0-20210424212123-eaa011a774a4 h1:g1VVXY1xRKoO17aKY3g9KeJxDW0lGx1n2Y+WPSWkOL8= +inet.af/wf v0.0.0-20210424212123-eaa011a774a4/go.mod h1:56/0QVlZ4NmPRh1QuU2OfrKqjSgt5P39R534gD2JMpQ= rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w= rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= diff --git a/wf/firewall.go b/wf/firewall.go new file mode 100644 index 000000000..0eda66e7b --- /dev/null +++ b/wf/firewall.go @@ -0,0 +1,510 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package wf + +import ( + "fmt" + "os" + + "golang.org/x/sys/windows" + "inet.af/netaddr" + "inet.af/wf" +) + +// Known addresses. +var ( + linkLocalRange = netaddr.MustParseIPPrefix("ff80::/10") + linkLocalDHCPMulticast = netaddr.MustParseIP("ff02::1:2") + siteLocalDHCPMulticast = netaddr.MustParseIP("ff05::1:3") + linkLocalRouterMulticast = netaddr.MustParseIP("ff02::2") +) + +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 + + permittedRoutes map[netaddr.IPPrefix][]*wf.Rule +} + +// New returns a new Firewall for the provdied 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[netaddr.IPPrefix][]*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(weightKnownTraffic); 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 []netaddr.IPPrefix) error { + var routesToAdd []netaddr.IPPrefix + routeMap := make(map[netaddr.IPPrefix]bool) + for _, r := range newRoutes { + routeMap[r] = true + if _, ok := f.permittedRoutes[r]; !ok { + routesToAdd = append(routesToAdd, r) + } + } + var routesToRemove []netaddr.IPPrefix + 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.IP.Is4() { + p = protocolV4 + } else { + p = protocolV6 + } + rules, err := f.addRules("local route", weightKnownTraffic, conditions, wf.ActionPermit, p, directionBoth) + if err != nil { + return err + } + f.permittedRoutes[r] = rules + } + return nil +} + +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 + + var icmpConditions = func(t, c uint16, remoteAddress interface{}) []*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 ...interface{}) []*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 ...interface{}) []*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.MatchTypeEqual, + 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 +}