diff --git a/cmd/tailscale/cli/funnel.go b/cmd/tailscale/cli/funnel.go index bacea357c..875c5f38e 100644 --- a/cmd/tailscale/cli/funnel.go +++ b/cmd/tailscale/cli/funnel.go @@ -9,7 +9,6 @@ import ( "fmt" "net" "os" - "slices" "strconv" "strings" @@ -147,15 +146,13 @@ 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, st *ipnstate.Status, port uint16) error { - hasFunnelAttrs := func(attrs []tailcfg.NodeCapability) bool { - hasHTTPS := slices.Contains(attrs, tailcfg.CapabilityHTTPS) - hasFunnel := slices.Contains(attrs, tailcfg.NodeAttrFunnel) - return hasHTTPS && hasFunnel + hasFunnelAttrs := func(selfNode *ipnstate.PeerStatus) bool { + return selfNode.HasCap(tailcfg.CapabilityHTTPS) && selfNode.HasCap(tailcfg.NodeAttrFunnel) } - if hasFunnelAttrs(st.Self.Capabilities) { + if hasFunnelAttrs(st.Self) { return nil // already enabled } - enableErr := e.enableFeatureInteractive(ctx, "funnel", hasFunnelAttrs) + enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel) st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block switch { case statusErr != nil: diff --git a/cmd/tailscale/cli/serve.go b/cmd/tailscale/cli/serve.go index 2a96f8c13..6bb1dece0 100644 --- a/cmd/tailscale/cli/serve.go +++ b/cmd/tailscale/cli/serve.go @@ -18,7 +18,6 @@ import ( "path/filepath" "reflect" "runtime" - "slices" "sort" "strconv" "strings" @@ -269,9 +268,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", func(caps []tailcfg.NodeCapability) bool { - return slices.Contains(caps, tailcfg.CapabilityHTTPS) - }) + e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS) } srcPort, err := parseServePort(srcPortStr) @@ -829,7 +826,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, hasRequiredCapabilities func(caps []tailcfg.NodeCapability) bool) (err error) { +func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...tailcfg.NodeCapability) (err error) { info, err := e.lc.QueryFeature(ctx, feature) if err != nil { return err @@ -875,7 +872,16 @@ func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, return err } if nm := n.NetMap; nm != nil && nm.SelfNode.Valid() { - if hasRequiredCapabilities(nm.SelfNode.Capabilities().AsSlice()) { + gotAll := true + for _, c := range caps { + if !nm.SelfNode.HasCap(c) { + // The feature is not yet enabled. + // Continue blocking until it is. + gotAll = false + break + } + } + if gotAll { e.lc.IncrementCounter(ctx, fmt.Sprintf("%s_enabled", feature), 1) fmt.Fprintln(os.Stdout, "Success.") return nil diff --git a/cmd/tailscale/cli/serve_dev.go b/cmd/tailscale/cli/serve_dev.go index 7fb663def..7e98baa3e 100644 --- a/cmd/tailscale/cli/serve_dev.go +++ b/cmd/tailscale/cli/serve_dev.go @@ -15,7 +15,6 @@ import ( "os/signal" "path" "path/filepath" - "slices" "sort" "strconv" "strings" @@ -233,9 +232,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", func(caps []tailcfg.NodeCapability) bool { - return slices.Contains(caps, tailcfg.CapabilityHTTPS) - }); err != nil { + if err := e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS); err != nil { return fmt.Errorf("error enabling https feature: %w", err) } } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index df380cfe0..d6a727023 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -4094,8 +4094,8 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) { } func hasCapability(nm *netmap.NetworkMap, cap tailcfg.NodeCapability) bool { - if nm != nil && nm.SelfNode.Valid() { - return views.SliceContains(nm.SelfNode.Capabilities(), cap) + if nm != nil { + return nm.SelfNode.HasCap(cap) } return false } diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index e892c9252..855c34e1a 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -1035,7 +1035,7 @@ func (h *peerAPIHandler) canPutFile() bool { // canDebug reports whether h can debug this node (goroutines, metrics, // magicsock internal state, etc). func (h *peerAPIHandler) canDebug() bool { - if !views.SliceContains(h.selfNode.Capabilities(), tailcfg.CapabilityDebug) { + if !h.selfNode.HasCap(tailcfg.CapabilityDebug) { // This node does not expose debug info. return false } diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 13940c20b..a5a9b1efd 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -291,6 +291,11 @@ type PeerStatus struct { Location *tailcfg.Location `json:",omitempty"` } +// HasCap reports whether ps has the given capability. +func (ps *PeerStatus) HasCap(cap tailcfg.NodeCapability) bool { + return slices.Contains(ps.Capabilities, cap) +} + // StatusBuilder is a request to construct a Status. A new StatusBuilder is // passed to various subsystems which then call methods on it to populate state. // Call its Status method to return the final constructed Status. diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 50e5d568d..414dc4ca3 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -368,6 +368,16 @@ type Node struct { ExitNodeDNSResolvers []*dnstype.Resolver `json:",omitempty"` } +// HasCap reports whether the node has the given capability. +func (v NodeView) HasCap(cap NodeCapability) bool { + return v.ж.HasCap(cap) +} + +// HasCap reports whether the node has the given capability. +func (v *Node) HasCap(cap NodeCapability) bool { + return v != nil && slices.Contains(v.Capabilities, cap) +} + // DisplayName returns the user-facing name for a node which should // be shown in client UIs. // diff --git a/wgengine/wgcfg/nmcfg/nmcfg.go b/wgengine/wgcfg/nmcfg/nmcfg.go index c64992524..a7787e509 100644 --- a/wgengine/wgcfg/nmcfg/nmcfg.go +++ b/wgengine/wgcfg/nmcfg/nmcfg.go @@ -15,7 +15,6 @@ import ( "tailscale.com/types/logger" "tailscale.com/types/logid" "tailscale.com/types/netmap" - "tailscale.com/types/views" "tailscale.com/wgengine/wgcfg" ) @@ -63,7 +62,7 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags, // Setup log IDs for data plane audit logging. if nm.SelfNode.Valid() { cfg.NodeID = nm.SelfNode.StableID() - canNetworkLog := views.SliceContains(nm.SelfNode.Capabilities(), tailcfg.CapabilityDataPlaneAuditLogs) + canNetworkLog := nm.SelfNode.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) if canNetworkLog && nm.SelfNode.DataPlaneAuditLogID() != "" && nm.DomainAuditLogID != "" { nodeID, errNode := logid.ParsePrivateID(nm.SelfNode.DataPlaneAuditLogID()) if errNode != nil {