mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
tailcfg: add NodeCapMap
Like PeerCapMap, add a field to `tailcfg.Node` which provides a map of Capability to raw JSON messages which are deferred to be parsed later by the application code which cares about the specific capabilities. This effectively allows us to prototype new behavior without having to commit to a schema in tailcfg, and it also opens up the possibilities to develop custom behavior in tsnet applications w/o having to plumb through application specific data in the MapResponse. Updates #4217 Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
parent
4da0689c2c
commit
19a9d9037f
@ -169,6 +169,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/exp/maps from tailscale.com/tailcfg
|
||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
|
@ -175,7 +175,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
W golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe
|
||||
golang.org/x/exp/maps from tailscale.com/cmd/tailscale/cli
|
||||
golang.org/x/exp/maps from tailscale.com/cmd/tailscale/cli+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
|
@ -237,9 +237,10 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
slice := u
|
||||
sElem := slice.Elem()
|
||||
switch x := sElem.(type) {
|
||||
case *types.Basic:
|
||||
case *types.Basic, *types.Named:
|
||||
sElem := it.QualifiedName(sElem)
|
||||
args.MapValueView = fmt.Sprintf("views.Slice[%v]", sElem)
|
||||
args.MapValueType = "[]" + sElem.String()
|
||||
args.MapValueType = "[]" + sElem
|
||||
args.MapFn = "views.SliceOf(t)"
|
||||
template = "mapFnField"
|
||||
case *types.Pointer:
|
||||
|
@ -187,8 +187,9 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
if resp.Node != nil {
|
||||
if DevKnob.StripCaps() {
|
||||
resp.Node.Capabilities = nil
|
||||
resp.Node.CapMap = nil
|
||||
}
|
||||
ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.Capabilities)
|
||||
ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.Capabilities, resp.Node.CapMap)
|
||||
}
|
||||
|
||||
// Call Node.InitDisplayNames on any changed nodes.
|
||||
@ -324,6 +325,7 @@ var (
|
||||
patchLastSeen = clientmetric.NewCounter("controlclient_patch_lastseen")
|
||||
patchKeyExpiry = clientmetric.NewCounter("controlclient_patch_keyexpiry")
|
||||
patchCapabilities = clientmetric.NewCounter("controlclient_patch_capabilities")
|
||||
patchCapMap = clientmetric.NewCounter("controlclient_patch_capmap")
|
||||
patchKeySignature = clientmetric.NewCounter("controlclient_patch_keysig")
|
||||
|
||||
patchifiedPeer = clientmetric.NewCounter("controlclient_patchified_peer")
|
||||
@ -452,6 +454,10 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
|
||||
mut.KeySignature = v
|
||||
patchKeySignature.Add(1)
|
||||
}
|
||||
if v := pc.CapMap; v != nil {
|
||||
mut.CapMap = v
|
||||
patchCapMap.Add(1)
|
||||
}
|
||||
*vp = mut.View()
|
||||
}
|
||||
|
||||
@ -647,6 +653,10 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
if was.Cap() != n.Cap {
|
||||
pc().Cap = n.Cap
|
||||
}
|
||||
case "CapMap":
|
||||
if n.CapMap != nil {
|
||||
pc().CapMap = n.CapMap
|
||||
}
|
||||
case "Tags":
|
||||
if !views.SliceEqual(was.Tags(), views.SliceOf(n.Tags)) {
|
||||
return nil, false
|
||||
|
@ -6,6 +6,7 @@
|
||||
package controlknobs
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
@ -48,39 +49,30 @@ type Knobs struct {
|
||||
|
||||
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
|
||||
// node attributes (Node.Capabilities).
|
||||
func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability) {
|
||||
func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability, capMap tailcfg.NodeCapMap) {
|
||||
if k == nil {
|
||||
return
|
||||
}
|
||||
var (
|
||||
keepFullWG bool
|
||||
disableDRPO bool
|
||||
disableUPnP bool
|
||||
randomizeClientPort bool
|
||||
disableDeltaUpdates bool
|
||||
oneCGNAT opt.Bool
|
||||
forceBackgroundSTUN bool
|
||||
)
|
||||
for _, attr := range selfNodeAttrs {
|
||||
switch attr {
|
||||
case tailcfg.NodeAttrDebugDisableWGTrim:
|
||||
keepFullWG = true
|
||||
case tailcfg.NodeAttrDebugDisableDRPO:
|
||||
disableDRPO = true
|
||||
case tailcfg.NodeAttrDisableUPnP:
|
||||
disableUPnP = true
|
||||
case tailcfg.NodeAttrRandomizeClientPort:
|
||||
randomizeClientPort = true
|
||||
case tailcfg.NodeAttrOneCGNATEnable:
|
||||
oneCGNAT.Set(true)
|
||||
case tailcfg.NodeAttrOneCGNATDisable:
|
||||
oneCGNAT.Set(false)
|
||||
case tailcfg.NodeAttrDebugForceBackgroundSTUN:
|
||||
forceBackgroundSTUN = true
|
||||
case tailcfg.NodeAttrDisableDeltaUpdates:
|
||||
disableDeltaUpdates = true
|
||||
}
|
||||
has := func(attr tailcfg.NodeCapability) bool {
|
||||
_, ok := capMap[attr]
|
||||
return ok || slices.Contains(selfNodeAttrs, attr)
|
||||
}
|
||||
var (
|
||||
keepFullWG = has(tailcfg.NodeAttrDebugDisableWGTrim)
|
||||
disableDRPO = has(tailcfg.NodeAttrDebugDisableDRPO)
|
||||
disableUPnP = has(tailcfg.NodeAttrDisableUPnP)
|
||||
randomizeClientPort = has(tailcfg.NodeAttrRandomizeClientPort)
|
||||
disableDeltaUpdates = has(tailcfg.NodeAttrDisableDeltaUpdates)
|
||||
oneCGNAT opt.Bool
|
||||
forceBackgroundSTUN = has(tailcfg.NodeAttrDebugForceBackgroundSTUN)
|
||||
)
|
||||
|
||||
if has(tailcfg.NodeAttrOneCGNATEnable) {
|
||||
oneCGNAT.Set(true)
|
||||
} else if has(tailcfg.NodeAttrOneCGNATDisable) {
|
||||
oneCGNAT.Set(false)
|
||||
}
|
||||
|
||||
k.KeepFullWGConfig.Store(keepFullWG)
|
||||
k.DisableDRPO.Store(disableDRPO)
|
||||
k.DisableUPnP.Store(disableUPnP)
|
||||
|
@ -746,6 +746,13 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
if c := sn.Capabilities(); c.Len() > 0 {
|
||||
ss.Capabilities = c.AsSlice()
|
||||
}
|
||||
if cm := sn.CapMap(); cm.Len() > 0 {
|
||||
ss.CapMap = make(tailcfg.NodeCapMap, sn.CapMap().Len())
|
||||
cm.Range(func(k tailcfg.NodeCapability, v views.Slice[tailcfg.RawMessage]) bool {
|
||||
ss.CapMap[k] = v.AsSlice()
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, addr := range tailscaleIPs {
|
||||
ss.TailscaleIPs = append(ss.TailscaleIPs, addr)
|
||||
|
@ -258,6 +258,9 @@ type PeerStatus struct {
|
||||
// "funnel"
|
||||
Capabilities []tailcfg.NodeCapability `json:",omitempty"`
|
||||
|
||||
// CapMap is a map of capabilities to their values.
|
||||
CapMap tailcfg.NodeCapMap `json:",omitempty"`
|
||||
|
||||
// SSH_HostKeys are the node's SSH host keys, if known.
|
||||
SSH_HostKeys []string `json:"sshHostKeys,omitempty"`
|
||||
|
||||
@ -293,7 +296,7 @@ type PeerStatus struct {
|
||||
|
||||
// HasCap reports whether ps has the given capability.
|
||||
func (ps *PeerStatus) HasCap(cap tailcfg.NodeCapability) bool {
|
||||
return slices.Contains(ps.Capabilities, cap)
|
||||
return ps.CapMap.Contains(cap) || slices.Contains(ps.Capabilities, cap)
|
||||
}
|
||||
|
||||
// StatusBuilder is a request to construct a Status. A new StatusBuilder is
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/opt"
|
||||
@ -112,7 +113,8 @@ type CapabilityVersion int
|
||||
// - 71: 2023-08-17: added NodeAttrOneCGNATEnable, NodeAttrOneCGNATDisable
|
||||
// - 72: 2023-08-23: TS-2023-006 UPnP issue fixed; UPnP can now be used again
|
||||
// - 73: 2023-09-01: Non-Windows clients expect to receive ClientVersion
|
||||
const CurrentCapabilityVersion CapabilityVersion = 73
|
||||
// - 74: 2023-09-18: Client understands NodeCapMap
|
||||
const CurrentCapabilityVersion CapabilityVersion = 74
|
||||
|
||||
type StableID string
|
||||
|
||||
@ -315,8 +317,22 @@ type Node struct {
|
||||
// such as:
|
||||
// "https://tailscale.com/cap/is-admin"
|
||||
// "https://tailscale.com/cap/file-sharing"
|
||||
//
|
||||
// Deprecated: use CapMap instead.
|
||||
Capabilities []NodeCapability `json:",omitempty"`
|
||||
|
||||
// CapMap is a map of capabilities to their optional argument/data values.
|
||||
//
|
||||
// It is valid for a capability to not have any argument/data values; such
|
||||
// capabilities can be tested for using the HasCap method. These type of
|
||||
// capabilities are used to indicate that a node has a capability, but there
|
||||
// is no additional data associated with it. These were previously
|
||||
// represented by the Capabilities field, but can now be represented by
|
||||
// CapMap with an empty value.
|
||||
//
|
||||
// See NodeCapability for more information on keys.
|
||||
CapMap NodeCapMap `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
|
||||
// network access. It can only access this node's peerapi, which may not let
|
||||
@ -369,13 +385,15 @@ type Node struct {
|
||||
}
|
||||
|
||||
// HasCap reports whether the node has the given capability.
|
||||
// It is safe to call on an invalid NodeView.
|
||||
func (v NodeView) HasCap(cap NodeCapability) bool {
|
||||
return v.ж.HasCap(cap)
|
||||
}
|
||||
|
||||
// HasCap reports whether the node has the given capability.
|
||||
// It is safe to call on a nil Node.
|
||||
func (v *Node) HasCap(cap NodeCapability) bool {
|
||||
return v != nil && slices.Contains(v.Capabilities, cap)
|
||||
return v != nil && (v.CapMap.Contains(cap) || slices.Contains(v.Capabilities, cap))
|
||||
}
|
||||
|
||||
// DisplayName returns the user-facing name for a node which should
|
||||
@ -1285,6 +1303,45 @@ const (
|
||||
PeerCapabilityIngress PeerCapability = "https://tailscale.com/cap/ingress"
|
||||
)
|
||||
|
||||
// NodeCapMap is a map of capabilities to their optional values. It is valid for
|
||||
// a capability to have no values (nil slice); such capabilities can be tested
|
||||
// for by using the Contains method.
|
||||
//
|
||||
// See [NodeCapability] for more information on keys.
|
||||
type NodeCapMap map[NodeCapability][]RawMessage
|
||||
|
||||
// Equal reports whether c and c2 are equal.
|
||||
func (c NodeCapMap) Equal(c2 NodeCapMap) bool {
|
||||
return maps.EqualFunc(c, c2, slices.Equal)
|
||||
}
|
||||
|
||||
// UnmarshalNodeCapJSON unmarshals each JSON value in cm[cap] as T.
|
||||
// If cap does not exist in cm, it returns (nil, nil).
|
||||
// It returns an error if the values cannot be unmarshaled into the provided type.
|
||||
func UnmarshalNodeCapJSON[T any](cm NodeCapMap, cap NodeCapability) ([]T, error) {
|
||||
vals, ok := cm[cap]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]T, 0, len(vals))
|
||||
for _, v := range vals {
|
||||
var t T
|
||||
if err := json.Unmarshal([]byte(v), &t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Contains reports whether c has the capability cap. This is used to test for
|
||||
// the existence of a capability, especially when the capability has no
|
||||
// associated argument/data values.
|
||||
func (c NodeCapMap) Contains(cap NodeCapability) bool {
|
||||
_, ok := c[cap]
|
||||
return ok
|
||||
}
|
||||
|
||||
// PeerCapMap is a map of capabilities to their optional values. It is valid for
|
||||
// a capability to have no values (nil slice); such capabilities can be tested
|
||||
// for by using the HasCapability method.
|
||||
@ -1312,9 +1369,9 @@ func UnmarshalCapJSON[T any](cm PeerCapMap, cap PeerCapability) ([]T, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// HasCapability reports whether c has the capability cap.
|
||||
// This is used to test for the existence of a capability, especially
|
||||
// when the capability has no values.
|
||||
// HasCapability reports whether c has the capability cap. This is used to test
|
||||
// for the existence of a capability, especially when the capability has no
|
||||
// associated argument/data values.
|
||||
func (c PeerCapMap) HasCapability(cap PeerCapability) bool {
|
||||
_, ok := c[cap]
|
||||
return ok
|
||||
@ -1876,6 +1933,7 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
eqTimePtr(n.LastSeen, n2.LastSeen) &&
|
||||
n.MachineAuthorized == n2.MachineAuthorized &&
|
||||
slices.Equal(n.Capabilities, n2.Capabilities) &&
|
||||
n.CapMap.Equal(n2.CapMap) &&
|
||||
n.ComputedName == n2.ComputedName &&
|
||||
n.computedHostIfDifferent == n2.computedHostIfDifferent &&
|
||||
n.ComputedNameWithHost == n2.ComputedNameWithHost &&
|
||||
@ -2450,6 +2508,9 @@ type PeerChange struct {
|
||||
// Cap, if non-zero, means that NodeID's capability version has changed.
|
||||
Cap CapabilityVersion `json:",omitempty"`
|
||||
|
||||
// CapMap, if non-nil, means that NodeID's capability map has changed.
|
||||
CapMap NodeCapMap `json:",omitempty"`
|
||||
|
||||
// Endpoints, if non-empty, means that NodeID's UDP Endpoints
|
||||
// have changed to these.
|
||||
Endpoints []string `json:",omitempty"`
|
||||
|
@ -62,6 +62,12 @@ func (src *Node) Clone() *Node {
|
||||
dst.Online = ptr.To(*src.Online)
|
||||
}
|
||||
dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...)
|
||||
if dst.CapMap != nil {
|
||||
dst.CapMap = map[NodeCapability][]RawMessage{}
|
||||
for k := range src.CapMap {
|
||||
dst.CapMap[k] = append([]RawMessage{}, src.CapMap[k]...)
|
||||
}
|
||||
}
|
||||
if dst.SelfNodeV4MasqAddrForThisPeer != nil {
|
||||
dst.SelfNodeV4MasqAddrForThisPeer = ptr.To(*src.SelfNodeV4MasqAddrForThisPeer)
|
||||
}
|
||||
@ -99,6 +105,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
|
||||
Online *bool
|
||||
MachineAuthorized bool
|
||||
Capabilities []NodeCapability
|
||||
CapMap NodeCapMap
|
||||
UnsignedPeerAPIOnly bool
|
||||
ComputedName string
|
||||
computedHostIfDifferent string
|
||||
|
@ -346,7 +346,7 @@ func TestNodeEqual(t *testing.T) {
|
||||
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
||||
"Created", "Cap", "Tags", "PrimaryRoutes",
|
||||
"LastSeen", "Online", "MachineAuthorized",
|
||||
"Capabilities",
|
||||
"Capabilities", "CapMap",
|
||||
"UnsignedPeerAPIOnly",
|
||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||
"DataPlaneAuditLogID", "Expired", "SelfNodeV4MasqAddrForThisPeer",
|
||||
@ -545,6 +545,45 @@ func TestNodeEqual(t *testing.T) {
|
||||
&Node{SelfNodeV4MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("100.64.0.1"))},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{
|
||||
CapMap: NodeCapMap{
|
||||
"foo": []RawMessage{`"foo"`},
|
||||
},
|
||||
},
|
||||
&Node{
|
||||
CapMap: NodeCapMap{
|
||||
"foo": []RawMessage{`"foo"`},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{
|
||||
CapMap: NodeCapMap{
|
||||
"bar": []RawMessage{`"foo"`},
|
||||
},
|
||||
},
|
||||
&Node{
|
||||
CapMap: NodeCapMap{
|
||||
"foo": []RawMessage{`"bar"`},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Node{
|
||||
CapMap: NodeCapMap{
|
||||
"foo": nil,
|
||||
},
|
||||
},
|
||||
&Node{
|
||||
CapMap: NodeCapMap{
|
||||
"foo": []RawMessage{`"bar"`},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.a.Equal(tt.b)
|
||||
|
@ -167,11 +167,17 @@ func (v NodeView) Online() *bool {
|
||||
|
||||
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) CapMap() views.MapFn[NodeCapability, []RawMessage, views.Slice[RawMessage]] {
|
||||
return views.MapFnOf(v.ж.CapMap, func(t []RawMessage) views.Slice[RawMessage] {
|
||||
return views.SliceOf(t)
|
||||
})
|
||||
}
|
||||
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
|
||||
@ -211,6 +217,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
|
||||
Online *bool
|
||||
MachineAuthorized bool
|
||||
Capabilities []NodeCapability
|
||||
CapMap NodeCapMap
|
||||
UnsignedPeerAPIOnly bool
|
||||
ComputedName string
|
||||
computedHostIfDifferent string
|
||||
|
@ -185,8 +185,13 @@ func (nm *NetworkMap) SelfCapabilities() views.Slice[tailcfg.NodeCapability] {
|
||||
if nm == nil || !nm.SelfNode.Valid() {
|
||||
return zero
|
||||
}
|
||||
out := nm.SelfNode.Capabilities().AsSlice()
|
||||
nm.SelfNode.CapMap().Range(func(k tailcfg.NodeCapability, _ views.Slice[tailcfg.RawMessage]) (cont bool) {
|
||||
out = append(out, k)
|
||||
return true
|
||||
})
|
||||
|
||||
return nm.SelfNode.Capabilities()
|
||||
return views.SliceOf(out)
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) String() string {
|
||||
|
Loading…
x
Reference in New Issue
Block a user