diff --git a/appc/conn25.go b/appc/conn25.go index e4c999a7a..33a9552bb 100644 --- a/appc/conn25.go +++ b/appc/conn25.go @@ -11,6 +11,7 @@ "tailscale.com/ipn/ipnext" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/appctype" "tailscale.com/types/dnstype" "tailscale.com/util/set" @@ -60,7 +61,7 @@ func PickConnector(nb ipnext.NodeBackend, app appctype.Conn25Attr) []tailcfg.Nod // time. const DNSAddrScheme = "tailscale-app" -func AppDNSRoutes(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.NodeView) map[string][]*dnstype.Resolver { +func AppDNSRoutes(hasCap func(c nodecap.Cap) bool, self tailcfg.NodeView) map[string][]*dnstype.Resolver { if !hasCap(AppConnectorsExperimentalAttrName) { return nil } diff --git a/appc/conn25_test.go b/appc/conn25_test.go index 7194a67fe..1cca32950 100644 --- a/appc/conn25_test.go +++ b/appc/conn25_test.go @@ -11,6 +11,7 @@ "github.com/google/go-cmp/cmp" "tailscale.com/ipn/ipnext" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/appctype" "tailscale.com/types/dnstype" "tailscale.com/types/opt" @@ -139,11 +140,11 @@ func TestAppDNSRoutes(t *testing.T) { selfNode := &tailcfg.Node{} if tt.config != nil { selfNode.CapMap = tailcfg.NodeCapMap{ - tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): tt.config, + nodecap.Cap(AppConnectorsExperimentalAttrName): tt.config, } } selfView := selfNode.View() - got := AppDNSRoutes(func(_ tailcfg.NodeCapability) bool { + got := AppDNSRoutes(func(_ nodecap.Cap) bool { return tt.hasCap }, selfView) if diff := cmp.Diff(tt.want, got); diff != "" { diff --git a/client/systray/systray.go b/client/systray/systray.go index 706cccc03..29cb89c98 100644 --- a/client/systray/systray.go +++ b/client/systray/systray.go @@ -33,6 +33,7 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/util/slicesx" "tailscale.com/util/stringsx" ) @@ -624,7 +625,7 @@ func (menu *Menu) rebuildExitNodeMenu(ctx context.Context) { setExitNodeOnClick(noExitNodeMenu, "") // Show recommended exit node if available. - if status.Self.CapMap.Contains(tailcfg.NodeAttrSuggestExitNodeUI) { + if status.Self.CapMap.Contains(nodecap.SuggestExitNodeUI) { sugg, err := menu.lc.SuggestExitNode(ctx) if err == nil { // Location is invalid for suggested exit nodes that have diff --git a/client/web/auth.go b/client/web/auth.go index 1281695a9..cc4c902da 100644 --- a/client/web/auth.go +++ b/client/web/auth.go @@ -18,6 +18,7 @@ "tailscale.com/client/tailscale/apitype" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" ) const ( @@ -335,7 +336,7 @@ func toPeerCapabilities(status *ipnstate.Status, whois *apitype.WhoIsResponse) ( // For tagged nodes, we actually look at the granted capabilities. caps := peerCapabilities{} - rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, tailcfg.PeerCapabilityWebUI) + rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, peercap.WebUI) if err != nil { return nil, fmt.Errorf("failed to unmarshal capability: %v", err) } diff --git a/client/web/web_test.go b/client/web/web_test.go index 51b6a8ac5..7bf2d8d2a 100644 --- a/client/web/web_test.go +++ b/client/web/web_test.go @@ -26,6 +26,7 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/net/memnet" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/views" "tailscale.com/util/httpm" "tailscale.com/util/syspolicy/policyclient" @@ -105,7 +106,7 @@ func TestServeAPI(t *testing.T) { remoteIPWithAllCapabilities: { Node: &tailcfg.Node{StableID: "node1"}, UserProfile: remoteUser, - CapMap: tailcfg.PeerCapMap{tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{"{\"canEdit\":[\"*\"]}"}}, + CapMap: tailcfg.PeerCapMap{peercap.WebUI: []tailcfg.RawMessage{"{\"canEdit\":[\"*\"]}"}}, }, remoteIPWithNoCapabilities: { Node: &tailcfg.Node{StableID: "node2"}, @@ -1248,7 +1249,7 @@ func TestPeerCapabilities(t *testing.T) { UserProfile: &tailcfg.UserProfile{ID: tailcfg.UserID(2)}, Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", }, }, @@ -1262,7 +1263,7 @@ func TestPeerCapabilities(t *testing.T) { UserProfile: &tailcfg.UserProfile{ID: tailcfg.UserID(1)}, Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", }, }, @@ -1275,7 +1276,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityDebugPeer: []tailcfg.RawMessage{}, + peercap.DebugPeer: []tailcfg.RawMessage{}, }, }, wantCaps: peerCapabilities{}, @@ -1286,7 +1287,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", }, }, @@ -1302,7 +1303,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", "{\"canEdit\":[\"subnets\",\"exitnodes\",\"*\"]}", }, @@ -1321,7 +1322,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"SSH\",\"sUBnets\"]}", }, }, @@ -1337,7 +1338,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"unknown-feature\"]}", }, }, @@ -1350,7 +1351,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canDoSomething\":[\"*\"]}", }, }, @@ -1363,7 +1364,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1), Tags: tags.AsSlice()}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", }, }, diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 865dbab42..2e12aed9c 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -124,6 +124,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa 💣 tailscale.com/safesocket from tailscale.com/client/local tailscale.com/syncs from tailscale.com/cmd/derper+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/cmd/derper tailscale.com/tka from tailscale.com/client/local+ tailscale.com/tsconst from tailscale.com/net/netmon+ diff --git a/cmd/hello/helloserver/helloserver.go b/cmd/hello/helloserver/helloserver.go index 41e7dbce2..62b000f0a 100644 --- a/cmd/hello/helloserver/helloserver.go +++ b/cmd/hello/helloserver/helloserver.go @@ -16,6 +16,7 @@ "tailscale.com/client/local" "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" ) //go:embed hello.tmpl.html @@ -94,7 +95,7 @@ func tailscaleIP(who *apitype.WhoIsResponse) string { if who == nil { return "" } - vals, err := tailcfg.UnmarshalNodeCapJSON[string](who.Node.CapMap, tailcfg.NodeAttrNativeIPV4) + vals, err := tailcfg.UnmarshalNodeCapJSON[string](who.Node.CapMap, nodecap.NativeIPV4) if err == nil && len(vals) > 0 { return vals[0] } diff --git a/cmd/k8s-operator/depaware.txt b/cmd/k8s-operator/depaware.txt index 3e5117f15..25d7a91fb 100644 --- a/cmd/k8s-operator/depaware.txt +++ b/cmd/k8s-operator/depaware.txt @@ -840,6 +840,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/ tailscale.com/sessionrecording from tailscale.com/k8s-operator/sessionrecording+ tailscale.com/syncs from tailscale.com/control/controlknobs+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/feature/acme tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/httprec from tailscale.com/feature/c2n diff --git a/cmd/proxy-to-grafana/proxy-to-grafana.go b/cmd/proxy-to-grafana/proxy-to-grafana.go index 23f264059..573a640d9 100644 --- a/cmd/proxy-to-grafana/proxy-to-grafana.go +++ b/cmd/proxy-to-grafana/proxy-to-grafana.go @@ -55,6 +55,7 @@ "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsnet" ) @@ -67,7 +68,7 @@ ) // aclCap is the Tailscale ACL capability used to configure proxy-to-grafana. -const aclCap tailcfg.PeerCapability = "tailscale.com/cap/proxy-to-grafana" +const aclCap peercap.Cap = "tailscale.com/cap/proxy-to-grafana" // aclGrant is an access control rule that assigns Grafana permissions // while provisioning a user. diff --git a/cmd/stund/depaware.txt b/cmd/stund/depaware.txt index 50538b6c3..65e5361f5 100644 --- a/cmd/stund/depaware.txt +++ b/cmd/stund/depaware.txt @@ -62,6 +62,8 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar tailscale.com/net/tsaddr from tailscale.com/tsweb tailscale.com/syncs from tailscale.com/metrics+ tailscale.com/tailcfg from tailscale.com/version+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg tailscale.com/tstime from tailscale.com/tsweb tailscale.com/tsweb from tailscale.com/cmd/stund+ tailscale.com/tsweb/promvarz from tailscale.com/cmd/stund diff --git a/cmd/tailscale/cli/exitnode.go b/cmd/tailscale/cli/exitnode.go index 8b8333121..65daa9ec9 100644 --- a/cmd/tailscale/cli/exitnode.go +++ b/cmd/tailscale/cli/exitnode.go @@ -19,6 +19,7 @@ "tailscale.com/feature/buildfeatures" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/util/slicesx" ) @@ -175,7 +176,7 @@ func runExitNodeSuggest(ctx context.Context, args []string) error { func hasAnyExitNodeSuggestions(peers []*ipnstate.PeerStatus) bool { for _, peer := range peers { - if peer.HasCap(tailcfg.NodeAttrSuggestExitNode) { + if peer.HasCap(nodecap.SuggestExitNode) { return true } } diff --git a/cmd/tailscale/cli/funnel.go b/cmd/tailscale/cli/funnel.go index f16f571e0..694f7bfaa 100644 --- a/cmd/tailscale/cli/funnel.go +++ b/cmd/tailscale/cli/funnel.go @@ -15,7 +15,7 @@ "github.com/peterbourgon/ff/v3/ffcli" "tailscale.com/ipn" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" ) func init() { @@ -137,7 +137,7 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error { // // verifyFunnelEnabled may refresh the local state and modify the st input. func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, port uint16) error { - enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel) + enableErr := e.enableFeatureInteractive(ctx, "funnel", nodecap.HTTPS, nodecap.Funnel) st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block switch { case statusErr != nil: diff --git a/cmd/tailscale/cli/serve_legacy.go b/cmd/tailscale/cli/serve_legacy.go index b8d9e2766..002ede4a1 100644 --- a/cmd/tailscale/cli/serve_legacy.go +++ b/cmd/tailscale/cli/serve_legacy.go @@ -29,6 +29,8 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/util/slicesx" "tailscale.com/version" ) @@ -163,19 +165,19 @@ type serveEnv struct { json bool // output JSON (status only for now) // v2 specific flags - bg bgBoolFlag // background mode - setPath string // serve path - https uint // HTTP port - http uint // HTTP port - tcp uint // TCP port - tlsTerminatedTCP uint // a TLS terminated TCP port - proxyProtocol uint // PROXY protocol version (1 or 2) - subcmd serveMode // subcommand - yes bool // update without prompt - service tailcfg.ServiceName // service name - tun bool // redirect traffic to OS for service - allServices bool // apply config file to all services - acceptAppCaps []tailcfg.PeerCapability // app capabilities to forward + bg bgBoolFlag // background mode + setPath string // serve path + https uint // HTTP port + http uint // HTTP port + tcp uint // TCP port + tlsTerminatedTCP uint // a TLS terminated TCP port + proxyProtocol uint // PROXY protocol version (1 or 2) + subcmd serveMode // subcommand + yes bool // update without prompt + service tailcfg.ServiceName // service name + tun bool // redirect traffic to OS for service + allServices bool // apply config file to all services + acceptAppCaps []peercap.Cap // app capabilities to forward lc localServeClient // localClient interface, specific to serve // optional stuff for tests: @@ -276,7 +278,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error { // on, enableFeatureInteractive will error. For now, we hide that // error and maintain the previous behavior (prior to 2023-08-15) // of letting them edit the serve config before enabling certs. - e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS) + e.enableFeatureInteractive(ctx, "serve", nodecap.HTTPS) } srcPort, err := parseServePort(srcPortStr) @@ -790,7 +792,7 @@ func parseServePort(s string) (uint16, error) { // // 2023-08-09: The only valid feature values are "serve" and "funnel". // This can be moved to some CLI lib when expanded past serve/funnel. -func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...tailcfg.NodeCapability) (err error) { +func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...nodecap.Cap) (err error) { st, err := e.getLocalClientStatusWithoutPeers(ctx) if err != nil { return fmt.Errorf("getting client status: %w", err) diff --git a/cmd/tailscale/cli/serve_legacy_test.go b/cmd/tailscale/cli/serve_legacy_test.go index 27cbb5712..a5b0f9ece 100644 --- a/cmd/tailscale/cli/serve_legacy_test.go +++ b/cmd/tailscale/cli/serve_legacy_test.go @@ -22,6 +22,7 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/types/logger" ) @@ -777,7 +778,7 @@ func TestVerifyFunnelEnabled(t *testing.T) { // queryFeatureResponse is the mock response desired from the // call made to lc.QueryFeature by verifyFunnelEnabled. queryFeatureResponse mockQueryFeatureResponse - caps []tailcfg.NodeCapability // optionally set at fakeStatus.Capabilities + caps []nodecap.Cap // optionally set at fakeStatus.Capabilities wantErr string wantPanic string }{ @@ -794,13 +795,13 @@ func TestVerifyFunnelEnabled(t *testing.T) { { name: "fallback-flow-missing-acl-rule", queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")}, - caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS}, + caps: []nodecap.Cap{nodecap.HTTPS}, wantErr: `Funnel not available; "funnel" node attribute not set. See https://tailscale.com/s/no-funnel.`, }, { name: "fallback-flow-enabled", queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")}, - caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel, "https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090"}, + caps: []nodecap.Cap{nodecap.HTTPS, nodecap.Funnel, "https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090"}, wantErr: "", // no error, success }, { @@ -874,8 +875,8 @@ type fakeLocalServeClient struct { Self: &ipnstate.PeerStatus{ DNSName: "foo.test.ts.net", CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrFunnel: nil, - tailcfg.CapabilityFunnelPorts + "?ports=443,8443": nil, + nodecap.Funnel: nil, + nodecap.FunnelPorts + "?ports=443,8443": nil, }, }, CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, diff --git a/cmd/tailscale/cli/serve_v2.go b/cmd/tailscale/cli/serve_v2.go index 6a41f7156..eb2eef538 100644 --- a/cmd/tailscale/cli/serve_v2.go +++ b/cmd/tailscale/cli/serve_v2.go @@ -33,6 +33,8 @@ "tailscale.com/ipn/conffile" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/util/dnsname" "tailscale.com/util/mak" @@ -100,7 +102,7 @@ func (b *bgBoolFlag) String() string { } type acceptAppCapsFlag struct { - Value *[]tailcfg.PeerCapability + Value *[]peercap.Cap } // An application capability name has the form {domain}/{name}. @@ -120,7 +122,7 @@ func (u *acceptAppCapsFlag) Set(s string) error { if !validAppCap.MatchString(appCap) { return fmt.Errorf("%q does not match the form {domain}/{name}, where domain must be a fully qualified domain name", appCap) } - *u.Value = append(*u.Value, tailcfg.PeerCapability(appCap)) + *u.Value = append(*u.Value, peercap.Cap(appCap)) } return nil } @@ -468,7 +470,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc { // on, enableFeatureInteractive will error. For now, we hide that // error and maintain the previous behavior (prior to 2023-08-15) // of letting them edit the serve config before enabling certs. - if err := e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS); err != nil { + if err := e.enableFeatureInteractive(ctx, "serve", nodecap.HTTPS); err != nil { return fmt.Errorf("error enabling https feature: %w", err) } } @@ -990,7 +992,7 @@ func (e *serveEnv) runServeSetConfig(ctx context.Context, args []string) (err er return e.lc.SetServeConfig(ctx, sc) } -func (e *serveEnv) setServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool, mds string, caps []tailcfg.PeerCapability, proxyProtocol int) error { +func (e *serveEnv) setServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool, mds string, caps []peercap.Cap, proxyProtocol int) error { // update serve config based on the type switch srvType { case serveTypeHTTPS, serveTypeHTTP: @@ -1079,7 +1081,7 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN return "", "" } if forService { - serviceIPMaps, err := tailcfg.UnmarshalNodeCapJSON[tailcfg.ServiceIPMappings](st.Self.CapMap, tailcfg.NodeAttrServiceHost) + serviceIPMaps, err := tailcfg.UnmarshalNodeCapJSON[tailcfg.ServiceIPMappings](st.Self.CapMap, nodecap.ServiceHost) if err != nil || len(serviceIPMaps) == 0 || serviceIPMaps[0][svcName] == nil { // The capmap does not contain IPs for this service yet. Usually this means // the service hasn't been added to prefs and sent to control yet. @@ -1241,7 +1243,7 @@ func (e *serveEnv) shouldWarnRemoteDestCompatibility(ctx context.Context, target return nil } -func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, useTLS bool, mount, target, mds string, caps []tailcfg.PeerCapability) error { +func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, useTLS bool, mount, target, mds string, caps []peercap.Cap) error { h := new(ipn.HTTPHandler) switch { case strings.HasPrefix(target, "text:"): diff --git a/cmd/tailscale/cli/serve_v2_test.go b/cmd/tailscale/cli/serve_v2_test.go index b86c40139..37f29c02e 100644 --- a/cmd/tailscale/cli/serve_v2_test.go +++ b/cmd/tailscale/cli/serve_v2_test.go @@ -24,6 +24,8 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/views" ) @@ -886,8 +888,8 @@ type group struct { Self: &ipnstate.PeerStatus{ DNSName: "foo.test.ts.net", CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrFunnel: nil, - tailcfg.CapabilityFunnelPorts + "?ports=443,8443": nil, + nodecap.Funnel: nil, + nodecap.FunnelPorts + "?ports=443,8443": nil, }, Tags: ptrToReadOnlySlice([]string{"some-tag"}), }, @@ -931,7 +933,7 @@ type group struct { "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{ "/": { Proxy: "http://127.0.0.1:3000", - AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo"}, + AcceptAppCaps: []peercap.Cap{"example.com/cap/foo"}, }, }}, }, @@ -945,7 +947,7 @@ type group struct { "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{ "/": { Proxy: "http://127.0.0.1:3000", - AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo", "example.com/cap/bar"}, + AcceptAppCaps: []peercap.Cap{"example.com/cap/foo", "example.com/cap/bar"}, }, }}, }, @@ -959,7 +961,7 @@ type group struct { "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{ "/": { Proxy: "http://127.0.0.1:3000", - AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/bar"}, + AcceptAppCaps: []peercap.Cap{"example.com/cap/bar"}, }, }}, }, @@ -1189,37 +1191,37 @@ func TestAcceptSetAppCapsFlag(t *testing.T) { inputs []string expectErr bool expectErrToMatch *regexp.Regexp - expectedValue []tailcfg.PeerCapability + expectedValue []peercap.Cap }{ { name: "valid_simple", inputs: []string{"example.com/name"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"example.com/name"}, + expectedValue: []peercap.Cap{"example.com/name"}, }, { name: "valid_unicode", inputs: []string{"bücher.de/something"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"bücher.de/something"}, + expectedValue: []peercap.Cap{"bücher.de/something"}, }, { name: "more_valid_unicode", inputs: []string{"example.tw/某某某"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"example.tw/某某某"}, + expectedValue: []peercap.Cap{"example.tw/某某某"}, }, { name: "valid_path_slashes", inputs: []string{"domain.com/path/to/name"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"domain.com/path/to/name"}, + expectedValue: []peercap.Cap{"domain.com/path/to/name"}, }, { name: "valid_multiple_sets", inputs: []string{"one.com/foo,two.com/bar"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"one.com/foo", "two.com/bar"}, + expectedValue: []peercap.Cap{"one.com/foo", "two.com/bar"}, }, { name: "valid_empty_string", @@ -1238,7 +1240,7 @@ func TestAcceptSetAppCapsFlag(t *testing.T) { name: "valid_subdomain", inputs: []string{"sub.domain.com/name"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"sub.domain.com/name"}, + expectedValue: []peercap.Cap{"sub.domain.com/name"}, }, { name: "invalid_no_path", @@ -1259,13 +1261,13 @@ func TestAcceptSetAppCapsFlag(t *testing.T) { inputs: []string{"one.com/foo,bad/bar,two.com/baz"}, expectErr: true, expectErrToMatch: regexp.MustCompile(`"bad/bar"`), - expectedValue: []tailcfg.PeerCapability{"one.com/foo"}, // Parsing will stop after first error + expectedValue: []peercap.Cap{"one.com/foo"}, // Parsing will stop after first error }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - var v []tailcfg.PeerCapability + var v []peercap.Cap flag := &acceptAppCapsFlag{Value: &v} var err error @@ -1531,7 +1533,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, @@ -1575,7 +1577,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, @@ -1619,7 +1621,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, @@ -1654,7 +1656,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, @@ -1689,7 +1691,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 69a287eb5..465159b08 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -260,6 +260,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep 💣 tailscale.com/safesocket from tailscale.com/client/local+ tailscale.com/syncs from tailscale.com/control/controlhttp+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+ tailscale.com/tka from tailscale.com/client/local+ tailscale.com/tsconst from tailscale.com/net/netmon+ diff --git a/cmd/tailscaled/depaware-min.txt b/cmd/tailscaled/depaware-min.txt index 395dcd40f..0c44c30ae 100644 --- a/cmd/tailscaled/depaware-min.txt +++ b/cmd/tailscaled/depaware-min.txt @@ -127,6 +127,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/safesocket from tailscale.com/cmd/tailscaled+ tailscale.com/syncs from tailscale.com/cmd/tailscaled+ tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tka from tailscale.com/control/controlclient+ tailscale.com/tsconst from tailscale.com/net/netns+ diff --git a/cmd/tailscaled/depaware-minbox.txt b/cmd/tailscaled/depaware-minbox.txt index 8edf656b1..397e092dc 100644 --- a/cmd/tailscaled/depaware-minbox.txt +++ b/cmd/tailscaled/depaware-minbox.txt @@ -145,6 +145,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/safesocket from tailscale.com/cmd/tailscaled+ tailscale.com/syncs from tailscale.com/cmd/tailscaled+ tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+ tailscale.com/tka from tailscale.com/control/controlclient+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 9bb37a049..e2d17f7b7 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -421,6 +421,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de LD 💣 tailscale.com/ssh/tailssh from tailscale.com/feature/ssh tailscale.com/syncs from tailscale.com/cmd/tailscaled+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/feature/acme tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/httprec from tailscale.com/feature/c2n diff --git a/cmd/tsidp/depaware.txt b/cmd/tsidp/depaware.txt index af0ed1218..bd8e92ad9 100644 --- a/cmd/tsidp/depaware.txt +++ b/cmd/tsidp/depaware.txt @@ -230,6 +230,8 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar 💣 tailscale.com/safesocket from tailscale.com/client/local+ tailscale.com/syncs from tailscale.com/control/controlhttp+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/feature/acme tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/httprec from tailscale.com/feature/c2n diff --git a/cmd/tsidp/tsidp.go b/cmd/tsidp/tsidp.go index 24c823d71..13e4bf2be 100644 --- a/cmd/tsidp/tsidp.go +++ b/cmd/tsidp/tsidp.go @@ -44,6 +44,7 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsnet" "tailscale.com/types/key" "tailscale.com/types/lazy" @@ -672,7 +673,7 @@ func (s *idpServer) serveUserInfo(w http.ResponseWriter, r *http.Request) { // TODO(maisem): not sure if this is the right thing to do ui.UserName, _, _ = strings.Cut(ar.remoteUser.UserProfile.LoginName, "@") - rules, err := tailcfg.UnmarshalCapJSON[capRule](ar.remoteUser.CapMap, tailcfg.PeerCapabilityTsIDP) + rules, err := tailcfg.UnmarshalCapJSON[capRule](ar.remoteUser.CapMap, peercap.TsIDP) if err != nil { http.Error(w, "tsidp: failed to unmarshal capability: %v", http.StatusBadRequest) return @@ -964,7 +965,7 @@ func (s *idpServer) serveToken(w http.ResponseWriter, r *http.Request) { tsClaims.Issuer = s.loopbackURL } - rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, tailcfg.PeerCapabilityTsIDP) + rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, peercap.TsIDP) if err != nil { log.Printf("tsidp: failed to unmarshal capability: %v", err) http.Error(w, err.Error(), http.StatusBadRequest) diff --git a/cmd/tsidp/tsidp_test.go b/cmd/tsidp/tsidp_test.go index baf5aaee1..1f3811b02 100644 --- a/cmd/tsidp/tsidp_test.go +++ b/cmd/tsidp/tsidp_test.go @@ -42,6 +42,7 @@ "tailscale.com/client/local" "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/key" "tailscale.com/types/opt" "tailscale.com/types/views" @@ -556,7 +557,7 @@ func TestServeToken(t *testing.T) { remoteAddr: "127.0.0.1:12345", strictMode: false, caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -578,7 +579,7 @@ func TestServeToken(t *testing.T) { code: "valid-code", strictMode: false, caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -714,7 +715,7 @@ func TestExtraUserInfo(t *testing.T) { name: "extra-claim", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -731,7 +732,7 @@ func TestExtraUserInfo(t *testing.T) { name: "duplicate-claim-distinct-values", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -748,7 +749,7 @@ func TestExtraUserInfo(t *testing.T) { name: "multiple-extra-claims", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -773,7 +774,7 @@ func TestExtraUserInfo(t *testing.T) { name: "overwrite-protected-claim", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -789,7 +790,7 @@ func TestExtraUserInfo(t *testing.T) { name: "extra-claim-omitted", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: false, ExtraClaims: map[string]any{ diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 996f51ee1..bfc32b2e1 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -49,6 +49,7 @@ "tailscale.com/net/tsdial" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/tstime" "tailscale.com/types/events" @@ -1367,12 +1368,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap // DefaultAutoUpdate in its CapMap and deprecated top-level field forms. if self := resp.Node; self != nil { - for _, v := range self.CapMap[tailcfg.NodeAttrDefaultAutoUpdate] { + for _, v := range self.CapMap[nodecap.DefaultAutoUpdate] { switch v { case "true", "false": c.autoUpdatePub.Publish(AutoUpdate{c.controlClientID, v == "true"}) default: - c.logf("netmap: [unexpected] unknown %s in CapMap: %q", tailcfg.NodeAttrDefaultAutoUpdate, v) + c.logf("netmap: [unexpected] unknown %s in CapMap: %q", nodecap.DefaultAutoUpdate, v) } } } diff --git a/control/controlclient/map.go b/control/controlclient/map.go index 77ca0e68d..7e23b3f34 100644 --- a/control/controlclient/map.go +++ b/control/controlclient/map.go @@ -26,6 +26,7 @@ "tailscale.com/envknob" "tailscale.com/hostinfo" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" @@ -86,7 +87,7 @@ type mapSession struct { // Fields storing state over the course of multiple MapResponses. lastPrintMap time.Time lastNode tailcfg.NodeView - lastCapSet set.Set[tailcfg.NodeCapability] + lastCapSet set.Set[nodecap.Cap] lastDNSConfig *tailcfg.DNSConfig lastDERPMap *tailcfg.DERPMap lastUserProfile map[tailcfg.UserID]tailcfg.UserProfileView @@ -603,7 +604,7 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) { if resp.Node != nil { ms.lastNode = resp.Node.View() - capSet := set.Set[tailcfg.NodeCapability]{} + capSet := set.Set[nodecap.Cap]{} for _, c := range resp.Node.Capabilities { capSet.Add(c) } diff --git a/control/controlclient/map_test.go b/control/controlclient/map_test.go index fb4307c39..3ae7d66f5 100644 --- a/control/controlclient/map_test.go +++ b/control/controlclient/map_test.go @@ -24,6 +24,7 @@ "tailscale.com/health" "tailscale.com/ipn" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstime" "tailscale.com/types/dnstype" @@ -1250,38 +1251,38 @@ func TestPeerChangeDiff(t *testing.T) { }, { name: "patch-capmap-add-value-to-existing-key", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, - b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: []tailcfg.RawMessage{"true"}}}, - want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: []tailcfg.RawMessage{"true"}}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, + b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: []tailcfg.RawMessage{"true"}}}, + want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: []tailcfg.RawMessage{"true"}}}, }, { name: "patch-capmap-add-new-key", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, - b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil, tailcfg.CapabilityDebug: nil}}, - want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil, tailcfg.CapabilityDebug: nil}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, + b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil, nodecap.Debug: nil}}, + want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil, nodecap.Debug: nil}}, }, { name: "patch-capmap-remove-key", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{}}, want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{}}, }, { name: "patch-capmap-remove-as-nil", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, b: &tailcfg.Node{ID: 1}, want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{}}, }, { name: "patch-capmap-add-key-to-empty-map", a: &tailcfg.Node{ID: 1}, - b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, - want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, + b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, + want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, }, { name: "patch-capmap-no-change", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, - b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, + b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, wantEqual: true, }, } diff --git a/control/controlknobs/controlknobs.go b/control/controlknobs/controlknobs.go index 45bfde0fa..2c8fbe2ff 100644 --- a/control/controlknobs/controlknobs.go +++ b/control/controlknobs/controlknobs.go @@ -12,6 +12,7 @@ "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/opt" ) @@ -158,38 +159,38 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { } has := capMap.Contains var ( - disableUPnP = has(tailcfg.NodeAttrDisableUPnP) - randomizeClientPort = has(tailcfg.NodeAttrRandomizeClientPort) - disableDeltaUpdates = has(tailcfg.NodeAttrDisableDeltaUpdates) + disableUPnP = has(nodecap.DisableUPnP) + randomizeClientPort = has(nodecap.RandomizeClientPort) + disableDeltaUpdates = has(nodecap.DisableDeltaUpdates) oneCGNAT opt.Bool - forceBackgroundSTUN = has(tailcfg.NodeAttrDebugForceBackgroundSTUN) - peerMTUEnable = has(tailcfg.NodeAttrPeerMTUEnable) - dnsForwarderDisableTCPRetries = has(tailcfg.NodeAttrDNSForwarderDisableTCPRetries) - silentDisco = has(tailcfg.NodeAttrSilentDisco) - forceIPTables = has(tailcfg.NodeAttrLinuxMustUseIPTables) - forceNfTables = has(tailcfg.NodeAttrLinuxMustUseNfTables) - probeUDPLifetime = has(tailcfg.NodeAttrProbeUDPLifetime) - appCStoreRoutes = has(tailcfg.NodeAttrStoreAppCRoutes) - userDialUseRoutes = has(tailcfg.NodeAttrUserDialUseRoutes) - disableSplitDNSWhenNoCustomResolvers = has(tailcfg.NodeAttrDisableSplitDNSWhenNoCustomResolvers) - disableLocalDNSOverrideViaNRPT = has(tailcfg.NodeAttrDisableLocalDNSOverrideViaNRPT) - disableCaptivePortalDetection = has(tailcfg.NodeAttrDisableCaptivePortalDetection) - disableSkipStatusQueue = has(tailcfg.NodeAttrDisableSkipStatusQueue) - disableHostsFileUpdates = has(tailcfg.NodeAttrDisableHostsFileUpdates) - forceRegisterMagicDNSIPv4Only = has(tailcfg.NodeAttrForceRegisterMagicDNSIPv4Only) - emitRuntimeMetrics = has(tailcfg.NodeAttrEmitRuntimeMetrics) - disableUDPGRO = has(tailcfg.NodeAttrDisableUDPGRO) - disableUDPGSO = has(tailcfg.NodeAttrDisableUDPGSO) - disableTUNUDPGRO = has(tailcfg.NodeAttrDisableTUNUDPGRO) - disableTUNTCPGRO = has(tailcfg.NodeAttrDisableTUNTCPGRO) - neverGSOEqualTail = has(tailcfg.NodeAttrNeverGSOEqualTail) - cacheNetworkMaps = has(tailcfg.NodeAttrCacheNetworkMaps) - scopeQuad100OnMacOS = has(tailcfg.NodeAttrScopeQuad100OnMacOS) + forceBackgroundSTUN = has(nodecap.DebugForceBackgroundSTUN) + peerMTUEnable = has(nodecap.PeerMTUEnable) + dnsForwarderDisableTCPRetries = has(nodecap.DNSForwarderDisableTCPRetries) + silentDisco = has(nodecap.SilentDisco) + forceIPTables = has(nodecap.LinuxMustUseIPTables) + forceNfTables = has(nodecap.LinuxMustUseNfTables) + probeUDPLifetime = has(nodecap.ProbeUDPLifetime) + appCStoreRoutes = has(nodecap.StoreAppCRoutes) + userDialUseRoutes = has(nodecap.UserDialUseRoutes) + disableSplitDNSWhenNoCustomResolvers = has(nodecap.DisableSplitDNSWhenNoCustomResolvers) + disableLocalDNSOverrideViaNRPT = has(nodecap.DisableLocalDNSOverrideViaNRPT) + disableCaptivePortalDetection = has(nodecap.DisableCaptivePortalDetection) + disableSkipStatusQueue = has(nodecap.DisableSkipStatusQueue) + disableHostsFileUpdates = has(nodecap.DisableHostsFileUpdates) + forceRegisterMagicDNSIPv4Only = has(nodecap.ForceRegisterMagicDNSIPv4Only) + emitRuntimeMetrics = has(nodecap.EmitRuntimeMetrics) + disableUDPGRO = has(nodecap.DisableUDPGRO) + disableUDPGSO = has(nodecap.DisableUDPGSO) + disableTUNUDPGRO = has(nodecap.DisableTUNUDPGRO) + disableTUNTCPGRO = has(nodecap.DisableTUNTCPGRO) + neverGSOEqualTail = has(nodecap.NeverGSOEqualTail) + cacheNetworkMaps = has(nodecap.CacheNetworkMaps) + scopeQuad100OnMacOS = has(nodecap.ScopeQuad100OnMacOS) ) - if has(tailcfg.NodeAttrOneCGNATEnable) { + if has(nodecap.OneCGNATEnable) { oneCGNAT.Set(true) - } else if has(tailcfg.NodeAttrOneCGNATDisable) { + } else if has(nodecap.OneCGNATDisable) { oneCGNAT.Set(false) } diff --git a/feature/acme/cert.go b/feature/acme/cert.go index 0d9b7408e..5c4671ec9 100644 --- a/feature/acme/cert.go +++ b/feature/acme/cert.go @@ -26,7 +26,7 @@ "tailscale.com/health" "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" xacme "tailscale.com/tempfork/acme" "tailscale.com/types/logger" "tailscale.com/util/mak" @@ -442,12 +442,12 @@ func (e *extension) ensureAccount(ctx context.Context, ac *xacme.Client, logf lo } type acmeCertIssueArgs struct { - cs certStore // certificate and ACME account storage - logf logger.Logf // logs ACME progress and failures - traceACME func(any) // optional hook for logging ACME messages - domain string // certificate domain being issued + cs certStore // certificate and ACME account storage + logf logger.Logf // logs ACME progress and failures + traceACME func(any) // optional hook for logging ACME messages + domain string // certificate domain being issued opts []xacme.OrderOption // ACME order options - challengeType acmeChallengeType // challenge type to fulfill + challengeType acmeChallengeType // challenge type to fulfill } func (args acmeCertIssueArgs) baseDomain() string { return strings.TrimPrefix(args.domain, "*.") } @@ -672,7 +672,7 @@ func (e *extension) resolveCertDomain(b *ipnlocal.LocalBackend, domain string) ( // Wildcard request like "*.node.ts.net". if base, ok := strings.CutPrefix(domain, "*."); ok { - if !nm.AllCaps.Contains(tailcfg.NodeAttrDNSSubdomainResolve) { + if !nm.AllCaps.Contains(nodecap.DNSSubdomainResolve) { return "", fmt.Errorf("wildcard certificates are not enabled for this node") } if !slices.Contains(certDomains, base) { diff --git a/feature/acme/cert_test.go b/feature/acme/cert_test.go index 2ed4f28e5..0213136ed 100644 --- a/feature/acme/cert_test.go +++ b/feature/acme/cert_test.go @@ -34,6 +34,7 @@ "tailscale.com/ipn/ipnlocal/ipnlocaltest" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" xacme "tailscale.com/tempfork/acme" "tailscale.com/tsconst" "tailscale.com/tstest" @@ -194,9 +195,9 @@ func TestResolveCertDomain(t *testing.T) { e := extOf(t, b) if !tt.skipNetmap { - var allCaps set.Set[tailcfg.NodeCapability] + var allCaps set.Set[nodecap.Cap] if tt.hasCap { - allCaps = set.Of(tailcfg.NodeAttrDNSSubdomainResolve) + allCaps = set.Of(nodecap.DNSSubdomainResolve) } b.ForTest().SetNetMap(&netmap.NetworkMap{ SelfNode: (&tailcfg.Node{}).View(), diff --git a/feature/conn25/conn25_test.go b/feature/conn25/conn25_test.go index 779864618..10f894304 100644 --- a/feature/conn25/conn25_test.go +++ b/feature/conn25/conn25_test.go @@ -26,6 +26,7 @@ "tailscale.com/net/tsdial" "tailscale.com/net/tstun" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/appctype" @@ -457,10 +458,10 @@ func TestReserveIPs(t *testing.T) { func TestReconfig(t *testing.T) { rawCfg := `{"name":"app1","connectors":["tag:woo"],"domains":["example.com"]}` capMap := tailcfg.NodeCapMap{ - tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{ + nodecap.Cap(AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{ tailcfg.RawMessage(rawCfg), }, - tailcfg.NodeCapability(AppConnectorsExperimentalIPPoolsAttrName): []tailcfg.RawMessage{ + nodecap.Cap(AppConnectorsExperimentalIPPoolsAttrName): []tailcfg.RawMessage{ tailcfg.RawMessage("{}"), }, } @@ -648,8 +649,8 @@ func TestConfigFromNodeView(t *testing.T) { poolsCfg = getRawMessages(t, tt.poolsCfg) } capMap := tailcfg.NodeCapMap{ - tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): appCfg, - tailcfg.NodeCapability(AppConnectorsExperimentalIPPoolsAttrName): poolsCfg, + nodecap.Cap(AppConnectorsExperimentalAttrName): appCfg, + nodecap.Cap(AppConnectorsExperimentalIPPoolsAttrName): poolsCfg, } sn := (&tailcfg.Node{ CapMap: capMap, @@ -792,8 +793,8 @@ func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, pools appctype.Conn t.Fatalf("unexpected error marshaling pools in test setup: %v", err) } capMap := tailcfg.NodeCapMap{ - tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg, - tailcfg.NodeCapability(AppConnectorsExperimentalIPPoolsAttrName): {tailcfg.RawMessage(poolsBytes)}, + nodecap.Cap(AppConnectorsExperimentalAttrName): cfg, + nodecap.Cap(AppConnectorsExperimentalIPPoolsAttrName): {tailcfg.RawMessage(poolsBytes)}, } return (&tailcfg.Node{ diff --git a/feature/relayserver/relayserver.go b/feature/relayserver/relayserver.go index 2de5654c7..4ed84e4c6 100644 --- a/feature/relayserver/relayserver.go +++ b/feature/relayserver/relayserver.go @@ -21,6 +21,7 @@ "tailscale.com/net/udprelay/status" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/views" @@ -209,7 +210,7 @@ func (e *extension) handleRelayServerStaticAddrPortsLocked() { func (e *extension) selfNodeViewChanged(nodeView tailcfg.NodeView) { e.mu.Lock() defer e.mu.Unlock() - e.hasNodeAttrDisableRelayServer = nodeView.HasCap(tailcfg.NodeAttrDisableRelayServer) + e.hasNodeAttrDisableRelayServer = nodeView.HasCap(nodecap.DisableRelayServer) e.handleRelayServerLifetimeLocked() } diff --git a/feature/routecheck/routertracker_test.go b/feature/routecheck/routertracker_test.go index 753fb6bf4..8c6936c78 100644 --- a/feature/routecheck/routertracker_test.go +++ b/feature/routecheck/routertracker_test.go @@ -17,6 +17,7 @@ "tailscale.com/ipn/ipnstate" netroutecheck "tailscale.com/net/routecheck" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/opt" ) @@ -210,8 +211,8 @@ func makeSelfNodeWithRouteCheckEnabled(t *testing.T) tailcfg.NodeView { t.Helper() self := (&tailcfg.Node{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrClientSideReachability: nil, - tailcfg.NodeAttrClientSideReachabilityRouteCheck: nil, + nodecap.ClientSideReachability: nil, + nodecap.ClientSideReachabilityRouteCheck: nil, }, }).View() if !netroutecheck.IsEnabled(self) { diff --git a/feature/taildrop/ext.go b/feature/taildrop/ext.go index 5840c9d95..c95832cec 100644 --- a/feature/taildrop/ext.go +++ b/feature/taildrop/ext.go @@ -22,6 +22,8 @@ "tailscale.com/ipn/ipnext" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstime" "tailscale.com/types/empty" "tailscale.com/types/logger" @@ -124,7 +126,7 @@ func (e *Extension) onSelfChange(self tailcfg.NodeView) { if self.Valid() { e.selfUID = self.User() } - e.capFileSharing = self.Valid() && self.CapMap().Contains(tailcfg.CapabilityFileSharing) + e.capFileSharing = self.Valid() && self.CapMap().Contains(nodecap.FileSharing) osshare.SetFileSharingEnabled(e.capFileSharing, e.logf) } @@ -353,7 +355,7 @@ func (e *Extension) FileTargets() ([]*apitype.FileTarget, error) { if self == p.User() { return true } - if nb.PeerHasCap(p, tailcfg.PeerCapabilityFileSharingTarget) { + if nb.PeerHasCap(p, peercap.FileSharingTarget) { // Explicitly noted in the netmap ACL caps as a target. return true } @@ -400,7 +402,7 @@ func (e *Extension) taildropTargetStatus(p tailcfg.NodeView, nb ipnext.NodeBacke } if selfUID != p.User() { // Different user must have the explicit file sharing target capability - if !nb.PeerHasCap(p, tailcfg.PeerCapabilityFileSharingTarget) { + if !nb.PeerHasCap(p, peercap.FileSharingTarget) { return ipnstate.TaildropTargetOwnedByOtherUser } } diff --git a/feature/taildrop/peerapi.go b/feature/taildrop/peerapi.go index 8b92c8c85..53ea10dd9 100644 --- a/feature/taildrop/peerapi.go +++ b/feature/taildrop/peerapi.go @@ -13,7 +13,7 @@ "time" "tailscale.com/ipn/ipnlocal" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstime" "tailscale.com/util/clientmetric" "tailscale.com/util/httphdr" @@ -33,7 +33,7 @@ func canPutFile(h ipnlocal.PeerAPIHandler) bool { // Unsigned peers can't send files. return false } - return h.IsSelfUntagged() || h.PeerCaps().HasCapability(tailcfg.PeerCapabilityFileSharingSend) + return h.IsSelfUntagged() || h.PeerCaps().HasCapability(peercap.FileSharingSend) } func handlePeerPut(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) { diff --git a/feature/taildrop/peerapi_test.go b/feature/taildrop/peerapi_test.go index 65f881be9..0aa6fa56d 100644 --- a/feature/taildrop/peerapi_test.go +++ b/feature/taildrop/peerapi_test.go @@ -21,6 +21,7 @@ "tailscale.com/client/tailscale/apitype" "tailscale.com/ipn/ipnlocal" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstime" "tailscale.com/types/logger" @@ -481,7 +482,7 @@ func(t *testing.T, env *peerAPITestEnv) { }, } if tt.debugCap { - selfNode.CapMap = tailcfg.NodeCapMap{tailcfg.CapabilityDebug: nil} + selfNode.CapMap = tailcfg.NodeCapMap{nodecap.Debug: nil} } var rootDir string var fo FileOps diff --git a/feature/wakeonlan/wakeonlan.go b/feature/wakeonlan/wakeonlan.go index 5a567ad44..52c961eed 100644 --- a/feature/wakeonlan/wakeonlan.go +++ b/feature/wakeonlan/wakeonlan.go @@ -20,6 +20,7 @@ "tailscale.com/hostinfo" "tailscale.com/ipn/ipnlocal" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/util/clientmetric" ) @@ -90,7 +91,7 @@ func canWakeOnLAN(h ipnlocal.PeerAPIHandler) bool { if h.Peer().UnsignedPeerAPIOnly() { return false } - return h.IsSelfUntagged() || h.PeerCaps().HasCapability(tailcfg.PeerCapabilityWakeOnLAN) + return h.IsSelfUntagged() || h.PeerCaps().HasCapability(peercap.WakeOnLAN) } var metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol") diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 4b1f84e1e..6a5353aed 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -12,6 +12,7 @@ "tailscale.com/drive" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/opt" "tailscale.com/types/persist" "tailscale.com/types/preftype" @@ -247,7 +248,7 @@ func (src *HTTPHandler) Clone() *HTTPHandler { Path string Proxy string Text string - AcceptAppCaps []tailcfg.PeerCapability + AcceptAppCaps []peercap.Cap Redirect string }{}) diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 67ff02f6b..38695057b 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -15,6 +15,7 @@ "github.com/go-json-experiment/json/jsontext" "tailscale.com/drive" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/opt" "tailscale.com/types/persist" "tailscale.com/types/preftype" @@ -932,7 +933,7 @@ func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy } func (v HTTPHandlerView) Text() string { return v.ж.Text } // peer capabilities to forward in grant header, e.g. example.com/cap/mon -func (v HTTPHandlerView) AcceptAppCaps() views.Slice[tailcfg.PeerCapability] { +func (v HTTPHandlerView) AcceptAppCaps() views.Slice[peercap.Cap] { return views.SliceOf(v.ж.AcceptAppCaps) } @@ -950,7 +951,7 @@ func (v HTTPHandlerView) Redirect() string { return v.ж.Redirect } Path string Proxy string Text string - AcceptAppCaps []tailcfg.PeerCapability + AcceptAppCaps []peercap.Cap Redirect string }{}) diff --git a/ipn/ipnext/ipnext.go b/ipn/ipnext/ipnext.go index 261428721..1e817c084 100644 --- a/ipn/ipnext/ipnext.go +++ b/ipn/ipnext/ipnext.go @@ -18,6 +18,7 @@ "tailscale.com/ipn/ipnauth" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/tstime" "tailscale.com/types/key" @@ -526,7 +527,7 @@ type NodeBackend interface { PeerCaps(src netip.Addr) tailcfg.PeerCapMap // PeerHasCap reports whether the peer has the specified peer capability. - PeerHasCap(peer tailcfg.NodeView, cap tailcfg.PeerCapability) bool + PeerHasCap(peer tailcfg.NodeView, cap peercap.Cap) bool // PeerAPIBase returns the "http://ip:port" URL base to reach peer's // PeerAPI, or the empty string if the peer is invalid or doesn't support diff --git a/ipn/ipnlocal/dnsconfig_test.go b/ipn/ipnlocal/dnsconfig_test.go index 3198222f1..712ed9901 100644 --- a/ipn/ipnlocal/dnsconfig_test.go +++ b/ipn/ipnlocal/dnsconfig_test.go @@ -14,6 +14,7 @@ "tailscale.com/ipn" "tailscale.com/net/dns" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/types/dnstype" "tailscale.com/types/netmap" @@ -115,14 +116,14 @@ func TestDNSConfigForNetmap(t *testing.T) { Name: "myname.net.", Addresses: ipps("100.101.101.101"), }).View(), - AllCaps: set.SetOf([]tailcfg.NodeCapability{tailcfg.NodeAttrDNSSubdomainResolve}), + AllCaps: set.SetOf([]nodecap.Cap{nodecap.DNSSubdomainResolve}), }, peers: nodeViews([]*tailcfg.Node{ { ID: 1, Name: "peer-with-cap.net.", Addresses: ipps("100.102.0.1"), - CapMap: tailcfg.NodeCapMap{tailcfg.NodeAttrDNSSubdomainResolve: nil}, + CapMap: tailcfg.NodeCapMap{nodecap.DNSSubdomainResolve: nil}, }, { ID: 2, @@ -397,12 +398,12 @@ func TestDNSConfigForNetmap(t *testing.T) { Name: "a", Addresses: ipps("100.101.101.101"), CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeCapability(appc.AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{ + nodecap.Cap(appc.AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{ tailcfg.RawMessage(`{"name":"app1","connectors":["tag:woo"],"domains":["example.com"]}`), }, }, }).View(), - AllCaps: set.Of(tailcfg.NodeCapability(appc.AppConnectorsExperimentalAttrName)), + AllCaps: set.Of(nodecap.Cap(appc.AppConnectorsExperimentalAttrName)), }, peers: nodeViews([]*tailcfg.Node{ { diff --git a/ipn/ipnlocal/drive.go b/ipn/ipnlocal/drive.go index 70a52cb1d..81c328a84 100644 --- a/ipn/ipnlocal/drive.go +++ b/ipn/ipnlocal/drive.go @@ -17,7 +17,7 @@ "tailscale.com/drive" "tailscale.com/ipn" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/logger" "tailscale.com/types/views" "tailscale.com/util/httpm" @@ -381,7 +381,7 @@ func (s driveRemoteSource) Remotes() iter.Seq[*drive.Remote] { b.logf("[v2] taildrive: peer %s (%s, id=%v) PeerAPI unreachable", peerKey, peerName, peerID) return false } - if cn.PeerHasCap(peer, tailcfg.PeerCapabilityTaildriveSharer) { + if cn.PeerHasCap(peer, peercap.TaildriveSharer) { b.logf("[v2] taildrive: peer %s (%s, id=%v) available", peerKey, peerName, peerID) return true } diff --git a/ipn/ipnlocal/drive_test.go b/ipn/ipnlocal/drive_test.go index faa32f3d1..9d51d6516 100644 --- a/ipn/ipnlocal/drive_test.go +++ b/ipn/ipnlocal/drive_test.go @@ -24,6 +24,8 @@ "tailscale.com/ipn" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/ipproto" @@ -157,7 +159,7 @@ func TestDriveRemoteSourceAccessGate(t *testing.T) { }).View(), } - install := func(allCaps set.Set[tailcfg.NodeCapability]) { + install := func(allCaps set.Set[nodecap.Cap]) { b.mu.Lock() defer b.mu.Unlock() b.setNetMapLocked(&netmap.NetworkMap{ @@ -181,7 +183,7 @@ func TestDriveRemoteSourceAccessGate(t *testing.T) { t.Errorf("Remotes without DriveAccess cap: got %d entries, want 0", len(got)) } - install(set.Of(tailcfg.NodeAttrsTaildriveAccess)) + install(set.Of(nodecap.TaildriveAccess)) if got := collect(); len(got) != len(peers) { t.Errorf("Remotes with DriveAccess cap: got %d entries, want %d", len(got), len(peers)) } @@ -376,7 +378,7 @@ func (h *driveEndToEndHarness) filterMatchesFor(specs []peerSpec) []filtertype.M Srcs: []netip.Prefix{netip.PrefixFrom(s.addr, s.addr.BitLen())}, Caps: []filtertype.CapMatch{{ Dst: netip.PrefixFrom(h.selfAddr, h.selfAddr.BitLen()), - Cap: tailcfg.PeerCapabilityTaildriveSharer, + Cap: peercap.TaildriveSharer, }}, }) } @@ -403,7 +405,7 @@ func (h *driveEndToEndHarness) installNetMap(specs []peerSpec) { }).View(), Domain: h.domain, Peers: peers, - AllCaps: set.Of(tailcfg.NodeAttrsTaildriveAccess), + AllCaps: set.Of(nodecap.TaildriveAccess), PacketFilter: h.filterMatchesFor(specs), } h.cc.send(sendOpt{loginFinished: true, nm: nm}) diff --git a/ipn/ipnlocal/drive_tomove.go b/ipn/ipnlocal/drive_tomove.go index ccea48f7a..86f7fa883 100644 --- a/ipn/ipnlocal/drive_tomove.go +++ b/ipn/ipnlocal/drive_tomove.go @@ -7,7 +7,7 @@ package ipnlocal -import "tailscale.com/tailcfg" +import "tailscale.com/tailcfg/nodecap" const ( // DriveLocalPort is the port on which the Taildrive listens for location @@ -19,12 +19,12 @@ // enabled. This is currently based on checking for the drive:share node // attribute. func (b *LocalBackend) DriveSharingEnabled() bool { - return b.currentNode().SelfHasCap(tailcfg.NodeAttrsTaildriveShare) + return b.currentNode().SelfHasCap(nodecap.TaildriveShare) } // DriveAccessEnabled reports whether accessing Taildrive shares on remote nodes // is enabled. This is currently based on checking for the drive:access node // attribute. func (b *LocalBackend) DriveAccessEnabled() bool { - return b.currentNode().SelfHasCap(tailcfg.NodeAttrsTaildriveAccess) + return b.currentNode().SelfHasCap(nodecap.TaildriveAccess) } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 3f9e45310..57c91bd90 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -71,6 +71,7 @@ "tailscale.com/paths" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tsd" "tailscale.com/tstime" "tailscale.com/types/appctype" @@ -1535,7 +1536,7 @@ func (b *LocalBackend) updateStatusLocked(sb *ipnstate.StatusBuilder) { if sn := nm.SelfNode; sn.Valid() { peerStatusFromNode(ss, sn) if cm := sn.CapMap(); cm.Len() > 0 { - ss.Capabilities = make([]tailcfg.NodeCapability, 1, cm.Len()+1) + ss.Capabilities = make([]nodecap.Cap, 1, cm.Len()+1) ss.Capabilities[0] = "HTTPS://TAILSCALE.COM/s/DEPRECATED-NODE-CAPS#see-https://github.com/tailscale/tailscale/issues/11508" ss.CapMap = make(tailcfg.NodeCapMap, sn.CapMap().Len()) for k, v := range cm.All() { @@ -1991,7 +1992,7 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c // Perform all reconfiguration based on the netmap here. if st.NetMap != nil { - b.capTailnetLock = st.NetMap.HasCap(tailcfg.CapabilityTailnetLock) + b.capTailnetLock = st.NetMap.HasCap(nodecap.TailnetLock) b.setWebClientAtomicBoolLocked(st.NetMap.AllCaps) b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded @@ -2023,7 +2024,7 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c // Now complete the lock-free parts of what we started while locked. if st.NetMap != nil { - if envknob.NoLogsNoSupport() && st.NetMap.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) { + if envknob.NoLogsNoSupport() && st.NetMap.HasCap(nodecap.DataPlaneAuditLogs) { msg := "tailnet requires logging to be enabled. Remove --no-logs-no-support from tailscaled command line." b.health.SetLocalLogConfigHealth(errors.New(msg)) // Get the current prefs again, since we unlocked above. @@ -2068,7 +2069,7 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c b.MagicConn().SetDERPMap(st.NetMap.DERPMap) } - b.MagicConn().SetOnlyTCP443(st.NetMap.HasCap(tailcfg.NodeAttrOnlyTCP443)) + b.MagicConn().SetOnlyTCP443(st.NetMap.HasCap(nodecap.OnlyTCP443)) // Update our cached DERP map dnsfallback.UpdateCache(st.NetMap.DERPMap, b.logf) @@ -2552,7 +2553,7 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo // Note we do this AFTER the updates are applied in the nodeBackend, so that // we can get its updated views to put back into the cache. if buildfeatures.HasCacheNetMap && - cn.SelfHasCap(tailcfg.NodeAttrCacheNetworkMaps) && + cn.SelfHasCap(nodecap.CacheNetworkMaps) && envknob.BoolDefaultTrue("TS_USE_CACHED_NETMAP") { var peersToUpdate []tailcfg.NodeView @@ -3297,7 +3298,7 @@ func addServiceIPs(localNetsB *netipx.IPSetBuilder, selfNode tailcfg.NodeView) e return nil } - serviceMap, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](selfNode.CapMap(), tailcfg.NodeAttrServiceHost) + serviceMap, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](selfNode.CapMap(), nodecap.ServiceHost) if err != nil { return err } @@ -4996,7 +4997,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error { return nil } // Assume that we do have the SSH capability if don't have a netmap yet. - if !b.currentNode().SelfHasCapOr(tailcfg.CapabilitySSH, true) { + if !b.currentNode().SelfHasCapOr(nodecap.SSH, true) { if b.isDefaultServerLocked() { return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet. See https://tailscale.com/s/ssh") } @@ -5021,7 +5022,7 @@ func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage } isDefault := b.isDefaultServerLocked() - if !nm.HasCap(tailcfg.CapabilityAdmin) { + if !nm.HasCap(nodecap.Admin) { return healthmsg.TailscaleSSHOnBut + "access controls don't allow anyone to access this device. Ask your admin to update your tailnet's ACLs to allow access." } if !isDefault { @@ -6071,7 +6072,7 @@ func (b *LocalBackend) authReconfigLocked() { prefs := b.pm.CurrentPrefs() hasPAC := b.interfaceState.HasPAC() - disableSubnetsIfPAC := cn.SelfHasCap(tailcfg.NodeAttrDisableSubnetsIfPAC) + disableSubnetsIfPAC := cn.SelfHasCap(nodecap.DisableSubnetsIfPAC) dohURL, dohURLOK := cn.exitNodeCanProxyDNS(prefs.ExitNodeID()) dcfg := cn.dnsConfigForNetmap(prefs, b.keyExpired, cmp.Or(b.goos, runtime.GOOS)) // If the current node is an app connector, ensure the app connector machine is started @@ -6537,7 +6538,7 @@ func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView NetfilterMode: prefs.NetfilterMode(), Routes: b.currentNode().osRoutes(), NetfilterKind: netfilterKind, - RemoveCGNATDropRule: nm.HasCap(tailcfg.NodeAttrDisableLinuxCGNATDropRule), + RemoveCGNATDropRule: nm.HasCap(nodecap.DisableLinuxCGNATDropRule), } if buildfeatures.HasSynology && distro.Get() == distro.Synology { @@ -7027,10 +7028,10 @@ func (b *LocalBackend) ShouldExposeRemoteWebClient() bool { // if the caller has no netmap. // // b.mu must be held. -func (b *LocalBackend) setWebClientAtomicBoolLocked(caps set.Set[tailcfg.NodeCapability]) { +func (b *LocalBackend) setWebClientAtomicBoolLocked(caps set.Set[nodecap.Cap]) { syncs.RequiresMutex(&b.mu) - shouldRun := !caps.Contains(tailcfg.NodeAttrDisableWebClient) + shouldRun := !caps.Contains(nodecap.DisableWebClient) wasRunning := b.webClientAtomicBool.Swap(shouldRun) if wasRunning && !shouldRun { b.goTracker.Go(b.webClientShutdown) // stop web client @@ -7341,9 +7342,9 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { } if runtime.GOOS == "linux" && buildfeatures.HasOSRouter { - if nm.HasCap(tailcfg.NodeAttrLinuxMustUseIPTables) { + if nm.HasCap(nodecap.LinuxMustUseIPTables) { b.capForcedNetfilter = "iptables" - } else if nm.HasCap(tailcfg.NodeAttrLinuxMustUseNfTables) { + } else if nm.HasCap(nodecap.LinuxMustUseNfTables) { b.capForcedNetfilter = "nftables" } else { b.capForcedNetfilter = "" // empty string means client can auto-detect @@ -7359,7 +7360,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { } if buildfeatures.HasDebug { - var caps set.Set[tailcfg.NodeCapability] + var caps set.Set[nodecap.Cap] if nm != nil { caps = nm.AllCaps } @@ -7367,12 +7368,12 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { } // See the netns package for documentation on what these capability do. - netns.SetBindToInterfaceByRoute(b.logf, nm.HasCap(tailcfg.CapabilityBindToInterfaceByRoute)) + netns.SetBindToInterfaceByRoute(b.logf, nm.HasCap(nodecap.BindToInterfaceByRoute)) if runtime.GOOS == "android" { - netns.SetDisableAndroidBindToActiveNetwork(b.logf, nm.HasCap(tailcfg.NodeAttrDisableAndroidBindToActiveNetwork)) + netns.SetDisableAndroidBindToActiveNetwork(b.logf, nm.HasCap(nodecap.DisableAndroidBindToActiveNetwork)) } - netns.SetDisableBindConnToInterface(b.logf, nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterface)) - netns.SetDisableBindConnToInterfaceAppleExt(b.logf, nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterfaceAppleExt)) + netns.SetDisableBindConnToInterface(b.logf, nm.HasCap(nodecap.DebugDisableBindConnToInterface)) + netns.SetDisableBindConnToInterfaceAppleExt(b.logf, nm.HasCap(nodecap.DebugDisableBindConnToInterfaceAppleExt)) b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) if buildfeatures.HasServe { @@ -7443,7 +7444,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { // not being updated (because of the envknob) and could be read back when // the node starts up. if nm != nil { - if b.currentNode().SelfHasCap(tailcfg.NodeAttrCacheNetworkMaps) && envknob.BoolDefaultTrue("TS_USE_CACHED_NETMAP") { + if b.currentNode().SelfHasCap(nodecap.CacheNetworkMaps) && envknob.BoolDefaultTrue("TS_USE_CACHED_NETMAP") { if err := b.writeNetmapToDiskLockedWithPeers(nm); err != nil { b.logf("write netmap to cache: %v", err) } @@ -7500,10 +7501,10 @@ func roundTraffic(bytes int64) float64 { // setDebugLogsByCapabilityLocked sets debug logging based on the self node's // capabilities. caps may be nil if the caller has no netmap. -func (b *LocalBackend) setDebugLogsByCapabilityLocked(caps set.Set[tailcfg.NodeCapability]) { +func (b *LocalBackend) setDebugLogsByCapabilityLocked(caps set.Set[nodecap.Cap]) { // These are sufficiently cheap (atomic bools) that we don't need to // store state and compare. - if caps.Contains(tailcfg.CapabilityDebugTSDNSResolution) { + if caps.Contains(nodecap.DebugTSDNSResolution) { dnscache.SetDebugLoggingEnabled(true) } else { dnscache.SetDebugLoggingEnabled(false) @@ -8069,7 +8070,7 @@ func (s netLogNodeSource) NetLogIDs() (nodeID, domainID logid.PrivateID, logExit if nm == nil || !nm.SelfNode.Valid() { return } - if !nm.SelfNode.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) { + if !nm.SelfNode.HasCap(nodecap.DataPlaneAuditLogs) { return } if nm.SelfNode.DataPlaneAuditLogID() == "" || nm.DomainAuditLogID == "" { @@ -8086,7 +8087,7 @@ func (s netLogNodeSource) NetLogIDs() (nodeID, domainID logid.PrivateID, logExit if errNode != nil || errDomain != nil { return logid.PrivateID{}, logid.PrivateID{}, false, false } - return nodeID, domainID, nm.SelfNode.HasCap(tailcfg.NodeAttrLogExitFlows), true + return nodeID, domainID, nm.SelfNode.HasCap(nodecap.LogExitFlows), true } // Compile-time assertion that netLogNodeSource implements @@ -8784,7 +8785,7 @@ func fillAllowedSuggestions(polc policyclient.Client) (set.Set[tailcfg.StableNod // Errors are always logged. Suggestions are logged if they defer from prevSuggestion. func suggestExitNode(preferredDERP int, regionLatency map[int]time.Duration, rp RouteCheckReport, nb *nodeBackend, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) { switch { - case nb.SelfHasCap(tailcfg.NodeAttrTrafficSteering): + case nb.SelfHasCap(nodecap.TrafficSteering): // The traffic-steering feature flag is enabled on this tailnet. res, err = suggestExitNodeUsingTrafficSteering(rp, nb, allowList) default: @@ -8833,7 +8834,7 @@ func suggestExitNodeUsingDERP(preferredRegionID int, regionLatency map[int]time. if allowList != nil && !allowList.Contains(peer.StableID()) { return false } - return peer.CapMap().Contains(tailcfg.NodeAttrSuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) + return peer.CapMap().Contains(nodecap.SuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) }) if len(candidates) == 0 { return res, nil @@ -8966,7 +8967,7 @@ func suggestExitNodeUsingTrafficSteering(rp RouteCheckReport, nb *nodeBackend, a return apitype.ExitNodeSuggestionResponse{}, ErrNoNetMap } - if !nb.SelfHasCap(tailcfg.NodeAttrTrafficSteering) { + if !nb.SelfHasCap(nodecap.TrafficSteering) { panic("missing traffic-steering capability") } @@ -8980,7 +8981,7 @@ func suggestExitNodeUsingTrafficSteering(rp RouteCheckReport, nb *nodeBackend, a if allowed != nil && !allowed.Contains(p.StableID()) { return false } - if !p.CapMap().Contains(tailcfg.NodeAttrSuggestExitNode) { + if !p.CapMap().Contains(nodecap.SuggestExitNode) { return false } if !tsaddr.ContainsExitRoutes(p.AllowedIPs()) { @@ -9128,7 +9129,7 @@ func isAllowedAutoExitNodeID(polc policyclient.Client, exitNodeID tailcfg.Stable // // TODO(bradfitz): optimize this later if/when it matters. // TODO(nickkhyl): move this into [nodeBackend] along with [LocalBackend.updateFilterLocked]. -func (b *LocalBackend) srcIPHasCapForFilter(srcIP netip.Addr, cap tailcfg.NodeCapability) bool { +func (b *LocalBackend) srcIPHasCapForFilter(srcIP netip.Addr, cap nodecap.Cap) bool { if cap == "" { // Shouldn't happen, but just in case. // But the empty cap also shouldn't be found in Node.CapMap. diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index b00759563..a1721fadc 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -49,6 +49,7 @@ "tailscale.com/net/tsaddr" "tailscale.com/net/tsdial" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/tstest/deptest" @@ -672,7 +673,7 @@ func TestUpdateNetMapCache(t *testing.T) { // Now enable the netmap caching attribute, and send another update. // After doing so, the cache should have real data in it. - testMap.AllCaps = set.Of(tailcfg.NodeAttrCacheNetworkMaps) + testMap.AllCaps = set.Of(nodecap.CacheNetworkMaps) clb.mu.Lock() clb.setNetMapLocked(testMap) @@ -1803,7 +1804,7 @@ func TestStatusPeerCapabilities(t *testing.T) { tests := []struct { name string peers []tailcfg.NodeView - expectedPeerCapabilities map[tailcfg.StableNodeID][]tailcfg.NodeCapability + expectedPeerCapabilities map[tailcfg.StableNodeID][]nodecap.Cap expectedPeerCapMap map[tailcfg.StableNodeID]tailcfg.NodeCapMap }{ { @@ -1815,9 +1816,9 @@ func TestStatusPeerCapabilities(t *testing.T) { Key: makeNodeKeyFromID(1), IsWireGuardOnly: true, Hostinfo: (&tailcfg.Hostinfo{}).View(), - Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilitySSH}, - CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilitySSH: nil, + Capabilities: []nodecap.Cap{nodecap.SSH}, + CapMap: (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.SSH: nil, }), }).View(), (&tailcfg.Node{ @@ -1825,9 +1826,9 @@ func TestStatusPeerCapabilities(t *testing.T) { StableID: "bar", Key: makeNodeKeyFromID(2), Hostinfo: (&tailcfg.Hostinfo{}).View(), - Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilityAdmin}, - CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilityAdmin: {`{"test": "true}`}, + Capabilities: []nodecap.Cap{nodecap.Admin}, + CapMap: (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.Admin: {`{"test": "true}`}, }), }).View(), (&tailcfg.Node{ @@ -1835,26 +1836,26 @@ func TestStatusPeerCapabilities(t *testing.T) { StableID: "baz", Key: makeNodeKeyFromID(3), Hostinfo: (&tailcfg.Hostinfo{}).View(), - Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilityOwner}, - CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilityOwner: nil, + Capabilities: []nodecap.Cap{nodecap.Owner}, + CapMap: (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.Owner: nil, }), }).View(), }, - expectedPeerCapabilities: map[tailcfg.StableNodeID][]tailcfg.NodeCapability{ - tailcfg.StableNodeID("foo"): {tailcfg.CapabilitySSH}, - tailcfg.StableNodeID("bar"): {tailcfg.CapabilityAdmin}, - tailcfg.StableNodeID("baz"): {tailcfg.CapabilityOwner}, + expectedPeerCapabilities: map[tailcfg.StableNodeID][]nodecap.Cap{ + tailcfg.StableNodeID("foo"): {nodecap.SSH}, + tailcfg.StableNodeID("bar"): {nodecap.Admin}, + tailcfg.StableNodeID("baz"): {nodecap.Owner}, }, expectedPeerCapMap: map[tailcfg.StableNodeID]tailcfg.NodeCapMap{ - tailcfg.StableNodeID("foo"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilitySSH: nil, + tailcfg.StableNodeID("foo"): (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.SSH: nil, }), - tailcfg.StableNodeID("bar"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilityAdmin: {`{"test": "true}`}, + tailcfg.StableNodeID("bar"): (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.Admin: {`{"test": "true}`}, }), - tailcfg.StableNodeID("baz"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilityOwner: nil, + tailcfg.StableNodeID("baz"): (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.Owner: nil, }), }, }, @@ -3395,7 +3396,7 @@ func TestReconfigureAppConnector(t *testing.T) { SelfNode: (&tailcfg.Node{ Name: "example.ts.net", Tags: []string{"tag:example"}, - CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ + CapMap: (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ "tailscale.com/app-connectors": {tailcfg.RawMessage(appCfg)}, }), }).View(), @@ -4753,7 +4754,7 @@ func TestTCPHandlerForDstWithVIPService(t *testing.T) { SelfNode: (&tailcfg.Node{ Name: "example.ts.net", CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}, + nodecap.ServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}, }, }).View(), UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{ @@ -5133,7 +5134,7 @@ func TestDriveManageShares(t *testing.T) { if !tt.disabled { nm := new(*b.currentNode().NetMap()) self := nm.SelfNode.AsStruct() - self.CapMap = tailcfg.NodeCapMap{tailcfg.NodeAttrsTaildriveShare: nil} + self.CapMap = tailcfg.NodeCapMap{nodecap.TaildriveShare: nil} nm.SelfNode = self.View() b.currentNode().SetNetMap(nm) b.sys.Set(driveimpl.NewFileSystemForRemote(b.logf)) @@ -5339,7 +5340,7 @@ func withExitRoutes() peerOptFunc { func withSuggest() peerOptFunc { return func(n *tailcfg.Node) { - mak.Set(&n.CapMap, tailcfg.NodeAttrSuggestExitNode, []tailcfg.RawMessage{}) + mak.Set(&n.CapMap, nodecap.SuggestExitNode, []tailcfg.RawMessage{}) } } @@ -6173,7 +6174,7 @@ func TestSuggestExitNodeTrafficSteering(t *testing.T) { netip.MustParsePrefix("fe70::1/128"), }, CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrTrafficSteering: []tailcfg.RawMessage{}, + nodecap.TrafficSteering: []tailcfg.RawMessage{}, }, } @@ -8014,7 +8015,7 @@ func TestSrcCapPacketFilter(t *testing.T) { }, PacketFilter: []filtertype.Match{{ IPProto: views.SliceOf([]ipproto.Proto{ipproto.TCP}), - SrcCaps: []tailcfg.NodeCapability{"cap-X"}, // cap in packet filter rule + SrcCaps: []nodecap.Cap{"cap-X"}, // cap in packet filter rule Dsts: []filtertype.NetPortRange{{ Net: netip.MustParsePrefix("1.1.1.1/32"), Ports: filtertype.PortRange{ @@ -8075,7 +8076,7 @@ func TestSrcCapPacketFilterUnsignedPeer(t *testing.T) { }, PacketFilter: []filtertype.Match{{ IPProto: views.SliceOf([]ipproto.Proto{ipproto.TCP}), - SrcCaps: []tailcfg.NodeCapability{"cap-X"}, + SrcCaps: []nodecap.Cap{"cap-X"}, Dsts: []filtertype.NetPortRange{{ Net: netip.MustParsePrefix("1.1.1.1/32"), Ports: filtertype.PortRange{ @@ -8897,7 +8898,7 @@ func TestRouteAllDisabled(t *testing.T) { pp("100.64.1.1/32"), }, CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{ + nodecap.ServiceHost: []tailcfg.RawMessage{ tailcfg.RawMessage(svcIPMapJSON), }, }, diff --git a/ipn/ipnlocal/netmapcache/netmapcache.go b/ipn/ipnlocal/netmapcache/netmapcache.go index 3c4b01cf3..531208a44 100644 --- a/ipn/ipnlocal/netmapcache/netmapcache.go +++ b/ipn/ipnlocal/netmapcache/netmapcache.go @@ -24,6 +24,7 @@ "tailscale.com/feature/buildfeatures" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/netmap" "tailscale.com/util/mak" "tailscale.com/util/set" @@ -339,7 +340,7 @@ func (c *Cache) Load(ctx context.Context) (*netmap.NetworkMap, error) { // If we successfully recovered a SelfNode, pull out its related fields. if s := nm.SelfNode; s.Valid() { nm.NodeKey = s.Key() - nm.AllCaps = make(set.Set[tailcfg.NodeCapability]) + nm.AllCaps = make(set.Set[nodecap.Cap]) for _, c := range s.Capabilities().All() { nm.AllCaps.Add(c) } diff --git a/ipn/ipnlocal/netmapcache/netmapcache_test.go b/ipn/ipnlocal/netmapcache/netmapcache_test.go index 884e450e4..befedccc1 100644 --- a/ipn/ipnlocal/netmapcache/netmapcache_test.go +++ b/ipn/ipnlocal/netmapcache/netmapcache_test.go @@ -23,6 +23,7 @@ "github.com/google/go-cmp/cmp/cmpopts" "tailscale.com/ipn/ipnlocal/netmapcache" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/types/ipproto" "tailscale.com/types/key" @@ -119,12 +120,12 @@ func init() { User: 30337, Name: "test.example.com.", Key: testNodeKey, - Capabilities: []tailcfg.NodeCapability{"cap1"}, - CapMap: map[tailcfg.NodeCapability][]tailcfg.RawMessage{ + Capabilities: []nodecap.Cap{"cap1"}, + CapMap: map[nodecap.Cap][]tailcfg.RawMessage{ "cap2": nil, }, }).View(), - AllCaps: set.Of[tailcfg.NodeCapability]("cap1", "cap2"), + AllCaps: set.Of[nodecap.Cap]("cap1", "cap2"), NodeKey: testNodeKey, DNS: tailcfg.DNSConfig{Domains: []string{"example1.com", "example2.ac.uk"}}, // "dns" @@ -276,11 +277,11 @@ func TestUpdateSelfOnly(t *testing.T) { Name: "alt.example.com.", Key: testNodeKey, HomeDERP: 6174, - Capabilities: []tailcfg.NodeCapability{"cap1", "cap3"}, + Capabilities: []nodecap.Cap{"cap1", "cap3"}, } updated := *testMap // shallow copy updated.SelfNode = newSelf.View() - updated.AllCaps = set.Of[tailcfg.NodeCapability]("cap1", "cap3") + updated.AllCaps = set.Of[nodecap.Cap]("cap1", "cap3") updated.DNS = tailcfg.DNSConfig{Domains: []string{"example3.org", "example4.horse"}} // Empty the peers and profiles so that we can verify the update does not diff --git a/ipn/ipnlocal/node_backend.go b/ipn/ipnlocal/node_backend.go index bf79600b0..68e065880 100644 --- a/ipn/ipnlocal/node_backend.go +++ b/ipn/ipnlocal/node_backend.go @@ -24,6 +24,8 @@ "tailscale.com/net/tsaddr" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/logger" @@ -227,13 +229,13 @@ func (nb *nodeBackend) SelfUserID() tailcfg.UserID { } // SelfHasCap reports whether the specified capability was granted to the self node in the most recent netmap. -func (nb *nodeBackend) SelfHasCap(wantCap tailcfg.NodeCapability) bool { +func (nb *nodeBackend) SelfHasCap(wantCap nodecap.Cap) bool { return nb.SelfHasCapOr(wantCap, false) } // SelfHasCapOr is like [nodeBackend.SelfHasCap], but returns the specified default value // if the netmap is not available yet. -func (nb *nodeBackend) SelfHasCapOr(wantCap tailcfg.NodeCapability, def bool) bool { +func (nb *nodeBackend) SelfHasCapOr(wantCap nodecap.Cap, def bool) bool { nb.mu.Lock() defer nb.mu.Unlock() if nb.netMap == nil { @@ -521,7 +523,7 @@ func (nb *nodeBackend) PeerCapsForService(src netip.Addr, svcName tailcfg.Servic // PeerHasCap reports whether the peer contains the given capability string, // with any value(s). -func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap tailcfg.PeerCapability) bool { +func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap peercap.Cap) bool { if !peer.Valid() { return false } @@ -536,7 +538,7 @@ func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap tailcfg.PeerCap return false } -func (nb *nodeBackend) peerHasCapLocked(addr netip.Addr, wantCap tailcfg.PeerCapability) bool { +func (nb *nodeBackend) peerHasCapLocked(addr netip.Addr, wantCap peercap.Cap) bool { return nb.peerCapsLocked(addr).HasCapability(wantCap) } @@ -583,7 +585,7 @@ func (nb *nodeBackend) PeerIsReachable(rp RouteCheckReport, p tailcfg.NodeView) self := nm.SelfNode useRouteCheck := isRouteCheckEnabled(self) - if !useRouteCheck && !self.HasCap(tailcfg.NodeAttrClientSideReachability) { + if !useRouteCheck && !self.HasCap(nodecap.ClientSideReachability) { // Legacy behavior is to always trust the control plane, which // isn’t always correct because the peer could be slow to check // in so that control marks it as offline. @@ -596,7 +598,7 @@ func (nb *nodeBackend) PeerIsReachable(rp RouteCheckReport, p tailcfg.NodeView) return true } - if !useRouteCheck && !self.HasCap(tailcfg.NodeAttrClientSideReachabilityRouteCheck) { + if !useRouteCheck && !self.HasCap(nodecap.ClientSideReachabilityRouteCheck) { // TODO(sfllaw): The following does not actually test for client-side // reachability. This would require a mechanism that tracks whether the // current node can actually reach this peer, either because they are @@ -1231,7 +1233,7 @@ func (nb *nodeBackend) magicDNSHostAddrs(fqdn dnsname.FQDN) (ips []netip.Addr, o !nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs4) { flags |= selfV6Only } - if nm.AllCaps.Contains(tailcfg.NodeAttrMagicDNSPeerAAAA) { + if nm.AllCaps.Contains(nodecap.MagicDNSPeerAAAA) { flags |= wantAAAA } return magicDNSAddrs(n.Addresses(), flags), true @@ -1274,9 +1276,9 @@ func (nb *nodeBackend) magicDNSSubdomainHost(fqdn dnsname.FQDN) bool { return false } if nm := nb.netMap; nm != nil && nm.SelfNode.Valid() && nm.SelfNode.ID() == n.ID() { - return nm.AllCaps.Contains(tailcfg.NodeAttrDNSSubdomainResolve) + return nm.AllCaps.Contains(nodecap.DNSSubdomainResolve) } - return n.CapMap().Contains(tailcfg.NodeAttrDNSSubdomainResolve) + return n.CapMap().Contains(nodecap.DNSSubdomainResolve) } // nodeByFQDNLocked returns the node (peer or self) with the given @@ -1474,7 +1476,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. addrFlags |= selfV6Only dcfg.OnlyIPv6 = true } - if nm.AllCaps.Contains(tailcfg.NodeAttrMagicDNSPeerAAAA) { + if nm.AllCaps.Contains(nodecap.MagicDNSPeerAAAA) { addrFlags |= wantAAAA } diff --git a/ipn/ipnlocal/node_backend_test.go b/ipn/ipnlocal/node_backend_test.go index 991ccf299..3548ccb4f 100644 --- a/ipn/ipnlocal/node_backend_test.go +++ b/ipn/ipnlocal/node_backend_test.go @@ -15,6 +15,7 @@ "tailscale.com/net/routecheck/peernode" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/types/key" "tailscale.com/types/netmap" @@ -239,10 +240,10 @@ func TestNodeBackendReachability(t *testing.T) { Name: "self", } if tc.cap { - mak.Set(&self.CapMap, tailcfg.NodeAttrClientSideReachability, nil) + mak.Set(&self.CapMap, nodecap.ClientSideReachability, nil) } if tc.rchk { - mak.Set(&self.CapMap, tailcfg.NodeAttrClientSideReachabilityRouteCheck, nil) + mak.Set(&self.CapMap, nodecap.ClientSideReachabilityRouteCheck, nil) } peer := &tailcfg.Node{ @@ -584,7 +585,7 @@ func TestNodeBackendMagicDNSHosts(t *testing.T) { netip.MustParsePrefix("100.64.0.2/32"), netip.MustParsePrefix("fd7a:115c:a1e0::2/128"), }, - CapMap: tailcfg.NodeCapMap{tailcfg.NodeAttrDNSSubdomainResolve: nil}, + CapMap: tailcfg.NodeCapMap{nodecap.DNSSubdomainResolve: nil}, } nb.SetNetMap(&netmap.NetworkMap{ SelfNode: self.View(), diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 53d1e6d35..6d4004392 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -35,6 +35,8 @@ "tailscale.com/net/netutil" "tailscale.com/net/sockstats" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/netmap" "tailscale.com/types/views" "tailscale.com/util/clientmetric" @@ -578,7 +580,7 @@ func (h *peerAPIHandler) CanDebug() bool { return h.canDebug() } // canDebug reports whether h can debug this node (goroutines, metrics, // magicsock internal state, etc). func (h *peerAPIHandler) canDebug() bool { - if !h.selfNode.HasCap(tailcfg.CapabilityDebug) { + if !h.selfNode.HasCap(nodecap.Debug) { // This node does not expose debug info. return false } @@ -586,17 +588,17 @@ func (h *peerAPIHandler) canDebug() bool { // Unsigned peers can't debug. return false } - return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityDebugPeer) + return h.isSelf || h.peerHasCap(peercap.DebugPeer) } var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS") // canIngress reports whether h can send ingress requests to this node. func (h *peerAPIHandler) canIngress() bool { - return h.peerHasCap(tailcfg.PeerCapabilityIngress) || (allowSelfIngress() && h.isSelf) + return h.peerHasCap(peercap.Ingress) || (allowSelfIngress() && h.isSelf) } -func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool { +func (h *peerAPIHandler) peerHasCap(wantCap peercap.Cap) bool { return h.PeerCaps().HasCapability(wantCap) } diff --git a/ipn/ipnlocal/peerapi_drive.go b/ipn/ipnlocal/peerapi_drive.go index d42843577..6bdae3d9d 100644 --- a/ipn/ipnlocal/peerapi_drive.go +++ b/ipn/ipnlocal/peerapi_drive.go @@ -11,7 +11,7 @@ "strings" "tailscale.com/drive" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/util/httpm" ) @@ -34,7 +34,7 @@ func handleServeDrive(hi PeerAPIHandler, w http.ResponseWriter, r *http.Request) } capsMap := h.PeerCaps() - driveCaps, ok := capsMap[tailcfg.PeerCapabilityTaildrive] + driveCaps, ok := capsMap[peercap.Taildrive] if !ok { h.logf("taildrive: not permitted") http.Error(w, "taildrive not permitted", http.StatusForbidden) diff --git a/ipn/ipnlocal/peerapi_test.go b/ipn/ipnlocal/peerapi_test.go index 63abf089c..fc31ed7a5 100644 --- a/ipn/ipnlocal/peerapi_test.go +++ b/ipn/ipnlocal/peerapi_test.go @@ -21,6 +21,7 @@ "tailscale.com/ipn" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/appctype" @@ -155,7 +156,7 @@ func TestHandlePeerAPI(t *testing.T) { }, } if tt.debugCap { - selfNode.CapMap = tailcfg.NodeCapMap{tailcfg.CapabilityDebug: nil} + selfNode.CapMap = tailcfg.NodeCapMap{nodecap.Debug: nil} } var e peerAPITestEnv lb := newTestLocalBackend(t) diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index 0012a214b..3730123a7 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -40,6 +40,7 @@ "tailscale.com/net/netutil" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/lazy" "tailscale.com/types/logger" "tailscale.com/types/views" @@ -92,7 +93,7 @@ type serveHTTPContext struct { // provides funnel-specific context, nil if not funneled Funnel *funnelFlow // AppCapabilities lists all PeerCapabilities that should be forwarded by serve - AppCapabilities views.Slice[tailcfg.PeerCapability] + AppCapabilities views.Slice[peercap.Cap] } // funnelFlow represents a funneled connection initiated via IngressPeer @@ -1137,7 +1138,7 @@ func (b *LocalBackend) addAppCapabilitiesHeader(r *httputil.ProxyRequest) error return nil } - peerCapsFiltered := make(map[tailcfg.PeerCapability][]tailcfg.RawMessage, acceptCaps.Len()) + peerCapsFiltered := make(map[peercap.Cap][]tailcfg.RawMessage, acceptCaps.Len()) for _, cap := range acceptCaps.AsSlice() { if peerCaps.HasCapability(cap) { peerCapsFiltered[cap] = peerCaps[cap] diff --git a/ipn/ipnlocal/serve_test.go b/ipn/ipnlocal/serve_test.go index 205dc674e..84c0b293f 100644 --- a/ipn/ipnlocal/serve_test.go +++ b/ipn/ipnlocal/serve_test.go @@ -33,6 +33,8 @@ "tailscale.com/ipn" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/logger" @@ -395,7 +397,7 @@ func TestServeConfigServices(t *testing.T) { SelfNode: (&tailcfg.Node{ Name: "example.ts.net", CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}, + nodecap.ServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}, }, }).View(), UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{ @@ -893,7 +895,7 @@ func(w http.ResponseWriter, r *http.Request) { "example.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{ "/": { Proxy: testServ.URL, - AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/interesting", "example.com/cap/boring"}, + AcceptAppCaps: []peercap.Cap{"example.com/cap/interesting", "example.com/cap/boring"}, }, }}, }, diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 3e2c79151..25a7e6e03 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -18,6 +18,7 @@ "time" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/types/key" "tailscale.com/types/views" @@ -302,7 +303,7 @@ type PeerStatus struct { // Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508 // Every value is Capabilities is also a key in CapMap, even if it // has no values in that map. - Capabilities []tailcfg.NodeCapability `json:",omitempty"` + Capabilities []nodecap.Cap `json:",omitempty"` // CapMap is a map of capabilities to their values. CapMap tailcfg.NodeCapMap `json:",omitempty"` @@ -356,7 +357,7 @@ type PeerStatus struct { ) // HasCap reports whether ps has the given capability. -func (ps *PeerStatus) HasCap(cap tailcfg.NodeCapability) bool { +func (ps *PeerStatus) HasCap(cap nodecap.Cap) bool { return ps.CapMap.Contains(cap) } diff --git a/ipn/localapi/localapi_test.go b/ipn/localapi/localapi_test.go index cc5a6b6a7..2a5db08db 100644 --- a/ipn/localapi/localapi_test.go +++ b/ipn/localapi/localapi_test.go @@ -32,6 +32,7 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/key" @@ -191,7 +192,7 @@ func TestWhoIsArgTypes(t *testing.T) { return match() }, peerCaps: map[netip.Addr]tailcfg.PeerCapMap{ - netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{ + netip.MustParseAddr("100.101.102.103"): map[peercap.Cap][]tailcfg.RawMessage{ "foo": {`"bar"`}, }, }, diff --git a/ipn/serve.go b/ipn/serve.go index 5c1fa2975..57ada0d26 100644 --- a/ipn/serve.go +++ b/ipn/serve.go @@ -17,6 +17,8 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/util/dnsname" "tailscale.com/util/mak" @@ -171,7 +173,7 @@ type HTTPHandler struct { Text string `json:",omitempty"` // plaintext to serve (primarily for testing) - AcceptAppCaps []tailcfg.PeerCapability `json:",omitempty"` // peer capabilities to forward in grant header, e.g. example.com/cap/mon + AcceptAppCaps []peercap.Cap `json:",omitempty"` // peer capabilities to forward in grant header, e.g. example.com/cap/mon // Redirect, if not empty, is the target URL to redirect requests to. // By default, we redirect with HTTP 302 (Found) status. @@ -652,10 +654,10 @@ func CheckFunnelAccess(port uint16, node *ipnstate.PeerStatus) error { // NodeCanFunnel returns an error if the given node is not configured to allow // for Tailscale Funnel usage. func NodeCanFunnel(node *ipnstate.PeerStatus) error { - if !node.HasCap(tailcfg.CapabilityHTTPS) { + if !node.HasCap(nodecap.HTTPS) { return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.") } - if !node.HasCap(tailcfg.NodeAttrFunnel) { + if !node.HasCap(nodecap.Funnel) { return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.") } return nil @@ -682,14 +684,14 @@ func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error { return "", deny("") } u.RawQuery = "" - if u.String() != string(tailcfg.CapabilityFunnelPorts) { + if u.String() != string(nodecap.FunnelPorts) { return "", deny("") } return portsStr, nil } for attr := range node.CapMap { attr := string(attr) - if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) { + if !strings.HasPrefix(attr, string(nodecap.FunnelPorts)) { continue } var err error @@ -702,7 +704,7 @@ func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error { if portsStr == "" { for attr := range node.CapMap { attr := string(attr) - if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) { + if !strings.HasPrefix(attr, string(nodecap.FunnelPorts)) { continue } var err error diff --git a/ipn/serve_test.go b/ipn/serve_test.go index b8a7aee79..7b19e65bc 100644 --- a/ipn/serve_test.go +++ b/ipn/serve_test.go @@ -8,24 +8,25 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" ) func TestCheckFunnelAccess(t *testing.T) { - caps := func(c ...tailcfg.NodeCapability) []tailcfg.NodeCapability { return c } - const portAttr tailcfg.NodeCapability = "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443," + caps := func(c ...nodecap.Cap) []nodecap.Cap { return c } + const portAttr nodecap.Cap = "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443," tests := []struct { port uint16 - caps []tailcfg.NodeCapability + caps []nodecap.Cap wantErr bool }{ {443, caps(portAttr), true}, // No "funnel" attribute - {443, caps(portAttr, tailcfg.NodeAttrFunnel), true}, - {443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false}, - {8443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false}, - {8321, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true}, - {8083, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false}, - {8091, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true}, - {3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true}, + {443, caps(portAttr, nodecap.Funnel), true}, + {443, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), false}, + {8443, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), false}, + {8321, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), true}, + {8083, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), false}, + {8091, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), true}, + {3000, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), true}, } for _, tt := range tests { cm := tailcfg.NodeCapMap{} diff --git a/k8s-operator/api-proxy/proxy.go b/k8s-operator/api-proxy/proxy.go index 73a9884b2..47a65d17b 100644 --- a/k8s-operator/api-proxy/proxy.go +++ b/k8s-operator/api-proxy/proxy.go @@ -35,6 +35,7 @@ "tailscale.com/net/netx" "tailscale.com/sessionrecording" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsnet" "tailscale.com/util/clientmetric" "tailscale.com/util/ctxkey" @@ -501,7 +502,7 @@ func (ap *APIServerProxy) authError(w http.ResponseWriter, err error) { // that is respected for this form is group impersonation - for // backwards compatibility reasons. // TODO (irbekrm): determine if anyone uses this and remove if possible. - oldCapabilityName = "https://" + tailcfg.PeerCapabilityKubernetes + oldCapabilityName = "https://" + peercap.Kubernetes ) // addImpersonationHeaders adds the appropriate headers to r to impersonate the @@ -510,7 +511,7 @@ func (ap *APIServerProxy) authError(w http.ResponseWriter, err error) { func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error { log = log.With("remote", r.RemoteAddr) who := whoIsKey.Value(r.Context()) - rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes) + rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, peercap.Kubernetes) if len(rules) == 0 && err == nil { // Try the old capability name for backwards compatibility. rules, err = tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, oldCapabilityName) @@ -571,7 +572,7 @@ func determineRecorderConfig(who *apitype.WhoIsResponse) (c recorderConfig, _ er c.failOpen = true c.enableEvents = false - rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes) + rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, peercap.Kubernetes) if err != nil { return c, fmt.Errorf("failed to unmarshal Kubernetes capability: %w", err) } diff --git a/k8s-operator/api-proxy/proxy_events_test.go b/k8s-operator/api-proxy/proxy_events_test.go index 1426f170c..7def9d303 100644 --- a/k8s-operator/api-proxy/proxy_events_test.go +++ b/k8s-operator/api-proxy/proxy_events_test.go @@ -22,6 +22,7 @@ "tailscale.com/net/netx" "tailscale.com/sessionrecording" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsnet" ) @@ -73,7 +74,7 @@ func TestRecordRequestAsEvent(t *testing.T) { LoginName: "user@example.com", }, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: []tailcfg.RawMessage{ + peercap.Kubernetes: []tailcfg.RawMessage{ tailcfg.RawMessage(`{"recorderAddrs":["127.0.0.1:1234"]}`), tailcfg.RawMessage(`{"enforceRecorder": true, "enableEvents": true}`), }, @@ -307,7 +308,7 @@ func TestRecordRequestAsEvent(t *testing.T) { Node: defaultWho.Node, UserProfile: defaultWho.UserProfile, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: []tailcfg.RawMessage{ + peercap.Kubernetes: []tailcfg.RawMessage{ tailcfg.RawMessage(`{"recorderAddrs":["127.0.0.1:1234", "127.0.0.1:5678"]}`), tailcfg.RawMessage(`{"enforceRecorder": true, "enableEvents": true}`), }, diff --git a/k8s-operator/api-proxy/proxy_test.go b/k8s-operator/api-proxy/proxy_test.go index 5d1606d76..c5b39a6fd 100644 --- a/k8s-operator/api-proxy/proxy_test.go +++ b/k8s-operator/api-proxy/proxy_test.go @@ -15,6 +15,7 @@ "go.uber.org/zap" "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/util/must" ) @@ -51,7 +52,7 @@ func TestImpersonationHeaders(t *testing.T) { name: "user-with-cap", emailish: "foo@example.com", capMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: { + peercap.Kubernetes: { tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group2"]}}`), tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group3"]}}`), // One group is duplicated. tailcfg.RawMessage(`{"impersonate":{"groups":["group4"]}}`), @@ -73,7 +74,7 @@ func TestImpersonationHeaders(t *testing.T) { emailish: "tagged-device", tags: []string{"tag:foo", "tag:bar"}, capMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: { + peercap.Kubernetes: { tailcfg.RawMessage(`{"impersonate":{"groups":["group1"]}}`), }, }, @@ -87,7 +88,7 @@ func TestImpersonationHeaders(t *testing.T) { emailish: "tagged-device", tags: []string{"tag:foo", "tag:bar"}, capMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: { + peercap.Kubernetes: { tailcfg.RawMessage(`{"impersonate":{"groups":["group1"]},"recorder":["tag:foo"],"enforceRecorder":true}`), }, }, @@ -101,7 +102,7 @@ func TestImpersonationHeaders(t *testing.T) { emailish: "tagged-device", tags: []string{"tag:foo", "tag:bar"}, capMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: { + peercap.Kubernetes: { tailcfg.RawMessage(`[]`), }, }, @@ -139,18 +140,18 @@ func Test_determineRecorderConfig(t *testing.T) { }{ { name: "two_ips_fail_closed", - who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"],"enforceRecorder":true}`}}), + who: whoResp(map[string][]string{string(peercap.Kubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"],"enforceRecorder":true}`}}), wantRecorderAddresses: []netip.AddrPort{addr1, addr2}, }, { name: "two_ips_fail_open", - who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"]}`}}), + who: whoResp(map[string][]string{string(peercap.Kubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"]}`}}), wantRecorderAddresses: []netip.AddrPort{addr1, addr2}, wantFailOpen: true, }, { name: "odd_rule_combination_fail_closed", - who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["100.99.99.99:80"],"enforceRecorder":false}`, `{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"]}`, `{"enforceRecorder":true,"impersonate":{"groups":["system:masters"]}}`}}), + who: whoResp(map[string][]string{string(peercap.Kubernetes): {`{"recorderAddrs":["100.99.99.99:80"],"enforceRecorder":false}`, `{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"]}`, `{"enforceRecorder":true,"impersonate":{"groups":["system:masters"]}}`}}), wantRecorderAddresses: []netip.AddrPort{addr2, addr1}, }, { @@ -160,7 +161,7 @@ func Test_determineRecorderConfig(t *testing.T) { }, { name: "no_recorder_caps", - who: whoResp(map[string][]string{"foo": {`{"x":"y"}`}, string(tailcfg.PeerCapabilityKubernetes): {`{"impersonate":{"groups":["system:masters"]}}`}}), + who: whoResp(map[string][]string{"foo": {`{"x":"y"}`}, string(peercap.Kubernetes): {`{"impersonate":{"groups":["system:masters"]}}`}}), wantFailOpen: true, }, } @@ -185,7 +186,7 @@ func whoResp(capMap map[string][]string) *apitype.WhoIsResponse { CapMap: tailcfg.PeerCapMap{}, } for cap, rules := range capMap { - resp.CapMap[tailcfg.PeerCapability(cap)] = raw(rules...) + resp.CapMap[peercap.Cap(cap)] = raw(rules...) } return resp } diff --git a/net/dns/resolver/forwarder.go b/net/dns/resolver/forwarder.go index bd1245ad1..ef609cae9 100644 --- a/net/dns/resolver/forwarder.go +++ b/net/dns/resolver/forwarder.go @@ -908,7 +908,7 @@ func ShouldUseRoutes(knobs *controlknobs.Knobs) bool { switch runtime.GOOS { case "android", "ios": // On mobile platforms with lower memory limits (e.g., 50MB on iOS), - // this behavior is still gated by the "user-dial-routes" nodeAttr. + // this behavior is still gated by the "user-dial-routes" nodecap. return knobs != nil && knobs.UserDialUseRoutes.Load() default: // On all other platforms, it is the default behavior, diff --git a/net/routecheck/routecheck.go b/net/routecheck/routecheck.go index 5195b12b7..cf0d11320 100644 --- a/net/routecheck/routecheck.go +++ b/net/routecheck/routecheck.go @@ -16,6 +16,7 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/net/netmon" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/logger" "tailscale.com/types/netmap" "tailscale.com/util/clientmetric" @@ -43,8 +44,8 @@ func IsEnabled(self tailcfg.NodeView) bool { return false } // TODO(sfllaw): We intend to eventually enable this behaviour by default. - return self.HasCap(tailcfg.NodeAttrClientSideReachability) && - self.HasCap(tailcfg.NodeAttrClientSideReachabilityRouteCheck) + return self.HasCap(nodecap.ClientSideReachability) && + self.HasCap(nodecap.ClientSideReachabilityRouteCheck) } // Client generates Reports describing the result of both passive and active diff --git a/ssh/tailssh/incubator.go b/ssh/tailssh/incubator.go index faf70af90..5eb409a5f 100644 --- a/ssh/tailssh/incubator.go +++ b/ssh/tailssh/incubator.go @@ -41,7 +41,7 @@ "golang.org/x/sys/unix" "tailscale.com/cmd/tailscaled/childproc" "tailscale.com/hostinfo" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/logger" "tailscale.com/version/distro" ) @@ -209,7 +209,7 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, forw } nm := ss.conn.srv.lb.NetMapNoPeers() - forceV1Behavior := nm.HasCap(tailcfg.NodeAttrSSHBehaviorV1) && !nm.HasCap(tailcfg.NodeAttrSSHBehaviorV2) + forceV1Behavior := nm.HasCap(nodecap.SSHBehaviorV1) && !nm.HasCap(nodecap.SSHBehaviorV2) if forceV1Behavior { incubatorArgs = append(incubatorArgs, "--force-v1-behavior") } @@ -233,7 +233,7 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, forw incubatorArgs = append(incubatorArgs, "--cmd="+ss.RawCommand()) } - allowSendEnv := nm.HasCap(tailcfg.NodeAttrSSHEnvironmentVariables) + allowSendEnv := nm.HasCap(nodecap.SSHEnvironmentVariables) if allowSendEnv { env, err := filterEnv(ss.conn.acceptEnv, ss.Session.Environ()) if err != nil { diff --git a/tailcfg/nodecap/nodecap.go b/tailcfg/nodecap/nodecap.go new file mode 100644 index 000000000..387c8c6a7 --- /dev/null +++ b/tailcfg/nodecap/nodecap.go @@ -0,0 +1,442 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +// Package nodecap defines the types of capabilities granted to tailnet nodes. +package nodecap + +// Cap represents a capability granted to the self node +// as listed in [tailscale.com/tailcfg/MapResponse.Node.Capabilities]. +// +// It must be a URL like "https://tailscale.com/cap/file-sharing" or "tailscale.com/cap/webui", +// or a well-known capability name like "funnel". +// The latter is only allowed for Tailscale-defined capabilities. +// +// Unlike [tailscale.com/tailcfg/peercap.Cap], +// Cap is not in context of a peer and is granted to the node itself. +// +// These are also referred to as "Node Attributes" in the ACL policy file. +type Cap string + +// Prefix is a prefix for [tailscale.com/tailcfg.NodeCapMap] keys that share a common namespace, +// where each entry represents a distinct named instance (e.g. one per service). +// The full key is formed by concatenating the prefix with the instance name. +type Prefix string + +// ToAttribute returns the full [Cap] key for the given value under this prefix, +// of the form prefix+value. +func (p Prefix) ToAttribute(value string) Cap { + return Cap(string(p) + value) +} + +const ( + FileSharing Cap = "https://tailscale.com/cap/file-sharing" + Admin Cap = "https://tailscale.com/cap/is-admin" + Owner Cap = "https://tailscale.com/cap/is-owner" + SSH Cap = "https://tailscale.com/cap/ssh" // feature enabled/available + SSHRuleIn Cap = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node + DataPlaneAuditLogs Cap = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled + Debug Cap = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI + HTTPS Cap = "https" + + // MacUIV2 makes the macOS GUI enable its v2 mode. + MacUIV2 Cap = "https://tailscale.com/cap/mac-ui-v2" + + // ServicesInDesktopClients enables services list/menu/section in desktop clients. + // If this capability is not present, desktop clients should not show services. + ServicesInDesktopClients Cap = "https://tailscale.com/cap/services-in-desktop-clients" + + // BindToInterfaceByRoute changes how Darwin nodes create + // sockets (in the net/netns package). See that package for more + // details on the behaviour of this capability. + BindToInterfaceByRoute Cap = "https://tailscale.com/cap/bind-to-interface-by-route" + + // DisableAndroidBindToActiveNetwork disables binding sockets to the + // currently active network on Android, which is enabled by default. + // This allows the control plane to turn off the behavior if it causes + // problems. + DisableAndroidBindToActiveNetwork Cap = "disable-android-bind-to-active-network" + + // DebugDisableAlternateDefaultRouteInterface changes how Darwin + // nodes get the default interface. There is an optional hook (used by the + // macOS and iOS clients) to override the default interface, this capability + // disables that and uses the default behavior (of parsing the routing + // table). + DebugDisableAlternateDefaultRouteInterface Cap = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface" + + // DebugDisableBindConnToInterface disables the automatic binding + // of connections to the default network interface on Darwin nodes. + DebugDisableBindConnToInterface Cap = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface" + + // DebugDisableBindConnToInterface disables the automatic binding + // of connections to the default network interface on Darwin nodes using network extensions + DebugDisableBindConnToInterfaceAppleExt Cap = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface-apple-ext" + + // TailnetLock indicates the node may initialize tailnet lock. + TailnetLock Cap = "https://tailscale.com/cap/tailnet-lock" + + //// Funnel warning capabilities used for reporting errors to the user. + + // WarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet. + // This cap is no longer used 2023-08-09 onwards. + WarnFunnelNoInvite Cap = "https://tailscale.com/cap/warn-funnel-no-invite" + + // WarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet. + // This cap is no longer used 2023-08-09 onwards. + WarnFunnelNoHTTPS Cap = "https://tailscale.com/cap/warn-funnel-no-https" + + //// Debug logging capabilities + + // DebugTSDNSResolution enables verbose debug logging for DNS + // resolution for Tailscale-controlled domains (the control server, log + // server, DERP servers, etc.) + DebugTSDNSResolution Cap = "https://tailscale.com/cap/debug-ts-dns-resolution" + + // FunnelPorts specifies the ports that the Funnel is available on. + // The ports are specified as a comma-separated list of port numbers or port + // ranges (e.g. "80,443,8080-8090") in the ports query parameter. + // e.g. https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090 + FunnelPorts Cap = "https://tailscale.com/cap/funnel-ports" + + // OnlyTCP443 specifies that the client should not attempt to generate + // any outbound traffic that isn't TCP on port 443 (HTTPS). This is used for + // clients in restricted environments where only HTTPS traffic is allowed + // other types of traffic trips outbound firewall alarms. This thus implies + // all traffic is over DERP. + OnlyTCP443 Cap = "only-tcp-443" + + // Funnel grants the ability for a node to host ingress traffic. + Funnel Cap = "funnel" + // SSHAggregator grants the ability for a node to collect SSH sessions. + SSHAggregator Cap = "ssh-aggregator" + + // DebugForceBackgroundSTUN forces a node to always do background + // STUN queries regardless of inactivity. + DebugForceBackgroundSTUN Cap = "debug-always-stun" + + // DebugDisableWGTrim disables the lazy WireGuard configuration, + // always giving WireGuard the full netmap, even for idle peers. + DebugDisableWGTrim Cap = "debug-no-wg-trim" + + // DisableSubnetsIfPAC controls whether subnet routers should be + // disabled if WPAD is present on the network. + DisableSubnetsIfPAC Cap = "debug-disable-subnets-if-pac" + + // DisableUPnP makes the client not perform a UPnP portmapping. + // By default, we want to enable it to see if it works on more clients. + // + // If UPnP catastrophically fails for people, this should be set kill + // new attempts at UPnP connections. + DisableUPnP Cap = "debug-disable-upnp" + + // DisableDeltaUpdates makes the client not process updates via the + // delta update mechanism and should instead treat all netmap changes as + // "full" ones as tailscaled did in 1.48.x and earlier. + DisableDeltaUpdates Cap = "disable-delta-updates" + + // RandomizeClientPort makes magicsock UDP bind to + // :0 to get a random local port, ignoring any configured + // fixed port. + RandomizeClientPort Cap = "randomize-client-port" + + // SilentDisco makes the client suppress disco heartbeats to its + // peers. + SilentDisco Cap = "silent-disco" + + // OneCGNATEnable makes the client prefer one big CGNAT /10 route + // rather than a /32 per peer. At most one of this or + // [OneCGNATDisable] may be set; if neither are, it's automatic. + OneCGNATEnable Cap = "one-cgnat?v=true" + + // OneCGNATDisable makes the client prefer a /32 route per peer + // rather than one big /10 CGNAT route. At most one of this or + // [OneCGNATEnable] may be set; if neither are, it's automatic. + OneCGNATDisable Cap = "one-cgnat?v=false" + + // PeerMTUEnable makes the client do path MTU discovery to its + // peers. If it isn't set, it defaults to the client default. + PeerMTUEnable Cap = "peer-mtu-enable" + + // DNSForwarderDisableTCPRetries disables retrying truncated + // DNS queries over TCP if the response is truncated. + DNSForwarderDisableTCPRetries Cap = "dns-forwarder-disable-tcp-retries" + + // LinuxMustUseIPTables forces Linux clients to use iptables for + // netfilter management. + // This cannot be set simultaneously with [LinuxMustUseNfTables]. + LinuxMustUseIPTables Cap = "linux-netfilter?v=iptables" + + // LinuxMustUseNfTables forces Linux clients to use nftables for + // netfilter management. + // This cannot be set simultaneously with [LinuxMustUseIPTables]. + LinuxMustUseNfTables Cap = "linux-netfilter?v=nftables" + + // ProbeUDPLifetime makes the client probe UDP path lifetime at the + // tail end of an active direct connection in magicsock. + ProbeUDPLifetime Cap = "probe-udp-lifetime" + + // TaildriveShare enables sharing via Taildrive. + TaildriveShare Cap = "drive:share" + + // TaildriveAccess enables accessing shares via Taildrive. + TaildriveAccess Cap = "drive:access" + + // SuggestExitNode is applied to each exit node which the control plane has determined + // is a recommended exit node. + SuggestExitNode Cap = "suggest-exit-node" + + // DisableWebClient disables using the web client. + DisableWebClient Cap = "disable-web-client" + + // LogExitFlows enables exit node destinations in network flow logs. + LogExitFlows Cap = "log-exit-flows" + + // AutoExitNode permits the automatic exit nodes feature. + AutoExitNode Cap = "auto-exit-node" + + // StoreAppCRoutes configures the node to store app connector routes persistently. + StoreAppCRoutes Cap = "store-appc-routes" + + // SuggestExitNodeUI allows the currently suggested exit node to appear in the client GUI. + SuggestExitNodeUI Cap = "suggest-exit-node-ui" + + // UserDialUseRoutes makes UserDial use either the peer dialer or the system dialer, + // depending on the destination address and the configured routes. When present, it also makes + // the DNS forwarder use UserDial instead of SystemDial when dialing resolvers. + UserDialUseRoutes Cap = "user-dial-routes" + + // SSHBehaviorV1 forces SSH to use the V1 behavior (no su, run SFTP in-process) + // Added 2024-05-29 in Tailscale version 1.68. + SSHBehaviorV1 Cap = "ssh-behavior-v1" + + // SSHBehaviorV2 forces SSH to use the V2 behavior (use su, run SFTP in child process). + // This overrides [SSHBehaviorV1] if set. + // See forceV1Behavior in ssh/tailssh/incubator.go for distinction between + // V1 and V2 behavior. + // Added 2024-08-06 in Tailscale version 1.72. + SSHBehaviorV2 Cap = "ssh-behavior-v2" + + // DisableSplitDNSWhenNoCustomResolvers indicates that the node's + // DNS manager should not adopt a split DNS configuration even though the + // Config of the resolver only contains routes that do not specify custom + // resolver(s), hence all DNS queries can be safely sent to the upstream + // DNS resolver and the node's DNS forwarder doesn't need to handle all + // DNS traffic. + // This is for now (2024-06-06) an iOS-specific battery life optimization, + // and this node attribute allows us to disable the optimization remotely + // if needed. + DisableSplitDNSWhenNoCustomResolvers Cap = "disable-split-dns-when-no-custom-resolvers" + + // ScopeQuad100OnMacOS makes sandboxed macOS clients scope quad-100 + // to its match domains instead of installing it as the OS's primary + // (catch-all) resolver, so that public names fall through to the OS + // resolver -- e.g. a user's DoH system profile -- rather than being + // shadowed. It has no effect on any other platform. Without this attribute, + // sandboxed macOS keeps the older behavior of making quad-100 the default + // resolver, as iOS still does. See tailscale/corp#45534. + ScopeQuad100OnMacOS Cap = "scope-quad100-macos" + + // DisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not + // create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled. + // Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers + // associated with all network adapters, even when "Override local DNS" is enabled and/or + // a Mullvad exit node is being used, resulting in DNS leaks. + // We began creating this rule on 2024-06-14, and this node attribute + // allows us to disable the new behavior remotely if needed. + DisableLocalDNSOverrideViaNRPT Cap = "disable-local-dns-override-via-nrpt" + + // DisableMagicSockCryptoRouting disables the use of the + // magicsock cryptorouting hook. See tailscale/corp#20732. + // + // Deprecated: [DisableMagicSockCryptoRouting] is deprecated as of + // [tailscale.com/tailcfg.CapabilityVersion] 124, + // CryptoRouting is now mandatory. See tailscale/corp#31083. + DisableMagicSockCryptoRouting Cap = "disable-magicsock-crypto-routing" + + // DisableCaptivePortalDetection instructs the client to not perform captive portal detection + // automatically when the network state changes. + DisableCaptivePortalDetection Cap = "disable-captive-portal-detection" + + // DisableSkipStatusQueue is set when the node should disable skipping + // of queued netmap.NetworkMap between the controlclient and LocalBackend. + // See tailscale/tailscale#14768. + DisableSkipStatusQueue Cap = "disable-skip-status-queue" + + // SSHEnvironmentVariables enables logic for handling environment variables sent + // via SendEnv in the SSH server and applying them to the SSH session. + SSHEnvironmentVariables Cap = "ssh-env-vars" + + // ServiceHost indicates the VIP Services for which the client is + // approved to act as a service host, and which IP addresses are assigned + // to those VIP Services. Any VIP Services that the client is not + // advertising can be ignored. + // Each value of this key in [NodeCapMap] is of type [ServiceIPMappings]. + // If multiple values of this key exist, they should be merged in sequence + // (replace conflicting keys). + ServiceHost Cap = "service-host" + + // MaxKeyDuration represents the MaxKeyDuration setting on the + // tailnet. The value of this key in [NodeCapMap] will be only one entry of + // type float64 representing the duration in seconds. This cap will be + // omitted if the tailnet's MaxKeyDuration is the default. + MaxKeyDuration Cap = "tailnet.maxKeyDuration" + + // NativeIPV4 contains the IPV4 address of the node in its + // native tailnet. This is currently only sent to Hello, in its + // peer node list. + NativeIPV4 Cap = "native-ipv4" + + // DisableRelayServer prevents the node from acting as an underlay + // UDP relay server. There are no expected values for this key; the key + // only needs to be present in [NodeCapMap] to take effect. + DisableRelayServer Cap = "disable-relay-server" + + // DisableRelayClient prevents the node from both allocating UDP + // relay server endpoints itself, and from using endpoints allocated by + // its peers. This attribute can be added to the node dynamically; if added + // while the node is already running, the node will be unable to allocate + // endpoints after it next updates its network map, and will be immediately + // unable to use new paths via a UDP relay server. Setting this attribute + // dynamically does not remove any existing paths, including paths that + // traverse a UDP relay server. There are no expected values for this key + // in [NodeCapMap]; the key only needs to be present in [NodeCapMap] to + // take effect. + DisableRelayClient Cap = "disable-relay-client" + + // MagicDNSPeerAAAA is a capability that tells the node's MagicDNS + // server to answer AAAA queries about its peers. See tailscale/tailscale#1152. + MagicDNSPeerAAAA Cap = "magicdns-aaaa" + + // DNSSubdomainResolve, when set on Self or a Peer node, indicates + // that the subdomains of that node's MagicDNS name should resolve to the + // same IP addresses as the node itself. + // For example, if node "myserver.tailnet.ts.net" has this capability, + // then "anything.myserver.tailnet.ts.net" will resolve to myserver's IPs. + DNSSubdomainResolve Cap = "dns-subdomain-resolve" + + // TrafficSteering configures the node to use the traffic + // steering subsystem for via routes. See tailscale/corp#29966. + TrafficSteering Cap = "traffic-steering" + + // TailnetDisplayName is an optional alternate name for the tailnet + // to be displayed to the user. + // If empty or absent, a default is used. + // If this value is present and set by a user this will only include letters, + // numbers, apostrophe, spaces, and hyphens. This may not be true for the default. + // Values can look like "foo.com" or "Foo's Test Tailnet - Staging". + TailnetDisplayName Cap = "tailnet-display-name" + + // ClientSideReachability configures the node to determine + // reachability itself when choosing connectors. When absent, the + // default behavior is to trust the control plane when it claims that a + // node is no longer online, but that is not a reliable signal. + // + // It is temporary and will be ignored once its behaviour becomes the default. + ClientSideReachability Cap = "client-side-reachability" + + // ClientSideReachabilityRouteCheck configures the node to use + // the routecheck subsystem to determine reachability when choosing + // connectors. This relies on [ClientSideReachability] being set. + // See tailscale/tailscale#17367. + // + // It is temporary and will be ignored once its behaviour becomes the default. + ClientSideReachabilityRouteCheck Cap = "client-side-reachability-routecheck" + + // DefaultAutoUpdate advertises the default node auto-update setting + // for this tailnet. The node is free to opt-in or out locally regardless of + // this value. Once this has been set and stored in the client, future + // changes from the control plane are ignored. + // + // The value of the key in [NodeCapMap] is a JSON boolean. + DefaultAutoUpdate Cap = "default-auto-update" + + // DisableHostsFileUpdates indicates that the node's DNS manager should + // not create hosts file entries when it normally would, such as when we're not + // the primary resolver on Windows or when the host is domain-joined and its + // primary domain takes precedence over MagicDNS. As of 2026-02-12, it is only + // used on Windows. + DisableHostsFileUpdates Cap = "disable-hosts-file-updates" + + // ForceRegisterMagicDNSIPv4Only forces the client to only register + // its MagicDNS IPv4 address with systemd/etc, and not both its IPv4 and IPv6 addresses. + // See https://github.com/tailscale/tailscale/issues/15404. + // TODO(bradfitz): remove this a few releases after 2026-02-16. + ForceRegisterMagicDNSIPv4Only Cap = "force-register-magicdns-ipv4-only" + + // CacheNetworkMaps instructs the node to persistently cache network + // maps and use them to establish peer connectivity on start, if doing so is + // supported by the client and storage is available. When this attribute is + // absent (or removed), a node that supports netmap caching will ignore and + // discard existing cached maps, and will not store any. + CacheNetworkMaps Cap = "cache-network-maps" + + // DisableCacheNetworkMaps indicates that the node should not cache + // network maps (as per [CacheNetworkMaps]) when it normally would. + // This attribute exists to allow the policy document to override the default. + // When set, it takes precedence over [CacheNetworkMaps]. + DisableCacheNetworkMaps Cap = "disable-cache-network-maps" + + // DisableLinuxCGNATDropRule tells Linux clients to not insert a + // blanket firewall DROP rule for inbound traffic from the CGNAT IP range + // that does not originate from the Tailscale network interface. + // This enables access to off-tailnet endpoints within that IP range. + DisableLinuxCGNATDropRule Cap = "disable-linux-cgnat-drop-rule" + + // EmitRuntimeMetrics enables emission of [runtime/metrics] as + // [tailscale.com/util/clientmetric]'s. + EmitRuntimeMetrics Cap = "emit-runtime-metrics" + + // DisableUDPGRO disables UDP GRO (UDP_GRO socket option on Linux) + // on the magicsock UDP socket. It exists so control can mitigate kernel + // regressions that cause throughput or correctness issues with UDP GRO on + // specific OS/kernel versions, without requiring a client release. See + // https://github.com/tailscale/tailscale/issues/19777 for example. + // Currently only consulted on Linux; may apply to other platforms as they + // gain UDP GRO support. + DisableUDPGRO Cap = "disable-udp-gro" + + // DisableUDPGSO disables UDP GSO (UDP_SEGMENT socket option on + // Linux) on the magicsock UDP socket. It exists so control can mitigate + // kernel regressions that cause throughput or correctness issues with UDP + // GSO on specific OS/kernel versions, without requiring a client release. + // See https://github.com/tailscale/tailscale/issues/19777 for example. + // Currently only consulted on Linux; may apply to other platforms as they + // gain UDP GSO support. + DisableUDPGSO Cap = "disable-udp-gso" + + // DisableTUNUDPGRO disables UDP GRO on the Tailscale TUN device. + // It exists so control can mitigate kernel regressions that cause + // throughput or correctness issues with TUN UDP GRO on specific OS/kernel + // versions, without requiring a client release. See + // https://github.com/tailscale/tailscale/issues/13041 for example. + // Currently only consulted on Linux; may apply to other platforms as they + // gain TUN UDP GRO support. + DisableTUNUDPGRO Cap = "disable-tun-udp-gro" + + // DisableTUNTCPGRO disables TCP GRO on the Tailscale TUN device. + // It exists so control can mitigate kernel regressions that cause + // throughput or correctness issues with TUN TCP GRO on specific OS/kernel + // versions, without requiring a client release. See + // https://github.com/tailscale/tailscale/issues/13041 for example. + // Currently only consulted on Linux; may apply to other platforms as they + // gain TUN TCP GRO support. + DisableTUNTCPGRO Cap = "disable-tun-tcp-gro" + + // NeverGSOEqualTail enables a sentinel-tail workaround in the + // underlay UDP packet TX path on Linux. Applies to magicsock and peer relay + // UDP sockets. The workaround avoids emitting UDP GSO batches whose + // fragments are all equal in length, at a small payload and packet overhead + // cost. It exists so control can mitigate kernel regressions that mangle + // UDP headers or checksums for equal-length GSO batches, without requiring + // a client release. See https://github.com/tailscale/tailscale/issues/19777. + NeverGSOEqualTail Cap = "never-gso-equal-tail" +) + +const ( + // ServicesPrefix is the prefix for per-service [NodeCapMap] + // entries describing Services visible (accessible) to this node. + // Each value under such a key is of type [ServiceDetails]. + // The suffix after the prefix is an opaque server-chosen identifier; + // consumers must use [ServiceDetails.Name] as the canonical service name + // rather than parsing it from the map key. + ServicesPrefix Prefix = "services/" +) diff --git a/tailcfg/peercap/peercap.go b/tailcfg/peercap/peercap.go new file mode 100644 index 000000000..3890b4d78 --- /dev/null +++ b/tailcfg/peercap/peercap.go @@ -0,0 +1,56 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +// Package peercap defines the capabilities that can be granted to peer nodes. +package peercap + +// Cap represents a capability granted to a peer by a [tailscale.com/tailcfg.FilterRule] +// when the peer communicates with the node that has this rule. +// Its meaning is application-defined. +// +// It must be a URL like "https://tailscale.com/cap/file-send" +// or "tailscale.com/cap/webui". +type Cap string + +const ( + // FileSharingTarget grants the current node the ability to send + // files to the peer which has this capability. + FileSharingTarget Cap = "https://tailscale.com/cap/file-sharing-target" + // FileSharingSend grants the ability to receive files from a + // node that's owned by a different user. + FileSharingSend Cap = "https://tailscale.com/cap/file-send" + // DebugPeer grants the ability for a peer to read this node's + // goroutines, metrics, magicsock internal state, etc. + DebugPeer Cap = "https://tailscale.com/cap/debug-peer" + // WakeOnLAN grants the ability to send a Wake-On-LAN packet. + WakeOnLAN Cap = "https://tailscale.com/cap/wake-on-lan" + // Ingress grants the ability for a peer to send ingress traffic. + Ingress Cap = "https://tailscale.com/cap/ingress" + // WebUI grants the ability for a peer to edit features from the + // device Web UI. + WebUI Cap = "tailscale.com/cap/webui" + // Taildrive grants the ability for a peer to access Taildrive + // shares. + Taildrive Cap = "tailscale.com/cap/drive" + // TaildriveSharer indicates that a peer has the ability to + // share folders with us. + TaildriveSharer Cap = "tailscale.com/cap/drive-sharer" + + // Kubernetes grants a peer Kubernetes-specific + // capabilities, such as the ability to impersonate specific Tailscale + // user groups as Kubernetes user groups. This capability is read by + // peers that are Tailscale Kubernetes operator instances. + Kubernetes Cap = "tailscale.com/cap/kubernetes" + + // Relay grants the ability for a peer to allocate relay + // endpoints. + Relay Cap = "tailscale.com/cap/relay" + // RelayTarget grants the current node the ability to allocate + // relay endpoints to the peer which has this capability. + RelayTarget Cap = "tailscale.com/cap/relay-target" + + // TsIDP grants a peer tsidp-specific + // capabilities, such as the ability to add user groups to the OIDC + // claim + TsIDP Cap = "tailscale.com/cap/tsidp" +) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 99276d7ae..d0a7a5418 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -22,6 +22,8 @@ "time" "tailscale.com/feature/buildfeatures" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -446,7 +448,7 @@ type Node struct { // "https://tailscale.com/cap/file-sharing" // // Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508 - Capabilities []NodeCapability `json:",omitempty"` + Capabilities []nodecap.Cap `json:",omitempty"` // CapMap is a map of capabilities to their optional argument/data values. // @@ -457,7 +459,7 @@ type Node struct { // represented by the Capabilities field, but can now be represented by // CapMap with an empty value. // - // See NodeCapability for more information on keys. + // See [nodecap.Cap] for more information on keys. // // Metadata about nodes can be transmitted in 3 ways: // 1. MapResponse.Node.CapMap describes attributes that affect behavior for @@ -543,13 +545,13 @@ type Node struct { // HasCap reports whether the node has the given capability. // It is safe to call on an invalid NodeView. -func (v NodeView) HasCap(cap NodeCapability) bool { +func (v NodeView) HasCap(cap nodecap.Cap) bool { return v.ж.HasCap(cap) } // HasCap reports whether the node has the given capability. // It is safe to call on a nil Node. -func (v *Node) HasCap(cap NodeCapability) bool { +func (v *Node) HasCap(cap nodecap.Cap) bool { return v != nil && v.CapMap.Contains(cap) } @@ -1565,7 +1567,7 @@ type CapGrant struct { // FilterRule.SrcIPs are granted to the destination IP, // matched by Dsts. // Deprecated: use CapMap instead. - Caps []PeerCapability `json:",omitempty"` + Caps []peercap.Cap `json:",omitempty"` // CapMap is a map of capabilities to their values. // The key is the capability name, and the value is a list of @@ -1573,62 +1575,40 @@ type CapGrant struct { CapMap PeerCapMap `json:",omitempty"` } -// PeerCapability represents a capability granted to a peer by a FilterRule when -// the peer communicates with the node that has this rule. Its meaning is -// application-defined. +// PeerCapability is a type alias to [peercap.Cap] +// and peer capabilities are now defined in the peercap package, +// see [PeerCapabilityFileSharingTarget]. // -// It must be a URL like "https://tailscale.com/cap/file-send". -type PeerCapability string +//go:fix inline +type PeerCapability = peercap.Cap +// Deprecated: Peer capabilities are now defined in the peercap package, see [peercap.Cap]. +// These constants are provided for backwards compatibility +// but no new [PeerCapability] aliases will be added to this list. +// +//go:fix inline const ( - // PeerCapabilityFileSharingTarget grants the current node the ability to send - // files to the peer which has this capability. - PeerCapabilityFileSharingTarget PeerCapability = "https://tailscale.com/cap/file-sharing-target" - // PeerCapabilityFileSharingSend grants the ability to receive files from a - // node that's owned by a different user. - PeerCapabilityFileSharingSend PeerCapability = "https://tailscale.com/cap/file-send" - // PeerCapabilityDebugPeer grants the ability for a peer to read this node's - // goroutines, metrics, magicsock internal state, etc. - PeerCapabilityDebugPeer PeerCapability = "https://tailscale.com/cap/debug-peer" - // PeerCapabilityWakeOnLAN grants the ability to send a Wake-On-LAN packet. - PeerCapabilityWakeOnLAN PeerCapability = "https://tailscale.com/cap/wake-on-lan" - // PeerCapabilityIngress grants the ability for a peer to send ingress traffic. - PeerCapabilityIngress PeerCapability = "https://tailscale.com/cap/ingress" - // PeerCapabilityWebUI grants the ability for a peer to edit features from the - // device Web UI. - PeerCapabilityWebUI PeerCapability = "tailscale.com/cap/webui" - // PeerCapabilityTaildrive grants the ability for a peer to access Taildrive - // shares. - PeerCapabilityTaildrive PeerCapability = "tailscale.com/cap/drive" - // PeerCapabilityTaildriveSharer indicates that a peer has the ability to - // share folders with us. - PeerCapabilityTaildriveSharer PeerCapability = "tailscale.com/cap/drive-sharer" - - // PeerCapabilityKubernetes grants a peer Kubernetes-specific - // capabilities, such as the ability to impersonate specific Tailscale - // user groups as Kubernetes user groups. This capability is read by - // peers that are Tailscale Kubernetes operator instances. - PeerCapabilityKubernetes PeerCapability = "tailscale.com/cap/kubernetes" - - // PeerCapabilityRelay grants the ability for a peer to allocate relay - // endpoints. - PeerCapabilityRelay PeerCapability = "tailscale.com/cap/relay" - // PeerCapabilityRelayTarget grants the current node the ability to allocate - // relay endpoints to the peer which has this capability. - PeerCapabilityRelayTarget PeerCapability = "tailscale.com/cap/relay-target" - - // PeerCapabilityTsIDP grants a peer tsidp-specific - // capabilities, such as the ability to add user groups to the OIDC - // claim - PeerCapabilityTsIDP PeerCapability = "tailscale.com/cap/tsidp" + PeerCapabilityFileSharingTarget = peercap.FileSharingTarget + PeerCapabilityFileSharingSend = peercap.FileSharingSend + PeerCapabilityDebugPeer = peercap.DebugPeer + PeerCapabilityWakeOnLAN = peercap.WakeOnLAN + PeerCapabilityIngress = peercap.Ingress + PeerCapabilityWebUI = peercap.WebUI + PeerCapabilityTaildrive = peercap.Taildrive + PeerCapabilityTaildriveSharer = peercap.TaildriveSharer + PeerCapabilityKubernetes = peercap.Kubernetes + PeerCapabilityRelay = peercap.Relay + PeerCapabilityRelayTarget = peercap.RelayTarget + PeerCapabilityTsIDP = peercap.TsIDP + // Deprecated: Do not add any further values here, use [peercap] instead. ) // NodeCapMap is a map of capabilities to their optional values. It is valid for // a capability to have no values (nil slice); such capabilities can be tested // for by using the [NodeCapMap.Contains] method. // -// See [NodeCapability] for more information on keys. -type NodeCapMap map[NodeCapability][]RawMessage +// See [nodecap.Cap] for more information on keys. +type NodeCapMap map[nodecap.Cap][]RawMessage // Equal reports whether c and c2 are equal. func (c NodeCapMap) Equal(c2 NodeCapMap) bool { @@ -1638,14 +1618,14 @@ func (c NodeCapMap) Equal(c2 NodeCapMap) bool { // UnmarshalNodeCapJSON unmarshals each JSON value in cm[cap] as T. // If cap does not exist in cm, it returns (nil, nil). // It returns an error if the values cannot be unmarshaled into the provided type. -func UnmarshalNodeCapJSON[T any](cm NodeCapMap, cap NodeCapability) ([]T, error) { +func UnmarshalNodeCapJSON[T any](cm NodeCapMap, cap nodecap.Cap) ([]T, error) { return UnmarshalNodeCapViewJSON[T](views.MapSliceOf(cm), cap) } // UnmarshalNodeCapViewJSON unmarshals each JSON value in cm.Get(cap) as T. // If cap does not exist in cm, it returns (nil, nil). // It returns an error if the values cannot be unmarshaled into the provided type. -func UnmarshalNodeCapViewJSON[T any](cm views.MapSlice[NodeCapability, RawMessage], cap NodeCapability) ([]T, error) { +func UnmarshalNodeCapViewJSON[T any](cm views.MapSlice[nodecap.Cap, RawMessage], cap nodecap.Cap) ([]T, error) { vals, ok := cm.GetOk(cap) if !ok { return nil, nil @@ -1664,7 +1644,7 @@ func UnmarshalNodeCapViewJSON[T any](cm views.MapSlice[NodeCapability, RawMessag // Contains reports whether c has the capability cap. This is used to test for // the existence of a capability, especially when the capability has no // associated argument/data values. -func (c NodeCapMap) Contains(cap NodeCapability) bool { +func (c NodeCapMap) Contains(cap nodecap.Cap) bool { _, ok := c[cap] return ok } @@ -1675,19 +1655,19 @@ func (c NodeCapMap) Contains(cap NodeCapability) bool { // // The values are opaque to Tailscale, but are passed through from the ACLs to // the application via the WhoIs API. -type PeerCapMap map[PeerCapability][]RawMessage +type PeerCapMap map[peercap.Cap][]RawMessage // UnmarshalCapJSON unmarshals each JSON value in cm[cap] as T. // If cap does not exist in cm, it returns (nil, nil). // It returns an error if the values cannot be unmarshaled into the provided type. -func UnmarshalCapJSON[T any](cm PeerCapMap, cap PeerCapability) ([]T, error) { +func UnmarshalCapJSON[T any](cm PeerCapMap, cap peercap.Cap) ([]T, error) { return UnmarshalCapViewJSON[T](views.MapSliceOf(cm), cap) } // UnmarshalCapViewJSON unmarshals each JSON value in cm.Get(cap) as T. // If cap does not exist in cm, it returns (nil, nil). // It returns an error if the values cannot be unmarshaled into the provided type. -func UnmarshalCapViewJSON[T any](cm views.MapSlice[PeerCapability, RawMessage], cap PeerCapability) ([]T, error) { +func UnmarshalCapViewJSON[T any](cm views.MapSlice[peercap.Cap, RawMessage], cap peercap.Cap) ([]T, error) { vals, ok := cm.GetOk(cap) if !ok { return nil, nil @@ -1706,7 +1686,7 @@ func UnmarshalCapViewJSON[T any](cm views.MapSlice[PeerCapability, RawMessage], // HasCapability reports whether c has the capability cap. This is used to test // for the existence of a capability, especially when the capability has no // associated argument/data values. -func (c PeerCapMap) HasCapability(cap PeerCapability) bool { +func (c PeerCapMap) HasCapability(cap peercap.Cap) bool { _, ok := c[cap] return ok } @@ -2475,441 +2455,110 @@ type Oauth2Token struct { Expiry time.Time `json:"expiry,omitzero"` } -// NodeCapability represents a capability granted to the self node as listed in -// MapResponse.Node.Capabilities. +// NodeCapability is a type alias to [nodecap.Cap] +// and node capabilities are now defined in the [nodecap] package, +// see [CapabilityFileSharing] and [NodeAttrDisableAndroidBindToActiveNetwork] respectively. // -// It must be a URL like "https://tailscale.com/cap/file-sharing", or a -// well-known capability name like "funnel". The latter is only allowed for -// Tailscale-defined capabilities. +//go:fix inline +type NodeCapability = nodecap.Cap + +// NodeCapabilityPrefix is a type alias to [nodecap.Prefix] +// and these prefixes are now defined in the [nodecap] package, +// see [NodeAttrPrefixServices]. // -// Unlike PeerCapability, NodeCapability is not in context of a peer and is -// granted to the node itself. +//go:fix inline +type NodeCapabilityPrefix = nodecap.Prefix + +// Deprecated: Capabilities and NodeAttrs are now defined in the nodecap package, see [nodecap.Cap]. +// These constants are provided for backwards compatibility +// but no new [NodeCapability] aliases will be added to this list. // -// These are also referred to as "Node Attributes" in the ACL policy file. -type NodeCapability string - -// NodeCapabilityPrefix is a prefix for [NodeCapMap] keys that share a common -// namespace, where each entry represents a distinct named instance (e.g. one -// per service). The full key is formed by concatenating the prefix with the -// instance name. -type NodeCapabilityPrefix string - -// ToAttribute returns the full [NodeCapability] key for the given value under -// this prefix, of the form prefix+value. -func (p NodeCapabilityPrefix) ToAttribute(value string) NodeCapability { - return NodeCapability(string(p) + value) -} - +//go:fix inline const ( - CapabilityFileSharing NodeCapability = "https://tailscale.com/cap/file-sharing" - CapabilityAdmin NodeCapability = "https://tailscale.com/cap/is-admin" - CapabilityOwner NodeCapability = "https://tailscale.com/cap/is-owner" - CapabilitySSH NodeCapability = "https://tailscale.com/cap/ssh" // feature enabled/available - CapabilitySSHRuleIn NodeCapability = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node - CapabilityDataPlaneAuditLogs NodeCapability = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled - CapabilityDebug NodeCapability = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI - CapabilityHTTPS NodeCapability = "https" - - // CapabilityMacUIV2 makes the macOS GUI enable its v2 mode. - CapabilityMacUIV2 NodeCapability = "https://tailscale.com/cap/mac-ui-v2" - - // CapabilityServicesInDesktopClients enables services list/menu/section in desktop clients. - // If this capability is not present, desktop clients should not show services. - CapabilityServicesInDesktopClients NodeCapability = "https://tailscale.com/cap/services-in-desktop-clients" - - // CapabilityBindToInterfaceByRoute changes how Darwin nodes create - // sockets (in the net/netns package). See that package for more - // details on the behaviour of this capability. - CapabilityBindToInterfaceByRoute NodeCapability = "https://tailscale.com/cap/bind-to-interface-by-route" - - // NodeAttrDisableAndroidBindToActiveNetwork disables binding sockets to the - // currently active network on Android, which is enabled by default. - // This allows the control plane to turn off the behavior if it causes - // problems. - NodeAttrDisableAndroidBindToActiveNetwork NodeCapability = "disable-android-bind-to-active-network" - - // CapabilityDebugDisableAlternateDefaultRouteInterface changes how Darwin - // nodes get the default interface. There is an optional hook (used by the - // macOS and iOS clients) to override the default interface, this capability - // disables that and uses the default behavior (of parsing the routing - // table). - CapabilityDebugDisableAlternateDefaultRouteInterface NodeCapability = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface" - - // CapabilityDebugDisableBindConnToInterface disables the automatic binding - // of connections to the default network interface on Darwin nodes. - CapabilityDebugDisableBindConnToInterface NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface" - - // CapabilityDebugDisableBindConnToInterface disables the automatic binding - // of connections to the default network interface on Darwin nodes using network extensions - CapabilityDebugDisableBindConnToInterfaceAppleExt NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface-apple-ext" - - // CapabilityTailnetLock indicates the node may initialize tailnet lock. - CapabilityTailnetLock NodeCapability = "https://tailscale.com/cap/tailnet-lock" - - // Funnel warning capabilities used for reporting errors to the user. - - // CapabilityWarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet. - // This cap is no longer used 2023-08-09 onwards. - CapabilityWarnFunnelNoInvite NodeCapability = "https://tailscale.com/cap/warn-funnel-no-invite" - - // CapabilityWarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet. - // This cap is no longer used 2023-08-09 onwards. - CapabilityWarnFunnelNoHTTPS NodeCapability = "https://tailscale.com/cap/warn-funnel-no-https" - - // Debug logging capabilities - - // CapabilityDebugTSDNSResolution enables verbose debug logging for DNS - // resolution for Tailscale-controlled domains (the control server, log - // server, DERP servers, etc.) - CapabilityDebugTSDNSResolution NodeCapability = "https://tailscale.com/cap/debug-ts-dns-resolution" - - // CapabilityFunnelPorts specifies the ports that the Funnel is available on. - // The ports are specified as a comma-separated list of port numbers or port - // ranges (e.g. "80,443,8080-8090") in the ports query parameter. - // e.g. https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090 - CapabilityFunnelPorts NodeCapability = "https://tailscale.com/cap/funnel-ports" - - // NodeAttrOnlyTCP443 specifies that the client should not attempt to generate - // any outbound traffic that isn't TCP on port 443 (HTTPS). This is used for - // clients in restricted environments where only HTTPS traffic is allowed - // other types of traffic trips outbound firewall alarms. This thus implies - // all traffic is over DERP. - NodeAttrOnlyTCP443 NodeCapability = "only-tcp-443" - - // NodeAttrFunnel grants the ability for a node to host ingress traffic. - NodeAttrFunnel NodeCapability = "funnel" - // NodeAttrSSHAggregator grants the ability for a node to collect SSH sessions. - NodeAttrSSHAggregator NodeCapability = "ssh-aggregator" - - // NodeAttrDebugForceBackgroundSTUN forces a node to always do background - // STUN queries regardless of inactivity. - NodeAttrDebugForceBackgroundSTUN NodeCapability = "debug-always-stun" - - // NodeAttrDebugDisableWGTrim disables the lazy WireGuard configuration, - // always giving WireGuard the full netmap, even for idle peers. - NodeAttrDebugDisableWGTrim NodeCapability = "debug-no-wg-trim" - - // NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be - // disabled if WPAD is present on the network. - NodeAttrDisableSubnetsIfPAC NodeCapability = "debug-disable-subnets-if-pac" - - // NodeAttrDisableUPnP makes the client not perform a UPnP portmapping. - // By default, we want to enable it to see if it works on more clients. - // - // If UPnP catastrophically fails for people, this should be set kill - // new attempts at UPnP connections. - NodeAttrDisableUPnP NodeCapability = "debug-disable-upnp" - - // NodeAttrDisableDeltaUpdates makes the client not process updates via the - // delta update mechanism and should instead treat all netmap changes as - // "full" ones as tailscaled did in 1.48.x and earlier. - NodeAttrDisableDeltaUpdates NodeCapability = "disable-delta-updates" - - // NodeAttrRandomizeClientPort makes magicsock UDP bind to - // :0 to get a random local port, ignoring any configured - // fixed port. - NodeAttrRandomizeClientPort NodeCapability = "randomize-client-port" - - // NodeAttrSilentDisco makes the client suppress disco heartbeats to its - // peers. - NodeAttrSilentDisco NodeCapability = "silent-disco" - - // NodeAttrOneCGNATEnable makes the client prefer one big CGNAT /10 route - // rather than a /32 per peer. At most one of this or - // NodeAttrOneCGNATDisable may be set; if neither are, it's automatic. - NodeAttrOneCGNATEnable NodeCapability = "one-cgnat?v=true" - - // NodeAttrOneCGNATDisable makes the client prefer a /32 route per peer - // rather than one big /10 CGNAT route. At most one of this or - // NodeAttrOneCGNATEnable may be set; if neither are, it's automatic. - NodeAttrOneCGNATDisable NodeCapability = "one-cgnat?v=false" - - // NodeAttrPeerMTUEnable makes the client do path MTU discovery to its - // peers. If it isn't set, it defaults to the client default. - NodeAttrPeerMTUEnable NodeCapability = "peer-mtu-enable" - - // NodeAttrDNSForwarderDisableTCPRetries disables retrying truncated - // DNS queries over TCP if the response is truncated. - NodeAttrDNSForwarderDisableTCPRetries NodeCapability = "dns-forwarder-disable-tcp-retries" - - // NodeAttrLinuxMustUseIPTables forces Linux clients to use iptables for - // netfilter management. - // This cannot be set simultaneously with NodeAttrLinuxMustUseNfTables. - NodeAttrLinuxMustUseIPTables NodeCapability = "linux-netfilter?v=iptables" - - // NodeAttrLinuxMustUseNfTables forces Linux clients to use nftables for - // netfilter management. - // This cannot be set simultaneously with NodeAttrLinuxMustUseIPTables. - NodeAttrLinuxMustUseNfTables NodeCapability = "linux-netfilter?v=nftables" - - // NodeAttrProbeUDPLifetime makes the client probe UDP path lifetime at the - // tail end of an active direct connection in magicsock. - NodeAttrProbeUDPLifetime NodeCapability = "probe-udp-lifetime" - - // NodeAttrsTaildriveShare enables sharing via Taildrive. - NodeAttrsTaildriveShare NodeCapability = "drive:share" - - // NodeAttrsTaildriveAccess enables accessing shares via Taildrive. - NodeAttrsTaildriveAccess NodeCapability = "drive:access" - - // NodeAttrSuggestExitNode is applied to each exit node which the control plane has determined - // is a recommended exit node. - NodeAttrSuggestExitNode NodeCapability = "suggest-exit-node" - - // NodeAttrDisableWebClient disables using the web client. - NodeAttrDisableWebClient NodeCapability = "disable-web-client" - - // NodeAttrLogExitFlows enables exit node destinations in network flow logs. - NodeAttrLogExitFlows NodeCapability = "log-exit-flows" - - // NodeAttrAutoExitNode permits the automatic exit nodes feature. - NodeAttrAutoExitNode NodeCapability = "auto-exit-node" - - // NodeAttrStoreAppCRoutes configures the node to store app connector routes persistently. - NodeAttrStoreAppCRoutes NodeCapability = "store-appc-routes" - - // NodeAttrSuggestExitNodeUI allows the currently suggested exit node to appear in the client GUI. - NodeAttrSuggestExitNodeUI NodeCapability = "suggest-exit-node-ui" - - // NodeAttrUserDialUseRoutes makes UserDial use either the peer dialer or the system dialer, - // depending on the destination address and the configured routes. When present, it also makes - // the DNS forwarder use UserDial instead of SystemDial when dialing resolvers. - NodeAttrUserDialUseRoutes NodeCapability = "user-dial-routes" - - // NodeAttrSSHBehaviorV1 forces SSH to use the V1 behavior (no su, run SFTP in-process) - // Added 2024-05-29 in Tailscale version 1.68. - NodeAttrSSHBehaviorV1 NodeCapability = "ssh-behavior-v1" - - // NodeAttrSSHBehaviorV2 forces SSH to use the V2 behavior (use su, run SFTP in child process). - // This overrides NodeAttrSSHBehaviorV1 if set. - // See forceV1Behavior in ssh/tailssh/incubator.go for distinction between - // V1 and V2 behavior. - // Added 2024-08-06 in Tailscale version 1.72. - NodeAttrSSHBehaviorV2 NodeCapability = "ssh-behavior-v2" - - // NodeAttrDisableSplitDNSWhenNoCustomResolvers indicates that the node's - // DNS manager should not adopt a split DNS configuration even though the - // Config of the resolver only contains routes that do not specify custom - // resolver(s), hence all DNS queries can be safely sent to the upstream - // DNS resolver and the node's DNS forwarder doesn't need to handle all - // DNS traffic. - // This is for now (2024-06-06) an iOS-specific battery life optimization, - // and this node attribute allows us to disable the optimization remotely - // if needed. - NodeAttrDisableSplitDNSWhenNoCustomResolvers NodeCapability = "disable-split-dns-when-no-custom-resolvers" - - // NodeAttrScopeQuad100OnMacOS makes sandboxed macOS clients scope quad-100 - // to its match domains instead of installing it as the OS's primary - // (catch-all) resolver, so that public names fall through to the OS - // resolver -- e.g. a user's DoH system profile -- rather than being - // shadowed. It has no effect on any other platform. Without this attribute, - // sandboxed macOS keeps the older behavior of making quad-100 the default - // resolver, as iOS still does. See tailscale/corp#45534. - NodeAttrScopeQuad100OnMacOS NodeCapability = "scope-quad100-macos" - - // NodeAttrDisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not - // create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled. - // Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers - // associated with all network adapters, even when "Override local DNS" is enabled and/or - // a Mullvad exit node is being used, resulting in DNS leaks. - // We began creating this rule on 2024-06-14, and this node attribute - // allows us to disable the new behavior remotely if needed. - NodeAttrDisableLocalDNSOverrideViaNRPT NodeCapability = "disable-local-dns-override-via-nrpt" - - // NodeAttrDisableMagicSockCryptoRouting disables the use of the - // magicsock cryptorouting hook. See tailscale/corp#20732. - // - // Deprecated: NodeAttrDisableMagicSockCryptoRouting is deprecated as of - // CapabilityVersion 124, CryptoRouting is now mandatory. See tailscale/corp#31083. - NodeAttrDisableMagicSockCryptoRouting NodeCapability = "disable-magicsock-crypto-routing" - - // NodeAttrDisableCaptivePortalDetection instructs the client to not perform captive portal detection - // automatically when the network state changes. - NodeAttrDisableCaptivePortalDetection NodeCapability = "disable-captive-portal-detection" - - // NodeAttrDisableSkipStatusQueue is set when the node should disable skipping - // of queued netmap.NetworkMap between the controlclient and LocalBackend. - // See tailscale/tailscale#14768. - NodeAttrDisableSkipStatusQueue NodeCapability = "disable-skip-status-queue" - - // NodeAttrSSHEnvironmentVariables enables logic for handling environment variables sent - // via SendEnv in the SSH server and applying them to the SSH session. - NodeAttrSSHEnvironmentVariables NodeCapability = "ssh-env-vars" - - // NodeAttrServiceHost indicates the VIP Services for which the client is - // approved to act as a service host, and which IP addresses are assigned - // to those VIP Services. Any VIP Services that the client is not - // advertising can be ignored. - // Each value of this key in [NodeCapMap] is of type [ServiceIPMappings]. - // If multiple values of this key exist, they should be merged in sequence - // (replace conflicting keys). - NodeAttrServiceHost NodeCapability = "service-host" - - // NodeAttrMaxKeyDuration represents the MaxKeyDuration setting on the - // tailnet. The value of this key in [NodeCapMap] will be only one entry of - // type float64 representing the duration in seconds. This cap will be - // omitted if the tailnet's MaxKeyDuration is the default. - NodeAttrMaxKeyDuration NodeCapability = "tailnet.maxKeyDuration" - - // NodeAttrNativeIPV4 contains the IPV4 address of the node in its - // native tailnet. This is currently only sent to Hello, in its - // peer node list. - NodeAttrNativeIPV4 NodeCapability = "native-ipv4" - - // NodeAttrDisableRelayServer prevents the node from acting as an underlay - // UDP relay server. There are no expected values for this key; the key - // only needs to be present in [NodeCapMap] to take effect. - NodeAttrDisableRelayServer NodeCapability = "disable-relay-server" - - // NodeAttrDisableRelayClient prevents the node from both allocating UDP - // relay server endpoints itself, and from using endpoints allocated by - // its peers. This attribute can be added to the node dynamically; if added - // while the node is already running, the node will be unable to allocate - // endpoints after it next updates its network map, and will be immediately - // unable to use new paths via a UDP relay server. Setting this attribute - // dynamically does not remove any existing paths, including paths that - // traverse a UDP relay server. There are no expected values for this key - // in [NodeCapMap]; the key only needs to be present in [NodeCapMap] to - // take effect. - NodeAttrDisableRelayClient NodeCapability = "disable-relay-client" - - // NodeAttrMagicDNSPeerAAAA is a capability that tells the node's MagicDNS - // server to answer AAAA queries about its peers. See tailscale/tailscale#1152. - NodeAttrMagicDNSPeerAAAA NodeCapability = "magicdns-aaaa" - - // NodeAttrDNSSubdomainResolve, when set on Self or a Peer node, indicates - // that the subdomains of that node's MagicDNS name should resolve to the - // same IP addresses as the node itself. - // For example, if node "myserver.tailnet.ts.net" has this capability, - // then "anything.myserver.tailnet.ts.net" will resolve to myserver's IPs. - NodeAttrDNSSubdomainResolve NodeCapability = "dns-subdomain-resolve" - - // NodeAttrTrafficSteering configures the node to use the traffic - // steering subsystem for via routes. See tailscale/corp#29966. - NodeAttrTrafficSteering NodeCapability = "traffic-steering" - - // NodeAttrTailnetDisplayName is an optional alternate name for the tailnet - // to be displayed to the user. - // If empty or absent, a default is used. - // If this value is present and set by a user this will only include letters, - // numbers, apostrophe, spaces, and hyphens. This may not be true for the default. - // Values can look like "foo.com" or "Foo's Test Tailnet - Staging". - NodeAttrTailnetDisplayName NodeCapability = "tailnet-display-name" - - // NodeAttrClientSideReachability configures the node to determine - // reachability itself when choosing connectors. When absent, the - // default behavior is to trust the control plane when it claims that a - // node is no longer online, but that is not a reliable signal. - // - // It is temporary and will be ignored once its behaviour becomes the default. - NodeAttrClientSideReachability NodeCapability = "client-side-reachability" - - // NodeAttrClientSideReachabilityRouteCheck configures the node to use - // the routecheck subsystem to determine reachability when choosing - // connectors. This relies on [NodeAttrClientSideReachability] being set. - // See tailscale/tailscale#17367. - // - // It is temporary and will be ignored once its behaviour becomes the default. - NodeAttrClientSideReachabilityRouteCheck NodeCapability = "client-side-reachability-routecheck" - - // NodeAttrDefaultAutoUpdate advertises the default node auto-update setting - // for this tailnet. The node is free to opt-in or out locally regardless of - // this value. Once this has been set and stored in the client, future - // changes from the control plane are ignored. - // - // The value of the key in [NodeCapMap] is a JSON boolean. - NodeAttrDefaultAutoUpdate NodeCapability = "default-auto-update" - - // NodeAttrDisableHostsFileUpdates indicates that the node's DNS manager should - // not create hosts file entries when it normally would, such as when we're not - // the primary resolver on Windows or when the host is domain-joined and its - // primary domain takes precedence over MagicDNS. As of 2026-02-12, it is only - // used on Windows. - NodeAttrDisableHostsFileUpdates NodeCapability = "disable-hosts-file-updates" - - // NodeAttrForceRegisterMagicDNSIPv4Only forces the client to only register - // its MagicDNS IPv4 address with systemd/etc, and not both its IPv4 and IPv6 addresses. - // See https://github.com/tailscale/tailscale/issues/15404. - // TODO(bradfitz): remove this a few releases after 2026-02-16. - NodeAttrForceRegisterMagicDNSIPv4Only NodeCapability = "force-register-magicdns-ipv4-only" - - // NodeAttrCacheNetworkMaps instructs the node to persistently cache network - // maps and use them to establish peer connectivity on start, if doing so is - // supported by the client and storage is available. When this attribute is - // absent (or removed), a node that supports netmap caching will ignore and - // discard existing cached maps, and will not store any. - NodeAttrCacheNetworkMaps NodeCapability = "cache-network-maps" - - // NodeAttrDisableCacheNetworkMaps indicates that the node should not cache - // network maps (as per [NodeAttrCacheNetworkMaps]) when it normally would. - // This attribute exists to allow the policy document to override the default. - // When set, it takes precedence over [NodeAttrCacheNetworkMaps]. - NodeAttrDisableCacheNetworkMaps NodeCapability = "disable-cache-network-maps" - - // NodeAttrDisableLinuxCGNATDropRule tells Linux clients to not insert a - // blanket firewall DROP rule for inbound traffic from the CGNAT IP range - // that does not originate from the Tailscale network interface. - // This enables access to off-tailnet endpoints within that IP range. - NodeAttrDisableLinuxCGNATDropRule NodeCapability = "disable-linux-cgnat-drop-rule" - - // NodeAttrEmitRuntimeMetrics enables emission of [runtime/metrics] as - // [tailscale.com/util/clientmetric]'s. - NodeAttrEmitRuntimeMetrics NodeCapability = "emit-runtime-metrics" - - // NodeAttrDisableUDPGRO disables UDP GRO (UDP_GRO socket option on Linux) - // on the magicsock UDP socket. It exists so control can mitigate kernel - // regressions that cause throughput or correctness issues with UDP GRO on - // specific OS/kernel versions, without requiring a client release. See - // https://github.com/tailscale/tailscale/issues/19777 for example. - // Currently only consulted on Linux; may apply to other platforms as they - // gain UDP GRO support. - NodeAttrDisableUDPGRO NodeCapability = "disable-udp-gro" - - // NodeAttrDisableUDPGSO disables UDP GSO (UDP_SEGMENT socket option on - // Linux) on the magicsock UDP socket. It exists so control can mitigate - // kernel regressions that cause throughput or correctness issues with UDP - // GSO on specific OS/kernel versions, without requiring a client release. - // See https://github.com/tailscale/tailscale/issues/19777 for example. - // Currently only consulted on Linux; may apply to other platforms as they - // gain UDP GSO support. - NodeAttrDisableUDPGSO NodeCapability = "disable-udp-gso" - - // NodeAttrDisableTUNUDPGRO disables UDP GRO on the Tailscale TUN device. - // It exists so control can mitigate kernel regressions that cause - // throughput or correctness issues with TUN UDP GRO on specific OS/kernel - // versions, without requiring a client release. See - // https://github.com/tailscale/tailscale/issues/13041 for example. - // Currently only consulted on Linux; may apply to other platforms as they - // gain TUN UDP GRO support. - NodeAttrDisableTUNUDPGRO NodeCapability = "disable-tun-udp-gro" - - // NodeAttrDisableTUNTCPGRO disables TCP GRO on the Tailscale TUN device. - // It exists so control can mitigate kernel regressions that cause - // throughput or correctness issues with TUN TCP GRO on specific OS/kernel - // versions, without requiring a client release. See - // https://github.com/tailscale/tailscale/issues/13041 for example. - // Currently only consulted on Linux; may apply to other platforms as they - // gain TUN TCP GRO support. - NodeAttrDisableTUNTCPGRO NodeCapability = "disable-tun-tcp-gro" - - // NodeAttrNeverGSOEqualTail enables a sentinel-tail workaround in the - // underlay UDP packet TX path on Linux. Applies to magicsock and peer relay - // UDP sockets. The workaround avoids emitting UDP GSO batches whose - // fragments are all equal in length, at a small payload and packet overhead - // cost. It exists so control can mitigate kernel regressions that mangle - // UDP headers or checksums for equal-length GSO batches, without requiring - // a client release. See https://github.com/tailscale/tailscale/issues/19777. - NodeAttrNeverGSOEqualTail NodeCapability = "never-gso-equal-tail" -) - -const ( - // NodeAttrPrefixServices is the prefix for per-service [NodeCapMap] - // entries describing Services visible (accessible) to this node. - // Each value under such a key is of type [ServiceDetails]. - // The suffix after the prefix is an opaque server-chosen identifier; - // consumers must use [ServiceDetails.Name] as the canonical service name - // rather than parsing it from the map key. - NodeAttrPrefixServices NodeCapabilityPrefix = "services/" + CapabilityFileSharing = nodecap.FileSharing + CapabilityAdmin = nodecap.Admin + CapabilityOwner = nodecap.Owner + CapabilitySSH = nodecap.SSH + CapabilitySSHRuleIn = nodecap.SSHRuleIn + CapabilityDataPlaneAuditLogs = nodecap.DataPlaneAuditLogs + CapabilityDebug = nodecap.Debug + CapabilityHTTPS = nodecap.HTTPS + CapabilityMacUIV2 = nodecap.MacUIV2 + CapabilityServicesInDesktopClients = nodecap.ServicesInDesktopClients + CapabilityBindToInterfaceByRoute = nodecap.BindToInterfaceByRoute + CapabilityDebugDisableAlternateDefaultRouteInterface = nodecap.DebugDisableAlternateDefaultRouteInterface + CapabilityDebugDisableBindConnToInterface = nodecap.DebugDisableBindConnToInterface + CapabilityDebugDisableBindConnToInterfaceAppleExt = nodecap.DebugDisableBindConnToInterfaceAppleExt + CapabilityTailnetLock = nodecap.TailnetLock + CapabilityWarnFunnelNoInvite = nodecap.WarnFunnelNoInvite + CapabilityWarnFunnelNoHTTPS = nodecap.WarnFunnelNoHTTPS + CapabilityDebugTSDNSResolution = nodecap.DebugTSDNSResolution + CapabilityFunnelPorts = nodecap.FunnelPorts + // Deprecated: Do not add any further values here, use [nodecap] instead. + + NodeAttrDisableAndroidBindToActiveNetwork = nodecap.DisableAndroidBindToActiveNetwork + NodeAttrOnlyTCP443 = nodecap.OnlyTCP443 + NodeAttrFunnel = nodecap.Funnel + NodeAttrSSHAggregator = nodecap.SSHAggregator + NodeAttrDebugForceBackgroundSTUN = nodecap.DebugForceBackgroundSTUN + NodeAttrDebugDisableWGTrim = nodecap.DebugDisableWGTrim + NodeAttrDisableSubnetsIfPAC = nodecap.DisableSubnetsIfPAC + NodeAttrDisableUPnP = nodecap.DisableUPnP + NodeAttrDisableDeltaUpdates = nodecap.DisableDeltaUpdates + NodeAttrRandomizeClientPort = nodecap.RandomizeClientPort + NodeAttrSilentDisco = nodecap.SilentDisco + NodeAttrOneCGNATEnable = nodecap.OneCGNATEnable + NodeAttrOneCGNATDisable = nodecap.OneCGNATDisable + NodeAttrPeerMTUEnable = nodecap.PeerMTUEnable + NodeAttrDNSForwarderDisableTCPRetries = nodecap.DNSForwarderDisableTCPRetries + NodeAttrLinuxMustUseIPTables = nodecap.LinuxMustUseIPTables + NodeAttrLinuxMustUseNfTables = nodecap.LinuxMustUseNfTables + NodeAttrProbeUDPLifetime = nodecap.ProbeUDPLifetime + NodeAttrsTaildriveShare = nodecap.TaildriveShare + NodeAttrsTaildriveAccess = nodecap.TaildriveAccess + NodeAttrSuggestExitNode = nodecap.SuggestExitNode + NodeAttrDisableWebClient = nodecap.DisableWebClient + NodeAttrLogExitFlows = nodecap.LogExitFlows + NodeAttrAutoExitNode = nodecap.AutoExitNode + NodeAttrStoreAppCRoutes = nodecap.StoreAppCRoutes + NodeAttrSuggestExitNodeUI = nodecap.SuggestExitNodeUI + NodeAttrUserDialUseRoutes = nodecap.UserDialUseRoutes + NodeAttrSSHBehaviorV1 = nodecap.SSHBehaviorV1 + NodeAttrSSHBehaviorV2 = nodecap.SSHBehaviorV2 + NodeAttrDisableSplitDNSWhenNoCustomResolvers = nodecap.DisableSplitDNSWhenNoCustomResolvers + NodeAttrScopeQuad100OnMacOS = nodecap.ScopeQuad100OnMacOS + NodeAttrDisableLocalDNSOverrideViaNRPT = nodecap.DisableLocalDNSOverrideViaNRPT + NodeAttrDisableMagicSockCryptoRouting = nodecap.DisableMagicSockCryptoRouting + NodeAttrDisableCaptivePortalDetection = nodecap.DisableCaptivePortalDetection + NodeAttrDisableSkipStatusQueue = nodecap.DisableSkipStatusQueue + NodeAttrSSHEnvironmentVariables = nodecap.SSHEnvironmentVariables + NodeAttrServiceHost = nodecap.ServiceHost + NodeAttrMaxKeyDuration = nodecap.MaxKeyDuration + NodeAttrNativeIPV4 = nodecap.NativeIPV4 + NodeAttrDisableRelayServer = nodecap.DisableRelayServer + NodeAttrDisableRelayClient = nodecap.DisableRelayClient + NodeAttrMagicDNSPeerAAAA = nodecap.MagicDNSPeerAAAA + NodeAttrDNSSubdomainResolve = nodecap.DNSSubdomainResolve + NodeAttrTrafficSteering = nodecap.TrafficSteering + NodeAttrTailnetDisplayName = nodecap.TailnetDisplayName + NodeAttrClientSideReachability = nodecap.ClientSideReachability + NodeAttrClientSideReachabilityRouteCheck = nodecap.ClientSideReachabilityRouteCheck + NodeAttrDefaultAutoUpdate = nodecap.DefaultAutoUpdate + NodeAttrDisableHostsFileUpdates = nodecap.DisableHostsFileUpdates + NodeAttrForceRegisterMagicDNSIPv4Only = nodecap.ForceRegisterMagicDNSIPv4Only + NodeAttrCacheNetworkMaps = nodecap.CacheNetworkMaps + NodeAttrDisableCacheNetworkMaps = nodecap.DisableCacheNetworkMaps + NodeAttrDisableLinuxCGNATDropRule = nodecap.DisableLinuxCGNATDropRule + NodeAttrEmitRuntimeMetrics = nodecap.EmitRuntimeMetrics + NodeAttrDisableUDPGRO = nodecap.DisableUDPGRO + NodeAttrDisableUDPGSO = nodecap.DisableUDPGSO + NodeAttrDisableTUNUDPGRO = nodecap.DisableTUNUDPGRO + NodeAttrDisableTUNTCPGRO = nodecap.DisableTUNTCPGRO + NodeAttrNeverGSOEqualTail = nodecap.NeverGSOEqualTail + // Deprecated: Do not add any further values here, use [nodecap] instead. + + NodeAttrPrefixServices = nodecap.ServicesPrefix + // Deprecated: Do not add any further values here, use [nodecap] instead. ) // SetDNSRequest is a request to add a DNS record. diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index e1d5551cd..e8162ce64 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -10,6 +10,7 @@ "net/netip" "time" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -59,7 +60,7 @@ func (src *Node) Clone() *Node { } dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...) if dst.CapMap != nil { - dst.CapMap = map[NodeCapability][]RawMessage{} + dst.CapMap = map[nodecap.Cap][]RawMessage{} for k := range src.CapMap { dst.CapMap[k] = append([]RawMessage{}, src.CapMap[k]...) } @@ -108,7 +109,7 @@ func (src *Node) Clone() *Node { LastSeen *time.Time Online *bool MachineAuthorized bool - Capabilities []NodeCapability + Capabilities []nodecap.Cap CapMap NodeCapMap UnsignedPeerAPIOnly bool ComputedName string diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index 5658f3111..b25bcc77f 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -17,6 +17,7 @@ "tailscale.com/ipn/ipnstate" . "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstest/deptest" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -1081,23 +1082,23 @@ type testRule struct { } tests := []struct { name string - capType PeerCapability + capType peercap.Cap val testRule }{ { name: "empty", val: testRule{}, - capType: PeerCapability("foo"), + capType: peercap.Cap("foo"), }, { name: "some-values", val: testRule{Ports: []int{80, 443}, Name: "foo"}, - capType: PeerCapability("foo"), + capType: peercap.Cap("foo"), }, { name: "all-values", val: testRule{Ports: []int{80, 443}, Name: "foo", ToggleOn: true, Groups: inner{Groups: []string{"foo", "bar"}}, Addrs: []netip.AddrPort{testip}}, - capType: PeerCapability("foo"), + capType: peercap.Cap("foo"), }, } for _, tc := range tests { diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 008c6fcfa..f50483bb1 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -13,6 +13,7 @@ jsonv2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -277,7 +278,7 @@ func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } // "https://tailscale.com/cap/file-sharing" // // Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508 -func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.SliceOf(v.ж.Capabilities) } +func (v NodeView) Capabilities() views.Slice[nodecap.Cap] { return views.SliceOf(v.ж.Capabilities) } // CapMap is a map of capabilities to their optional argument/data values. // @@ -288,7 +289,7 @@ func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.Slic // represented by the Capabilities field, but can now be represented by // CapMap with an empty value. // -// See NodeCapability for more information on keys. +// See [nodecap.Cap] for more information on keys. // // Metadata about nodes can be transmitted in 3 ways: // 1. MapResponse.Node.CapMap describes attributes that affect behavior for @@ -299,7 +300,7 @@ func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.Slic // 3. MapResponse.Peers[].CapMap describes attributes regarding a peer node, // such as which features the peer supports or if that peer is preferred // for a particular task vs other peers that could also be chosen. -func (v NodeView) CapMap() views.MapSlice[NodeCapability, RawMessage] { +func (v NodeView) CapMap() views.MapSlice[nodecap.Cap, RawMessage] { return views.MapSliceOf(v.ж.CapMap) } @@ -402,7 +403,7 @@ func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) } LastSeen *time.Time Online *bool MachineAuthorized bool - Capabilities []NodeCapability + Capabilities []nodecap.Cap CapMap NodeCapMap UnsignedPeerAPIOnly bool ComputedName string diff --git a/tsnet/depaware.txt b/tsnet/depaware.txt index 302f5e0d8..efd3f4117 100644 --- a/tsnet/depaware.txt +++ b/tsnet/depaware.txt @@ -226,6 +226,8 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware) 💣 tailscale.com/safesocket from tailscale.com/client/local+ tailscale.com/syncs from tailscale.com/control/controlhttp+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/feature/acme tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/httprec from tailscale.com/feature/c2n diff --git a/tsnet/tailnetlock_test.go b/tsnet/tailnetlock_test.go index 397578bfd..67f87d7fd 100644 --- a/tsnet/tailnetlock_test.go +++ b/tsnet/tailnetlock_test.go @@ -39,6 +39,7 @@ "time" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/tstest" "tailscale.com/tstest/integration/testcontrol" @@ -73,7 +74,7 @@ func setupTailnetLockedServer(t *testing.T, ctx context.Context, extraTrustedKey // Hand out the tailnet-lock capability so the server can call // TailnetLockInit. control.DefaultNodeCapabilities = &tailcfg.NodeCapMap{ - tailcfg.CapabilityTailnetLock: nil, + nodecap.TailnetLock: nil, } s, _, s1Key = startServer(t, ctx, controlURL, "s1") diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index 14a35cbf3..3693326f8 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -186,6 +186,7 @@ "tailscale.com/net/socks5" "tailscale.com/net/tsdial" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/types/bools" "tailscale.com/types/logger" @@ -1650,12 +1651,12 @@ func (ServiceModeHTTP) network() string { return "tcp" } func (m ServiceModeHTTP) port() uint16 { return m.Port } -func (m ServiceModeHTTP) capsMap() map[string][]tailcfg.PeerCapability { - capsMap := map[string][]tailcfg.PeerCapability{} +func (m ServiceModeHTTP) capsMap() map[string][]peercap.Cap { + capsMap := map[string][]peercap.Cap{} for path, capNames := range m.AcceptAppCaps { - caps := make([]tailcfg.PeerCapability, 0, len(capNames)) + caps := make([]peercap.Cap, 0, len(capNames)) for _, c := range capNames { - caps = append(caps, tailcfg.PeerCapability(c)) + caps = append(caps, peercap.Cap(c)) } capsMap[path] = caps } diff --git a/tsnet/tsnet_test.go b/tsnet/tsnet_test.go index 85640d5bc..1d8c01d91 100644 --- a/tsnet/tsnet_test.go +++ b/tsnet/tsnet_test.go @@ -53,6 +53,7 @@ "tailscale.com/net/netns" "tailscale.com/net/packet" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstest/deptest" "tailscale.com/tstest/integration" @@ -1011,17 +1012,17 @@ func setUpServiceState(t *testing.T, name, ip string, host, client *Server, // is a mapping from the Service name to the Service VIP. cm := host.lb.NetMap().SelfNode.CapMap() svcIPMap := make(tailcfg.ServiceIPMappings) - if cm.Contains(tailcfg.NodeAttrServiceHost) { - parsed := must.Get(tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](cm, tailcfg.NodeAttrServiceHost)) + if cm.Contains(nodecap.ServiceHost) { + parsed := must.Get(tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](cm, nodecap.ServiceHost)) if len(parsed) != 1 { - t.Fatalf("expected only one capability for %v, got %d", tailcfg.NodeAttrServiceHost, len(parsed)) + t.Fatalf("expected only one capability for %v, got %d", nodecap.ServiceHost, len(parsed)) } svcIPMap = parsed[0] } svcIPMap[serviceName] = []netip.Addr{netip.MustParseAddr(ip)} svcIPMapJSON := must.Get(json.Marshal(svcIPMap)) newCM := cm.AsMap() - mak.Set(&newCM, tailcfg.NodeAttrServiceHost, []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}) + mak.Set(&newCM, nodecap.ServiceHost, []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}) control.SetNodeCapMap(host.lb.NodeKey(), newCM) // The Service host must be allowed to advertise the Service VIP. diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go index 8a894c7cf..d11b0bbed 100644 --- a/tstest/integration/integration_test.go +++ b/tstest/integration/integration_test.go @@ -45,6 +45,7 @@ "tailscale.com/net/tstun" "tailscale.com/net/udprelay/status" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstest/integration/testcontrol" "tailscale.com/types/key" @@ -1669,7 +1670,7 @@ func testAutoUpdateDefaults(t *testing.T, useCap bool) { if mr.Node.CapMap == nil { mr.Node.CapMap = make(tailcfg.NodeCapMap) } - mr.Node.CapMap[tailcfg.NodeAttrDefaultAutoUpdate] = []tailcfg.RawMessage{ + mr.Node.CapMap[nodecap.DefaultAutoUpdate] = []tailcfg.RawMessage{ tailcfg.RawMessage(fmt.Sprintf("%t", send)), } } else { @@ -2500,7 +2501,7 @@ func TestTailnetLock(t *testing.T) { env := NewTestEnv(t) env.Control.DefaultNodeCapabilities = &tailcfg.NodeCapMap{ - tailcfg.CapabilityTailnetLock: []tailcfg.RawMessage{}, + nodecap.TailnetLock: []tailcfg.RawMessage{}, } // Start two nodes which will be our signing nodes. diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index fda31b398..033eb00db 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -33,6 +33,8 @@ "tailscale.com/net/tsaddr" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tka" "tailscale.com/tstest/tkatest" "tailscale.com/types/key" @@ -1057,10 +1059,10 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key. capMap = *s.DefaultNodeCapabilities } else { capMap = tailcfg.NodeCapMap{ - tailcfg.CapabilityHTTPS: []tailcfg.RawMessage{}, - tailcfg.NodeAttrFunnel: []tailcfg.RawMessage{}, - tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{}, - tailcfg.CapabilityFunnelPorts + "?ports=8080,443": []tailcfg.RawMessage{}, + nodecap.HTTPS: []tailcfg.RawMessage{}, + nodecap.Funnel: []tailcfg.RawMessage{}, + nodecap.FileSharing: []tailcfg.RawMessage{}, + nodecap.FunnelPorts + "?ports=8080,443": []tailcfg.RawMessage{}, } } @@ -1605,12 +1607,12 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey key.Machi func packetFilterWithIngress(addRelayCaps bool, allowSrcs []string) []tailcfg.FilterRule { out := slices.Clone(tailcfg.FilterAllowAll) out[0].SrcIPs = allowSrcs - caps := []tailcfg.PeerCapability{ - tailcfg.PeerCapabilityIngress, + caps := []peercap.Cap{ + peercap.Ingress, } if addRelayCaps { - caps = append(caps, tailcfg.PeerCapabilityRelay) - caps = append(caps, tailcfg.PeerCapabilityRelayTarget) + caps = append(caps, peercap.Relay) + caps = append(caps, peercap.RelayTarget) } out = append(out, tailcfg.FilterRule{ SrcIPs: []string{"*"}, @@ -1647,9 +1649,9 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, s.mu.Unlock() node.CapMap = nodeCapMap - node.Capabilities = append(node.Capabilities, tailcfg.NodeAttrDisableUPnP) + node.Capabilities = append(node.Capabilities, nodecap.DisableUPnP) if sshPolicy != nil { - mak.Set(&node.CapMap, tailcfg.CapabilitySSH, nil) + mak.Set(&node.CapMap, nodecap.SSH, nil) } t := time.Date(2020, 8, 3, 0, 0, 0, 1, time.UTC) diff --git a/tstest/largetailnet/largetailnet.go b/tstest/largetailnet/largetailnet.go index 73ec2da80..478973792 100644 --- a/tstest/largetailnet/largetailnet.go +++ b/tstest/largetailnet/largetailnet.go @@ -32,6 +32,7 @@ "go4.org/mem" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest/integration/testcontrol" "tailscale.com/types/key" ) @@ -179,7 +180,7 @@ func (s *Streamer) sendInitial(w testcontrol.MapStreamWriter, req *tailcfg.MapRe MachineAuthorized: true, Addresses: []netip.Prefix{selfIP4, selfIP6}, AllowedIPs: []netip.Prefix{selfIP4, selfIP6}, - CapMap: map[tailcfg.NodeCapability][]tailcfg.RawMessage{}, + CapMap: map[nodecap.Cap][]tailcfg.RawMessage{}, } initial := &tailcfg.MapResponse{ diff --git a/tstest/natlab/vmtest/connectivity_test.go b/tstest/natlab/vmtest/connectivity_test.go index 8cbbfdc29..8085655cf 100644 --- a/tstest/natlab/vmtest/connectivity_test.go +++ b/tstest/natlab/vmtest/connectivity_test.go @@ -8,6 +8,7 @@ "testing" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest/natlab/vmtest" "tailscale.com/tstest/natlab/vnet" ) @@ -241,7 +242,7 @@ func TestNonTailscaleCGNATEndpoint(t *testing.T) { vmtest.OS(vmtest.Gokrazy)) n1 := env.AddNode("node-1", cgnatNW, - tailcfg.NodeCapMap{tailcfg.NodeAttrDisableLinuxCGNATDropRule: nil}, + tailcfg.NodeCapMap{nodecap.DisableLinuxCGNATDropRule: nil}, vmtest.OS(vmtest.Gokrazy)) env.Start() diff --git a/tstest/natlab/vmtest/vmtest_test.go b/tstest/natlab/vmtest/vmtest_test.go index 576982eef..1c43d2334 100644 --- a/tstest/natlab/vmtest/vmtest_test.go +++ b/tstest/natlab/vmtest/vmtest_test.go @@ -17,6 +17,7 @@ "tailscale.com/ipn" "tailscale.com/net/udprelay/status" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstest/integration/testcontrol" "tailscale.com/tstest/natlab/vmtest" @@ -1116,10 +1117,10 @@ func TestCachedNetmapAfterRestart(t *testing.T) { a := env.AddNode("a", aNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) b := env.AddNode("b", bNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) connectStep := env.AddStep("Establish initial TSMP tunnel") cutControlStep := env.AddStep("Cut control server access") @@ -1206,10 +1207,10 @@ func TestDirectConnectionWithCachedNetmapOnOneNode(t *testing.T) { // Node "a" is the offline peer, node "b" is the online peer. a := env.AddNode("a", aNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) b := env.AddNode("b", bNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) pStr := "Ping a → b" if testPingFrom == "online" { @@ -1295,10 +1296,10 @@ func TestDirectConnectionWithCachedNetmapOnTwoNodes(t *testing.T) { a := env.AddNode("a", aNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) b := env.AddNode("b", bNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) checkInitialMetrics := env.AddStep("Check initial client metrics") cutControlStep := env.AddStep("Cut control server access") diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go index fbf415be0..604a5d363 100644 --- a/types/netmap/netmap.go +++ b/types/netmap/netmap.go @@ -15,6 +15,7 @@ "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/types/key" "tailscale.com/types/views" @@ -30,7 +31,7 @@ type NetworkMap struct { Cached bool // whether this NetworkMap was loaded from disk cache (as opposed to live from network) SelfNode tailcfg.NodeView - AllCaps set.Set[tailcfg.NodeCapability] // set version of SelfNode.Capabilities + SelfNode.CapMap + AllCaps set.Set[nodecap.Cap] // set version of SelfNode.Capabilities + SelfNode.CapMap NodeKey key.NodePublic MachineKey key.MachinePublic @@ -112,7 +113,7 @@ func (nm *NetworkMap) GetVIPServiceIPMap() tailcfg.ServiceIPMappings { return nil } - ipMaps, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](nm.SelfNode.CapMap(), tailcfg.NodeAttrServiceHost) + ipMaps, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](nm.SelfNode.CapMap(), nodecap.ServiceHost) if len(ipMaps) != 1 || err != nil { return nil } @@ -160,7 +161,7 @@ func (nm *NetworkMap) Services() map[tailcfg.ServiceName]tailcfg.ServiceDetails } result := make(map[tailcfg.ServiceName]tailcfg.ServiceDetails) for cap := range nm.SelfNode.CapMap().All() { - if !strings.HasPrefix(string(cap), string(tailcfg.NodeAttrPrefixServices)) { + if !strings.HasPrefix(string(cap), string(nodecap.ServicesPrefix)) { continue } svcs, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceDetails](nm.SelfNode.CapMap(), cap) @@ -207,7 +208,7 @@ func (nm *NetworkMap) GetMachineStatus() tailcfg.MachineStatus { } // HasCap reports whether nm is non-nil and nm.AllCaps contains c. -func (nm *NetworkMap) HasCap(c tailcfg.NodeCapability) bool { +func (nm *NetworkMap) HasCap(c nodecap.Cap) bool { return nm != nil && nm.AllCaps.Contains(c) } @@ -304,7 +305,7 @@ func (nm *NetworkMap) TailnetDisplayName() string { return "" } - tailnetDisplayNames, err := tailcfg.UnmarshalNodeCapViewJSON[string](nm.SelfNode.CapMap(), tailcfg.NodeAttrTailnetDisplayName) + tailnetDisplayNames, err := tailcfg.UnmarshalNodeCapViewJSON[string](nm.SelfNode.CapMap(), nodecap.TailnetDisplayName) if err != nil || len(tailnetDisplayNames) == 0 { return "" } diff --git a/util/deephash/deephash_test.go b/util/deephash/deephash_test.go index a82203d50..d3eaaa6ca 100644 --- a/util/deephash/deephash_test.go +++ b/util/deephash/deephash_test.go @@ -23,6 +23,7 @@ "go4.org/mem" "go4.org/netipx" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/key" "tailscale.com/util/deephash/testtype" "tailscale.com/util/hashx" @@ -645,7 +646,7 @@ type I struct{ IPProto: []int{1, 2, 3, 4}, CapGrant: []tailcfg.CapGrant{{ Dsts: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32")}, - Caps: []tailcfg.PeerCapability{"foo"}, + Caps: []peercap.Cap{"foo"}, }}, }, { diff --git a/wgengine/filter/filter_test.go b/wgengine/filter/filter_test.go index 56e35b88c..8ba319fa0 100644 --- a/wgengine/filter/filter_test.go +++ b/wgengine/filter/filter_test.go @@ -23,6 +23,8 @@ "tailscale.com/net/packet" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstest" "tailscale.com/tstime/rate" "tailscale.com/types/ipproto" @@ -47,12 +49,12 @@ // or tailcfg.NodeCapability values. Other values panic. func m(srcs []netip.Prefix, dsts []NetPortRange, opts ...any) Match { var protos []ipproto.Proto - var caps []tailcfg.NodeCapability + var caps []nodecap.Cap for _, o := range opts { switch o := o.(type) { case ipproto.Proto: protos = append(protos, o) - case tailcfg.NodeCapability: + case nodecap.Cap: caps = append(caps, o) default: panic(fmt.Sprintf("unknown option type %T", o)) @@ -83,7 +85,7 @@ func newFilter(logf logger.Logf) *Filter { m(nets("::/0"), netports("::/0:443")), m(nets("0.0.0.0/0"), netports("0.0.0.0/0:*"), testAllowedProto), m(nets("::/0"), netports("::/0:*"), testAllowedProto), - m(nil, netports("1.2.3.4:22"), tailcfg.NodeCapability("cap-hit-1234-ssh")), + m(nil, netports("1.2.3.4:22"), nodecap.Cap("cap-hit-1234-ssh")), } // Expects traffic to 100.122.98.50, 1.2.3.4, 5.6.7.8, @@ -106,7 +108,7 @@ func TestFilter(t *testing.T) { ipWithCap := netip.MustParseAddr("10.0.0.1") ipWithoutCap := netip.MustParseAddr("10.0.0.2") - filt.srcIPHasCap = func(ip netip.Addr, cap tailcfg.NodeCapability) bool { + filt.srcIPHasCap = func(ip netip.Addr, cap nodecap.Cap) bool { return cap == "cap-hit-1234-ssh" && ip == ipWithCap } @@ -313,7 +315,7 @@ func TestParseIPSet(t *testing.T) { capTests := []struct { in string - want tailcfg.NodeCapability + want nodecap.Cap }{ {"cap:foo", "foo"}, {"cap:people-in-8.8.8.0/24", "people-in-8.8.8.0/24"}, // test precedence of "/" search @@ -990,7 +992,7 @@ func TestPeerCaps(t *testing.T) { Dsts: []netip.Prefix{ netip.MustParsePrefix("0.0.0.0/0"), }, - Caps: []tailcfg.PeerCapability{"is_ipv4"}, + Caps: []peercap.Cap{"is_ipv4"}, }}, }, { @@ -999,7 +1001,7 @@ func TestPeerCaps(t *testing.T) { Dsts: []netip.Prefix{ netip.MustParsePrefix("::/0"), }, - Caps: []tailcfg.PeerCapability{"is_ipv6"}, + Caps: []peercap.Cap{"is_ipv6"}, }}, }, { @@ -1008,7 +1010,7 @@ func TestPeerCaps(t *testing.T) { Dsts: []netip.Prefix{ netip.MustParsePrefix("100.200.0.0/16"), }, - Caps: []tailcfg.PeerCapability{"some_super_admin"}, + Caps: []peercap.Cap{"some_super_admin"}, }}, }, }) @@ -1019,37 +1021,37 @@ func TestPeerCaps(t *testing.T) { tests := []struct { name string src, dst string // IP - want []tailcfg.PeerCapability + want []peercap.Cap }{ { name: "v4", src: "1.2.3.4", dst: "2.4.5.5", - want: []tailcfg.PeerCapability{"is_ipv4"}, + want: []peercap.Cap{"is_ipv4"}, }, { name: "v6", src: "1::1", dst: "2::2", - want: []tailcfg.PeerCapability{"is_ipv6"}, + want: []peercap.Cap{"is_ipv6"}, }, { name: "admin", src: "100.199.1.2", dst: "100.200.3.4", - want: []tailcfg.PeerCapability{"is_ipv4", "some_super_admin"}, + want: []peercap.Cap{"is_ipv4", "some_super_admin"}, }, { name: "not_admin_bad_src", src: "100.198.1.2", // 198, not 199 dst: "100.200.3.4", - want: []tailcfg.PeerCapability{"is_ipv4"}, + want: []peercap.Cap{"is_ipv4"}, }, { name: "not_admin_bad_dst", src: "100.199.1.2", dst: "100.201.3.4", // 201, not 200 - want: []tailcfg.PeerCapability{"is_ipv4"}, + want: []peercap.Cap{"is_ipv4"}, }, } for _, tt := range tests { diff --git a/wgengine/filter/filtertype/filtertype.go b/wgengine/filter/filtertype/filtertype.go index aab5fe8ee..18a4020b1 100644 --- a/wgengine/filter/filtertype/filtertype.go +++ b/wgengine/filter/filtertype/filtertype.go @@ -10,6 +10,8 @@ "strings" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/types/views" ) @@ -56,7 +58,7 @@ type CapMatch struct { // Cap is the capability that's granted if the destination IP addresses // matches Dst. - Cap tailcfg.PeerCapability + Cap peercap.Cap // Values are the raw JSON values of the capability. // See tailcfg.PeerCapability and tailcfg.PeerCapMap for details. @@ -84,7 +86,7 @@ type Match struct { // has one of these capabilities, it's also permitted. The peers are only // looked up by their self address (Node.Addresses) and not by subnet routes // they advertise. - SrcCaps []tailcfg.NodeCapability + SrcCaps []nodecap.Cap Dsts []NetPortRange // optional, if source matches Caps []CapMatch // optional, if source match diff --git a/wgengine/filter/filtertype/filtertype_clone.go b/wgengine/filter/filtertype/filtertype_clone.go index 094063a5d..34975e2d5 100644 --- a/wgengine/filter/filtertype/filtertype_clone.go +++ b/wgengine/filter/filtertype/filtertype_clone.go @@ -9,6 +9,8 @@ "net/netip" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/types/views" ) @@ -39,7 +41,7 @@ func (src *Match) Clone() *Match { IPProto views.Slice[ipproto.Proto] Srcs []netip.Prefix SrcsContains func(netip.Addr) bool - SrcCaps []tailcfg.NodeCapability + SrcCaps []nodecap.Cap Dsts []NetPortRange Caps []CapMatch }{}) @@ -59,6 +61,6 @@ func (src *CapMatch) Clone() *CapMatch { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _CapMatchCloneNeedsRegeneration = CapMatch(struct { Dst netip.Prefix - Cap tailcfg.PeerCapability + Cap peercap.Cap Values []tailcfg.RawMessage }{}) diff --git a/wgengine/filter/match.go b/wgengine/filter/match.go index 6c157d124..33ce3814b 100644 --- a/wgengine/filter/match.go +++ b/wgengine/filter/match.go @@ -7,7 +7,7 @@ "net/netip" "tailscale.com/net/packet" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/views" "tailscale.com/wgengine/filter/filtertype" ) @@ -57,7 +57,7 @@ func srcMatches(m *filtertype.Match, srcAddr netip.Addr, hasCap CapTestFunc) boo // has a given capability. // // It is used in the fast path of evaluating filter rules so should be fast. -type CapTestFunc = func(srcIP netip.Addr, cap tailcfg.NodeCapability) bool +type CapTestFunc = func(srcIP netip.Addr, cap nodecap.Cap) bool func (ms matches) matchIPsOnly(q *packet.Parsed, hasCap CapTestFunc) bool { srcAddr := q.Src.Addr() diff --git a/wgengine/filter/tailcfg.go b/wgengine/filter/tailcfg.go index 3360b169d..06f289c95 100644 --- a/wgengine/filter/tailcfg.go +++ b/wgengine/filter/tailcfg.go @@ -12,6 +12,8 @@ "tailscale.com/net/ipset" "tailscale.com/net/netaddr" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/types/views" ) @@ -104,7 +106,7 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) { for cap, val := range cm.CapMap { m.Caps = append(m.Caps, CapMatch{ Dst: dstNet, - Cap: tailcfg.PeerCapability(cap), + Cap: peercap.Cap(cap), Values: val, }) } @@ -133,7 +135,7 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) { // around, and ultimately use a new version of IPSet.ContainsFunc like // Contains16Func that works in [16]byte address, so we can match // at runtime without allocating? -func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap tailcfg.NodeCapability, err error) { +func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap nodecap.Cap, err error) { if arg == "*" { // User explicitly requested wildcard. return []netip.Prefix{ @@ -142,7 +144,7 @@ func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap tailcfg.NodeCapabi }, "", nil } if cap, ok := strings.CutPrefix(arg, "cap:"); ok { - return nil, tailcfg.NodeCapability(cap), nil + return nil, nodecap.Cap(cap), nil } if strings.Contains(arg, "/") { pfx, err := netip.ParsePrefix(arg) diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index c8f8a6280..8b0ee245b 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -51,6 +51,8 @@ "tailscale.com/net/tstun" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsconst" "tailscale.com/tstime" "tailscale.com/tstime/mono" @@ -2481,7 +2483,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src epAddr, shouldBeRelayHandshake // unexpected return } - if !nodeHasCap(c.filt, peer, c.self, tailcfg.PeerCapabilityRelay) { + if !nodeHasCap(c.filt, peer, c.self, peercap.Relay) { return } // [Conn.mu] must not be held while publishing, or [Conn.onUDPRelayAllocResp] @@ -2934,7 +2936,7 @@ func (c *Conn) updateRelayServersSet(filt *filter.Filter, self tailcfg.NodeView, // compiled [tailcfg.CurrentCapabilityVersion]) forward. continue } - if !nodeHasCap(filt, maybeCandidate, self, tailcfg.PeerCapabilityRelayTarget) { + if !nodeHasCap(filt, maybeCandidate, self, peercap.RelayTarget) { continue } relayServers.Add(candidatePeerRelay{ @@ -2949,7 +2951,7 @@ func (c *Conn) updateRelayServersSet(filt *filter.Filter, self tailcfg.NodeView, } // nodeHasCap returns true if src has cap on dst, otherwise it returns false. -func nodeHasCap(filt *filter.Filter, src, dst tailcfg.NodeView, cap tailcfg.PeerCapability) bool { +func nodeHasCap(filt *filter.Filter, src, dst tailcfg.NodeView, cap peercap.Cap) bool { if filt == nil || !src.Valid() || !dst.Valid() { @@ -3025,8 +3027,8 @@ func (c *Conn) setNetworkMapInternal(self tailcfg.NodeView, peers []tailcfg.Node peersChanged, selfWasValid := c.updateNodes(self, peers) relayClientEnabled := self.Valid() && - !self.HasCap(tailcfg.NodeAttrDisableRelayClient) && - !self.HasCap(tailcfg.NodeAttrOnlyTCP443) + !self.HasCap(nodecap.DisableRelayClient) && + !self.HasCap(nodecap.OnlyTCP443) udpOffloadKnobsChanged := false var curGRO, curGSO bool @@ -3391,7 +3393,7 @@ func (c *Conn) relayCandidateLocked(p tailcfg.NodeView) (ok bool, cp candidatePe if !capVerIsRelayCapable(p.Cap()) { return false, candidatePeerRelay{} } - if !nodeHasCap(c.filt, p, c.self, tailcfg.PeerCapabilityRelayTarget) { + if !nodeHasCap(c.filt, p, c.self, peercap.RelayTarget) { return false, candidatePeerRelay{} } return true, candidatePeerRelay{ diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 34cf2fb8c..654df9352 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -60,6 +60,8 @@ "tailscale.com/net/tsaddr" "tailscale.com/net/tstun" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstest" "tailscale.com/tstest/natlab" "tailscale.com/tstime/mono" @@ -3829,7 +3831,7 @@ func Test_nodeHasCap(t *testing.T) { filt *filter.Filter src tailcfg.NodeView dst tailcfg.NodeView - cap tailcfg.PeerCapability + cap peercap.Cap want bool }{ { @@ -3840,14 +3842,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("1.1.1.1/32"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeCOnlyIPv4.View(), dst: nodeAOnlyIPv4.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: true, }, { @@ -3858,14 +3860,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("::1/128"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeDOnlyIPv6.View(), dst: nodeBOnlyIPv6.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: true, }, { @@ -3876,14 +3878,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("::3/128"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeDOnlyIPv6.View(), dst: nodeBOnlyIPv6.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, { @@ -3894,14 +3896,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("::1/128"), - Cap: tailcfg.PeerCapabilityIngress, + Cap: peercap.Ingress, }, }, }, }, nil, nil, nil, nil, nil), src: nodeDOnlyIPv6.View(), dst: nodeBOnlyIPv6.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, { @@ -3912,14 +3914,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("1.1.1.1/32"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: tailcfg.NodeView{}, dst: nodeAOnlyIPv4.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, { @@ -3930,14 +3932,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("1.1.1.1/32"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeCOnlyIPv4.View(), dst: tailcfg.NodeView{}, - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, { @@ -3948,14 +3950,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("1.1.1.1/32"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeCUnsigned.View(), dst: nodeAOnlyIPv4.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, } @@ -4004,11 +4006,11 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { selfNodeNodeAttrDisableRelayClient := selfNode.Clone() selfNodeNodeAttrDisableRelayClient.CapMap = make(tailcfg.NodeCapMap) - selfNodeNodeAttrDisableRelayClient.CapMap[tailcfg.NodeAttrDisableRelayClient] = nil + selfNodeNodeAttrDisableRelayClient.CapMap[nodecap.DisableRelayClient] = nil selfNodeNodeAttrOnlyTCP443 := selfNode.Clone() selfNodeNodeAttrOnlyTCP443.CapMap = make(tailcfg.NodeCapMap) - selfNodeNodeAttrOnlyTCP443.CapMap[tailcfg.NodeAttrOnlyTCP443] = nil + selfNodeNodeAttrOnlyTCP443.CapMap[nodecap.OnlyTCP443] = nil tests := []struct { name string @@ -4026,7 +4028,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNode.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, @@ -4050,7 +4052,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNodeNodeAttrDisableRelayClient.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, @@ -4068,7 +4070,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNodeNodeAttrOnlyTCP443.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, @@ -4086,7 +4088,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNode.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, @@ -4110,7 +4112,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNode.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, },