Skip to content

Commit ce27840

Browse files
committed
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
1 parent 40dc601 commit ce27840

File tree

8 files changed

+84
-181
lines changed

8 files changed

+84
-181
lines changed

cmd/cloudflared/tunnel/config_test.go

Lines changed: 0 additions & 15 deletions
This file was deleted.

cmd/cloudflared/tunnel/configuration.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func prepareTunnelConfig(
140140
transportProtocol := c.String(flags.Protocol)
141141
isPostQuantumEnforced := c.Bool(flags.PostQuantum)
142142

143-
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, c.StringSlice("features"), c.Bool("post-quantum"), log)
143+
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, c.StringSlice(flags.Features), c.Bool(flags.PostQuantum), log)
144144
if err != nil {
145145
return nil, nil, errors.Wrap(err, "Failed to create feature selector")
146146
}

connection/control.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
"github.com/cloudflare/cloudflared/management"
1212
"github.com/cloudflare/cloudflared/tunnelrpc"
13-
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
13+
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
1414
)
1515

1616
// registerClient derives a named tunnel rpc client that can then be used to register and unregister connections.
@@ -36,7 +36,7 @@ type controlStream struct {
3636
// ControlStreamHandler registers connections with origintunneld and initiates graceful shutdown.
3737
type ControlStreamHandler interface {
3838
// ServeControlStream handles the control plane of the transport in the current goroutine calling this
39-
ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions, tunnelConfigGetter TunnelConfigJSONGetter) error
39+
ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *pogs.ConnectionOptions, tunnelConfigGetter TunnelConfigJSONGetter) error
4040
// IsStopped tells whether the method above has finished
4141
IsStopped() bool
4242
}
@@ -78,7 +78,7 @@ func NewControlStream(
7878
func (c *controlStream) ServeControlStream(
7979
ctx context.Context,
8080
rw io.ReadWriteCloser,
81-
connOptions *tunnelpogs.ConnectionOptions,
81+
connOptions *pogs.ConnectionOptions,
8282
tunnelConfigGetter TunnelConfigJSONGetter,
8383
) error {
8484
registrationClient := c.registerClientFunc(ctx, rw, c.registerTimeout)

connection/http2.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
cfdflow "github.com/cloudflare/cloudflared/flow"
2020

2121
"github.com/cloudflare/cloudflared/tracing"
22-
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
22+
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
2323
)
2424

2525
// note: these constants are exported so we can reuse them in the edge-side code
@@ -39,7 +39,7 @@ type HTTP2Connection struct {
3939
conn net.Conn
4040
server *http2.Server
4141
orchestrator Orchestrator
42-
connOptions *tunnelpogs.ConnectionOptions
42+
connOptions *pogs.ConnectionOptions
4343
observer *Observer
4444
connIndex uint8
4545

@@ -54,7 +54,7 @@ type HTTP2Connection struct {
5454
func NewHTTP2Connection(
5555
conn net.Conn,
5656
orchestrator Orchestrator,
57-
connOptions *tunnelpogs.ConnectionOptions,
57+
connOptions *pogs.ConnectionOptions,
5858
observer *Observer,
5959
connIndex uint8,
6060
controlStreamHandler ControlStreamHandler,

connection/quic_connection.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
cfdquic "github.com/cloudflare/cloudflared/quic"
2323
"github.com/cloudflare/cloudflared/tracing"
2424
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
25-
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
2625
rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic"
2726
)
2827

@@ -44,7 +43,7 @@ type quicConnection struct {
4443
orchestrator Orchestrator
4544
datagramHandler DatagramSessionHandler
4645
controlStreamHandler ControlStreamHandler
47-
connOptions *tunnelpogs.ConnectionOptions
46+
connOptions *pogs.ConnectionOptions
4847
connIndex uint8
4948

5049
rpcTimeout time.Duration
@@ -235,7 +234,7 @@ func (q *quicConnection) dispatchRequest(ctx context.Context, stream *rpcquic.Re
235234
}
236235

237236
// UpdateConfiguration is the RPC method invoked by edge when there is a new configuration
238-
func (q *quicConnection) UpdateConfiguration(ctx context.Context, version int32, config []byte) *tunnelpogs.UpdateConfigurationResponse {
237+
func (q *quicConnection) UpdateConfiguration(ctx context.Context, version int32, config []byte) *pogs.UpdateConfigurationResponse {
239238
return q.orchestrator.UpdateConfig(version, config)
240239
}
241240

features/features.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package features
22

3+
import "slices"
4+
35
const (
46
FeatureSerializedHeaders = "serialized_headers"
57
FeatureQuickReconnects = "quick_reconnects"
@@ -8,7 +10,9 @@ const (
810
FeaturePostQuantum = "postquantum"
911
FeatureQUICSupportEOF = "support_quic_eof"
1012
FeatureManagementLogs = "management_logs"
11-
FeatureDatagramV3 = "support_datagram_v3"
13+
FeatureDatagramV3_1 = "support_datagram_v3_1"
14+
15+
DeprecatedFeatureDatagramV3 = "support_datagram_v3" // Deprecated: TUN-9291
1216
)
1317

1418
var defaultFeatures = []string{
@@ -19,6 +23,11 @@ var defaultFeatures = []string{
1923
FeatureManagementLogs,
2024
}
2125

26+
// List of features that are no longer in-use.
27+
var deprecatedFeatures = []string{
28+
DeprecatedFeatureDatagramV3,
29+
}
30+
2231
// Features set by user provided flags
2332
type staticFeatures struct {
2433
PostQuantumMode *PostQuantumMode
@@ -40,15 +49,19 @@ const (
4049
// DatagramV2 is the currently supported datagram protocol for UDP and ICMP packets
4150
DatagramV2 DatagramVersion = FeatureDatagramV2
4251
// DatagramV3 is a new datagram protocol for UDP and ICMP packets. It is not backwards compatible with datagram v2.
43-
DatagramV3 DatagramVersion = FeatureDatagramV3
52+
DatagramV3 DatagramVersion = FeatureDatagramV3_1
4453
)
4554

46-
// Remove any duplicates from the slice
47-
func Dedup(slice []string) []string {
55+
// Remove any duplicate features from the list and remove deprecated features
56+
func dedupAndRemoveFeatures(features []string) []string {
4857
// Convert the slice into a set
49-
set := make(map[string]bool, 0)
50-
for _, str := range slice {
51-
set[str] = true
58+
set := map[string]bool{}
59+
for _, feature := range features {
60+
// Remove deprecated features from the provided list
61+
if slices.Contains(deprecatedFeatures, feature) {
62+
continue
63+
}
64+
set[feature] = true
5265
}
5366

5467
// Convert the set back into a slice

features/selector.go

Lines changed: 12 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,41 @@ import (
77
"hash/fnv"
88
"net"
99
"slices"
10-
"sync"
1110
"time"
1211

1312
"github.com/rs/zerolog"
1413
)
1514

1615
const (
1716
featureSelectorHostname = "cfd-features.argotunnel.com"
18-
defaultRefreshFreq = time.Hour * 6
1917
lookupTimeout = time.Second * 10
2018
)
2119

2220
// If the TXT record adds other fields, the umarshal logic will ignore those keys
2321
// If the TXT record is missing a key, the field will unmarshal to the default Go value
2422

2523
type featuresRecord struct {
26-
// support_datagram_v3
27-
DatagramV3Percentage int32 `json:"dv3"`
28-
24+
// DatagramV3Percentage int32 `json:"dv3"` // Removed in TUN-9291
2925
// PostQuantumPercentage int32 `json:"pq"` // Removed in TUN-7970
3026
}
3127

3228
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)
29+
return newFeatureSelector(ctx, accountTag, logger, newDNSResolver(), cliFeatures, pq)
3430
}
3531

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.
32+
// FeatureSelector determines if this account will try new features; loaded once during startup.
3833
type FeatureSelector struct {
39-
accountHash int32
34+
accountHash uint32
4035
logger *zerolog.Logger
4136
resolver resolver
4237

4338
staticFeatures staticFeatures
4439
cliFeatures []string
4540

46-
// lock protects concurrent access to dynamic features
47-
lock sync.RWMutex
4841
features featuresRecord
4942
}
5043

51-
func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.Logger, resolver resolver, cliFeatures []string, pq bool, refreshFreq time.Duration) (*FeatureSelector, error) {
44+
func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.Logger, resolver resolver, cliFeatures []string, pq bool) (*FeatureSelector, error) {
5245
// Combine default features and user-provided features
5346
var pqMode *PostQuantumMode
5447
if pq {
@@ -64,22 +57,16 @@ func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.
6457
logger: logger,
6558
resolver: resolver,
6659
staticFeatures: staticFeatures,
67-
cliFeatures: Dedup(cliFeatures),
60+
cliFeatures: dedupAndRemoveFeatures(cliFeatures),
6861
}
6962

70-
if err := selector.refresh(ctx); err != nil {
63+
if err := selector.init(ctx); err != nil {
7164
logger.Err(err).Msg("Failed to fetch features, default to disable")
7265
}
7366

74-
go selector.refreshLoop(ctx, refreshFreq)
75-
7667
return selector, nil
7768
}
7869

79-
func (fs *FeatureSelector) accountEnabled(percentage int32) bool {
80-
return percentage > fs.accountHash
81-
}
82-
8370
func (fs *FeatureSelector) PostQuantumMode() PostQuantumMode {
8471
if fs.staticFeatures.PostQuantumMode != nil {
8572
return *fs.staticFeatures.PostQuantumMode
@@ -89,48 +76,25 @@ func (fs *FeatureSelector) PostQuantumMode() PostQuantumMode {
8976
}
9077

9178
func (fs *FeatureSelector) DatagramVersion() DatagramVersion {
92-
fs.lock.RLock()
93-
defer fs.lock.RUnlock()
94-
9579
// If user provides the feature via the cli, we take it as priority over remote feature evaluation
96-
if slices.Contains(fs.cliFeatures, FeatureDatagramV3) {
80+
if slices.Contains(fs.cliFeatures, FeatureDatagramV3_1) {
9781
return DatagramV3
9882
}
9983
// If the user specifies DatagramV2, we also take that over remote
10084
if slices.Contains(fs.cliFeatures, FeatureDatagramV2) {
10185
return DatagramV2
10286
}
10387

104-
if fs.accountEnabled(fs.features.DatagramV3Percentage) {
105-
return DatagramV3
106-
}
10788
return DatagramV2
10889
}
10990

11091
// 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.
11392
func (fs *FeatureSelector) ClientFeatures() []string {
11493
// 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())}))
94+
return dedupAndRemoveFeatures(slices.Concat(defaultFeatures, fs.cliFeatures, []string{string(fs.DatagramVersion())}))
11695
}
11796

118-
func (fs *FeatureSelector) refreshLoop(ctx context.Context, refreshFreq time.Duration) {
119-
ticker := time.NewTicker(refreshFreq)
120-
for {
121-
select {
122-
case <-ctx.Done():
123-
return
124-
case <-ticker.C:
125-
err := fs.refresh(ctx)
126-
if err != nil {
127-
fs.logger.Err(err).Msg("Failed to refresh feature selector")
128-
}
129-
}
130-
}
131-
}
132-
133-
func (fs *FeatureSelector) refresh(ctx context.Context) error {
97+
func (fs *FeatureSelector) init(ctx context.Context) error {
13498
record, err := fs.resolver.lookupRecord(ctx)
13599
if err != nil {
136100
return err
@@ -141,9 +105,6 @@ func (fs *FeatureSelector) refresh(ctx context.Context) error {
141105
return err
142106
}
143107

144-
fs.lock.Lock()
145-
defer fs.lock.Unlock()
146-
147108
fs.features = features
148109

149110
return nil
@@ -180,8 +141,8 @@ func (dr *dnsResolver) lookupRecord(ctx context.Context) ([]byte, error) {
180141
return []byte(records[0]), nil
181142
}
182143

183-
func switchThreshold(accountTag string) int32 {
144+
func switchThreshold(accountTag string) uint32 {
184145
h := fnv.New32a()
185146
_, _ = h.Write([]byte(accountTag))
186-
return int32(h.Sum32() % 100)
147+
return h.Sum32() % 100
187148
}

0 commit comments

Comments
 (0)