state: replace zcache with bounded LRU for auth cache
Replace zcache with golang-lru/v2/expirable for both the state auth cache and the OIDC state cache. Add tuning.register_cache_max_entries (default 1024) to cap the number of pending registration entries. Introduce types.RegistrationData to replace caching a full *Node; only the fields the registration callback path reads are retained. Remove the dead HSDatabase.regCache field. Drop zgo.at/zcache/v2 from go.mod.
This commit is contained in:
parent
3587225a88
commit
0d4f2293ff
21 changed files with 343 additions and 258 deletions
|
|
@ -221,40 +221,65 @@ func (r AuthID) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// AuthRequest represent a pending authentication request from a user or a node.
|
||||
// If it is a registration request, the node field will be populate with the node that is trying to register.
|
||||
// When the authentication process is finished, the node that has been authenticated will be sent through the Finished channel.
|
||||
// The closed field is used to ensure that the Finished channel is only closed once, and that no more nodes are sent through it after it has been closed.
|
||||
// AuthRequest represents a pending authentication request from a user or a
|
||||
// node. It carries the minimum data needed to either complete a node
|
||||
// registration (regData populated) or signal the verdict of an interactive
|
||||
// auth flow (no payload). Verdict delivery is via the finished channel; the
|
||||
// closed flag guards FinishAuth against double-close.
|
||||
//
|
||||
// AuthRequest is always handled by pointer so the channel and atomic flag
|
||||
// have a single canonical instance even when stored in caches that
|
||||
// internally copy values.
|
||||
type AuthRequest struct {
|
||||
node *Node
|
||||
// regData is populated for node-registration flows (interactive web
|
||||
// or OIDC). It carries only the minimal subset of registration data
|
||||
// the auth callback needs to promote this request into a real node;
|
||||
// see RegistrationData for the rationale behind keeping the payload
|
||||
// small.
|
||||
//
|
||||
// nil for non-registration flows (e.g. SSH check). Use
|
||||
// RegistrationData() to read it safely.
|
||||
regData *RegistrationData
|
||||
|
||||
finished chan AuthVerdict
|
||||
closed *atomic.Bool
|
||||
}
|
||||
|
||||
func NewAuthRequest() AuthRequest {
|
||||
return AuthRequest{
|
||||
// NewAuthRequest creates a pending auth request with no payload, suitable
|
||||
// for non-registration flows that only need a verdict channel.
|
||||
func NewAuthRequest() *AuthRequest {
|
||||
return &AuthRequest{
|
||||
finished: make(chan AuthVerdict, 1),
|
||||
closed: &atomic.Bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewRegisterAuthRequest(node Node) AuthRequest {
|
||||
return AuthRequest{
|
||||
node: &node,
|
||||
// NewRegisterAuthRequest creates a pending auth request carrying the
|
||||
// minimal RegistrationData for a node-registration flow. The data is
|
||||
// stored by pointer; callers must not mutate it after handing it off.
|
||||
func NewRegisterAuthRequest(data *RegistrationData) *AuthRequest {
|
||||
return &AuthRequest{
|
||||
regData: data,
|
||||
finished: make(chan AuthVerdict, 1),
|
||||
closed: &atomic.Bool{},
|
||||
}
|
||||
}
|
||||
|
||||
// Node returns the node that is trying to register.
|
||||
// It will panic if the AuthRequest is not a registration request.
|
||||
// Can _only_ be used in the registration path.
|
||||
func (rn *AuthRequest) Node() NodeView {
|
||||
if rn.node == nil {
|
||||
panic("Node can only be used in registration requests")
|
||||
// RegistrationData returns the cached registration payload. It panics if
|
||||
// called on an AuthRequest that was not created via
|
||||
// NewRegisterAuthRequest, mirroring the previous Node() contract.
|
||||
func (rn *AuthRequest) RegistrationData() *RegistrationData {
|
||||
if rn.regData == nil {
|
||||
panic("RegistrationData can only be used in registration requests")
|
||||
}
|
||||
|
||||
return rn.node.View()
|
||||
return rn.regData
|
||||
}
|
||||
|
||||
// IsRegistration reports whether this auth request carries registration
|
||||
// data (i.e. it was created via NewRegisterAuthRequest).
|
||||
func (rn *AuthRequest) IsRegistration() bool {
|
||||
return rn.regData != nil
|
||||
}
|
||||
|
||||
func (rn *AuthRequest) FinishAuth(verdict AuthVerdict) {
|
||||
|
|
|
|||
|
|
@ -278,14 +278,16 @@ type Tuning struct {
|
|||
// updates for connected clients.
|
||||
BatcherWorkers int
|
||||
|
||||
// RegisterCacheCleanup is the interval between cleanup operations for
|
||||
// expired registration cache entries.
|
||||
RegisterCacheCleanup time.Duration
|
||||
|
||||
// RegisterCacheExpiration is how long registration cache entries remain
|
||||
// valid before being eligible for cleanup.
|
||||
// valid before being eligible for eviction.
|
||||
RegisterCacheExpiration time.Duration
|
||||
|
||||
// RegisterCacheMaxEntries bounds the number of pending registration
|
||||
// entries the auth cache will hold. Older entries are evicted (LRU)
|
||||
// when the cap is reached, preventing unauthenticated cache-fill DoS.
|
||||
// A value of 0 falls back to defaultRegisterCacheMaxEntries (1024).
|
||||
RegisterCacheMaxEntries int
|
||||
|
||||
// NodeStoreBatchSize controls how many write operations are accumulated
|
||||
// before rebuilding the in-memory node snapshot.
|
||||
//
|
||||
|
|
@ -1192,8 +1194,8 @@ func LoadServerConfig() (*Config, error) {
|
|||
|
||||
return DefaultBatcherWorkers()
|
||||
}(),
|
||||
RegisterCacheCleanup: viper.GetDuration("tuning.register_cache_cleanup"),
|
||||
RegisterCacheExpiration: viper.GetDuration("tuning.register_cache_expiration"),
|
||||
RegisterCacheMaxEntries: viper.GetInt("tuning.register_cache_max_entries"),
|
||||
NodeStoreBatchSize: viper.GetInt("tuning.node_store_batch_size"),
|
||||
NodeStoreBatchTimeout: viper.GetDuration("tuning.node_store_batch_timeout"),
|
||||
},
|
||||
|
|
|
|||
55
hscontrol/types/registration.go
Normal file
55
hscontrol/types/registration.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// RegistrationData is the payload cached for a pending node registration.
|
||||
// It replaces the previous practice of caching a full *Node and carries
|
||||
// only the fields the registration callback path actually consumes when
|
||||
// promoting a pending registration to a real node.
|
||||
//
|
||||
// Combined with the bounded-LRU cache that holds these entries, this caps
|
||||
// the worst-case memory footprint of unauthenticated cache-fill attempts
|
||||
// at (max_entries × per_entry_size). The cache is sized so that the
|
||||
// product is bounded to a few MiB even with attacker-supplied 1 MiB
|
||||
// Hostinfos (the Noise body limit).
|
||||
type RegistrationData struct {
|
||||
// MachineKey is the cryptographic identity of the machine being
|
||||
// registered. Required.
|
||||
MachineKey key.MachinePublic
|
||||
|
||||
// NodeKey is the cryptographic identity of the node session.
|
||||
// Required.
|
||||
NodeKey key.NodePublic
|
||||
|
||||
// DiscoKey is the disco public key for peer-to-peer connections.
|
||||
DiscoKey key.DiscoPublic
|
||||
|
||||
// Hostname is the resolved hostname for the registering node.
|
||||
// Already validated/normalised by EnsureHostname at producer time.
|
||||
Hostname string
|
||||
|
||||
// Hostinfo is the original Hostinfo from the RegisterRequest,
|
||||
// stored so that the auth callback can populate the new node's
|
||||
// initial Hostinfo (and so that observability/CLI consumers see
|
||||
// fields like OS, OSVersion, and IPNVersion before the first
|
||||
// MapRequest restores the live set).
|
||||
//
|
||||
// May be nil if the client did not send Hostinfo in the original
|
||||
// RegisterRequest.
|
||||
Hostinfo *tailcfg.Hostinfo
|
||||
|
||||
// Endpoints is the initial set of WireGuard endpoints the node
|
||||
// reported. The first MapRequest after registration overwrites
|
||||
// this with the live set.
|
||||
Endpoints []netip.AddrPort
|
||||
|
||||
// Expiry is the optional client-requested expiry for this node.
|
||||
// May be nil if the client did not request a specific expiry.
|
||||
Expiry *time.Time
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue