Skip to content

Commit 3b522a2

Browse files
committed
TUN-8807: Add support_datagram_v3 to remote feature rollout
Support rolling out the `support_datagram_v3` feature via remote feature rollout (DNS TXT record) with `dv3` key. Consolidated some of the feature evaluation code into the features module to simplify the lookup of available features at runtime. Reduced complexity for management logs feature lookup since it's a default feature. Closes TUN-8807
1 parent 5cfe9be commit 3b522a2

File tree

7 files changed

+274
-95
lines changed

7 files changed

+274
-95
lines changed

cmd/cloudflared/tunnel/cmd.go

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import (
3131
"github.com/cloudflare/cloudflared/credentials"
3232
"github.com/cloudflare/cloudflared/diagnostic"
3333
"github.com/cloudflare/cloudflared/edgediscovery"
34-
"github.com/cloudflare/cloudflared/features"
3534
"github.com/cloudflare/cloudflared/ingress"
3635
"github.com/cloudflare/cloudflared/logger"
3736
"github.com/cloudflare/cloudflared/management"
@@ -515,26 +514,23 @@ func StartServer(
515514
tunnelConfig.ICMPRouterServer = nil
516515
}
517516

518-
internalRules := []ingress.Rule{}
519-
if features.Contains(features.FeatureManagementLogs) {
520-
serviceIP := c.String("service-op-ip")
521-
if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil {
522-
if serviceAddr, err := edgeAddrs.GetAddrForRPC(); err == nil {
523-
serviceIP = serviceAddr.TCP.String()
524-
}
517+
serviceIP := c.String("service-op-ip")
518+
if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil {
519+
if serviceAddr, err := edgeAddrs.GetAddrForRPC(); err == nil {
520+
serviceIP = serviceAddr.TCP.String()
525521
}
526-
527-
mgmt := management.New(
528-
c.String("management-hostname"),
529-
c.Bool("management-diagnostics"),
530-
serviceIP,
531-
clientID,
532-
c.String(connectorLabelFlag),
533-
logger.ManagementLogger.Log,
534-
logger.ManagementLogger,
535-
)
536-
internalRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
537522
}
523+
524+
mgmt := management.New(
525+
c.String("management-hostname"),
526+
c.Bool("management-diagnostics"),
527+
serviceIP,
528+
clientID,
529+
c.String(connectorLabelFlag),
530+
logger.ManagementLogger.Log,
531+
logger.ManagementLogger,
532+
)
533+
internalRules := []ingress.Rule{ingress.NewManagementRule(mgmt)}
538534
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, internalRules, tunnelConfig.Log)
539535
if err != nil {
540536
return err

cmd/cloudflared/tunnel/configuration.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,28 +137,22 @@ func prepareTunnelConfig(
137137

138138
transportProtocol := c.String("protocol")
139139

140-
clientFeatures := features.Dedup(append(c.StringSlice("features"), features.DefaultFeatures...))
141-
142-
staticFeatures := features.StaticFeatures{}
143-
if c.Bool("post-quantum") {
144-
if FipsEnabled {
145-
return nil, nil, fmt.Errorf("post-quantum not supported in FIPS mode")
146-
}
147-
pqMode := features.PostQuantumStrict
148-
staticFeatures.PostQuantumMode = &pqMode
140+
if c.Bool("post-quantum") && FipsEnabled {
141+
return nil, nil, fmt.Errorf("post-quantum not supported in FIPS mode")
149142
}
150-
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, staticFeatures, log)
143+
144+
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, c.StringSlice("features"), c.Bool("post-quantum"), log)
151145
if err != nil {
152146
return nil, nil, errors.Wrap(err, "Failed to create feature selector")
153147
}
148+
clientFeatures := featureSelector.ClientFeatures()
154149
pqMode := featureSelector.PostQuantumMode()
155150
if pqMode == features.PostQuantumStrict {
156151
// Error if the user tries to force a non-quic transport protocol
157152
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
158153
return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport")
159154
}
160155
transportProtocol = connection.QUIC.String()
161-
clientFeatures = append(clientFeatures, features.FeaturePostQuantum)
162156

163157
log.Info().Msgf(
164158
"Using hybrid post-quantum key agreement %s",

features/features.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const (
1212
)
1313

1414
var (
15-
DefaultFeatures = []string{
15+
defaultFeatures = []string{
1616
FeatureAllowRemoteConfig,
1717
FeatureSerializedHeaders,
1818
FeatureDatagramV2,
@@ -21,15 +21,30 @@ var (
2121
}
2222
)
2323

24-
func Contains(feature string) bool {
25-
for _, f := range DefaultFeatures {
26-
if f == feature {
27-
return true
28-
}
29-
}
30-
return false
24+
// Features set by user provided flags
25+
type staticFeatures struct {
26+
PostQuantumMode *PostQuantumMode
3127
}
3228

29+
type PostQuantumMode uint8
30+
31+
const (
32+
// Prefer post quantum, but fallback if connection cannot be established
33+
PostQuantumPrefer PostQuantumMode = iota
34+
// If the user passes the --post-quantum flag, we override
35+
// CurvePreferences to only support hybrid post-quantum key agreements.
36+
PostQuantumStrict
37+
)
38+
39+
type DatagramVersion string
40+
41+
const (
42+
// DatagramV2 is the currently supported datagram protocol for UDP and ICMP packets
43+
DatagramV2 DatagramVersion = FeatureDatagramV2
44+
// DatagramV3 is a new datagram protocol for UDP and ICMP packets. It is not backwards compatible with datagram v2.
45+
DatagramV3 DatagramVersion = FeatureDatagramV3
46+
)
47+
3348
// Remove any duplicates from the slice
3449
func Dedup(slice []string) []string {
3550

features/selector.go

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"hash/fnv"
88
"net"
9+
"slices"
910
"sync"
1011
"time"
1112

@@ -18,61 +19,67 @@ const (
1819
lookupTimeout = time.Second * 10
1920
)
2021

21-
type PostQuantumMode uint8
22-
23-
const (
24-
// Prefer post quantum, but fallback if connection cannot be established
25-
PostQuantumPrefer PostQuantumMode = iota
26-
// If the user passes the --post-quantum flag, we override
27-
// CurvePreferences to only support hybrid post-quantum key agreements.
28-
PostQuantumStrict
29-
)
30-
3122
// If the TXT record adds other fields, the umarshal logic will ignore those keys
3223
// If the TXT record is missing a key, the field will unmarshal to the default Go value
33-
// pq was removed in TUN-7970
34-
type featuresRecord struct{}
3524

36-
func NewFeatureSelector(ctx context.Context, accountTag string, staticFeatures StaticFeatures, logger *zerolog.Logger) (*FeatureSelector, error) {
37-
return newFeatureSelector(ctx, accountTag, logger, newDNSResolver(), staticFeatures, defaultRefreshFreq)
25+
type featuresRecord struct {
26+
// support_datagram_v3
27+
DatagramV3Percentage int32 `json:"dv3"`
28+
29+
// PostQuantumPercentage int32 `json:"pq"` // Removed in TUN-7970
3830
}
3931

40-
// FeatureSelector determines if this account will try new features. It preiodically queries a DNS TXT record
41-
// to see which features are turned on
32+
func NewFeatureSelector(ctx context.Context, accountTag string, cliFeatures []string, pq bool, logger *zerolog.Logger) (*FeatureSelector, error) {
33+
return newFeatureSelector(ctx, accountTag, logger, newDNSResolver(), cliFeatures, pq, defaultRefreshFreq)
34+
}
35+
36+
// FeatureSelector determines if this account will try new features. It periodically queries a DNS TXT record
37+
// to see which features are turned on.
4238
type FeatureSelector struct {
4339
accountHash int32
4440
logger *zerolog.Logger
4541
resolver resolver
4642

47-
staticFeatures StaticFeatures
43+
staticFeatures staticFeatures
44+
cliFeatures []string
4845

4946
// lock protects concurrent access to dynamic features
5047
lock sync.RWMutex
5148
features featuresRecord
5249
}
5350

54-
// Features set by user provided flags
55-
type StaticFeatures struct {
56-
PostQuantumMode *PostQuantumMode
57-
}
58-
59-
func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.Logger, resolver resolver, staticFeatures StaticFeatures, refreshFreq time.Duration) (*FeatureSelector, error) {
51+
func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.Logger, resolver resolver, cliFeatures []string, pq bool, refreshFreq time.Duration) (*FeatureSelector, error) {
52+
// Combine default features and user-provided features
53+
var pqMode *PostQuantumMode
54+
if pq {
55+
mode := PostQuantumStrict
56+
pqMode = &mode
57+
cliFeatures = append(cliFeatures, FeaturePostQuantum)
58+
}
59+
staticFeatures := staticFeatures{
60+
PostQuantumMode: pqMode,
61+
}
6062
selector := &FeatureSelector{
6163
accountHash: switchThreshold(accountTag),
6264
logger: logger,
6365
resolver: resolver,
6466
staticFeatures: staticFeatures,
67+
cliFeatures: Dedup(cliFeatures),
6568
}
6669

6770
if err := selector.refresh(ctx); err != nil {
6871
logger.Err(err).Msg("Failed to fetch features, default to disable")
6972
}
7073

71-
// Run refreshLoop next time we have a new feature to rollout
74+
go selector.refreshLoop(ctx, refreshFreq)
7275

7376
return selector, nil
7477
}
7578

79+
func (fs *FeatureSelector) accountEnabled(percentage int32) bool {
80+
return percentage > fs.accountHash
81+
}
82+
7683
func (fs *FeatureSelector) PostQuantumMode() PostQuantumMode {
7784
if fs.staticFeatures.PostQuantumMode != nil {
7885
return *fs.staticFeatures.PostQuantumMode
@@ -81,6 +88,33 @@ func (fs *FeatureSelector) PostQuantumMode() PostQuantumMode {
8188
return PostQuantumPrefer
8289
}
8390

91+
func (fs *FeatureSelector) DatagramVersion() DatagramVersion {
92+
fs.lock.RLock()
93+
defer fs.lock.RUnlock()
94+
95+
// If user provides the feature via the cli, we take it as priority over remote feature evaluation
96+
if slices.Contains(fs.cliFeatures, FeatureDatagramV3) {
97+
return DatagramV3
98+
}
99+
// If the user specifies DatagramV2, we also take that over remote
100+
if slices.Contains(fs.cliFeatures, FeatureDatagramV2) {
101+
return DatagramV2
102+
}
103+
104+
if fs.accountEnabled(fs.features.DatagramV3Percentage) {
105+
return DatagramV3
106+
}
107+
return DatagramV2
108+
}
109+
110+
// ClientFeatures will return the list of currently available features that cloudflared should provide to the edge.
111+
//
112+
// This list is dynamic and can change in-between returns.
113+
func (fs *FeatureSelector) ClientFeatures() []string {
114+
// Evaluate any remote features along with static feature list to construct the list of features
115+
return Dedup(slices.Concat(defaultFeatures, fs.cliFeatures, []string{string(fs.DatagramVersion())}))
116+
}
117+
84118
func (fs *FeatureSelector) refreshLoop(ctx context.Context, refreshFreq time.Duration) {
85119
ticker := time.NewTicker(refreshFreq)
86120
for {

0 commit comments

Comments
 (0)