Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 39c41f4

Browse files
committedMar 26, 2025·
feat: add telemetry enrichment to StartRequest
1 parent e0a11dd commit 39c41f4

File tree

6 files changed

+251
-157
lines changed

6 files changed

+251
-157
lines changed
 
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Coder.Desktop.Vpn.Proto;
2+
using Coder.Desktop.Vpn.Service;
3+
4+
namespace Coder.Desktop.Tests.Vpn.Service;
5+
6+
[TestFixture]
7+
public class TelemetryEnricherTest
8+
{
9+
[Test]
10+
public void EnrichStartRequest()
11+
{
12+
var req = new StartRequest
13+
{
14+
CoderUrl = "https://coder.example.com",
15+
};
16+
var enricher = new TelemetryEnricher();
17+
req = enricher.EnrichStartRequest(req);
18+
19+
// quick sanity check that non-telemetry fields aren't lost or overwritten
20+
Assert.That(req.CoderUrl, Is.EqualTo("https://coder.example.com"));
21+
22+
Assert.That(req.DeviceOs, Is.EqualTo("Windows"));
23+
// seems that test assemblies always set 1.0.0.0
24+
Assert.That(req.CoderDesktopVersion, Is.EqualTo("1.0.0.0"));
25+
Assert.That(req.DeviceId, Is.Not.Empty);
26+
var deviceId = req.DeviceId;
27+
28+
// deviceId is different on different machines, but we can test that
29+
// each instance of the TelemetryEnricher produces the same value.
30+
enricher = new TelemetryEnricher();
31+
req = enricher.EnrichStartRequest(new StartRequest());
32+
Assert.That(req.DeviceId, Is.EqualTo(deviceId));
33+
}
34+
}

