Skip to content

Commit 3bf9217

Browse files
committed
TUN-9319: Add dynamic loading of features to connections via ConnectionOptionsSnapshot
Make sure to enforce snapshots of features and client information for each connection so that the feature information can change in the background. This allows for new features to only be applied to a connection if it completely disconnects and attempts a reconnect. Updates the feature refresh time to 1 hour from previous cloudflared versions which refreshed every 6 hours. Closes TUN-9319
1 parent 02705c4 commit 3bf9217

File tree

14 files changed

+359
-106
lines changed

14 files changed

+359
-106
lines changed

client/config.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package client
2+
3+
import (
4+
"fmt"
5+
"net"
6+
7+
"github.com/google/uuid"
8+
"github.com/rs/zerolog"
9+
10+
"github.com/cloudflare/cloudflared/features"
11+
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
12+
)
13+
14+
// Config captures the local client runtime configuration.
15+
type Config struct {
16+
ConnectorID uuid.UUID
17+
Version string
18+
Arch string
19+
20+
featureSelector features.FeatureSelector
21+
}
22+
23+
func NewConfig(version string, arch string, featureSelector features.FeatureSelector) (*Config, error) {
24+
connectorID, err := uuid.NewRandom()
25+
if err != nil {
26+
return nil, fmt.Errorf("unable to generate a connector UUID: %w", err)
27+
}
28+
return &Config{
29+
ConnectorID: connectorID,
30+
Version: version,
31+
Arch: arch,
32+
featureSelector: featureSelector,
33+
}, nil
34+
}
35+
36+
// ConnectionOptionsSnapshot is a snapshot of the current client information used to initialize a connection.
37+
//
38+
// The FeatureSnapshot is the features that are available for this connection. At the client level they may
39+
// change, but they will not change within the scope of this struct.
40+
type ConnectionOptionsSnapshot struct {
41+
client pogs.ClientInfo
42+
originLocalIP net.IP
43+
numPreviousAttempts uint8
44+
FeatureSnapshot features.FeatureSnapshot
45+
}
46+
47+
func (c *Config) ConnectionOptionsSnapshot(originIP net.IP, previousAttempts uint8) *ConnectionOptionsSnapshot {
48+
snapshot := c.featureSelector.Snapshot()
49+
return &ConnectionOptionsSnapshot{
50+
client: pogs.ClientInfo{
51+
ClientID: c.ConnectorID[:],
52+
Version: c.Version,
53+
Arch: c.Arch,
54+
Features: snapshot.FeaturesList,
55+
},
56+
originLocalIP: originIP,
57+
numPreviousAttempts: previousAttempts,
58+
FeatureSnapshot: snapshot,
59+
}
60+
}
61+
62+
func (c ConnectionOptionsSnapshot) ConnectionOptions() *pogs.ConnectionOptions {
63+
return &pogs.ConnectionOptions{
64+
Client: c.client,
65+
OriginLocalIP: c.originLocalIP,
66+
ReplaceExisting: false,
67+
CompressionQuality: 0,
68+
NumPreviousAttempts: c.numPreviousAttempts,
69+
}
70+
}
71+
72+
func (c ConnectionOptionsSnapshot) LogFields(event *zerolog.Event) *zerolog.Event {
73+
return event.Strs("features", c.client.Features)
74+
}

