Skip to content

Commit 3b473bc

Browse files
committed
chore: add DRPC tailnet & cli implementation
1 parent fb9d5aa commit 3b473bc

File tree

14 files changed

+500
-71
lines changed

14 files changed

+500
-71
lines changed

cli/ping.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ func (r *RootCmd) ping() *serpent.Command {
5858
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
5959
opts.BlockEndpoints = true
6060
}
61+
if r.noNetworkTelemetry {
62+
opts.EnableTelemetry = true
63+
}
6164
conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts)
6265
if err != nil {
6366
return err

cli/portforward.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ func (r *RootCmd) portForward() *serpent.Command {
106106
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
107107
opts.BlockEndpoints = true
108108
}
109+
if r.noNetworkTelemetry {
110+
opts.EnableTelemetry = true
111+
}
109112
conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts)
110113
if err != nil {
111114
return err

cli/root.go

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,21 @@ var (
5252
)
5353

5454
const (
55-
varURL = "url"
56-
varToken = "token"
57-
varAgentToken = "agent-token"
58-
varAgentTokenFile = "agent-token-file"
59-
varAgentURL = "agent-url"
60-
varHeader = "header"
61-
varHeaderCommand = "header-command"
62-
varNoOpen = "no-open"
63-
varNoVersionCheck = "no-version-warning"
64-
varNoFeatureWarning = "no-feature-warning"
65-
varForceTty = "force-tty"
66-
varVerbose = "verbose"
67-
varOrganizationSelect = "organization"
68-
varDisableDirect = "disable-direct-connections"
55+
varURL = "url"
56+
varToken = "token"
57+
varAgentToken = "agent-token"
58+
varAgentTokenFile = "agent-token-file"
59+
varAgentURL = "agent-url"
60+
varHeader = "header"
61+
varHeaderCommand = "header-command"
62+
varNoOpen = "no-open"
63+
varNoVersionCheck = "no-version-warning"
64+
varNoFeatureWarning = "no-feature-warning"
65+
varForceTty = "force-tty"
66+
varVerbose = "verbose"
67+
varOrganizationSelect = "organization"
68+
varDisableDirect = "disable-direct-connections"
69+
varDisableNetworkTelemetry = "disable-network-telemetry"
6970

7071
notLoggedInMessage = "You are not logged in. Try logging in using 'coder login <url>'."
7172

@@ -436,6 +437,13 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err
436437
Value: serpent.BoolOf(&r.disableDirect),
437438
Group: globalGroup,
438439
},
440+
{
441+
Flag: varDisableNetworkTelemetry,
442+
Env: "CODER_DISABLE_NETWORK_TELEMETRY",
443+
Description: "Disable network telemetry.",
444+
Value: serpent.BoolOf(&r.noNetworkTelemetry),
445+
Group: globalGroup,
446+
},
439447
{
440448
Flag: "debug-http",
441449
Description: "Debug codersdk HTTP requests.",
@@ -491,6 +499,7 @@ type RootCmd struct {
491499
versionFlag bool
492500
disableDirect bool
493501
debugHTTP bool
502+
noNetworkTelemetry bool
494503

495504
noVersionCheck bool
496505
noFeatureWarning bool

cli/speedtest.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ func (r *RootCmd) speedtest() *serpent.Command {
102102
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
103103
opts.BlockEndpoints = true
104104
}
105+
if r.noNetworkTelemetry {
106+
opts.EnableTelemetry = true
107+
}
105108
if pcapFile != "" {
106109
s := capture.New()
107110
opts.CaptureHook = s.LogPacket

cli/ssh.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,9 @@ func (r *RootCmd) ssh() *serpent.Command {
243243
}
244244
conn, err := workspacesdk.New(client).
245245
DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{
246-
Logger: logger,
247-
BlockEndpoints: r.disableDirect,
246+
Logger: logger,
247+
BlockEndpoints: r.disableDirect,
248+
EnableTelemetry: !r.noNetworkTelemetry,
248249
})
249250
if err != nil {
250251
return xerrors.Errorf("dial agent: %w", err)

cli/testdata/coder_--help.golden

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ variables or flags.
6666
--disable-direct-connections bool, $CODER_DISABLE_DIRECT_CONNECTIONS
6767
Disable direct (P2P) connections to workspaces.
6868

69+
--disable-network-telemetry bool, $CODER_DISABLE_NETWORK_TELEMETRY
70+
Disable network telemetry.
71+
6972
--global-config string, $CODER_CONFIG_DIR (default: ~/.config/coderv2)
7073
Path to the global `coder` config directory.
7174

codersdk/workspacesdk/connector.go

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -58,32 +58,28 @@ type tailnetAPIConnector struct {
5858
coordinateURL string
5959
dialOptions *websocket.DialOptions
6060
conn tailnetConn
61+
customDialFn func() (proto.DRPCTailnetClient, error)
62+
63+
clientMu sync.RWMutex
64+
client proto.DRPCTailnetClient
6165

6266
connected chan error
6367
isFirst bool
6468
closed chan struct{}
6569
}
6670

67-
// runTailnetAPIConnector creates and runs a tailnetAPIConnector
68-
func runTailnetAPIConnector(
69-
ctx context.Context, logger slog.Logger,
70-
agentID uuid.UUID, coordinateURL string, dialOptions *websocket.DialOptions,
71-
conn tailnetConn,
72-
) *tailnetAPIConnector {
73-
tac := &tailnetAPIConnector{
71+
// Create a new tailnetAPIConnector without running it
72+
func newTailnetAPIConnector(ctx context.Context, logger slog.Logger, agentID uuid.UUID, coordinateURL string, dialOptions *websocket.DialOptions) *tailnetAPIConnector {
73+
return &tailnetAPIConnector{
7474
ctx: ctx,
7575
logger: logger,
7676
agentID: agentID,
7777
coordinateURL: coordinateURL,
7878
dialOptions: dialOptions,
79-
conn: conn,
79+
conn: nil,
8080
connected: make(chan error, 1),
8181
closed: make(chan struct{}),
8282
}
83-
tac.gracefulCtx, tac.cancelGracefulCtx = context.WithCancel(context.Background())
84-
go tac.manageGracefulTimeout()
85-
go tac.run()
86-
return tac
8783
}
8884

8985
// manageGracefulTimeout allows the gracefulContext to last 1 second longer than the main context
@@ -99,21 +95,27 @@ func (tac *tailnetAPIConnector) manageGracefulTimeout() {
9995
}
10096
}
10197

102-
func (tac *tailnetAPIConnector) run() {
103-
tac.isFirst = true
104-
defer close(tac.closed)
105-
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); {
106-
tailnetClient, err := tac.dial()
107-
if xerrors.Is(err, &codersdk.Error{}) {
108-
return
109-
}
110-
if err != nil {
111-
continue
98+
// Runs a tailnetAPIConnector using the provided connection
99+
func (tac *tailnetAPIConnector) runConnector(conn tailnetConn) {
100+
tac.conn = conn
101+
tac.gracefulCtx, tac.cancelGracefulCtx = context.WithCancel(context.Background())
102+
go tac.manageGracefulTimeout()
103+
go func() {
104+
tac.isFirst = true
105+
defer close(tac.closed)
106+
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(tac.ctx); {
107+
tailnetClient, err := tac.dial()
108+
if err != nil {
109+
continue
110+
}
111+
tac.clientMu.Lock()
112+
tac.client = tailnetClient
113+
tac.clientMu.Unlock()
114+
tac.logger.Debug(tac.ctx, "obtained tailnet API v2+ client")
115+
tac.coordinateAndDERPMap(tailnetClient)
116+
tac.logger.Debug(tac.ctx, "tailnet API v2+ connection lost")
112117
}
113-
tac.logger.Debug(tac.ctx, "obtained tailnet API v2+ client")
114-
tac.coordinateAndDERPMap(tailnetClient)
115-
tac.logger.Debug(tac.ctx, "tailnet API v2+ connection lost")
116-
}
118+
}()
117119
}
118120

119121
var permanentErrorStatuses = []int{
@@ -123,6 +125,10 @@ var permanentErrorStatuses = []int{
123125
}
124126

125127
func (tac *tailnetAPIConnector) dial() (proto.DRPCTailnetClient, error) {
128+
if tac.customDialFn != nil {
129+
return tac.customDialFn()
130+
}
131+
126132
tac.logger.Debug(tac.ctx, "dialing Coder tailnet v2+ API")
127133
// nolint:bodyclose
128134
ws, res, err := websocket.Dial(tac.ctx, tac.coordinateURL, tac.dialOptions)
@@ -194,7 +200,10 @@ func (tac *tailnetAPIConnector) coordinateAndDERPMap(client proto.DRPCTailnetCli
194200
// we do NOT want to gracefully disconnect on the coordinate() routine. So, we'll just
195201
// close the underlying connection. This will trigger a retry of the control plane in
196202
// run().
203+
tac.clientMu.Lock()
197204
client.DRPCConn().Close()
205+
tac.client = nil
206+
tac.clientMu.Unlock()
198207
// Note that derpMap() logs it own errors, we don't bother here.
199208
}
200209
}()
@@ -258,3 +267,18 @@ func (tac *tailnetAPIConnector) derpMap(client proto.DRPCTailnetClient) error {
258267
tac.conn.SetDERPMap(dm)
259268
}
260269
}
270+
271+
func (tac *tailnetAPIConnector) SendTelemetryEvent(event *proto.TelemetryEvent) {
272+
tac.clientMu.RLock()
273+
// We hold the lock for the entire telemetry request, but this would only block
274+
// a coordinate retry, and closing the connection.
275+
defer tac.clientMu.RUnlock()
276+
if tac.client == nil {
277+
return
278+
}
279+
ctx, cancel := context.WithTimeout(tac.ctx, 5*time.Second)
280+
defer cancel()
281+
_, _ = tac.client.PostTelemetry(ctx, &proto.TelemetryRequest{
282+
Events: []*proto.TelemetryEvent{event},
283+
})
284+
}

codersdk/workspacesdk/connector_internal_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ func TestTailnetAPIConnector_Disconnects(t *testing.T) {
7272

7373
fConn := newFakeTailnetConn()
7474

75-
uut := runTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{}, fConn)
75+
uut := newTailnetAPIConnector(ctx, logger, agentID, svr.URL, &websocket.DialOptions{})
76+
uut.runConnector(fConn)
7677

7778
call := testutil.RequireRecvCtx(ctx, t, fCoord.CoordinateCalls)
7879
reqTun := testutil.RequireRecvCtx(ctx, t, call.Reqs)

codersdk/workspacesdk/workspacesdk.go

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ type DialAgentOptions struct {
181181
// CaptureHook is a callback that captures Disco packets and packets sent
182182
// into the tailnet tunnel.
183183
CaptureHook capture.Callback
184+
// Whether the client will send network telemetry events
185+
// Enable instead of Disable so it's initialized to false
186+
EnableTelemetry bool
184187
}
185188

186189
func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options *DialAgentOptions) (agentConn *AgentConn, err error) {
@@ -196,29 +199,6 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options *
196199
options.BlockEndpoints = true
197200
}
198201

199-
ip := tailnet.IP()
200-
var header http.Header
201-
if headerTransport, ok := c.client.HTTPClient.Transport.(*codersdk.HeaderTransport); ok {
202-
header = headerTransport.Header
203-
}
204-
conn, err := tailnet.NewConn(&tailnet.Options{
205-
Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)},
206-
DERPMap: connInfo.DERPMap,
207-
DERPHeader: &header,
208-
DERPForceWebSockets: connInfo.DERPForceWebSockets,
209-
Logger: options.Logger,
210-
BlockEndpoints: c.client.DisableDirectConnections || options.BlockEndpoints,
211-
CaptureHook: options.CaptureHook,
212-
})
213-
if err != nil {
214-
return nil, xerrors.Errorf("create tailnet: %w", err)
215-
}
216-
defer func() {
217-
if err != nil {
218-
_ = conn.Close()
219-
}
220-
}()
221-
222202
headers := make(http.Header)
223203
tokenHeader := codersdk.SessionTokenHeader
224204
if c.client.SessionTokenHeader != "" {
@@ -251,16 +231,43 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options *
251231
q.Add("version", "2.0")
252232
coordinateURL.RawQuery = q.Encode()
253233

254-
connector := runTailnetAPIConnector(ctx, options.Logger,
255-
agentID, coordinateURL.String(),
234+
connector := newTailnetAPIConnector(ctx, options.Logger, agentID, coordinateURL.String(),
256235
&websocket.DialOptions{
257236
HTTPClient: c.client.HTTPClient,
258237
HTTPHeader: headers,
259238
// Need to disable compression to avoid a data-race.
260239
CompressionMode: websocket.CompressionDisabled,
261-
},
262-
conn,
263-
)
240+
})
241+
242+
ip := tailnet.IP()
243+
var header http.Header
244+
if headerTransport, ok := c.client.HTTPClient.Transport.(*codersdk.HeaderTransport); ok {
245+
header = headerTransport.Header
246+
}
247+
var telemetrySink tailnet.TelemetrySink
248+
if options.EnableTelemetry {
249+
telemetrySink = connector
250+
}
251+
conn, err := tailnet.NewConn(&tailnet.Options{
252+
Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)},
253+
DERPMap: connInfo.DERPMap,
254+
DERPHeader: &header,
255+
DERPForceWebSockets: connInfo.DERPForceWebSockets,
256+
Logger: options.Logger,
257+
BlockEndpoints: c.client.DisableDirectConnections || options.BlockEndpoints,
258+
CaptureHook: options.CaptureHook,
259+
TelemetrySink: telemetrySink,
260+
})
261+
if err != nil {
262+
return nil, xerrors.Errorf("create tailnet: %w", err)
263+
}
264+
defer func() {
265+
if err != nil {
266+
_ = conn.Close()
267+
}
268+
}()
269+
connector.runConnector(conn)
270+
264271
options.Logger.Debug(ctx, "running tailnet API v2+ connector")
265272

266273
select {

docs/cli.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ Enable verbose output.
149149

150150
Disable direct (P2P) connections to workspaces.
151151

152+
### --disable-network-telemetry
153+
154+
| | |
155+
| ----------- | --------------------------------------------- |
156+
| Type | <code>bool</code> |
157+
| Environment | <code>$CODER_DISABLE_NETWORK_TELEMETRY</code> |
158+
159+
Disable network telemetry.
160+
152161
### --global-config
153162

154163
| | |

enterprise/cli/testdata/coder_--help.golden

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ variables or flags.
3030
--disable-direct-connections bool, $CODER_DISABLE_DIRECT_CONNECTIONS
3131
Disable direct (P2P) connections to workspaces.
3232

33+
--disable-network-telemetry bool, $CODER_DISABLE_NETWORK_TELEMETRY
34+
Disable network telemetry.
35+
3336
--global-config string, $CODER_CONFIG_DIR (default: ~/.config/coderv2)
3437
Path to the global `coder` config directory.
3538

0 commit comments

Comments
 (0)