Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
@@ -67,11 +67,11 @@ func (b *UpdateBuilder) AddKey(key Key) error {
|
||||
}
|
||||
|
||||
if _, err := b.state.GetKey(keyID); err == nil {
|
||||
return fmt.Errorf("cannot add key %v: already exists", key)
|
||||
return fmt.Errorf("cannot add key tlpub:%x: already exists", key.Public)
|
||||
}
|
||||
|
||||
if len(b.state.Keys) >= maxKeys {
|
||||
return fmt.Errorf("cannot add key %v: maximum number of keys reached", key)
|
||||
return fmt.Errorf("cannot add key tlpub:%x: maximum number of keys reached", key.Public)
|
||||
}
|
||||
|
||||
return b.mkUpdate(AUM{MessageKind: AUMAddKey, Key: &key})
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
|
||||
+7
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_tailnetlock
|
||||
@@ -8,6 +8,7 @@ package tka
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -158,3 +159,8 @@ func SignByCredential(privKey []byte, wrapped *NodeKeySignature, nodeKey key.Nod
|
||||
}
|
||||
|
||||
func (s NodeKeySignature) String() string { return "" }
|
||||
|
||||
type CompactionOptions struct {
|
||||
MinChain int
|
||||
MinAge time.Duration
|
||||
}
|
||||
+4
-7
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tka
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
@@ -31,7 +32,7 @@ func (k KeyKind) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// Key describes the public components of a key known to network-lock.
|
||||
// Key describes the public components of a key known to tailnet-lock.
|
||||
type Key struct {
|
||||
Kind KeyKind `cbor:"1,keyasint"`
|
||||
|
||||
@@ -64,9 +65,7 @@ func (k Key) Clone() Key {
|
||||
|
||||
if k.Meta != nil {
|
||||
out.Meta = make(map[string]string, len(k.Meta))
|
||||
for k, v := range k.Meta {
|
||||
out.Meta[k] = v
|
||||
}
|
||||
maps.Copy(out.Meta, k.Meta)
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -105,8 +104,6 @@ func (k Key) Ed25519() (ed25519.PublicKey, error) {
|
||||
}
|
||||
}
|
||||
|
||||
const maxMetaBytes = 512
|
||||
|
||||
func (k Key) StaticValidate() error {
|
||||
if k.Votes > 4096 {
|
||||
return fmt.Errorf("excessive key weight: %d > 4096", k.Votes)
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tka
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Upper bound on checkpoint elements, chosen arbitrarily. Intended
|
||||
// to cap the size of large AUMs.
|
||||
maxDisablementValues = 32
|
||||
maxKeys = 512
|
||||
|
||||
// Max amount of metadata that can be associated with a key, chosen arbitrarily.
|
||||
// Intended to avoid people abusing TKA as a key-value score.
|
||||
maxMetaBytes = 512
|
||||
|
||||
// Max iterations searching for any intersection during the sync process.
|
||||
maxSyncIter = 2000
|
||||
|
||||
// Max iterations searching for a head intersection during the sync process.
|
||||
maxSyncHeadIntersectionIter = 1000
|
||||
|
||||
// Limit on scanning AUM trees, chosen arbitrarily.
|
||||
maxScanIterations = 2000
|
||||
)
|
||||
|
||||
var (
|
||||
CompactionDefaults = CompactionOptions{
|
||||
MinChain: 24, // Keep at minimum 24 AUMs since head.
|
||||
MinAge: 14 * 24 * time.Hour, // Keep 2 weeks of AUMs.
|
||||
}
|
||||
)
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
@@ -178,7 +178,7 @@ func (s NodeKeySignature) UnverifiedAuthorizingKeyID() (tkatype.KeyID, error) {
|
||||
return s.authorizingKeyID()
|
||||
}
|
||||
|
||||
// authorizingKeyID returns the KeyID of the key trusted by network-lock which authorizes
|
||||
// authorizingKeyID returns the KeyID of the key trusted by tailnet-lock which authorizes
|
||||
// this signature.
|
||||
func (s NodeKeySignature) authorizingKeyID() (tkatype.KeyID, error) {
|
||||
switch s.SigKind {
|
||||
@@ -349,14 +349,14 @@ func (s *NodeKeySignature) rotationDetails() (*RotationDetails, error) {
|
||||
|
||||
// ResignNKS re-signs a node-key signature for a new node-key.
|
||||
//
|
||||
// This only matters on network-locked tailnets, because node-key signatures are
|
||||
// This only matters on tailnet-locked tailnets, because node-key signatures are
|
||||
// how other nodes know that a node-key is authentic. When the node-key is
|
||||
// rotated then the existing signature becomes invalid, so this function is
|
||||
// responsible for generating a new wrapping signature to certify the new node-key.
|
||||
//
|
||||
// The signature itself is a SigRotation signature, which embeds the old signature
|
||||
// and certifies the new node-key as a replacement for the old by signing the new
|
||||
// signature with RotationPubkey (which is the node's own network-lock key).
|
||||
// signature with RotationPubkey (which is the node's own tailnet-lock key).
|
||||
func ResignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.MarshaledSignature) (tkatype.MarshaledSignature, error) {
|
||||
var oldSig NodeKeySignature
|
||||
if err := oldSig.Unserialize(oldNKS); err != nil {
|
||||
|
||||
+38
-24
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
@@ -7,11 +7,13 @@ package tka
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// ErrNoSuchKey is returned if the key referenced by a KeyID does not exist.
|
||||
@@ -28,9 +30,13 @@ type State struct {
|
||||
// is the same as the LastAUMHash.
|
||||
LastAUMHash *AUMHash `cbor:"1,keyasint"`
|
||||
|
||||
// DisablementSecrets are KDF-derived values which can be used
|
||||
// to turn off the TKA in the event of a consensus-breaking bug.
|
||||
DisablementSecrets [][]byte `cbor:"2,keyasint"`
|
||||
// DisablementValues are KDF-derived values used to verify that a caller
|
||||
// possesses a valid DisablementSecret. These values are used during the
|
||||
// Tailnet Lock deactivation process.
|
||||
//
|
||||
// These are safe to share publicly or store in the clear. They cannot be
|
||||
// used to derive the original DisablementSecret.
|
||||
DisablementValues [][]byte `cbor:"2,keyasint"`
|
||||
|
||||
// Keys are the public keys of either:
|
||||
//
|
||||
@@ -78,11 +84,11 @@ func (s State) Clone() State {
|
||||
out.LastAUMHash = &dupe
|
||||
}
|
||||
|
||||
if s.DisablementSecrets != nil {
|
||||
out.DisablementSecrets = make([][]byte, len(s.DisablementSecrets))
|
||||
for i := range s.DisablementSecrets {
|
||||
out.DisablementSecrets[i] = make([]byte, len(s.DisablementSecrets[i]))
|
||||
copy(out.DisablementSecrets[i], s.DisablementSecrets[i])
|
||||
if s.DisablementValues != nil {
|
||||
out.DisablementValues = make([][]byte, len(s.DisablementValues))
|
||||
for i := range s.DisablementValues {
|
||||
out.DisablementValues[i] = make([]byte, len(s.DisablementValues[i]))
|
||||
copy(out.DisablementValues[i], s.DisablementValues[i])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +119,7 @@ var disablementSalt = []byte("tailscale network-lock disablement salt")
|
||||
// key authority, but cannot be reversed to find the input secret.
|
||||
//
|
||||
// When the output of this function is stored in tka state (i.e. in
|
||||
// tka.State.DisablementSecrets) a call to Authority.ValidDisablement()
|
||||
// tka.State.DisablementValues) a call to Authority.ValidDisablement()
|
||||
// with the input of this function as the argument will return true.
|
||||
func DisablementKDF(secret []byte) []byte {
|
||||
// time = 4 (3 recommended, booped to 4 to compensate for less memory)
|
||||
@@ -126,8 +132,8 @@ func DisablementKDF(secret []byte) []byte {
|
||||
// checkDisablement returns true for a valid disablement secret.
|
||||
func (s State) checkDisablement(secret []byte) bool {
|
||||
derived := DisablementKDF(secret)
|
||||
for _, candidate := range s.DisablementSecrets {
|
||||
if bytes.Equal(derived, candidate) {
|
||||
for _, candidate := range s.DisablementValues {
|
||||
if subtle.ConstantTimeCompare(derived, candidate) == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -247,30 +253,23 @@ func (s State) applyVerifiedAUM(update AUM) (State, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Upper bound on checkpoint elements, chosen arbitrarily. Intended to
|
||||
// cap out insanely large AUMs.
|
||||
const (
|
||||
maxDisablementSecrets = 32
|
||||
maxKeys = 512
|
||||
)
|
||||
|
||||
// staticValidateCheckpoint validates that the state is well-formed for
|
||||
// inclusion in a checkpoint AUM.
|
||||
func (s *State) staticValidateCheckpoint() error {
|
||||
if s.LastAUMHash != nil {
|
||||
return errors.New("cannot specify a parent AUM")
|
||||
}
|
||||
if len(s.DisablementSecrets) == 0 {
|
||||
if len(s.DisablementValues) == 0 {
|
||||
return errors.New("at least one disablement secret required")
|
||||
}
|
||||
if numDS := len(s.DisablementSecrets); numDS > maxDisablementSecrets {
|
||||
return fmt.Errorf("too many disablement secrets (%d, max %d)", numDS, maxDisablementSecrets)
|
||||
if numDS := len(s.DisablementValues); numDS > maxDisablementValues {
|
||||
return fmt.Errorf("too many disablement values (%d, max %d)", numDS, maxDisablementValues)
|
||||
}
|
||||
for i, ds := range s.DisablementSecrets {
|
||||
for i, ds := range s.DisablementValues {
|
||||
if len(ds) != disablementLength {
|
||||
return fmt.Errorf("disablement[%d]: invalid length (got %d, want %d)", i, len(ds), disablementLength)
|
||||
}
|
||||
for j, ds2 := range s.DisablementSecrets {
|
||||
for j, ds2 := range s.DisablementValues {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
@@ -315,3 +314,18 @@ func (s *State) staticValidateCheckpoint() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateStateForTest creates a [State] that marks the given keys as trusted
|
||||
// with an arbitrary disablement value.
|
||||
//
|
||||
// This is only for use in tests, and will panic if called outside a test.
|
||||
func CreateStateForTest(keys ...Key) State {
|
||||
testenv.AssertInTest()
|
||||
|
||||
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||||
|
||||
return State{
|
||||
Keys: keys,
|
||||
DisablementValues: [][]byte{DisablementKDF(disablementSecret)},
|
||||
}
|
||||
}
|
||||
+73
-33
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
@@ -9,13 +9,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
// Max iterations searching for any intersection.
|
||||
maxSyncIter = 2000
|
||||
// Max iterations searching for a head intersection.
|
||||
maxSyncHeadIntersectionIter = 400
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// ErrNoIntersection is returned when a shared AUM could
|
||||
@@ -67,18 +62,6 @@ func FromSyncOffer(offer SyncOffer) (head string, ancestors []string, err error)
|
||||
return string(headBytes), ancestors, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// The starting number of AUMs to skip when listing
|
||||
// ancestors in a SyncOffer.
|
||||
ancestorsSkipStart = 4
|
||||
|
||||
// How many bits to advance the skip count when listing
|
||||
// ancestors in a SyncOffer.
|
||||
//
|
||||
// 2 bits, so (4<<2), so after skipping 4 it skips 16.
|
||||
ancestorsSkipShift = 2
|
||||
)
|
||||
|
||||
// SyncOffer returns an abbreviated description of the current AUM
|
||||
// chain, which can be used to synchronize with another (untrusted)
|
||||
// Authority instance.
|
||||
@@ -99,20 +82,10 @@ func (a *Authority) SyncOffer(storage Chonk) (SyncOffer, error) {
|
||||
Ancestors: make([]AUMHash, 0, 6), // 6 chosen arbitrarily.
|
||||
}
|
||||
|
||||
// We send some subset of our ancestors to help the remote
|
||||
// find a more-recent 'head intersection'.
|
||||
// The number of AUMs between each ancestor entry gets
|
||||
// exponentially larger.
|
||||
var (
|
||||
skipAmount uint64 = ancestorsSkipStart
|
||||
curs AUMHash = a.Head()
|
||||
)
|
||||
for i := uint64(0); i < maxSyncHeadIntersectionIter; i++ {
|
||||
if i > 0 && (i%skipAmount) == 0 {
|
||||
out.Ancestors = append(out.Ancestors, curs)
|
||||
skipAmount = skipAmount << ancestorsSkipShift
|
||||
}
|
||||
|
||||
// We send all our checkpoints to help the remote find a
|
||||
// more-recent 'head intersection'.
|
||||
curs := a.Head()
|
||||
for range maxSyncHeadIntersectionIter {
|
||||
parent, err := storage.AUM(curs)
|
||||
if err != nil {
|
||||
if err != os.ErrNotExist {
|
||||
@@ -125,6 +98,11 @@ func (a *Authority) SyncOffer(storage Chonk) (SyncOffer, error) {
|
||||
if parent.Hash() == oldest {
|
||||
break
|
||||
}
|
||||
|
||||
if parent.MessageKind == AUMCheckpoint {
|
||||
out.Ancestors = append(out.Ancestors, curs)
|
||||
}
|
||||
|
||||
copy(curs[:], parent.PrevAUMHash)
|
||||
}
|
||||
|
||||
@@ -281,3 +259,65 @@ func (a *Authority) MissingAUMs(storage Chonk, remoteOffer SyncOffer) ([]AUM, er
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// SeedNode is an authority-chonk pair that can be seeded by [SeedAUMs].
|
||||
type SeedNode struct {
|
||||
authority *Authority
|
||||
storage Chonk
|
||||
}
|
||||
|
||||
// CreateSeedNode creates a node for use with [SeedAUMs].
|
||||
func CreateSeedNode(t testenv.TB, authority *Authority, storage Chonk) SeedNode {
|
||||
t.Helper()
|
||||
return SeedNode{authority, storage}
|
||||
}
|
||||
|
||||
type SeedAUMConfig struct {
|
||||
Count int
|
||||
Signer Signer
|
||||
Nodes []SeedNode
|
||||
}
|
||||
|
||||
// SeedAUMs generates many AUMs by repeatedly adding and removing keys
|
||||
// from the TKA.
|
||||
//
|
||||
// The AUMs are written to all the supplied nodes, so if you pass more
|
||||
// than one, you can build up a long sync history.
|
||||
//
|
||||
// This is only for use in testing.
|
||||
func SeedAUMs(t testenv.TB, config SeedAUMConfig) {
|
||||
t.Helper()
|
||||
|
||||
if len(config.Nodes) == 0 {
|
||||
panic("called SeedAUMs without any nodes")
|
||||
}
|
||||
primaryNode := config.Nodes[0]
|
||||
|
||||
// The key that we'll repeatedly add/remove in the TKA.
|
||||
key := Key{Kind: Key25519, Public: []byte{1, 1, 1}, Votes: 1}
|
||||
|
||||
for i := 0; i < config.Count/2; i++ {
|
||||
for _, action := range []string{"add", "remove"} {
|
||||
updater := primaryNode.authority.NewUpdater(config.Signer)
|
||||
if action == "add" {
|
||||
if err := updater.AddKey(key); err != nil {
|
||||
t.Fatalf("error from updater.AddKey: %v")
|
||||
}
|
||||
} else {
|
||||
if err := updater.RemoveKey(key.MustID()); err != nil {
|
||||
t.Fatalf("error from updater.RemoveKey: %v")
|
||||
}
|
||||
}
|
||||
aum, err := updater.Finalize(primaryNode.storage)
|
||||
if err != nil {
|
||||
t.Fatalf("error from authority.Finalize: %v", err)
|
||||
}
|
||||
|
||||
for _, n := range config.Nodes {
|
||||
if err := n.authority.Inform(n.storage, aum); err != nil {
|
||||
t.Fatalf("error from authority.Inform: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
-53
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
@@ -286,14 +287,30 @@ func (c *Mem) PurgeAUMs(hashes []AUMHash) error {
|
||||
|
||||
// FS implements filesystem storage of TKA state.
|
||||
//
|
||||
// FS caches graph metadata derived from its storage directory. Mutations made
|
||||
// through this instance invalidate the cache. Graph changes made through
|
||||
// another FS or directly to the directory are not reflected in the indexes
|
||||
// until they are invalidated or this instance is reopened.
|
||||
//
|
||||
// FS implements the Chonk interface.
|
||||
type FS struct {
|
||||
base string
|
||||
mu sync.RWMutex
|
||||
|
||||
// aumIndex and parentIndex are initialized together by buildIndexLocked.
|
||||
// They avoid repeatedly scanning every AUM file while walking the authority
|
||||
// graph. AUM contents are still decoded from disk on demand. A nil aumIndex
|
||||
// means the indexes need to be rebuilt.
|
||||
aumIndex set.Set[AUMHash]
|
||||
parentIndex map[AUMHash][]AUMHash
|
||||
}
|
||||
|
||||
// ChonkDir returns an implementation of Chonk which uses the
|
||||
// given directory to store TKA state.
|
||||
// ChonkDir returns an implementation of Chonk which uses the given directory
|
||||
// to store TKA state.
|
||||
//
|
||||
// The returned FS expects exclusive write access to dir for its lifetime.
|
||||
// Graph changes made through another FS or directly to dir may not be reflected
|
||||
// in queries until the returned FS is reopened.
|
||||
func ChonkDir(dir string) (*FS, error) {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
|
||||
return nil, fmt.Errorf("creating chonk root dir: %v", err)
|
||||
@@ -354,7 +371,12 @@ func (c *FS) aumDir(h AUMHash) (dir, base string) {
|
||||
func (c *FS) AUM(hash AUMHash) (AUM, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.aumLocked(hash)
|
||||
}
|
||||
|
||||
// aumLocked reads an AUM from disk. The caller must hold c.mu for reading or
|
||||
// writing.
|
||||
func (c *FS) aumLocked(hash AUMHash) (AUM, error) {
|
||||
info, err := c.get(hash)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -401,20 +423,36 @@ func (c *FS) CommitTime(h AUMHash) (time.Time, error) {
|
||||
return s.ModTime(), nil
|
||||
}
|
||||
|
||||
// AUM returns any known AUMs with a specific parent hash.
|
||||
// ChildAUMs returns any known AUMs with a specific parent hash.
|
||||
func (c *FS) ChildAUMs(prevAUMHash AUMHash) ([]AUM, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.aumIndex != nil {
|
||||
defer c.mu.RUnlock()
|
||||
return c.childAUMsFromIndexLocked(prevAUMHash)
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
var out []AUM
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if err := c.buildIndexLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.childAUMsFromIndexLocked(prevAUMHash)
|
||||
}
|
||||
|
||||
err := c.scanHashes(func(info *fsHashInfo) {
|
||||
if info.AUM != nil && bytes.Equal(info.AUM.PrevAUMHash, prevAUMHash[:]) {
|
||||
out = append(out, *info.AUM)
|
||||
// childAUMsFromIndexLocked returns children from the in-memory index. The
|
||||
// caller must hold c.mu for reading or writing.
|
||||
func (c *FS) childAUMsFromIndexLocked(prevAUMHash AUMHash) ([]AUM, error) {
|
||||
children := c.parentIndex[prevAUMHash]
|
||||
out := make([]AUM, 0, len(children))
|
||||
for i, h := range children {
|
||||
aum, err := c.aumLocked(h)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading child %d of %x: %w", i, prevAUMHash, err)
|
||||
}
|
||||
})
|
||||
|
||||
return out, err
|
||||
out = append(out, aum)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *FS) get(h AUMHash) (*fsHashInfo, error) {
|
||||
@@ -442,72 +480,99 @@ func (c *FS) get(h AUMHash) (*fsHashInfo, error) {
|
||||
|
||||
// Heads returns AUMs for which there are no children. In other
|
||||
// words, the latest AUM in all possible chains (the 'leaves').
|
||||
//
|
||||
// Heads is expected to be called infrequently compared to AUM() or
|
||||
// ChildAUMs(), so we haven't put any work into maintaining an index.
|
||||
// Instead, the full set of AUMs is scanned.
|
||||
func (c *FS) Heads() ([]AUM, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.aumIndex != nil {
|
||||
defer c.mu.RUnlock()
|
||||
return c.headsFromIndexLocked()
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Scan the complete list of AUMs, and build a list of all parent hashes.
|
||||
// This tells us which AUMs have children.
|
||||
var parentHashes []AUMHash
|
||||
|
||||
allAUMs, err := c.AllAUMs()
|
||||
if err != nil {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if err := c.buildIndexLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.headsFromIndexLocked()
|
||||
}
|
||||
|
||||
for _, h := range allAUMs {
|
||||
aum, err := c.AUM(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parent, hasParent := aum.Parent()
|
||||
if !hasParent {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(parentHashes, parent) {
|
||||
parentHashes = append(parentHashes, parent)
|
||||
}
|
||||
}
|
||||
|
||||
// Now scan a second time, and only include AUMs which weren't marked as
|
||||
// the parent of any other AUM.
|
||||
// headsFromIndexLocked returns heads from the in-memory index. The caller
|
||||
// must hold c.mu for reading or writing.
|
||||
func (c *FS) headsFromIndexLocked() ([]AUM, error) {
|
||||
out := make([]AUM, 0, 6) // 6 is arbitrary.
|
||||
|
||||
for _, h := range allAUMs {
|
||||
if slices.Contains(parentHashes, h) {
|
||||
for h := range c.aumIndex {
|
||||
if len(c.parentIndex[h]) != 0 {
|
||||
continue
|
||||
}
|
||||
aum, err := c.AUM(h)
|
||||
aum, err := c.aumLocked(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("reading head %x: %w", h, err)
|
||||
}
|
||||
out = append(out, aum)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RemoveAll permanently and completely clears the TKA state.
|
||||
func (c *FS) RemoveAll() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.invalidateIndexLocked()
|
||||
return os.RemoveAll(c.base)
|
||||
}
|
||||
|
||||
// AllAUMs returns all AUMs stored in the chonk.
|
||||
func (c *FS) AllAUMs() ([]AUMHash, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.aumIndex != nil {
|
||||
out := slices.Collect(maps.Keys(c.aumIndex))
|
||||
c.mu.RUnlock()
|
||||
return out, nil
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
out := make([]AUMHash, 0, 6) // 6 is arbitrary.
|
||||
err := c.scanHashes(func(info *fsHashInfo) {
|
||||
if info.AUM != nil {
|
||||
out = append(out, info.AUM.Hash())
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if err := c.buildIndexLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return slices.Collect(maps.Keys(c.aumIndex)), nil
|
||||
}
|
||||
|
||||
// buildIndexLocked builds indexes of the active AUMs in a single filesystem
|
||||
// scan. If the index already exists, this is a no-op. The caller must hold
|
||||
// c.mu for writing.
|
||||
func (c *FS) buildIndexLocked() error {
|
||||
if c.aumIndex != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
aumIndex := make(set.Set[AUMHash])
|
||||
parentIndex := make(map[AUMHash][]AUMHash)
|
||||
if err := c.scanHashes(func(info *fsHashInfo) {
|
||||
if info.AUM == nil {
|
||||
return
|
||||
}
|
||||
})
|
||||
return out, err
|
||||
h := info.AUM.Hash()
|
||||
aumIndex.Add(h)
|
||||
if parent, ok := info.AUM.Parent(); ok {
|
||||
parentIndex[parent] = append(parentIndex[parent], h)
|
||||
}
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.aumIndex = aumIndex
|
||||
c.parentIndex = parentIndex
|
||||
return nil
|
||||
}
|
||||
|
||||
// invalidateIndexLocked invalidates the in-memory indexes after storage is
|
||||
// changed. The caller must hold c.mu for writing.
|
||||
func (c *FS) invalidateIndexLocked() {
|
||||
c.aumIndex = nil
|
||||
c.parentIndex = nil
|
||||
}
|
||||
|
||||
func (c *FS) scanHashes(eachHashInfo func(*fsHashInfo)) error {
|
||||
@@ -594,6 +659,7 @@ func (c *FS) CommitVerifiedAUMs(updates []AUM) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.invalidateIndexLocked()
|
||||
for i, aum := range updates {
|
||||
h := aum.Hash()
|
||||
err := c.commit(h, func(info *fsHashInfo) {
|
||||
@@ -610,9 +676,14 @@ func (c *FS) CommitVerifiedAUMs(updates []AUM) error {
|
||||
|
||||
// PurgeAUMs marks the specified AUMs for deletion from storage.
|
||||
func (c *FS) PurgeAUMs(hashes []AUMHash) error {
|
||||
if len(hashes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.invalidateIndexLocked()
|
||||
now := time.Now()
|
||||
for i, h := range hashes {
|
||||
stored, err := c.get(h)
|
||||
@@ -715,7 +786,7 @@ func markActiveChain(storage Chonk, verdict map[AUMHash]retainState, minChain in
|
||||
|
||||
parent, hasParent := next.Parent()
|
||||
if !hasParent {
|
||||
// Genesis AUM (beginning of time). The chain isnt long enough to need truncating.
|
||||
// Genesis AUM (beginning of time). The chain isn't long enough to need truncating.
|
||||
return h, nil
|
||||
}
|
||||
|
||||
|
||||
+2
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
@@ -31,9 +31,6 @@ var cborDecOpts = cbor.DecOptions{
|
||||
MaxMapPairs: 1024,
|
||||
}
|
||||
|
||||
// Arbitrarily chosen limit on scanning AUM trees.
|
||||
const maxScanIterations = 2000
|
||||
|
||||
// Authority is a Tailnet Key Authority. This type is the main coupling
|
||||
// point to the rest of the tailscale client.
|
||||
//
|
||||
@@ -570,7 +567,7 @@ func Bootstrap(storage Chonk, bootstrap AUM) (*Authority, error) {
|
||||
// ValidDisablement returns true if the disablement secret was correct.
|
||||
//
|
||||
// If this method returns true, the caller should shut down the authority
|
||||
// and purge all network-lock state.
|
||||
// and purge all tailnet-lock state.
|
||||
func (a *Authority) ValidDisablement(secret []byte) bool {
|
||||
return a.state.checkDisablement(secret)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_tailnetlock
|
||||
|
||||
Reference in new issue
Block a user