From a61caea9111b8304414be768a89063721a5ae46c Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Wed, 6 Sep 2023 10:17:25 -0700 Subject: [PATCH] tailcfg: define a type for NodeCapability Instead of untyped string, add a type to identify these. Updates #cleanup Signed-off-by: Maisem Ali --- cmd/tailscale/cli/funnel.go | 2 +- cmd/tailscale/cli/serve.go | 4 +- cmd/tailscale/cli/serve_dev.go | 2 +- cmd/tailscale/cli/serve_test.go | 8 +- control/controlclient/map_test.go | 14 +-- control/controlknobs/controlknobs.go | 2 +- envknob/logknob/logknob.go | 7 +- envknob/logknob/logknob_test.go | 2 +- ipn/ipnlocal/local.go | 2 +- ipn/ipnstate/ipnstate.go | 2 +- ipn/serve.go | 9 +- ipn/serve_test.go | 21 ++--- tailcfg/tailcfg.go | 89 ++++++++++--------- tailcfg/tailcfg_clone.go | 2 +- tailcfg/tailcfg_view.go | 16 ++-- tstest/integration/testcontrol/testcontrol.go | 2 +- types/netmap/netmap.go | 4 +- wgengine/filter/filter_test.go | 2 +- 18 files changed, 100 insertions(+), 90 deletions(-) diff --git a/cmd/tailscale/cli/funnel.go b/cmd/tailscale/cli/funnel.go index 52180a1ed..bacea357c 100644 --- a/cmd/tailscale/cli/funnel.go +++ b/cmd/tailscale/cli/funnel.go @@ -147,7 +147,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, st *ipnstate.Status, port uint16) error { - hasFunnelAttrs := func(attrs []string) bool { + hasFunnelAttrs := func(attrs []tailcfg.NodeCapability) bool { hasHTTPS := slices.Contains(attrs, tailcfg.CapabilityHTTPS) hasFunnel := slices.Contains(attrs, tailcfg.NodeAttrFunnel) return hasHTTPS && hasFunnel diff --git a/cmd/tailscale/cli/serve.go b/cmd/tailscale/cli/serve.go index d873ae06a..2a96f8c13 100644 --- a/cmd/tailscale/cli/serve.go +++ b/cmd/tailscale/cli/serve.go @@ -269,7 +269,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 []string) bool { + e.enableFeatureInteractive(ctx, "serve", func(caps []tailcfg.NodeCapability) bool { return slices.Contains(caps, tailcfg.CapabilityHTTPS) }) } @@ -829,7 +829,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 []string) bool) (err error) { +func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, hasRequiredCapabilities func(caps []tailcfg.NodeCapability) bool) (err error) { info, err := e.lc.QueryFeature(ctx, feature) if err != nil { return err diff --git a/cmd/tailscale/cli/serve_dev.go b/cmd/tailscale/cli/serve_dev.go index 5cdb041bc..6705710b0 100644 --- a/cmd/tailscale/cli/serve_dev.go +++ b/cmd/tailscale/cli/serve_dev.go @@ -233,7 +233,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 []string) bool { + if err := e.enableFeatureInteractive(ctx, "serve", func(caps []tailcfg.NodeCapability) bool { return slices.Contains(caps, tailcfg.CapabilityHTTPS) }); err != nil { return fmt.Errorf("error enabling https feature: %w", err) diff --git a/cmd/tailscale/cli/serve_test.go b/cmd/tailscale/cli/serve_test.go index a876c74f0..95e278bf6 100644 --- a/cmd/tailscale/cli/serve_test.go +++ b/cmd/tailscale/cli/serve_test.go @@ -763,7 +763,7 @@ func TestVerifyFunnelEnabled(t *testing.T) { // queryFeatureResponse is the mock response desired from the // call made to lc.QueryFeature by verifyFunnelEnabled. queryFeatureResponse mockQueryFeatureResponse - caps []string // optionally set at fakeStatus.Capabilities + caps []tailcfg.NodeCapability // optionally set at fakeStatus.Capabilities wantErr string wantPanic string }{ @@ -780,13 +780,13 @@ func TestVerifyFunnelEnabled(t *testing.T) { { name: "fallback-flow-missing-acl-rule", queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")}, - caps: []string{tailcfg.CapabilityHTTPS}, + caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS}, 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: []string{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, + caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, wantErr: "", // no error, success }, { @@ -858,7 +858,7 @@ type fakeLocalServeClient struct { BackendState: ipn.Running.String(), Self: &ipnstate.PeerStatus{ DNSName: "foo.test.ts.net", - Capabilities: []string{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"}, + Capabilities: []tailcfg.NodeCapability{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"}, }, } diff --git a/control/controlclient/map_test.go b/control/controlclient/map_test.go index f4daf074e..2b9b0ac6e 100644 --- a/control/controlclient/map_test.go +++ b/control/controlclient/map_test.go @@ -329,13 +329,13 @@ func TestUpdatePeersStateFromResponse(t *testing.T) { mapRes: &tailcfg.MapResponse{ PeersChangedPatch: []*tailcfg.PeerChange{{ NodeID: 1, - Capabilities: ptr.To([]string{"foo"}), + Capabilities: ptr.To([]tailcfg.NodeCapability{"foo"}), }}, }, want: peers(&tailcfg.Node{ ID: 1, Name: "foo", - Capabilities: []string{"foo"}, + Capabilities: []tailcfg.NodeCapability{"foo"}, }), wantStats: updateStats{changed: 1}, }} @@ -685,15 +685,15 @@ func TestPeerChangeDiff(t *testing.T) { }, { name: "patch-capabilities-to-nonempty", - a: &tailcfg.Node{ID: 1, Capabilities: []string{"foo"}}, - b: &tailcfg.Node{ID: 1, Capabilities: []string{"bar"}}, - want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]string{"bar"})}, + a: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"foo"}}, + b: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"bar"}}, + want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]tailcfg.NodeCapability{"bar"})}, }, { name: "patch-capabilities-to-empty", - a: &tailcfg.Node{ID: 1, Capabilities: []string{"foo"}}, + a: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"foo"}}, b: &tailcfg.Node{ID: 1}, - want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]string(nil))}, + want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]tailcfg.NodeCapability(nil))}, }, { name: "patch-online-to-true", diff --git a/control/controlknobs/controlknobs.go b/control/controlknobs/controlknobs.go index b5b49e150..50b0298a9 100644 --- a/control/controlknobs/controlknobs.go +++ b/control/controlknobs/controlknobs.go @@ -48,7 +48,7 @@ type Knobs struct { // UpdateFromNodeAttributes updates k (if non-nil) based on the provided self // node attributes (Node.Capabilities). -func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []string) { +func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability) { if k == nil { return } diff --git a/envknob/logknob/logknob.go b/envknob/logknob/logknob.go index 7e032a0ed..350384b86 100644 --- a/envknob/logknob/logknob.go +++ b/envknob/logknob/logknob.go @@ -9,6 +9,7 @@ "sync/atomic" "tailscale.com/envknob" + "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/types/views" ) @@ -22,7 +23,7 @@ // c2n log level changes), and via capabilities from a NetMap (so users can // enable logging via the ACL JSON). type LogKnob struct { - capName string + capName tailcfg.NodeCapability cap atomic.Bool env func() bool manual atomic.Bool @@ -30,7 +31,7 @@ type LogKnob struct { // NewLogKnob creates a new LogKnob, with the provided environment variable // name and/or NetMap capability. -func NewLogKnob(env, cap string) *LogKnob { +func NewLogKnob(env string, cap tailcfg.NodeCapability) *LogKnob { if env == "" && cap == "" { panic("must provide either an environment variable or capability") } @@ -58,7 +59,7 @@ func (lk *LogKnob) Set(v bool) { // about; we use this rather than a concrete type to avoid a circular // dependency. type NetMap interface { - SelfCapabilities() views.Slice[string] + SelfCapabilities() views.Slice[tailcfg.NodeCapability] } // UpdateFromNetMap will enable logging if the SelfNode in the provided NetMap diff --git a/envknob/logknob/logknob_test.go b/envknob/logknob/logknob_test.go index 16957169b..b2a376a25 100644 --- a/envknob/logknob/logknob_test.go +++ b/envknob/logknob/logknob_test.go @@ -64,7 +64,7 @@ func TestLogKnob(t *testing.T) { testKnob.UpdateFromNetMap(&netmap.NetworkMap{ SelfNode: (&tailcfg.Node{ - Capabilities: []string{ + Capabilities: []tailcfg.NodeCapability{ "https://tailscale.com/cap/testing", }, }).View(), diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index e4ccc6235..ab22c8c8f 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -4093,7 +4093,7 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) { cc.SetNetInfo(ni) } -func hasCapability(nm *netmap.NetworkMap, cap string) bool { +func hasCapability(nm *netmap.NetworkMap, cap tailcfg.NodeCapability) bool { if nm != nil && nm.SelfNode.Valid() { return views.SliceContains(nm.SelfNode.Capabilities(), cap) } diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 125378610..13940c20b 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -256,7 +256,7 @@ type PeerStatus struct { // "https://tailscale.com/cap/is-admin" // "https://tailscale.com/cap/file-sharing" // "funnel" - Capabilities []string `json:",omitempty"` + Capabilities []tailcfg.NodeCapability `json:",omitempty"` // SSH_HostKeys are the node's SSH host keys, if known. SSH_HostKeys []string `json:"sshHostKeys,omitempty"` diff --git a/ipn/serve.go b/ipn/serve.go index 08d4dd747..74975d6b0 100644 --- a/ipn/serve.go +++ b/ipn/serve.go @@ -240,7 +240,7 @@ func (sc *ServeConfig) IsFunnelOn() bool { // The nodeAttrs arg should be the node's Self.Capabilities which should contain // the attribute we're checking for and possibly warning-capabilities for // Funnel. -func CheckFunnelAccess(port uint16, nodeAttrs []string) error { +func CheckFunnelAccess(port uint16, nodeAttrs []tailcfg.NodeCapability) error { if !slices.Contains(nodeAttrs, tailcfg.CapabilityHTTPS) { return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.") } @@ -253,7 +253,7 @@ func CheckFunnelAccess(port uint16, nodeAttrs []string) error { // CheckFunnelPort checks whether the given port is allowed for Funnel. // It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed // ports. -func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error { +func CheckFunnelPort(wantedPort uint16, nodeAttrs []tailcfg.NodeCapability) error { deny := func(allowedPorts string) error { if allowedPorts == "" { return fmt.Errorf("port %d is not allowed for funnel", wantedPort) @@ -262,7 +262,8 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error { } var portsStr string for _, attr := range nodeAttrs { - if !strings.HasPrefix(attr, tailcfg.CapabilityFunnelPorts) { + attr := string(attr) + if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) { continue } u, err := url.Parse(attr) @@ -274,7 +275,7 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error { return deny("") } u.RawQuery = "" - if u.String() != tailcfg.CapabilityFunnelPorts { + if u.String() != string(tailcfg.CapabilityFunnelPorts) { return deny("") } } diff --git a/ipn/serve_test.go b/ipn/serve_test.go index cf3b14768..87ae2eba4 100644 --- a/ipn/serve_test.go +++ b/ipn/serve_test.go @@ -9,20 +9,21 @@ ) func TestCheckFunnelAccess(t *testing.T) { - portAttr := "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443," + caps := func(c ...tailcfg.NodeCapability) []tailcfg.NodeCapability { return c } + const portAttr tailcfg.NodeCapability = "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443," tests := []struct { port uint16 - caps []string + caps []tailcfg.NodeCapability wantErr bool }{ - {443, []string{portAttr}, true}, // No "funnel" attribute - {443, []string{portAttr, tailcfg.NodeAttrFunnel}, true}, - {443, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false}, - {8443, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false}, - {8321, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true}, - {8083, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false}, - {8091, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true}, - {3000, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true}, + {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}, } for _, tt := range tests { err := CheckFunnelAccess(tt.port, tt.caps) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index f5c9f3d41..89fc678f9 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -12,6 +12,7 @@ "fmt" "net/netip" "reflect" + "slices" "strings" "time" @@ -289,7 +290,7 @@ type Node struct { // such as: // "https://tailscale.com/cap/is-admin" // "https://tailscale.com/cap/file-sharing" - Capabilities []string `json:",omitempty"` + Capabilities []NodeCapability `json:",omitempty"` // UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA // restrictions. However, in exchange for that privilege, it does not get @@ -1226,10 +1227,11 @@ type CapGrant struct { CapMap PeerCapMap `json:",omitempty"` } -// PeerCapability is a capability granted to a node by a FilterRule. -// It's a string, but its meaning is application-defined. -// It must be a URL, like "https://tailscale.com/cap/file-sharing-target" or -// "https://example.com/cap/read-access". +// 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. +// +// It must be a URL like "https://tailscale.com/cap/file-send". type PeerCapability string const ( @@ -1838,7 +1840,7 @@ func (n *Node) Equal(n2 *Node) bool { n.Created.Equal(n2.Created) && eqTimePtr(n.LastSeen, n2.LastSeen) && n.MachineAuthorized == n2.MachineAuthorized && - eqStrings(n.Capabilities, n2.Capabilities) && + slices.Equal(n.Capabilities, n2.Capabilities) && n.ComputedName == n2.ComputedName && n.computedHostIfDifferent == n2.computedHostIfDifferent && n.ComputedNameWithHost == n2.ComputedNameWithHost && @@ -1911,112 +1913,117 @@ type Oauth2Token struct { Expiry time.Time `json:"expiry,omitempty"` } -const ( - // These are the capabilities that the self node has as listed in - // MapResponse.Node.Capabilities. - // - // We've since started referring to these as "Node Attributes" ("nodeAttrs" - // in the ACL policy file). +// NodeCapability represents a capability granted to the self node as listed in +// MapResponse.Node.Capabilities. +// +// 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. +// +// Unlike PeerCapability, NodeCapability 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 NodeCapability string - CapabilityFileSharing = "https://tailscale.com/cap/file-sharing" - CapabilityAdmin = "https://tailscale.com/cap/is-admin" - CapabilitySSH = "https://tailscale.com/cap/ssh" // feature enabled/available - CapabilitySSHRuleIn = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node - CapabilityDataPlaneAuditLogs = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled - CapabilityDebug = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI - CapabilityHTTPS = "https" // https cert provisioning enabled on tailnet +const ( + CapabilityFileSharing NodeCapability = "https://tailscale.com/cap/file-sharing" + CapabilityAdmin NodeCapability = "https://tailscale.com/cap/is-admin" + 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" // https cert provisioning enabled on tailnet // 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 = "https://tailscale.com/cap/bind-to-interface-by-route" + CapabilityBindToInterfaceByRoute NodeCapability = "https://tailscale.com/cap/bind-to-interface-by-route" // 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 = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface" + 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 = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface" + CapabilityDebugDisableBindConnToInterface NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface" // CapabilityTailnetLock indicates the node may initialize tailnet lock. - CapabilityTailnetLock = "https://tailscale.com/cap/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 = "https://tailscale.com/cap/warn-funnel-no-invite" + 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 = "https://tailscale.com/cap/warn-funnel-no-https" + 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 = "https://tailscale.com/cap/debug-ts-dns-resolution" + 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 = "https://tailscale.com/cap/funnel-ports" -) + CapabilityFunnelPorts NodeCapability = "https://tailscale.com/cap/funnel-ports" -const ( // NodeAttrFunnel grants the ability for a node to host ingress traffic. - NodeAttrFunnel = "funnel" + NodeAttrFunnel NodeCapability = "funnel" // NodeAttrSSHAggregator grants the ability for a node to collect SSH sessions. - NodeAttrSSHAggregator = "ssh-aggregator" + NodeAttrSSHAggregator NodeCapability = "ssh-aggregator" // NodeAttrDebugForceBackgroundSTUN forces a node to always do background // STUN queries regardless of inactivity. - NodeAttrDebugForceBackgroundSTUN = "debug-always-stun" + NodeAttrDebugForceBackgroundSTUN NodeCapability = "debug-always-stun" // NodeAttrDebugDisableWGTrim disables the lazy WireGuard configuration, // always giving WireGuard the full netmap, even for idle peers. - NodeAttrDebugDisableWGTrim = "debug-no-wg-trim" + NodeAttrDebugDisableWGTrim NodeCapability = "debug-no-wg-trim" // NodeAttrDebugDisableDRPO disables the DERP Return Path Optimization. // See Issue 150. - NodeAttrDebugDisableDRPO = "debug-disable-drpo" + NodeAttrDebugDisableDRPO NodeCapability = "debug-disable-drpo" // NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be // disabled if WPAD is present on the network. - NodeAttrDisableSubnetsIfPAC = "debug-disable-subnets-if-pac" + 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 = "debug-disable-upnp" + 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 = "disable-delta-updates" + NodeAttrDisableDeltaUpdates NodeCapability = "disable-delta-updates" // NodeAttrRandomizeClientPort makes magicsock UDP bind to // :0 to get a random local port, ignoring any configured // fixed port. - NodeAttrRandomizeClientPort = "randomize-client-port" + NodeAttrRandomizeClientPort NodeCapability = "randomize-client-port" // 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 = "one-cgnat?v=true" + 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 = "one-cgnat?v=false" + NodeAttrOneCGNATDisable NodeCapability = "one-cgnat?v=false" ) // SetDNSRequest is a request to add a DNS record. @@ -2434,7 +2441,7 @@ type PeerChange struct { // Capabilities, if non-nil, means that the NodeID's capabilities changed. // It's a pointer to a slice for "omitempty", to allow differentiating // a change to empty from no change. - Capabilities *[]string `json:",omitempty"` + Capabilities *[]NodeCapability `json:",omitempty"` } // DerpMagicIP is a fake WireGuard endpoint IP address that means to diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 76e727444..69e5d6344 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -98,7 +98,7 @@ func (src *Node) Clone() *Node { LastSeen *time.Time Online *bool MachineAuthorized bool - Capabilities []string + Capabilities []NodeCapability UnsignedPeerAPIOnly bool ComputedName string computedHostIfDifferent string diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 0ec0142f6..15d12ec7a 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -165,13 +165,13 @@ func (v NodeView) Online() *bool { return &x } -func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } -func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) } -func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly } -func (v NodeView) ComputedName() string { return v.ж.ComputedName } -func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost } -func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID } -func (v NodeView) Expired() bool { return v.ж.Expired } +func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } +func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.SliceOf(v.ж.Capabilities) } +func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly } +func (v NodeView) ComputedName() string { return v.ж.ComputedName } +func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost } +func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID } +func (v NodeView) Expired() bool { return v.ж.Expired } func (v NodeView) SelfNodeV4MasqAddrForThisPeer() *netip.Addr { if v.ж.SelfNodeV4MasqAddrForThisPeer == nil { return nil @@ -210,7 +210,7 @@ func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) } LastSeen *time.Time Online *bool MachineAuthorized bool - Capabilities []string + Capabilities []NodeCapability UnsignedPeerAPIOnly bool ComputedName string computedHostIfDifferent string diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index 316bf3ca3..0d7c6e833 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -585,7 +585,7 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key. AllowedIPs: allowedIPs, Hostinfo: req.Hostinfo.View(), Name: req.Hostinfo.Hostname, - Capabilities: []string{ + Capabilities: []tailcfg.NodeCapability{ tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=8080,443", diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go index f3723ee72..df950401d 100644 --- a/types/netmap/netmap.go +++ b/types/netmap/netmap.go @@ -203,8 +203,8 @@ func (nm *NetworkMap) MagicDNSSuffix() string { // SelfCapabilities returns SelfNode.Capabilities if nm and nm.SelfNode are // non-nil. This is a method so we can use it in envknob/logknob without a // circular dependency. -func (nm *NetworkMap) SelfCapabilities() views.Slice[string] { - var zero views.Slice[string] +func (nm *NetworkMap) SelfCapabilities() views.Slice[tailcfg.NodeCapability] { + var zero views.Slice[tailcfg.NodeCapability] if nm == nil || !nm.SelfNode.Valid() { return zero } diff --git a/wgengine/filter/filter_test.go b/wgengine/filter/filter_test.go index c4bc167bb..1d7521821 100644 --- a/wgengine/filter/filter_test.go +++ b/wgengine/filter/filter_test.go @@ -873,7 +873,7 @@ func TestMatchesMatchProtoAndIPsOnlyIfAllPorts(t *testing.T) { } } -func TestCaps(t *testing.T) { +func TestPeerCaps(t *testing.T) { mm, err := MatchesFromFilterRules([]tailcfg.FilterRule{ { SrcIPs: []string{"*"},