‎Vpn.Proto/RpcVersion.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Coder.Desktop.Vpn.Proto;
55
/// </summary>
66
public class RpcVersion
77
{
8-
public static readonly RpcVersion Current = new(1, 0);
8+
public static readonly RpcVersion Current = new(1, 1);
99

1010
public ulong Major { get; }
1111
public ulong Minor { get; }

‎Vpn.Proto/vpn.proto

Lines changed: 160 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -17,75 +17,75 @@ package vpn;
1717
// msg_id which it sets on the request, the responder sets response_to that msg_id on the response
1818
// message
1919
message RPC {
20-
uint64 msg_id = 1;
21-
uint64 response_to = 2;
20+
uint64 msg_id = 1;
21+
uint64 response_to = 2;
2222
}
2323

2424
// ManagerMessage is a message from the manager (to the tunnel).
2525
message ManagerMessage {
26-
RPC rpc = 1;
27-
oneof msg {
28-
GetPeerUpdate get_peer_update = 2;
29-
NetworkSettingsResponse network_settings = 3;
30-
StartRequest start = 4;
31-
StopRequest stop = 5;
32-
}
26+
RPC rpc = 1;
27+
oneof msg {
28+
GetPeerUpdate get_peer_update = 2;
29+
NetworkSettingsResponse network_settings = 3;
30+
StartRequest start = 4;
31+
StopRequest stop = 5;
32+
}
3333
}
3434

3535
// TunnelMessage is a message from the tunnel (to the manager).
3636
message TunnelMessage {
37-
RPC rpc = 1;
38-
oneof msg {
39-
Log log = 2;
40-
PeerUpdate peer_update = 3;
41-
NetworkSettingsRequest network_settings = 4;
42-
StartResponse start = 5;
43-
StopResponse stop = 6;
44-
}
37+
RPC rpc = 1;
38+
oneof msg {
39+
Log log = 2;
40+
PeerUpdate peer_update = 3;
41+
NetworkSettingsRequest network_settings = 4;
42+
StartResponse start = 5;
43+
StopResponse stop = 6;
44+
}
4545
}
4646

4747
// ClientMessage is a message from the client (to the service). Windows only.
4848
message ClientMessage {
49-
RPC rpc = 1;
50-
oneof msg {
51-
StartRequest start = 2;
52-
StopRequest stop = 3;
53-
StatusRequest status = 4;
49+
RPC rpc = 1;
50+
oneof msg {
51+
StartRequest start = 2;
52+
StopRequest stop = 3;
53+
StatusRequest status = 4;
5454
}
5555
}
5656

5757
// ServiceMessage is a message from the service (to the client). Windows only.
5858
message ServiceMessage {
59-
RPC rpc = 1;
60-
oneof msg {
61-
StartResponse start = 2;
62-
StopResponse stop = 3;
63-
Status status = 4; // either in reply to a StatusRequest or broadcasted
64-
}
59+
RPC rpc = 1;
60+
oneof msg {
61+
StartResponse start = 2;
62+
StopResponse stop = 3;
63+
Status status = 4; // either in reply to a StatusRequest or broadcasted
64+
}
6565
}
6666

6767
// Log is a log message generated by the tunnel. The manager should log it to the system log. It is
6868
// one-way tunnel -> manager with no response.
6969
message Log {
70-
enum Level {
71-
// these are designed to match slog levels
72-
DEBUG = 0;
73-
INFO = 1;
74-
WARN = 2;
75-
ERROR = 3;
76-
CRITICAL = 4;
77-
FATAL = 5;
78-
}
79-
Level level = 1;
80-
81-
string message = 2;
82-
repeated string logger_names = 3;
83-
84-
message Field {
85-
string name = 1;
86-
string value = 2;
87-
}
88-
repeated Field fields = 4;
70+
enum Level {
71+
// these are designed to match slog levels
72+
DEBUG = 0;
73+
INFO = 1;
74+
WARN = 2;
75+
ERROR = 3;
76+
CRITICAL = 4;
77+
FATAL = 5;
78+
}
79+
Level level = 1;
80+
81+
string message = 2;
82+
repeated string logger_names = 3;
83+
84+
message Field {
85+
string name = 1;
86+
string value = 2;
87+
}
88+
repeated Field fields = 4;
8989
}
9090

9191
// GetPeerUpdate asks for a PeerUpdate with a full set of data.
@@ -95,132 +95,138 @@ message GetPeerUpdate {}
9595
// response to GetPeerUpdate (which dumps the full set). It is also generated on any changes (not in
9696
// response to any request).
9797
message PeerUpdate {
98-
repeated Workspace upserted_workspaces = 1;
99-
repeated Agent upserted_agents = 2;
100-
repeated Workspace deleted_workspaces = 3;
101-
repeated Agent deleted_agents = 4;
98+
repeated Workspace upserted_workspaces = 1;
99+
repeated Agent upserted_agents = 2;
100+
repeated Workspace deleted_workspaces = 3;
101+
repeated Agent deleted_agents = 4;
102102
}
103103

104104
message Workspace {
105-
bytes id = 1; // UUID
106-
string name = 2;
107-
108-
enum Status {
109-
UNKNOWN = 0;
110-
PENDING = 1;
111-
STARTING = 2;
112-
RUNNING = 3;
113-
STOPPING = 4;
114-
STOPPED = 5;
115-
FAILED = 6;
116-
CANCELING = 7;
117-
CANCELED = 8;
118-
DELETING = 9;
119-
DELETED = 10;
120-
}
121-
Status status = 3;
105+
bytes id = 1; // UUID
106+
string name = 2;
107+
108+
enum Status {
109+
UNKNOWN = 0;
110+
PENDING = 1;
111+
STARTING = 2;
112+
RUNNING = 3;
113+
STOPPING = 4;
114+
STOPPED = 5;
115+
FAILED = 6;
116+
CANCELING = 7;
117+
CANCELED = 8;
118+
DELETING = 9;
119+
DELETED = 10;
120+
}
121+
Status status = 3;
122122
}
123123

124124
message Agent {
125-
bytes id = 1; // UUID
126-
string name = 2;
127-
bytes workspace_id = 3; // UUID
128-
repeated string fqdn = 4;
129-
repeated string ip_addrs = 5;
130-
// last_handshake is the primary indicator of whether we are connected to a peer. Zero value or
131-
// anything longer than 5 minutes ago means there is a problem.
132-
google.protobuf.Timestamp last_handshake = 6;
125+
bytes id = 1; // UUID
126+
string name = 2;
127+
bytes workspace_id = 3; // UUID
128+
repeated string fqdn = 4;
129+
repeated string ip_addrs = 5;
130+
// last_handshake is the primary indicator of whether we are connected to a peer. Zero value or
131+
// anything longer than 5 minutes ago means there is a problem.
132+
google.protobuf.Timestamp last_handshake = 6;
133133
}
134134

135135
// NetworkSettingsRequest is based on
136136
// https://developer.apple.com/documentation/networkextension/nepackettunnelnetworksettings for
137137
// macOS. It is a request/response message with response NetworkSettingsResponse
138138
message NetworkSettingsRequest {
139-
uint32 tunnel_overhead_bytes = 1;
140-
uint32 mtu = 2;
141-
142-
message DNSSettings {
143-
repeated string servers = 1;
144-
repeated string search_domains = 2;
145-
// domain_name is the primary domain name of the tunnel
146-
string domain_name = 3;
147-
repeated string match_domains = 4;
148-
// match_domains_no_search specifies if the domains in the matchDomains list should not be
149-
// appended to the resolver’s list of search domains.
150-
bool match_domains_no_search = 5;
151-
}
152-
DNSSettings dns_settings = 3;
153-
154-
string tunnel_remote_address = 4;
155-
156-
message IPv4Settings {
157-
repeated string addrs = 1;
158-
repeated string subnet_masks = 2;
159-
// router is the next-hop router in dotted-decimal format
160-
string router = 3;
161-
162-
message IPv4Route {
163-
string destination = 1;
164-
string mask = 2;
165-
// router is the next-hop router in dotted-decimal format
166-
string router = 3;
167-
}
168-
repeated IPv4Route included_routes = 4;
169-
repeated IPv4Route excluded_routes = 5;
170-
}
171-
IPv4Settings ipv4_settings = 5;
172-
173-
message IPv6Settings {
174-
repeated string addrs = 1;
175-
repeated uint32 prefix_lengths = 2;
176-
177-
message IPv6Route {
178-
string destination = 1;
179-
uint32 prefix_length = 2;
180-
// router is the address of the next-hop
181-
string router = 3;
182-
}
183-
repeated IPv6Route included_routes = 3;
184-
repeated IPv6Route excluded_routes = 4;
185-
}
186-
IPv6Settings ipv6_settings = 6;
139+
uint32 tunnel_overhead_bytes = 1;
140+
uint32 mtu = 2;
141+
142+
message DNSSettings {
143+
repeated string servers = 1;
144+
repeated string search_domains = 2;
145+
// domain_name is the primary domain name of the tunnel
146+
string domain_name = 3;
147+
repeated string match_domains = 4;
148+
// match_domains_no_search specifies if the domains in the matchDomains list should not be
149+
// appended to the resolver’s list of search domains.
150+
bool match_domains_no_search = 5;
151+
}
152+
DNSSettings dns_settings = 3;
153+
154+
string tunnel_remote_address = 4;
155+
156+
message IPv4Settings {
157+
repeated string addrs = 1;
158+
repeated string subnet_masks = 2;
159+
// router is the next-hop router in dotted-decimal format
160+
string router = 3;
161+
162+
message IPv4Route {
163+
string destination = 1;
164+
string mask = 2;
165+
// router is the next-hop router in dotted-decimal format
166+
string router = 3;
167+
}
168+
repeated IPv4Route included_routes = 4;
169+
repeated IPv4Route excluded_routes = 5;
170+
}
171+
IPv4Settings ipv4_settings = 5;
172+
173+
message IPv6Settings {
174+
repeated string addrs = 1;
175+
repeated uint32 prefix_lengths = 2;
176+
177+
message IPv6Route {
178+
string destination = 1;
179+
uint32 prefix_length = 2;
180+
// router is the address of the next-hop
181+
string router = 3;
182+
}
183+
repeated IPv6Route included_routes = 3;
184+
repeated IPv6Route excluded_routes = 4;
185+
}
186+
IPv6Settings ipv6_settings = 6;
187187
}
188188

189189
// NetworkSettingsResponse is the response from the manager to the tunnel for a
190190
// NetworkSettingsRequest
191191
message NetworkSettingsResponse {
192-
bool success = 1;
193-
string error_message = 2;
192+
bool success = 1;
193+
string error_message = 2;
194194
}
195195

196196
// StartRequest is a request from the manager to start the tunnel. The tunnel replies with a
197197
// StartResponse.
198198
message StartRequest {
199-
int32 tunnel_file_descriptor = 1;
200-
string coder_url = 2;
201-
string api_token = 3;
202-
// Additional HTTP headers added to all requests
203-
message Header {
204-
string name = 1;
205-
string value = 2;
206-
}
207-
repeated Header headers = 4;
199+
int32 tunnel_file_descriptor = 1;
200+
string coder_url = 2;
201+
string api_token = 3;
202+
// Additional HTTP headers added to all requests
203+
message Header {
204+
string name = 1;
205+
string value = 2;
206+
}
207+
repeated Header headers = 4;
208+
// Device ID from Coder Desktop
209+
string device_id = 5;
210+
// Device OS from Coder Desktop
211+
string device_os = 6;
212+
// Coder Desktop version
213+
string coder_desktop_version = 7;
208214
}
209215

210216
message StartResponse {
211-
bool success = 1;
212-
string error_message = 2;
217+
bool success = 1;
218+
string error_message = 2;
213219
}
214220

215-
// StopRequest is a request to stop the tunnel. The tunnel replies with a
221+
// StopRequest is a request from the manager to stop the tunnel. The tunnel replies with a
216222
// StopResponse.
217223
message StopRequest {}
218224

219225
// StopResponse is a response to stopping the tunnel. After sending this response, the tunnel closes
220226
// its side of the bidirectional stream for writing.
221227
message StopResponse {
222-
bool success = 1;
223-
string error_message = 2;
228+
bool success = 1;
229+
string error_message = 2;
224230
}
225231

226232
// StatusRequest is a request to get the status of the tunnel. The manager
@@ -230,18 +236,18 @@ message StatusRequest {}
230236
// Status is sent in response to a StatusRequest or broadcasted to all clients
231237
// when the status changes.
232238
message Status {
233-
enum Lifecycle {
234-
UNKNOWN = 0;
235-
STARTING = 1;
236-
STARTED = 2;
237-
STOPPING = 3;
238-
STOPPED = 4;
239-
}
240-
Lifecycle lifecycle = 1;
241-
string error_message = 2;
242-
243-
// This will be a FULL update with all workspaces and agents, so clients
244-
// should replace their current peer state. Only the Upserted fields will
245-
// be populated.
246-
PeerUpdate peer_update = 3;
239+
enum Lifecycle {
240+
UNKNOWN = 0;
241+
STARTING = 1;
242+
STARTED = 2;
243+
STOPPING = 3;
244+
STOPPED = 4;
245+
}
246+
Lifecycle lifecycle = 1;
247+
string error_message = 2;
248+
249+
// This will be a FULL update with all workspaces and agents, so clients
250+
// should replace their current peer state. Only the Upserted fields will
251+
// be populated.
252+
PeerUpdate peer_update = 3;
247253
}

‎Vpn.Service/Manager.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class Manager : IManager
3131
private readonly ILogger<Manager> _logger;
3232
private readonly ITunnelSupervisor _tunnelSupervisor;
3333
private readonly IManagerRpc _managerRpc;
34+
private readonly ITelemetryEnricher _telemetryEnricher;
3435

3536
private volatile TunnelStatus _status = TunnelStatus.Stopped;
3637

@@ -46,14 +47,15 @@ public class Manager : IManager
4647

4748
// ReSharper disable once ConvertToPrimaryConstructor
4849
public Manager(IOptions<ManagerConfig> config, ILogger<Manager> logger, IDownloader downloader,
49-
ITunnelSupervisor tunnelSupervisor, IManagerRpc managerRpc)
50+
ITunnelSupervisor tunnelSupervisor, IManagerRpc managerRpc, ITelemetryEnricher telemetryEnricher)
5051
{
5152
_config = config.Value;
5253
_logger = logger;
5354
_downloader = downloader;
5455
_tunnelSupervisor = tunnelSupervisor;
5556
_managerRpc = managerRpc;
5657
_managerRpc.OnReceive += HandleClientRpcMessage;
58+
_telemetryEnricher = telemetryEnricher;
5759
}
5860

5961
public void Dispose()
@@ -159,7 +161,7 @@ await _tunnelSupervisor.StartAsync(_config.TunnelBinaryPath, HandleTunnelRpcMess
159161

160162
var reply = await _tunnelSupervisor.SendRequestAwaitReply(new ManagerMessage
161163
{
162-
Start = message.Start,
164+
Start = _telemetryEnricher.EnrichStartRequest(message.Start),
163165
}, ct);
164166
if (reply.MsgCase != TunnelMessage.MsgOneofCase.Start)
165167
throw new InvalidOperationException("Tunnel did not reply with a Start response");

‎Vpn.Service/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ private static async Task BuildAndRun(string[] args)
8585
builder.Services.AddSingleton<ITunnelSupervisor, TunnelSupervisor>();
8686
builder.Services.AddSingleton<IManagerRpc, ManagerRpc>();
8787
builder.Services.AddSingleton<IManager, Manager>();
88+
builder.Services.AddSingleton<ITelemetryEnricher, TelemetryEnricher>();
8889

8990
// Services
9091
if (!Environment.UserInteractive)

‎Vpn.Service/TelemetryEnricher.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Reflection;
2+
using System.Security.Cryptography;
3+
using System.Text;
4+
using Coder.Desktop.Vpn.Proto;
5+
using Microsoft.Win32;
6+
7+
namespace Coder.Desktop.Vpn.Service;
8+
9+
// <summary>
10+
// ITelemetryEnricher contains methods for enriching messages with telemetry
11+
// information
12+
// </summary>
13+
public interface ITelemetryEnricher
14+
{
15+
public StartRequest EnrichStartRequest(StartRequest original);
16+
}
17+
18+
public class TelemetryEnricher : ITelemetryEnricher
19+
{
20+
private readonly string? _version;
21+
private readonly string? _deviceID;
22+
23+
public TelemetryEnricher()
24+
{
25+
var assembly = Assembly.GetExecutingAssembly();
26+
_version = assembly.GetName().Version?.ToString();
27+
28+
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\SQMClient");
29+
if (key != null)
30+
{
31+
// this is the "Device ID" shown in settings. I don't think it's personally
32+
// identifiable, but let's hash it just to be sure.
33+
var deviceID = key.GetValue("MachineId") as string;
34+
if (!string.IsNullOrEmpty(deviceID))
35+
{
36+
var idBytes = Encoding.UTF8.GetBytes(deviceID);
37+
var hash = SHA256.HashData(idBytes);
38+
_deviceID = Convert.ToBase64String(hash);
39+
}
40+
}
41+
}
42+
43+
public StartRequest EnrichStartRequest(StartRequest original)
44+
{
45+
var req = original.Clone();
46+
req.DeviceOs = "Windows";
47+
if (_version != null) req.CoderDesktopVersion = _version;
48+
if (_deviceID != null) req.DeviceId = _deviceID;
49+
return req;
50+
}
51+
}

0 commit comments

Comments
 (0)
Please sign in to comment.