client/config_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package client
2+
3+
import (
4+
"net"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/cloudflare/cloudflared/features"
10+
)
11+
12+
func TestGenerateConnectionOptions(t *testing.T) {
13+
version := "1234"
14+
arch := "linux_amd64"
15+
originIP := net.ParseIP("192.168.1.1")
16+
var previousAttempts uint8 = 4
17+
18+
config, err := NewConfig(version, arch, &mockFeatureSelector{})
19+
require.NoError(t, err)
20+
require.Equal(t, version, config.Version)
21+
require.Equal(t, arch, config.Arch)
22+
23+
// Validate ConnectionOptionsSnapshot fields
24+
connOptions := config.ConnectionOptionsSnapshot(originIP, previousAttempts)
25+
require.Equal(t, version, connOptions.client.Version)
26+
require.Equal(t, arch, connOptions.client.Arch)
27+
require.Equal(t, config.ConnectorID[:], connOptions.client.ClientID)
28+
29+
// Vaidate snapshot feature fields against the connOptions generated
30+
snapshot := config.featureSelector.Snapshot()
31+
require.Equal(t, features.DatagramV3, snapshot.DatagramVersion)
32+
require.Equal(t, features.DatagramV3, connOptions.FeatureSnapshot.DatagramVersion)
33+
34+
pogsConnOptions := connOptions.ConnectionOptions()
35+
require.Equal(t, connOptions.client, pogsConnOptions.Client)
36+
require.Equal(t, originIP, pogsConnOptions.OriginLocalIP)
37+
require.False(t, pogsConnOptions.ReplaceExisting)
38+
require.Equal(t, uint8(0), pogsConnOptions.CompressionQuality)
39+
require.Equal(t, previousAttempts, pogsConnOptions.NumPreviousAttempts)
40+
}
41+
42+
type mockFeatureSelector struct{}
43+
44+
func (m *mockFeatureSelector) Snapshot() features.FeatureSnapshot {
45+
return features.FeatureSnapshot{
46+
PostQuantum: features.PostQuantumPrefer,
47+
DatagramVersion: features.DatagramV3,
48+
FeaturesList: []string{features.FeaturePostQuantum, features.FeatureDatagramV3_1},
49+
}
50+
}

cmd/cloudflared/tunnel/cmd.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/coreos/go-systemd/v22/daemon"
1616
"github.com/facebookgo/grace/gracenet"
1717
"github.com/getsentry/sentry-go"
18-
"github.com/google/uuid"
1918
"github.com/mitchellh/go-homedir"
2019
"github.com/pkg/errors"
2120
"github.com/rs/zerolog"
@@ -446,14 +445,7 @@ func StartServer(
446445
log.Err(err).Msg("Couldn't start tunnel")
447446
return err
448447
}
449-
var clientID uuid.UUID
450-
if tunnelConfig.NamedTunnel != nil {
451-
clientID, err = uuid.FromBytes(tunnelConfig.NamedTunnel.Client.ClientID)
452-
if err != nil {
453-
// set to nil for classic tunnels
454-
clientID = uuid.Nil
455-
}
456-
}
448+
connectorID := tunnelConfig.ClientConfig.ConnectorID
457449

