diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index a06cf96ea8616..b8b71b330275b 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 899708ce1fb06..c4519f731b203 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() - require.NoError(t, err) +} - beforeDisconnectTime := time.Now() - err = wsConn.Close(websocket.StatusNormalClosure, "done") +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) - 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()) + 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{}, + }, + } + + // 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 d267355d37096..8a341ee742a76 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 9093c277783fa..0650c3c32097d 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 1e9b471ad46f4..01ed0c919a835 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 5985043307107..9ec795bc033b8 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 e40732ae10e38..611e7189f4e75 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 6cd18085ab302..3689bd37ac6f6 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 1962dc36d4501..91aac9175f748 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 863f11bba0a4b..db9b8ddd4ff75 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 10dfeb3916aa6..71a5994f88d54 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 {