From 117e4c2fe7e16cda9917d817c20887d09fe8fceb Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Tue, 25 Mar 2025 15:26:05 +0400 Subject: [PATCH] feat: adds device_id, device_os, and coder_desktop_version to telemetry (#17086) Records the Device ID, Device OS and Coder Desktop version to telemetry. These values are provided by the Coder Desktop client in the StartRequest method of the VPN protocol. We render them as an HTTP header to transmit to Coderd, where they are decoded and added to telemetry. --- coderd/workspaceagents.go | 30 ++++++ coderd/workspaceagents_test.go | 170 +++++++++++++++++++++++++------ codersdk/client.go | 26 +++++ codersdk/client_internal_test.go | 1 + site/src/api/typesGenerated.ts | 3 + vpn/speaker_internal_test.go | 15 +-- vpn/tunnel.go | 22 +++- vpn/tunnel_internal_test.go | 6 +- vpn/version.go | 6 +- vpn/vpn.pb.go | 66 +++++++++--- vpn/vpn.proto | 6 ++ 11 files changed, 292 insertions(+), 59 deletions(-) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index a06cf96ea8..b8b71b3302 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1652,6 +1652,8 @@ func (api *API) tailnetRPCConn(rw http.ResponseWriter, r *http.Request) { DeviceOS: nil, CoderDesktopVersion: nil, } + + fillCoderDesktopTelemetry(r, &connectionTelemetryEvent, api.Logger) api.Telemetry.Report(&telemetry.Snapshot{ UserTailnetConnections: []telemetry.UserTailnetConnection{connectionTelemetryEvent}, }) @@ -1681,6 +1683,34 @@ func (api *API) tailnetRPCConn(rw http.ResponseWriter, r *http.Request) { } } +// fillCoderDesktopTelemetry fills out the provided event based on a Coder Desktop telemetry header on the request, if +// present. +func fillCoderDesktopTelemetry(r *http.Request, event *telemetry.UserTailnetConnection, logger slog.Logger) { + // Parse desktop telemetry from header if it exists + desktopTelemetryHeader := r.Header.Get(codersdk.CoderDesktopTelemetryHeader) + if desktopTelemetryHeader != "" { + var telemetryData codersdk.CoderDesktopTelemetry + if err := telemetryData.FromHeader(desktopTelemetryHeader); err == nil { + // Only set fields if they aren't empty + if telemetryData.DeviceID != "" { + event.DeviceID = &telemetryData.DeviceID + } + if telemetryData.DeviceOS != "" { + event.DeviceOS = &telemetryData.DeviceOS + } + if telemetryData.CoderDesktopVersion != "" { + event.CoderDesktopVersion = &telemetryData.CoderDesktopVersion + } + logger.Debug(r.Context(), "received desktop telemetry", + slog.F("device_id", telemetryData.DeviceID), + slog.F("device_os", telemetryData.DeviceOS), + slog.F("desktop_version", telemetryData.CoderDesktopVersion)) + } else { + logger.Warn(r.Context(), "failed to parse desktop telemetry header", slog.Error(err)) + } + } +} + // createExternalAuthResponse creates an ExternalAuthResponse based on the // provider type. This is to support legacy `/workspaceagents/me/gitauth` // which uses `Username` and `Password`. diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 899708ce1f..c4519f731b 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -51,6 +51,7 @@ import ( "github.com/coder/coder/v2/coderd/jwtutils" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/telemetry" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" @@ -2135,12 +2136,8 @@ func TestOwnedWorkspacesCoordinate(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) logger := testutil.Logger(t) - - fTelemetry := newFakeTelemetryReporter(ctx, t, 200) - fTelemetry.enabled = false firstClient, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ - Coordinator: tailnet.NewCoordinator(logger), - TelemetryReporter: fTelemetry, + Coordinator: tailnet.NewCoordinator(logger), }) firstUser := coderdtest.CreateFirstUser(t, firstClient) member, memberUser := coderdtest.CreateAnotherUser(t, firstClient, firstUser.OrganizationID, rbac.RoleTemplateAdmin()) @@ -2148,17 +2145,12 @@ func TestOwnedWorkspacesCoordinate(t *testing.T) { // Create a workspace with an agent firstWorkspace := buildWorkspaceWithAgent(t, member, firstUser.OrganizationID, memberUser.ID, api.Database, api.Pubsub) - // enable telemetry now that workspace is built; we don't care about snapshots before this. - fTelemetry.enabled = true - u, err := member.URL.Parse("/api/v2/tailnet") require.NoError(t, err) q := u.Query() q.Set("version", "2.0") u.RawQuery = q.Encode() - predialTime := time.Now() - //nolint:bodyclose // websocket package closes this for you wsConn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ HTTPHeader: http.Header{ @@ -2173,15 +2165,6 @@ func TestOwnedWorkspacesCoordinate(t *testing.T) { } defer wsConn.Close(websocket.StatusNormalClosure, "done") - // Check telemetry - snapshot := testutil.RequireRecvCtx(ctx, t, fTelemetry.snapshots) - require.Len(t, snapshot.UserTailnetConnections, 1) - telemetryConnection := snapshot.UserTailnetConnections[0] - require.Equal(t, memberUser.ID.String(), telemetryConnection.UserID) - require.GreaterOrEqual(t, telemetryConnection.ConnectedAt, predialTime) - require.LessOrEqual(t, telemetryConnection.ConnectedAt, time.Now()) - require.NotEmpty(t, telemetryConnection.PeerID) - rpcClient, err := tailnet.NewDRPCClient( websocket.NetConn(ctx, wsConn, websocket.MessageBinary), logger, @@ -2229,23 +2212,135 @@ func TestOwnedWorkspacesCoordinate(t *testing.T) { NumAgents: 0, }, }) - err = stream.Close() +} + +func TestUserTailnetTelemetry(t *testing.T) { + t.Parallel() + + telemetryData := &codersdk.CoderDesktopTelemetry{ + DeviceOS: "Windows", + DeviceID: "device001", + CoderDesktopVersion: "0.22.1", + } + fullHeader, err := json.Marshal(telemetryData) require.NoError(t, err) - beforeDisconnectTime := time.Now() - err = wsConn.Close(websocket.StatusNormalClosure, "done") - require.NoError(t, err) + testCases := []struct { + name string + headers map[string]string + // only used for DeviceID, DeviceOS, CoderDesktopVersion + expected telemetry.UserTailnetConnection + }{ + { + name: "no header", + headers: map[string]string{}, + expected: telemetry.UserTailnetConnection{}, + }, + { + name: "full header", + headers: map[string]string{ + codersdk.CoderDesktopTelemetryHeader: string(fullHeader), + }, + expected: telemetry.UserTailnetConnection{ + DeviceOS: ptr.Ref("Windows"), + DeviceID: ptr.Ref("device001"), + CoderDesktopVersion: ptr.Ref("0.22.1"), + }, + }, + { + name: "empty header", + headers: map[string]string{ + codersdk.CoderDesktopTelemetryHeader: "", + }, + expected: telemetry.UserTailnetConnection{}, + }, + { + name: "invalid header", + headers: map[string]string{ + codersdk.CoderDesktopTelemetryHeader: "{\"device_os", + }, + expected: telemetry.UserTailnetConnection{}, + }, + } - snapshot = testutil.RequireRecvCtx(ctx, t, fTelemetry.snapshots) - require.Len(t, snapshot.UserTailnetConnections, 1) - telemetryDisconnection := snapshot.UserTailnetConnections[0] - require.Equal(t, memberUser.ID.String(), telemetryDisconnection.UserID) - require.Equal(t, telemetryConnection.ConnectedAt, telemetryDisconnection.ConnectedAt) - require.Equal(t, telemetryConnection.UserID, telemetryDisconnection.UserID) - require.Equal(t, telemetryConnection.PeerID, telemetryDisconnection.PeerID) - require.NotNil(t, telemetryDisconnection.DisconnectedAt) - require.GreaterOrEqual(t, *telemetryDisconnection.DisconnectedAt, beforeDisconnectTime) - require.LessOrEqual(t, *telemetryDisconnection.DisconnectedAt, time.Now()) + // nolint: paralleltest // no longer need to reinitialize loop vars in go 1.22 + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + logger := testutil.Logger(t) + + fTelemetry := newFakeTelemetryReporter(ctx, t, 200) + fTelemetry.enabled = false + firstClient := coderdtest.New(t, &coderdtest.Options{ + Logger: &logger, + TelemetryReporter: fTelemetry, + }) + firstUser := coderdtest.CreateFirstUser(t, firstClient) + member, memberUser := coderdtest.CreateAnotherUser(t, firstClient, firstUser.OrganizationID, rbac.RoleTemplateAdmin()) + + headers := http.Header{ + "Coder-Session-Token": []string{member.SessionToken()}, + } + for k, v := range tc.headers { + headers.Add(k, v) + } + + // enable telemetry now that user is created. + fTelemetry.enabled = true + + u, err := member.URL.Parse("/api/v2/tailnet") + require.NoError(t, err) + q := u.Query() + q.Set("version", "2.0") + u.RawQuery = q.Encode() + + predialTime := time.Now() + + //nolint:bodyclose // websocket package closes this for you + wsConn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ + HTTPHeader: headers, + }) + if err != nil { + if resp != nil && resp.StatusCode != http.StatusSwitchingProtocols { + err = codersdk.ReadBodyAsError(resp) + } + require.NoError(t, err) + } + defer wsConn.Close(websocket.StatusNormalClosure, "done") + + // Check telemetry + snapshot := testutil.RequireRecvCtx(ctx, t, fTelemetry.snapshots) + require.Len(t, snapshot.UserTailnetConnections, 1) + telemetryConnection := snapshot.UserTailnetConnections[0] + require.Equal(t, memberUser.ID.String(), telemetryConnection.UserID) + require.GreaterOrEqual(t, telemetryConnection.ConnectedAt, predialTime) + require.LessOrEqual(t, telemetryConnection.ConnectedAt, time.Now()) + require.NotEmpty(t, telemetryConnection.PeerID) + requireEqualOrBothNil(t, telemetryConnection.DeviceID, tc.expected.DeviceID) + requireEqualOrBothNil(t, telemetryConnection.DeviceOS, tc.expected.DeviceOS) + requireEqualOrBothNil(t, telemetryConnection.CoderDesktopVersion, tc.expected.CoderDesktopVersion) + + beforeDisconnectTime := time.Now() + err = wsConn.Close(websocket.StatusNormalClosure, "done") + require.NoError(t, err) + + snapshot = testutil.RequireRecvCtx(ctx, t, fTelemetry.snapshots) + require.Len(t, snapshot.UserTailnetConnections, 1) + telemetryDisconnection := snapshot.UserTailnetConnections[0] + require.Equal(t, memberUser.ID.String(), telemetryDisconnection.UserID) + require.Equal(t, telemetryConnection.ConnectedAt, telemetryDisconnection.ConnectedAt) + require.Equal(t, telemetryConnection.UserID, telemetryDisconnection.UserID) + require.Equal(t, telemetryConnection.PeerID, telemetryDisconnection.PeerID) + require.NotNil(t, telemetryDisconnection.DisconnectedAt) + require.GreaterOrEqual(t, *telemetryDisconnection.DisconnectedAt, beforeDisconnectTime) + require.LessOrEqual(t, *telemetryDisconnection.DisconnectedAt, time.Now()) + requireEqualOrBothNil(t, telemetryConnection.DeviceID, tc.expected.DeviceID) + requireEqualOrBothNil(t, telemetryConnection.DeviceOS, tc.expected.DeviceOS) + requireEqualOrBothNil(t, telemetryConnection.CoderDesktopVersion, tc.expected.CoderDesktopVersion) + }) + } } func buildWorkspaceWithAgent( @@ -2414,3 +2509,12 @@ func (f *fakeTelemetryReporter) Enabled() bool { // Close implements the telemetry.Reporter interface. func (*fakeTelemetryReporter) Close() {} + +func requireEqualOrBothNil[T any](t testing.TB, a, b *T) { + t.Helper() + if a != nil && b != nil { + require.Equal(t, *a, *b) + return + } + require.Equal(t, a, b) +} diff --git a/codersdk/client.go b/codersdk/client.go index d267355d37..8a341ee742 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -76,6 +76,10 @@ const ( // only. CLITelemetryHeader = "Coder-CLI-Telemetry" + // CoderDesktopTelemetryHeader contains a JSON-encoded representation of Desktop telemetry + // fields, including device ID, OS, and Desktop version. + CoderDesktopTelemetryHeader = "Coder-Desktop-Telemetry" + // ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK" @@ -523,6 +527,28 @@ func (e ValidationError) Error() string { var _ error = (*ValidationError)(nil) +// CoderDesktopTelemetry represents the telemetry data sent from Coder Desktop clients. +// @typescript-ignore CoderDesktopTelemetry +type CoderDesktopTelemetry struct { + DeviceID string `json:"device_id"` + DeviceOS string `json:"device_os"` + CoderDesktopVersion string `json:"coder_desktop_version"` +} + +// FromHeader parses the desktop telemetry from the provided header value. +// Returns nil if the header is empty or if parsing fails. +func (t *CoderDesktopTelemetry) FromHeader(headerValue string) error { + if headerValue == "" { + return nil + } + return json.Unmarshal([]byte(headerValue), t) +} + +// IsEmpty returns true if all fields in the telemetry data are empty. +func (t *CoderDesktopTelemetry) IsEmpty() bool { + return t.DeviceID == "" && t.DeviceOS == "" && t.CoderDesktopVersion == "" +} + // IsConnectionError is a convenience function for checking if the source of an // error is due to a 'connection refused', 'no such host', etc. func IsConnectionError(err error) bool { diff --git a/codersdk/client_internal_test.go b/codersdk/client_internal_test.go index 9093c27778..0650c3c320 100644 --- a/codersdk/client_internal_test.go +++ b/codersdk/client_internal_test.go @@ -27,6 +27,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" + "github.com/coder/coder/v2/testutil" ) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 1e9b471ad4..01ed0c919a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -290,6 +290,9 @@ export interface ChangePasswordWithOneTimePasscodeRequest { readonly one_time_passcode: string; } +// From codersdk/client.go +export const CoderDesktopTelemetryHeader = "Coder-Desktop-Telemetry"; + // From codersdk/insights.go export interface ConnectionLatency { readonly p50: number; diff --git a/vpn/speaker_internal_test.go b/vpn/speaker_internal_test.go index 5985043307..9ec795bc03 100644 --- a/vpn/speaker_internal_test.go +++ b/vpn/speaker_internal_test.go @@ -15,6 +15,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/testutil" ) @@ -47,7 +48,7 @@ func TestSpeaker_RawPeer(t *testing.T) { errCh <- err }() - expectedHandshake := "codervpn tunnel 1.0\n" + expectedHandshake := "codervpn tunnel 1.1\n" b := make([]byte, 256) n, err := mp.Read(b) @@ -155,7 +156,7 @@ func TestSpeaker_OversizeHandshake(t *testing.T) { errCh <- err }() - expectedHandshake := "codervpn tunnel 1.0\n" + expectedHandshake := "codervpn tunnel 1.1\n" b := make([]byte, 256) n, err := mp.Read(b) @@ -177,12 +178,12 @@ func TestSpeaker_HandshakeInvalid(t *testing.T) { for _, tc := range []struct { name, handshake string }{ - {name: "preamble", handshake: "ssh manager 1.0\n"}, + {name: "preamble", handshake: "ssh manager 1.1\n"}, {name: "2components", handshake: "ssh manager\n"}, {name: "newmajors", handshake: "codervpn manager 2.0,3.0\n"}, {name: "0version", handshake: "codervpn 0.1 manager\n"}, - {name: "unknown_role", handshake: "codervpn 1.0 supervisor\n"}, - {name: "unexpected_role", handshake: "codervpn 1.0 tunnel\n"}, + {name: "unknown_role", handshake: "codervpn 1.1 supervisor\n"}, + {name: "unexpected_role", handshake: "codervpn 1.1 tunnel\n"}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -208,7 +209,7 @@ func TestSpeaker_HandshakeInvalid(t *testing.T) { _, err = mp.Write([]byte(tc.handshake)) require.NoError(t, err) - expectedHandshake := "codervpn tunnel 1.0\n" + expectedHandshake := "codervpn tunnel 1.1\n" b := make([]byte, 256) n, err := mp.Read(b) require.NoError(t, err) @@ -246,7 +247,7 @@ func TestSpeaker_CorruptMessage(t *testing.T) { errCh <- err }() - expectedHandshake := "codervpn tunnel 1.0\n" + expectedHandshake := "codervpn tunnel 1.1\n" b := make([]byte, 256) n, err := mp.Read(b) diff --git a/vpn/tunnel.go b/vpn/tunnel.go index e40732ae10..611e7189f4 100644 --- a/vpn/tunnel.go +++ b/vpn/tunnel.go @@ -26,8 +26,10 @@ import ( "tailscale.com/wgengine/router" "cdr.dev/slog" - "github.com/coder/coder/v2/tailnet" "github.com/coder/quartz" + + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/tailnet" ) // netStatusInterval is the interval at which the tunnel sends network status updates to the manager. @@ -236,6 +238,24 @@ func (t *Tunnel) start(req *StartRequest) error { for _, h := range req.GetHeaders() { header.Add(h.GetName(), h.GetValue()) } + + // Add desktop telemetry if any fields are provided + telemetryData := codersdk.CoderDesktopTelemetry{ + DeviceID: req.GetDeviceId(), + DeviceOS: req.GetDeviceOs(), + CoderDesktopVersion: req.GetCoderDesktopVersion(), + } + if !telemetryData.IsEmpty() { + headerValue, err := json.Marshal(telemetryData) + if err == nil { + header.Set(codersdk.CoderDesktopTelemetryHeader, string(headerValue)) + t.logger.Debug(t.ctx, "added desktop telemetry header", + slog.F("data", telemetryData)) + } else { + t.logger.Warn(t.ctx, "failed to marshal telemetry data") + } + } + var networkingStack NetworkStack if t.networkingStackFn != nil { networkingStack, err = t.networkingStackFn(t, req, t.clientLogger) diff --git a/vpn/tunnel_internal_test.go b/vpn/tunnel_internal_test.go index 6cd18085ab..3689bd37ac 100644 --- a/vpn/tunnel_internal_test.go +++ b/vpn/tunnel_internal_test.go @@ -16,10 +16,11 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "tailscale.com/util/dnsname" + "github.com/coder/quartz" + "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/coder/v2/testutil" - "github.com/coder/quartz" ) func newFakeClient(ctx context.Context, t *testing.T) *fakeClient { @@ -103,6 +104,9 @@ func TestTunnel_StartStop(t *testing.T) { Headers: []*StartRequest_Header{ {Name: "X-Test-Header", Value: "test"}, }, + DeviceOs: "macOS", + DeviceId: "device001", + CoderDesktopVersion: "0.24.8", }, }, }) diff --git a/vpn/version.go b/vpn/version.go index 1962dc36d4..91aac9175f 100644 --- a/vpn/version.go +++ b/vpn/version.go @@ -12,7 +12,11 @@ import ( // implementation of the VPN RPC protocol. var CurrentSupportedVersions = RPCVersionList{ Versions: []RPCVersion{ - {Major: 1, Minor: 0}, + // 1.1 adds telemetry fields to StartRequest: + // - device_id: Coder Desktop device ID + // - device_os: Coder Desktop OS information + // - coder_desktop_version: Coder Desktop version + {Major: 1, Minor: 1}, }, } diff --git a/vpn/vpn.pb.go b/vpn/vpn.pb.go index 863f11bba0..db9b8ddd4f 100644 --- a/vpn/vpn.pb.go +++ b/vpn/vpn.pb.go @@ -957,6 +957,12 @@ type StartRequest struct { CoderUrl string `protobuf:"bytes,2,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"` ApiToken string `protobuf:"bytes,3,opt,name=api_token,json=apiToken,proto3" json:"api_token,omitempty"` Headers []*StartRequest_Header `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty"` + // Device ID from Coder Desktop + DeviceId string `protobuf:"bytes,5,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"` + // Device OS from Coder Desktop + DeviceOs string `protobuf:"bytes,6,opt,name=device_os,json=deviceOs,proto3" json:"device_os,omitempty"` + // Coder Desktop version + CoderDesktopVersion string `protobuf:"bytes,7,opt,name=coder_desktop_version,json=coderDesktopVersion,proto3" json:"coder_desktop_version,omitempty"` } func (x *StartRequest) Reset() { @@ -1019,6 +1025,27 @@ func (x *StartRequest) GetHeaders() []*StartRequest_Header { return nil } +func (x *StartRequest) GetDeviceId() string { + if x != nil { + return x.DeviceId + } + return "" +} + +func (x *StartRequest) GetDeviceOs() string { + if x != nil { + return x.DeviceOs + } + return "" +} + +func (x *StartRequest) GetCoderDesktopVersion() string { + if x != nil { + return x.CoderDesktopVersion + } + return "" +} + type StartResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1839,7 +1866,7 @@ var file_vpn_vpn_proto_rawDesc = []byte{ 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xe6, 0x01, 0x0a, 0x0c, + 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xd4, 0x02, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x16, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x74, 0x75, @@ -1851,25 +1878,32 @@ var file_vpn_vpn_proto_rawDesc = []byte{ 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x70, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, - 0x1a, 0x32, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4e, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, - 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1b, 0x0a, + 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6f, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4f, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x32, + 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x22, 0x4e, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x42, 0x39, 0x5a, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, - 0x76, 0x70, 0x6e, 0xaa, 0x02, 0x17, 0x43, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x73, 0x6b, - 0x74, 0x6f, 0x70, 0x2e, 0x56, 0x70, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x67, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x4d, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x42, 0x39, 0x5a, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x76, 0x70, + 0x6e, 0xaa, 0x02, 0x17, 0x43, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, + 0x70, 0x2e, 0x56, 0x70, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/vpn/vpn.proto b/vpn/vpn.proto index 10dfeb3916..71a5994f88 100644 --- a/vpn/vpn.proto +++ b/vpn/vpn.proto @@ -185,6 +185,12 @@ message StartRequest { string value = 2; } repeated Header headers = 4; + // Device ID from Coder Desktop + string device_id = 5; + // Device OS from Coder Desktop + string device_os = 6; + // Coder Desktop version + string coder_desktop_version = 7; } message StartResponse {