458450
// Disable ICMP packet routing for quick tunnels
459451
if quickTunnelURL != "" {
@@ -471,7 +463,7 @@ func StartServer(
471463
c.String("management-hostname"),
472464
c.Bool("management-diagnostics"),
473465
serviceIP,
474-
clientID,
466+
connectorID,
475467
c.String(cfdflags.ConnectorLabel),
476468
logger.ManagementLogger.Log,
477469
logger.ManagementLogger,
@@ -503,14 +495,14 @@ func StartServer(
503495
sources = append(sources, ipv6.String())
504496
}
505497

506-
readinessServer := metrics.NewReadyServer(clientID, tracker)
498+
readinessServer := metrics.NewReadyServer(connectorID, tracker)
507499
cliFlags := nonSecretCliFlags(log, c, nonSecretFlagsList)
508500
diagnosticHandler := diagnostic.NewDiagnosticHandler(
509501
log,
510502
0,
511503
diagnostic.NewSystemCollectorImpl(buildInfo.CloudflaredVersion),
512504
tunnelConfig.NamedTunnel.Credentials.TunnelID,
513-
clientID,
505+
connectorID,
514506
tracker,
515507
cliFlags,
516508
sources,

cmd/cloudflared/tunnel/configuration.go

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import (
1010
"strings"
1111
"time"
1212

13-
"github.com/google/uuid"
1413
"github.com/pkg/errors"
1514
"github.com/rs/zerolog"
1615
"github.com/urfave/cli/v2"
1716
"github.com/urfave/cli/v2/altsrc"
1817
"golang.org/x/term"
1918

19+
"github.com/cloudflare/cloudflared/client"
2020
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
2121
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
2222
"github.com/cloudflare/cloudflared/config"
@@ -125,27 +125,29 @@ func prepareTunnelConfig(
125125
observer *connection.Observer,
126126
namedTunnel *connection.TunnelProperties,
127127
) (*supervisor.TunnelConfig, *orchestration.Config, error) {
128-
clientID, err := uuid.NewRandom()
128+
transportProtocol := c.String(flags.Protocol)
129+
isPostQuantumEnforced := c.Bool(flags.PostQuantum)
130+
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, c.StringSlice(flags.Features), isPostQuantumEnforced, log)
129131
if err != nil {
130-
return nil, nil, errors.Wrap(err, "can't generate connector UUID")
132+
return nil, nil, errors.Wrap(err, "Failed to create feature selector")
131133
}
132-
log.Info().Msgf("Generated Connector ID: %s", clientID)
133-
tags, err := NewTagSliceFromCLI(c.StringSlice(flags.Tag))
134+
135+
clientConfig, err := client.NewConfig(info.Version(), info.OSArch(), featureSelector)
134136
if err != nil {
135-
log.Err(err).Msg("Tag parse failure")
136-
return nil, nil, errors.Wrap(err, "Tag parse failure")
137+
return nil, nil, err
137138
}
138-
tags = append(tags, pogs.Tag{Name: "ID", Value: clientID.String()})
139139

140-
transportProtocol := c.String(flags.Protocol)
141-
isPostQuantumEnforced := c.Bool(flags.PostQuantum)
140+
log.Info().Msgf("Generated Connector ID: %s", clientConfig.ConnectorID)
142141

143-
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, c.StringSlice(flags.Features), c.Bool(flags.PostQuantum), log)
142+
tags, err := NewTagSliceFromCLI(c.StringSlice(flags.Tag))
144143
if err != nil {
145-
return nil, nil, errors.Wrap(err, "Failed to create feature selector")
144+
log.Err(err).Msg("Tag parse failure")
145+
return nil, nil, errors.Wrap(err, "Tag parse failure")
146146
}
147-
clientFeatures := featureSelector.ClientFeatures()
148-
pqMode := featureSelector.PostQuantumMode()
147+
tags = append(tags, pogs.Tag{Name: "ID", Value: clientConfig.ConnectorID.String()})
148+
149+
clientFeatures := featureSelector.Snapshot()
150+
pqMode := clientFeatures.PostQuantum
149151
if pqMode == features.PostQuantumStrict {
150152
// Error if the user tries to force a non-quic transport protocol
151153
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
@@ -154,12 +156,6 @@ func prepareTunnelConfig(
154156
transportProtocol = connection.QUIC.String()
155157
}
156158

157-
namedTunnel.Client = pogs.ClientInfo{
158-
ClientID: clientID[:],
159-
Features: clientFeatures,
160-
Version: info.Version(),
161-
Arch: info.OSArch(),
162-
}
163159
cfg := config.GetConfiguration()
164160
ingressRules, err := ingress.ParseIngressFromConfigAndCLI(cfg, c, log)
165161
if err != nil {
@@ -224,10 +220,8 @@ func prepareTunnelConfig(
224220
}
225221

226222
tunnelConfig := &supervisor.TunnelConfig{
223+
ClientConfig: clientConfig,
227224
GracePeriod: gracePeriod,
228-
ReplaceExisting: c.Bool(flags.Force),
229-
OSArch: info.OSArch(),
230-
ClientID: clientID.String(),
231225
EdgeAddrs: c.StringSlice(flags.Edge),
232226
Region: resolvedRegion,
233227
EdgeIPVersion: edgeIPVersion,
@@ -246,7 +240,6 @@ func prepareTunnelConfig(
246240
NamedTunnel: namedTunnel,
247241
ProtocolSelector: protocolSelector,
248242
EdgeTLSConfigs: edgeTLSConfigs,
249-
FeatureSelector: featureSelector,
250243
MaxEdgeAddrRetries: uint8(c.Int(flags.MaxEdgeAddrRetries)), // nolint: gosec
251244
RPCTimeout: c.Duration(flags.RpcTimeout),
252245
WriteStreamTimeout: c.Duration(flags.WriteStreamTimeout),

connection/connection.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ type Orchestrator interface {
5757

5858
type TunnelProperties struct {
5959
Credentials Credentials
60-
Client pogs.ClientInfo
6160
QuickTunnelUrl string
6261
}
6362

connection/http2.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import (
1616
"github.com/rs/zerolog"
1717
"golang.org/x/net/http2"
1818

19+
"github.com/cloudflare/cloudflared/client"
1920
cfdflow "github.com/cloudflare/cloudflared/flow"
2021

2122
"github.com/cloudflare/cloudflared/tracing"
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 *pogs.ConnectionOptions
42+
connOptions *client.ConnectionOptionsSnapshot
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 *pogs.ConnectionOptions,
57+
connOptions *client.ConnectionOptionsSnapshot,
5858
observer *Observer,
5959
connIndex uint8,
6060
controlStreamHandler ControlStreamHandler,
@@ -118,7 +118,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
118118
var requestErr error
119119
switch connType {
120120
case TypeControlStream:
121-
requestErr = c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions, c.orchestrator)
121+
requestErr = c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions.ConnectionOptions(), c.orchestrator)
122122
if requestErr != nil {
123123
c.controlStreamErr = requestErr
124124
}

connection/http2_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/stretchr/testify/require"
2121
"golang.org/x/net/http2"
2222

23+
"github.com/cloudflare/cloudflared/client"
2324
"github.com/cloudflare/cloudflared/tracing"
2425

2526
"github.com/cloudflare/cloudflared/tunnelrpc"
@@ -51,7 +52,7 @@ func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
5152
cfdConn,
5253
// OriginProxy is set in testConfigManager
5354
testOrchestrator,
54-
&pogs.ConnectionOptions{},
55+
&client.ConnectionOptionsSnapshot{},
5556
obs,
5657
connIndex,
5758
controlStream,
@@ -74,7 +75,7 @@ func TestHTTP2ConfigurationSet(t *testing.T) {
7475
require.NoError(t, err)
7576

7677
reqBody := []byte(`{
77-
"version": 2,
78+
"version": 2,
7879
"config": {"warp-routing": {"enabled": true}, "originRequest" : {"connectTimeout": 10}, "ingress" : [ {"hostname": "test", "service": "https://localhost:8000" } , {"service": "http_status:404"} ]}}
7980
`)
8081
reader := bytes.NewReader(reqBody)

connection/quic_connection.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/rs/zerolog"
1818
"golang.org/x/sync/errgroup"
1919

20+
"github.com/cloudflare/cloudflared/client"
2021
cfdflow "github.com/cloudflare/cloudflared/flow"
2122

2223
cfdquic "github.com/cloudflare/cloudflared/quic"
@@ -43,7 +44,7 @@ type quicConnection struct {
4344
orchestrator Orchestrator
4445
datagramHandler DatagramSessionHandler
4546
controlStreamHandler ControlStreamHandler
46-
connOptions *pogs.ConnectionOptions
47+
connOptions *client.ConnectionOptionsSnapshot
4748
connIndex uint8
4849

4950
rpcTimeout time.Duration
@@ -59,7 +60,7 @@ func NewTunnelConnection(
5960
orchestrator Orchestrator,
6061
datagramSessionHandler DatagramSessionHandler,
6162
controlStreamHandler ControlStreamHandler,
62-
connOptions *pogs.ConnectionOptions,
63+
connOptions *client.ConnectionOptionsSnapshot,
6364
rpcTimeout time.Duration,
6465
streamWriteTimeout time.Duration,
6566
gracePeriod time.Duration,
@@ -130,7 +131,7 @@ func (q *quicConnection) Serve(ctx context.Context) error {
130131

131132
// serveControlStream will serve the RPC; blocking until the control plane is done.
132133
func (q *quicConnection) serveControlStream(ctx context.Context, controlStream quic.Stream) error {
133-
return q.controlStreamHandler.ServeControlStream(ctx, controlStream, q.connOptions, q.orchestrator)
134+
return q.controlStreamHandler.ServeControlStream(ctx, controlStream, q.connOptions.ConnectionOptions(), q.orchestrator)
134135
}
135136

136137
// Close the connection with no errors specified.

connection/quic_connection_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/stretchr/testify/require"
3030
"golang.org/x/net/nettest"
3131

32+
"github.com/cloudflare/cloudflared/client"
3233
cfdflow "github.com/cloudflare/cloudflared/flow"
3334

3435
"github.com/cloudflare/cloudflared/datagramsession"
@@ -843,7 +844,7 @@ func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8)
843844
&mockOrchestrator{originProxy: &mockOriginProxyWithRequest{}},
844845
datagramConn,
845846
fakeControlStream{},
846-
&pogs.ConnectionOptions{},
847+
&client.ConnectionOptionsSnapshot{},
847848
15*time.Second,
848849
0*time.Second,
849850
0*time.Second,

0 commit comments

Comments
 (0)