mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
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:
parent
3d37328af6
commit
a61caea911
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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"`
|
||||||
|
@ -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("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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{"*"},
|
||||||
|
Loading…
Reference in New Issue
Block a user