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 { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy