mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-07-27 22:00:02 +00:00
TUN-9291: Remove dynamic reloading of features for datagram v3
During a refresh of the supported features via the DNS TXT record, cloudflared would update the internal feature list, but would not propagate this information to the edge during a new connection. This meant that a situation could occur in which cloudflared would think that the client's connection could support datagram V3, and would setup that muxer locally, but would not propagate that information to the edge during a register connection in the `ClientInfo` of the `ConnectionOptions`. This meant that the edge still thought that the client was setup to support datagram V2 and since the protocols are not backwards compatible, the local muxer for datagram V3 would reject the incoming RPC calls. To address this, the feature list will be fetched only once during client bootstrapping and will persist as-is until the client is restarted. This helps reduce the complexity involved with different connections having possibly different sets of features when connecting to the edge. The features will now be tied to the client and never diverge across connections. Also, retires the use of `support_datagram_v3` in-favor of `support_datagram_v3_1` to reduce the risk of reusing the feature key. The `dv3` TXT feature key is also deprecated. Closes TUN-9291
This commit is contained in:
@@ -7,7 +7,6 @@ import (
|
||||
"hash/fnv"
|
||||
"net"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
|
||||
const (
|
||||
featureSelectorHostname = "cfd-features.argotunnel.com"
|
||||
defaultRefreshFreq = time.Hour * 6
|
||||
lookupTimeout = time.Second * 10
|
||||
)
|
||||
|
||||
@@ -23,32 +21,27 @@ const (
|
||||
// If the TXT record is missing a key, the field will unmarshal to the default Go value
|
||||
|
||||
type featuresRecord struct {
|
||||
// support_datagram_v3
|
||||
DatagramV3Percentage int32 `json:"dv3"`
|
||||
|
||||
// DatagramV3Percentage int32 `json:"dv3"` // Removed in TUN-9291
|
||||
// PostQuantumPercentage int32 `json:"pq"` // Removed in TUN-7970
|
||||
}
|
||||
|
||||
func NewFeatureSelector(ctx context.Context, accountTag string, cliFeatures []string, pq bool, logger *zerolog.Logger) (*FeatureSelector, error) {
|
||||
return newFeatureSelector(ctx, accountTag, logger, newDNSResolver(), cliFeatures, pq, defaultRefreshFreq)
|
||||
return newFeatureSelector(ctx, accountTag, logger, newDNSResolver(), cliFeatures, pq)
|
||||
}
|
||||
|
||||
// FeatureSelector determines if this account will try new features. It periodically queries a DNS TXT record
|
||||
// to see which features are turned on.
|
||||
// FeatureSelector determines if this account will try new features; loaded once during startup.
|
||||
type FeatureSelector struct {
|
||||
accountHash int32
|
||||
accountHash uint32
|
||||
logger *zerolog.Logger
|
||||
resolver resolver
|
||||
|
||||
staticFeatures staticFeatures
|
||||
cliFeatures []string
|
||||
|
||||
// lock protects concurrent access to dynamic features
|
||||
lock sync.RWMutex
|
||||
features featuresRecord
|
||||
}
|
||||
|
||||
func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.Logger, resolver resolver, cliFeatures []string, pq bool, refreshFreq time.Duration) (*FeatureSelector, error) {
|
||||
func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.Logger, resolver resolver, cliFeatures []string, pq bool) (*FeatureSelector, error) {
|
||||
// Combine default features and user-provided features
|
||||
var pqMode *PostQuantumMode
|
||||
if pq {
|
||||
@@ -64,22 +57,16 @@ func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.
|
||||
logger: logger,
|
||||
resolver: resolver,
|
||||
staticFeatures: staticFeatures,
|
||||
cliFeatures: Dedup(cliFeatures),
|
||||
cliFeatures: dedupAndRemoveFeatures(cliFeatures),
|
||||
}
|
||||
|
||||
if err := selector.refresh(ctx); err != nil {
|
||||
if err := selector.init(ctx); err != nil {
|
||||
logger.Err(err).Msg("Failed to fetch features, default to disable")
|
||||
}
|
||||
|
||||
go selector.refreshLoop(ctx, refreshFreq)
|
||||
|
||||
return selector, nil
|
||||
}
|
||||
|
||||
func (fs *FeatureSelector) accountEnabled(percentage int32) bool {
|
||||
return percentage > fs.accountHash
|
||||
}
|
||||
|
||||
func (fs *FeatureSelector) PostQuantumMode() PostQuantumMode {
|
||||
if fs.staticFeatures.PostQuantumMode != nil {
|
||||
return *fs.staticFeatures.PostQuantumMode
|
||||
@@ -89,11 +76,8 @@ func (fs *FeatureSelector) PostQuantumMode() PostQuantumMode {
|
||||
}
|
||||
|
||||
func (fs *FeatureSelector) DatagramVersion() DatagramVersion {
|
||||
fs.lock.RLock()
|
||||
defer fs.lock.RUnlock()
|
||||
|
||||
// If user provides the feature via the cli, we take it as priority over remote feature evaluation
|
||||
if slices.Contains(fs.cliFeatures, FeatureDatagramV3) {
|
||||
if slices.Contains(fs.cliFeatures, FeatureDatagramV3_1) {
|
||||
return DatagramV3
|
||||
}
|
||||
// If the user specifies DatagramV2, we also take that over remote
|
||||
@@ -101,36 +85,16 @@ func (fs *FeatureSelector) DatagramVersion() DatagramVersion {
|
||||
return DatagramV2
|
||||
}
|
||||
|
||||
if fs.accountEnabled(fs.features.DatagramV3Percentage) {
|
||||
return DatagramV3
|
||||
}
|
||||
return DatagramV2
|
||||
}
|
||||
|
||||
// ClientFeatures will return the list of currently available features that cloudflared should provide to the edge.
|
||||
//
|
||||
// This list is dynamic and can change in-between returns.
|
||||
func (fs *FeatureSelector) ClientFeatures() []string {
|
||||
// Evaluate any remote features along with static feature list to construct the list of features
|
||||
return Dedup(slices.Concat(defaultFeatures, fs.cliFeatures, []string{string(fs.DatagramVersion())}))
|
||||
return dedupAndRemoveFeatures(slices.Concat(defaultFeatures, fs.cliFeatures, []string{string(fs.DatagramVersion())}))
|
||||
}
|
||||
|
||||
func (fs *FeatureSelector) refreshLoop(ctx context.Context, refreshFreq time.Duration) {
|
||||
ticker := time.NewTicker(refreshFreq)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
err := fs.refresh(ctx)
|
||||
if err != nil {
|
||||
fs.logger.Err(err).Msg("Failed to refresh feature selector")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FeatureSelector) refresh(ctx context.Context) error {
|
||||
func (fs *FeatureSelector) init(ctx context.Context) error {
|
||||
record, err := fs.resolver.lookupRecord(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -141,9 +105,6 @@ func (fs *FeatureSelector) refresh(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fs.lock.Lock()
|
||||
defer fs.lock.Unlock()
|
||||
|
||||
fs.features = features
|
||||
|
||||
return nil
|
||||
@@ -180,8 +141,8 @@ func (dr *dnsResolver) lookupRecord(ctx context.Context) ([]byte, error) {
|
||||
return []byte(records[0]), nil
|
||||
}
|
||||
|
||||
func switchThreshold(accountTag string) int32 {
|
||||
func switchThreshold(accountTag string) uint32 {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(accountTag))
|
||||
return int32(h.Sum32() % 100)
|
||||
return h.Sum32() % 100
|
||||
}
|
||||
|
Reference in New Issue
Block a user