tailcfg: define a type for NodeCapability

Instead of untyped string, add a type to identify these.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali 2023-09-06 10:17:25 -07:00 committed by Maisem Ali
parent 3d37328af6
commit a61caea911
18 changed files with 100 additions and 90 deletions

View File

@ -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. // verifyFunnelEnabled may refresh the local state and modify the st input.
func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status, port uint16) error { 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) hasHTTPS := slices.Contains(attrs, tailcfg.CapabilityHTTPS)
hasFunnel := slices.Contains(attrs, tailcfg.NodeAttrFunnel) hasFunnel := slices.Contains(attrs, tailcfg.NodeAttrFunnel)
return hasHTTPS && hasFunnel return hasHTTPS && hasFunnel

View File

@ -269,7 +269,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
// on, enableFeatureInteractive will error. For now, we hide that // on, enableFeatureInteractive will error. For now, we hide that
// error and maintain the previous behavior (prior to 2023-08-15) // error and maintain the previous behavior (prior to 2023-08-15)
// of letting them edit the serve config before enabling certs. // 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) 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". // 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. // 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) info, err := e.lc.QueryFeature(ctx, feature)
if err != nil { if err != nil {
return err return err

View File

@ -233,7 +233,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
// on, enableFeatureInteractive will error. For now, we hide that // on, enableFeatureInteractive will error. For now, we hide that
// error and maintain the previous behavior (prior to 2023-08-15) // error and maintain the previous behavior (prior to 2023-08-15)
// of letting them edit the serve config before enabling certs. // 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) return slices.Contains(caps, tailcfg.CapabilityHTTPS)
}); err != nil { }); err != nil {
return fmt.Errorf("error enabling https feature: %w", err) return fmt.Errorf("error enabling https feature: %w", err)

View File

@ -763,7 +763,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
// queryFeatureResponse is the mock response desired from the // queryFeatureResponse is the mock response desired from the
// call made to lc.QueryFeature by verifyFunnelEnabled. // call made to lc.QueryFeature by verifyFunnelEnabled.
queryFeatureResponse mockQueryFeatureResponse queryFeatureResponse mockQueryFeatureResponse
caps []string // optionally set at fakeStatus.Capabilities caps []tailcfg.NodeCapability // optionally set at fakeStatus.Capabilities
wantErr string wantErr string
wantPanic string wantPanic string
}{ }{
@ -780,13 +780,13 @@ func TestVerifyFunnelEnabled(t *testing.T) {
{ {
name: "fallback-flow-missing-acl-rule", name: "fallback-flow-missing-acl-rule",
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")}, 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.`, wantErr: `Funnel not available; "funnel" node attribute not set. See https://tailscale.com/s/no-funnel.`,
}, },
{ {
name: "fallback-flow-enabled", name: "fallback-flow-enabled",
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")}, 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 wantErr: "", // no error, success
}, },
{ {
@ -858,7 +858,7 @@ type fakeLocalServeClient struct {
BackendState: ipn.Running.String(), BackendState: ipn.Running.String(),
Self: &ipnstate.PeerStatus{ Self: &ipnstate.PeerStatus{
DNSName: "foo.test.ts.net", DNSName: "foo.test.ts.net",
Capabilities: []string{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"}, Capabilities: []tailcfg.NodeCapability{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
}, },
} }

View File

@ -329,13 +329,13 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
mapRes: &tailcfg.MapResponse{ mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{ PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1, NodeID: 1,
Capabilities: ptr.To([]string{"foo"}), Capabilities: ptr.To([]tailcfg.NodeCapability{"foo"}),
}}, }},
}, },
want: peers(&tailcfg.Node{ want: peers(&tailcfg.Node{
ID: 1, ID: 1,
Name: "foo", Name: "foo",
Capabilities: []string{"foo"}, Capabilities: []tailcfg.NodeCapability{"foo"},
}), }),
wantStats: updateStats{changed: 1}, wantStats: updateStats{changed: 1},
}} }}
@ -685,15 +685,15 @@ func TestPeerChangeDiff(t *testing.T) {
}, },
{ {
name: "patch-capabilities-to-nonempty", name: "patch-capabilities-to-nonempty",
a: &tailcfg.Node{ID: 1, Capabilities: []string{"foo"}}, a: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"foo"}},
b: &tailcfg.Node{ID: 1, Capabilities: []string{"bar"}}, b: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"bar"}},
want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]string{"bar"})}, want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]tailcfg.NodeCapability{"bar"})},
}, },
{ {
name: "patch-capabilities-to-empty", 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}, 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", name: "patch-online-to-true",

View File

@ -48,7 +48,7 @@ type Knobs struct {
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self // UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
// node attributes (Node.Capabilities). // node attributes (Node.Capabilities).
func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []string) { func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability) {
if k == nil { if k == nil {
return return
} }

View File

@ -9,6 +9,7 @@
"sync/atomic" "sync/atomic"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/tailcfg"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/views" "tailscale.com/types/views"
) )
@ -22,7 +23,7 @@
// c2n log level changes), and via capabilities from a NetMap (so users can // c2n log level changes), and via capabilities from a NetMap (so users can
// enable logging via the ACL JSON). // enable logging via the ACL JSON).
type LogKnob struct { type LogKnob struct {
capName string capName tailcfg.NodeCapability
cap atomic.Bool cap atomic.Bool
env func() bool env func() bool
manual atomic.Bool manual atomic.Bool
@ -30,7 +31,7 @@ type LogKnob struct {
// NewLogKnob creates a new LogKnob, with the provided environment variable // NewLogKnob creates a new LogKnob, with the provided environment variable
// name and/or NetMap capability. // name and/or NetMap capability.
func NewLogKnob(env, cap string) *LogKnob { func NewLogKnob(env string, cap tailcfg.NodeCapability) *LogKnob {
if env == "" && cap == "" { if env == "" && cap == "" {
panic("must provide either an environment variable or capability") 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 // about; we use this rather than a concrete type to avoid a circular
// dependency. // dependency.
type NetMap interface { type NetMap interface {
SelfCapabilities() views.Slice[string] SelfCapabilities() views.Slice[tailcfg.NodeCapability]
} }
// UpdateFromNetMap will enable logging if the SelfNode in the provided NetMap // UpdateFromNetMap will enable logging if the SelfNode in the provided NetMap

View File

@ -64,7 +64,7 @@ func TestLogKnob(t *testing.T) {
testKnob.UpdateFromNetMap(&netmap.NetworkMap{ testKnob.UpdateFromNetMap(&netmap.NetworkMap{
SelfNode: (&tailcfg.Node{ SelfNode: (&tailcfg.Node{
Capabilities: []string{ Capabilities: []tailcfg.NodeCapability{
"https://tailscale.com/cap/testing", "https://tailscale.com/cap/testing",
}, },
}).View(), }).View(),

View File

@ -4093,7 +4093,7 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
cc.SetNetInfo(ni) 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() { if nm != nil && nm.SelfNode.Valid() {
return views.SliceContains(nm.SelfNode.Capabilities(), cap) return views.SliceContains(nm.SelfNode.Capabilities(), cap)
} }

View File

@ -256,7 +256,7 @@ type PeerStatus struct {
// "https://tailscale.com/cap/is-admin" // "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/file-sharing" // "https://tailscale.com/cap/file-sharing"
// "funnel" // "funnel"
Capabilities []string `json:",omitempty"` Capabilities []tailcfg.NodeCapability `json:",omitempty"`
// SSH_HostKeys are the node's SSH host keys, if known. // SSH_HostKeys are the node's SSH host keys, if known.
SSH_HostKeys []string `json:"sshHostKeys,omitempty"` SSH_HostKeys []string `json:"sshHostKeys,omitempty"`

View File

@ -240,7 +240,7 @@ func (sc *ServeConfig) IsFunnelOn() bool {
// The nodeAttrs arg should be the node's Self.Capabilities which should contain // The nodeAttrs arg should be the node's Self.Capabilities which should contain
// the attribute we're checking for and possibly warning-capabilities for // the attribute we're checking for and possibly warning-capabilities for
// Funnel. // Funnel.
func CheckFunnelAccess(port uint16, nodeAttrs []string) error { func CheckFunnelAccess(port uint16, nodeAttrs []tailcfg.NodeCapability) error {
if !slices.Contains(nodeAttrs, tailcfg.CapabilityHTTPS) { if !slices.Contains(nodeAttrs, tailcfg.CapabilityHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.") 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. // CheckFunnelPort checks whether the given port is allowed for Funnel.
// It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed // It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed
// ports. // ports.
func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error { func CheckFunnelPort(wantedPort uint16, nodeAttrs []tailcfg.NodeCapability) error {
deny := func(allowedPorts string) error { deny := func(allowedPorts string) error {
if allowedPorts == "" { if allowedPorts == "" {
return fmt.Errorf("port %d is not allowed for funnel", wantedPort) 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 var portsStr string
for _, attr := range nodeAttrs { for _, attr := range nodeAttrs {
if !strings.HasPrefix(attr, tailcfg.CapabilityFunnelPorts) { attr := string(attr)
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
continue continue
} }
u, err := url.Parse(attr) u, err := url.Parse(attr)
@ -274,7 +275,7 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error {
return deny("") return deny("")
} }
u.RawQuery = "" u.RawQuery = ""
if u.String() != tailcfg.CapabilityFunnelPorts { if u.String() != string(tailcfg.CapabilityFunnelPorts) {
return deny("") return deny("")
} }
} }

View File

@ -9,20 +9,21 @@
) )
func TestCheckFunnelAccess(t *testing.T) { 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 { tests := []struct {
port uint16 port uint16
caps []string caps []tailcfg.NodeCapability
wantErr bool wantErr bool
}{ }{
{443, []string{portAttr}, true}, // No "funnel" attribute {443, caps(portAttr), true}, // No "funnel" attribute
{443, []string{portAttr, tailcfg.NodeAttrFunnel}, true}, {443, caps(portAttr, tailcfg.NodeAttrFunnel), true},
{443, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false}, {443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
{8443, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false}, {8443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
{8321, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true}, {8321, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
{8083, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false}, {8083, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
{8091, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true}, {8091, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
{3000, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true}, {3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
} }
for _, tt := range tests { for _, tt := range tests {
err := CheckFunnelAccess(tt.port, tt.caps) err := CheckFunnelAccess(tt.port, tt.caps)

View File

@ -12,6 +12,7 @@
"fmt" "fmt"
"net/netip" "net/netip"
"reflect" "reflect"
"slices"
"strings" "strings"
"time" "time"
@ -289,7 +290,7 @@ type Node struct {
// such as: // such as:
// "https://tailscale.com/cap/is-admin" // "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/file-sharing" // "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 // UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA
// restrictions. However, in exchange for that privilege, it does not get // restrictions. However, in exchange for that privilege, it does not get
@ -1226,10 +1227,11 @@ type CapGrant struct {
CapMap PeerCapMap `json:",omitempty"` CapMap PeerCapMap `json:",omitempty"`
} }
// PeerCapability is a capability granted to a node by a FilterRule. // PeerCapability represents a capability granted to a peer by a FilterRule when
// It's a string, but its meaning is application-defined. // the peer communicates with the node that has this rule. Its meaning is
// It must be a URL, like "https://tailscale.com/cap/file-sharing-target" or // application-defined.
// "https://example.com/cap/read-access". //
// It must be a URL like "https://tailscale.com/cap/file-send".
type PeerCapability string type PeerCapability string
const ( const (
@ -1838,7 +1840,7 @@ func (n *Node) Equal(n2 *Node) bool {
n.Created.Equal(n2.Created) && n.Created.Equal(n2.Created) &&
eqTimePtr(n.LastSeen, n2.LastSeen) && eqTimePtr(n.LastSeen, n2.LastSeen) &&
n.MachineAuthorized == n2.MachineAuthorized && n.MachineAuthorized == n2.MachineAuthorized &&
eqStrings(n.Capabilities, n2.Capabilities) && slices.Equal(n.Capabilities, n2.Capabilities) &&
n.ComputedName == n2.ComputedName && n.ComputedName == n2.ComputedName &&
n.computedHostIfDifferent == n2.computedHostIfDifferent && n.computedHostIfDifferent == n2.computedHostIfDifferent &&
n.ComputedNameWithHost == n2.ComputedNameWithHost && n.ComputedNameWithHost == n2.ComputedNameWithHost &&
@ -1911,112 +1913,117 @@ type Oauth2Token struct {
Expiry time.Time `json:"expiry,omitempty"` Expiry time.Time `json:"expiry,omitempty"`
} }
const ( // NodeCapability represents a capability granted to the self node as listed in
// These are the capabilities that the self node has as listed in // MapResponse.Node.Capabilities.
// MapResponse.Node.Capabilities. //
// // It must be a URL like "https://tailscale.com/cap/file-sharing", or a
// We've since started referring to these as "Node Attributes" ("nodeAttrs" // well-known capability name like "funnel". The latter is only allowed for
// in the ACL policy file). // 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" const (
CapabilityAdmin = "https://tailscale.com/cap/is-admin" CapabilityFileSharing NodeCapability = "https://tailscale.com/cap/file-sharing"
CapabilitySSH = "https://tailscale.com/cap/ssh" // feature enabled/available CapabilityAdmin NodeCapability = "https://tailscale.com/cap/is-admin"
CapabilitySSHRuleIn = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node CapabilitySSH NodeCapability = "https://tailscale.com/cap/ssh" // feature enabled/available
CapabilityDataPlaneAuditLogs = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled CapabilitySSHRuleIn NodeCapability = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
CapabilityDebug = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI CapabilityDataPlaneAuditLogs NodeCapability = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
CapabilityHTTPS = "https" // https cert provisioning enabled on tailnet 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 // CapabilityBindToInterfaceByRoute changes how Darwin nodes create
// sockets (in the net/netns package). See that package for more // sockets (in the net/netns package). See that package for more
// details on the behaviour of this capability. // 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 // CapabilityDebugDisableAlternateDefaultRouteInterface changes how Darwin
// nodes get the default interface. There is an optional hook (used by the // nodes get the default interface. There is an optional hook (used by the
// macOS and iOS clients) to override the default interface, this capability // macOS and iOS clients) to override the default interface, this capability
// disables that and uses the default behavior (of parsing the routing // disables that and uses the default behavior (of parsing the routing
// table). // 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 // CapabilityDebugDisableBindConnToInterface disables the automatic binding
// of connections to the default network interface on Darwin nodes. // 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 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. // Funnel warning capabilities used for reporting errors to the user.
// CapabilityWarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet. // CapabilityWarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet.
// This cap is no longer used 2023-08-09 onwards. // 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. // CapabilityWarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet.
// This cap is no longer used 2023-08-09 onwards. // 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 // Debug logging capabilities
// CapabilityDebugTSDNSResolution enables verbose debug logging for DNS // CapabilityDebugTSDNSResolution enables verbose debug logging for DNS
// resolution for Tailscale-controlled domains (the control server, log // resolution for Tailscale-controlled domains (the control server, log
// server, DERP servers, etc.) // 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. // CapabilityFunnelPorts specifies the ports that the Funnel is available on.
// The ports are specified as a comma-separated list of port numbers or port // 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. // 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 // 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 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 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 // NodeAttrDebugForceBackgroundSTUN forces a node to always do background
// STUN queries regardless of inactivity. // STUN queries regardless of inactivity.
NodeAttrDebugForceBackgroundSTUN = "debug-always-stun" NodeAttrDebugForceBackgroundSTUN NodeCapability = "debug-always-stun"
// NodeAttrDebugDisableWGTrim disables the lazy WireGuard configuration, // NodeAttrDebugDisableWGTrim disables the lazy WireGuard configuration,
// always giving WireGuard the full netmap, even for idle peers. // 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. // NodeAttrDebugDisableDRPO disables the DERP Return Path Optimization.
// See Issue 150. // See Issue 150.
NodeAttrDebugDisableDRPO = "debug-disable-drpo" NodeAttrDebugDisableDRPO NodeCapability = "debug-disable-drpo"
// NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be // NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be
// disabled if WPAD is present on the network. // 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. // NodeAttrDisableUPnP makes the client not perform a UPnP portmapping.
// By default, we want to enable it to see if it works on more clients. // 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 // If UPnP catastrophically fails for people, this should be set kill
// new attempts at UPnP connections. // new attempts at UPnP connections.
NodeAttrDisableUPnP = "debug-disable-upnp" NodeAttrDisableUPnP NodeCapability = "debug-disable-upnp"
// NodeAttrDisableDeltaUpdates makes the client not process updates via the // NodeAttrDisableDeltaUpdates makes the client not process updates via the
// delta update mechanism and should instead treat all netmap changes as // delta update mechanism and should instead treat all netmap changes as
// "full" ones as tailscaled did in 1.48.x and earlier. // "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 // NodeAttrRandomizeClientPort makes magicsock UDP bind to
// :0 to get a random local port, ignoring any configured // :0 to get a random local port, ignoring any configured
// fixed port. // fixed port.
NodeAttrRandomizeClientPort = "randomize-client-port" NodeAttrRandomizeClientPort NodeCapability = "randomize-client-port"
// NodeAttrOneCGNATEnable makes the client prefer one big CGNAT /10 route // NodeAttrOneCGNATEnable makes the client prefer one big CGNAT /10 route
// rather than a /32 per peer. At most one of this or // rather than a /32 per peer. At most one of this or
// NodeAttrOneCGNATDisable may be set; if neither are, it's automatic. // 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 // NodeAttrOneCGNATDisable makes the client prefer a /32 route per peer
// rather than one big /10 CGNAT route. At most one of this or // rather than one big /10 CGNAT route. At most one of this or
// NodeAttrOneCGNATEnable may be set; if neither are, it's automatic. // 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. // 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. // Capabilities, if non-nil, means that the NodeID's capabilities changed.
// It's a pointer to a slice for "omitempty", to allow differentiating // It's a pointer to a slice for "omitempty", to allow differentiating
// a change to empty from no change. // 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 // DerpMagicIP is a fake WireGuard endpoint IP address that means to

View File

@ -98,7 +98,7 @@ func (src *Node) Clone() *Node {
LastSeen *time.Time LastSeen *time.Time
Online *bool Online *bool
MachineAuthorized bool MachineAuthorized bool
Capabilities []string Capabilities []NodeCapability
UnsignedPeerAPIOnly bool UnsignedPeerAPIOnly bool
ComputedName string ComputedName string
computedHostIfDifferent string computedHostIfDifferent string

View File

@ -165,13 +165,13 @@ func (v NodeView) Online() *bool {
return &x return &x
} }
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) } func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.SliceOf(v.ж.Capabilities) }
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly } func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
func (v NodeView) ComputedName() string { return v.ж.ComputedName } func (v NodeView) ComputedName() string { return v.ж.ComputedName }
func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost } func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost }
func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID } func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID }
func (v NodeView) Expired() bool { return v.ж.Expired } func (v NodeView) Expired() bool { return v.ж.Expired }
func (v NodeView) SelfNodeV4MasqAddrForThisPeer() *netip.Addr { func (v NodeView) SelfNodeV4MasqAddrForThisPeer() *netip.Addr {
if v.ж.SelfNodeV4MasqAddrForThisPeer == nil { if v.ж.SelfNodeV4MasqAddrForThisPeer == nil {
return nil return nil
@ -210,7 +210,7 @@ func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) }
LastSeen *time.Time LastSeen *time.Time
Online *bool Online *bool
MachineAuthorized bool MachineAuthorized bool
Capabilities []string Capabilities []NodeCapability
UnsignedPeerAPIOnly bool UnsignedPeerAPIOnly bool
ComputedName string ComputedName string
computedHostIfDifferent string computedHostIfDifferent string

View File

@ -585,7 +585,7 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key.
AllowedIPs: allowedIPs, AllowedIPs: allowedIPs,
Hostinfo: req.Hostinfo.View(), Hostinfo: req.Hostinfo.View(),
Name: req.Hostinfo.Hostname, Name: req.Hostinfo.Hostname,
Capabilities: []string{ Capabilities: []tailcfg.NodeCapability{
tailcfg.CapabilityHTTPS, tailcfg.CapabilityHTTPS,
tailcfg.NodeAttrFunnel, tailcfg.NodeAttrFunnel,
tailcfg.CapabilityFunnelPorts + "?ports=8080,443", tailcfg.CapabilityFunnelPorts + "?ports=8080,443",

View File

@ -203,8 +203,8 @@ func (nm *NetworkMap) MagicDNSSuffix() string {
// SelfCapabilities returns SelfNode.Capabilities if nm and nm.SelfNode are // 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 // non-nil. This is a method so we can use it in envknob/logknob without a
// circular dependency. // circular dependency.
func (nm *NetworkMap) SelfCapabilities() views.Slice[string] { func (nm *NetworkMap) SelfCapabilities() views.Slice[tailcfg.NodeCapability] {
var zero views.Slice[string] var zero views.Slice[tailcfg.NodeCapability]
if nm == nil || !nm.SelfNode.Valid() { if nm == nil || !nm.SelfNode.Valid() {
return zero return zero
} }

View File

@ -873,7 +873,7 @@ func TestMatchesMatchProtoAndIPsOnlyIfAllPorts(t *testing.T) {
} }
} }
func TestCaps(t *testing.T) { func TestPeerCaps(t *testing.T) {
mm, err := MatchesFromFilterRules([]tailcfg.FilterRule{ mm, err := MatchesFromFilterRules([]tailcfg.FilterRule{
{ {
SrcIPs: []string{"*"}, SrcIPs: []string{"*"},