From 31d7ab09a69b2695a21520abe5af5a6809bc4fd6 Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Thu, 13 Oct 2022 10:26:16 -0700 Subject: [PATCH] tailcfg: add accumulator API Signed-off-by: David Crawshaw --- tailcfg/tailcfg.go | 48 +++++++++++++++++++++++++++++++++- tailcfg/tailcfg_clone.go | 3 +++ tailcfg/tailcfg_test.go | 2 ++ tailcfg/tailcfg_view.go | 18 ++++++++----- util/deephash/deephash_test.go | 4 +-- 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index ab254a57e..bbbe7dfff 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -241,6 +241,15 @@ type Node struct { // DataPlaneAuditLogID is the per-node logtail ID used for data plane audit logging. DataPlaneAuditLogID string `json:",omitempty"` + + // Accumulators are scoped accumulator values allocated to this node. + Accumulators []Accumulator `json:",omitempty"` +} + +// Accumulator is a scoped monotonically-increasing accumulator value. +type Accumulator struct { + Scope string // default scope is "*" + Accumulator uint64 // allocated values are greater than zero } // DisplayName returns the user-facing name for a node which should @@ -513,6 +522,7 @@ type Hostinfo struct { Cloud string `json:",omitempty"` Userspace opt.Bool `json:",omitempty"` // if the client is running in userspace (netstack) mode UserspaceRouter opt.Bool `json:",omitempty"` // if the client's subnet router is running in userspace (netstack) mode + Accumulator opt.Bool `json:",omitempty"` // node participates in tailnet accumulator store // NOTE: any new fields containing pointers in this type // require changes to Hostinfo.Equal. @@ -1391,6 +1401,10 @@ type MapResponse struct { // server. An initial nil is equivalent to new(ControlDialPlan). // A subsequent streamed nil means no change. ControlDialPlan *ControlDialPlan `json:",omitempty"` + + // MaxAccumulator is the maximum accumulator allocated in this + // tailnet for each scope. + MaxAccumulators []Accumulator `json:",omitempty"` } // ControlDialPlan is instructions from the control server to the client on how @@ -1557,7 +1571,8 @@ func (n *Node) Equal(n2 *Node) bool { n.ComputedName == n2.ComputedName && n.computedHostIfDifferent == n2.computedHostIfDifferent && n.ComputedNameWithHost == n2.ComputedNameWithHost && - eqStrings(n.Tags, n2.Tags) + eqStrings(n.Tags, n2.Tags) && + eqAccumulators(n.Accumulators, n2.Accumulators) } func eqBoolPtr(a, b *bool) bool { @@ -1571,6 +1586,18 @@ func eqBoolPtr(a, b *bool) bool { } +func eqAccumulators(a, b []Accumulator) bool { + if len(a) != len(b) || ((a == nil) != (b == nil)) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + func eqStrings(a, b []string) bool { if len(a) != len(b) || ((a == nil) != (b == nil)) { return false @@ -1846,6 +1873,25 @@ type OverTLSPublicKeyResponse struct { PublicKey key.MachinePublic `json:"publicKey"` } +// AccumulatorRequest requests the accumulator for a scope be incremented. +// +// It is JSON-encoded and sent over Noise to "/machine/accumulator". +type AccumulatorRequest struct { + CapVersion CapabilityVersion // the clients' current CapabilityVersion + NodeKey key.NodePublic // the client's current node key + Scope string // always "*" for now + CurValue uint64 + // ToValue, if set, means the node does not want to bump the + // MaxAccumulator, but has upgraded its own local state to ToValue. + ToValue uint64 `json:",omitempty"` +} + +// An AccumulatorResponse is the response to an AccumulatorRequest, +// it is sent when the value is successfully incremented. +type AccumulatorResponse struct { + NewValue uint64 +} + // TokenRequest is a request to get an OIDC ID token for an audience. // The token can be presented to any resource provider which offers OIDC // Federation. diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index daec87fc9..f32ee304f 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -64,6 +64,7 @@ func (src *Node) Clone() *Node { *dst.Online = *src.Online } dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...) + dst.Accumulators = append(src.Accumulators[:0:0], src.Accumulators...) return dst } @@ -96,6 +97,7 @@ var _NodeCloneNeedsRegeneration = Node(struct { computedHostIfDifferent string ComputedNameWithHost string DataPlaneAuditLogID string + Accumulators []Accumulator }{}) // Clone makes a deep copy of Hostinfo. @@ -143,6 +145,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct { Cloud string Userspace opt.Bool UserspaceRouter opt.Bool + Accumulator opt.Bool }{}) // Clone makes a deep copy of NetInfo. diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index a7dcb93c9..8c0d43957 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -58,6 +58,7 @@ func TestHostinfoEqual(t *testing.T) { "Cloud", "Userspace", "UserspaceRouter", + "Accumulator", } if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) { t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n", @@ -333,6 +334,7 @@ func TestNodeEqual(t *testing.T) { "Capabilities", "ComputedName", "computedHostIfDifferent", "ComputedNameWithHost", "DataPlaneAuditLogID", + "Accumulators", } if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) { t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n", diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 30da1d02b..97b29d718 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -168,13 +168,14 @@ func (v NodeView) Online() *bool { return &x } -func (v NodeView) KeepAlive() bool { return v.ж.KeepAlive } -func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } -func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) } -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) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) } +func (v NodeView) KeepAlive() bool { return v.ж.KeepAlive } +func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } +func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) } +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) Accumulators() views.Slice[Accumulator] { return views.SliceOf(v.ж.Accumulators) } +func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _NodeViewNeedsRegeneration = Node(struct { @@ -205,6 +206,7 @@ var _NodeViewNeedsRegeneration = Node(struct { computedHostIfDifferent string ComputedNameWithHost string DataPlaneAuditLogID string + Accumulators []Accumulator }{}) // View returns a readonly view of Hostinfo. @@ -281,6 +283,7 @@ func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf( func (v HostinfoView) Cloud() string { return v.ж.Cloud } func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace } func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter } +func (v HostinfoView) Accumulator() opt.Bool { return v.ж.Accumulator } func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) } // A compilation failure here means this code must be regenerated, with the command at the top of this file. @@ -312,6 +315,7 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct { Cloud string Userspace opt.Bool UserspaceRouter opt.Bool + Accumulator opt.Bool }{}) // View returns a readonly view of NetInfo. diff --git a/util/deephash/deephash_test.go b/util/deephash/deephash_test.go index c085d28b9..83761505e 100644 --- a/util/deephash/deephash_test.go +++ b/util/deephash/deephash_test.go @@ -575,7 +575,7 @@ func TestGetTypeHasher(t *testing.T) { { name: "tailcfg.Node", val: &tailcfg.Node{}, - out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", }, } for _, tt := range tests { @@ -594,7 +594,7 @@ func TestGetTypeHasher(t *testing.T) { } h.sum() if got := string(hb.B); got != tt.out { - t.Fatalf("got %q; want %q", got, tt.out) + t.Fatalf("\n got %q;\nwant %q", got, tt.out) } }) }