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 d311dda

Browse files
authoredMar 27, 2025··
feat: add enrichment of StartRequest with OS, device ID, version (#123)
Enriches `StartRequest` protocol message with device ID, OS, and version, for Coder Desktop telemetry.
1 parent f53a99f commit d311dda

File tree

7 files changed

+608
-4
lines changed

7 files changed

+608
-4
lines changed
 

‎Coder-Desktop/VPN/Manager.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import VPNLib
66
actor Manager {
77
let ptp: PacketTunnelProvider
88
let cfg: ManagerConfig
9+
let telemetryEnricher: TelemetryEnricher
910

1011
let tunnelHandle: TunnelHandle
1112
let speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>
@@ -19,6 +20,7 @@ actor Manager {
1920
init(with: PacketTunnelProvider, cfg: ManagerConfig) async throws(ManagerError) {
2021
ptp = with
2122
self.cfg = cfg
23+
telemetryEnricher = TelemetryEnricher()
2224
#if arch(arm64)
2325
let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-darwin-arm64.dylib")
2426
#elseif arch(x86_64)
@@ -176,6 +178,7 @@ actor Manager {
176178
req.value = header.value
177179
}
178180
}
181+
req = telemetryEnricher.enrich(req)
179182
}
180183
})
181184
} catch {

‎Coder-Desktop/VPNLib/Speaker.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag
8888

8989
/// Does the VPN Protocol handshake and validates the result
9090
public func handshake() async throws(HandshakeError) {
91-
let hndsh = Handshaker(writeFD: writeFD, dispatch: dispatch, queue: queue, role: role)
92-
// ignore the version for now because we know it can only be 1.0
91+
let hndsh = Handshaker(writeFD: writeFD, dispatch: dispatch, queue: queue, role: role,
92+
versions: [ProtoVersion(1, 1)])
93+
// ignore the version for now because we know it can only be 1.0 or 1.1.
94+
// 1.1 adds support for telemetry to StartRequest, but since setting these
95+
// fields won't adversely affect a 1.0 speaker, we set them regardless.
9396
try _ = await hndsh.handshake()
9497
}
9598

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Foundation
2+
3+
public struct TelemetryEnricher {
4+
private let deviceID: String
5+
private let version: String?
6+
7+
public init() {
8+
version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
9+
10+
let userDefaults = UserDefaults.standard
11+
let key = "deviceID"
12+
13+
if let existingID = userDefaults.string(forKey: key) {
14+
deviceID = existingID
15+
} else {
16+
let newID = UUID().uuidString
17+
userDefaults.set(newID, forKey: key)
18+
deviceID = newID
19+
}
20+
}
21+
22+
public func enrich(_ original: Vpn_StartRequest) -> Vpn_StartRequest {
23+
var req = original
24+
req.deviceOs = "macOS"
25+
req.deviceID = deviceID
26+
if let version {
27+
req.coderDesktopVersion = version
28+
}
29+
return req
30+
}
31+
}

‎Coder-Desktop/VPNLib/vpn.pb.swift

Lines changed: 492 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Coder-Desktop/VPNLib/vpn.proto

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,26 @@ message TunnelMessage {
4444
}
4545
}
4646

47+
// ClientMessage is a message from the client (to the service). Windows only.
48+
message ClientMessage {
49+
RPC rpc = 1;
50+
oneof msg {
51+
StartRequest start = 2;
52+
StopRequest stop = 3;
53+
StatusRequest status = 4;
54+
}
55+
}
56+
57+
// ServiceMessage is a message from the service (to the client). Windows only.
58+
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+
}
65+
}
66+
4767
// Log is a log message generated by the tunnel. The manager should log it to the system log. It is
4868
// one-way tunnel -> manager with no response.
4969
message Log {
@@ -185,6 +205,12 @@ message StartRequest {
185205
string value = 2;
186206
}
187207
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;
188214
}
189215

190216
message StartResponse {
@@ -202,3 +228,26 @@ message StopResponse {
202228
bool success = 1;
203229
string error_message = 2;
204230
}
231+
232+
// StatusRequest is a request to get the status of the tunnel. The manager
233+
// replies with a Status.
234+
message StatusRequest {}
235+
236+
// Status is sent in response to a StatusRequest or broadcasted to all clients
237+
// when the status changes.
238+
message Status {
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;
253+
}

‎Coder-Desktop/VPNLibTests/SpeakerTests.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ struct SpeakerTests: Sendable {
2929
handshaker = Handshaker(
3030
writeFD: pipeMT.fileHandleForWriting,
3131
dispatch: dispatch, queue: queue,
32-
role: .manager
32+
role: .manager,
33+
versions: [ProtoVersion(1, 1)]
3334
)
3435
}
3536

3637
@Test func handshake() async throws {
3738
async let v = handshaker.handshake()
3839
try await uut.handshake()
39-
#expect(try await v == ProtoVersion(1, 0))
40+
#expect(try await v == ProtoVersion(1, 1))
4041
}
4142

4243
@Test func handleSingleMessage() async throws {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Testing
2+
@testable import VPNLib
3+
4+
@Suite(.timeLimit(.minutes(1)))
5+
struct TelemetryEnricherTests {
6+
@Test func testEnrichStartRequest() throws {
7+
let enricher0 = TelemetryEnricher()
8+
let original = Vpn_StartRequest.with { req in
9+
req.coderURL = "https://example.com"
10+
req.tunnelFileDescriptor = 123
11+
}
12+
var enriched = enricher0.enrich(original)
13+
#expect(enriched.coderURL == "https://example.com")
14+
#expect(enriched.tunnelFileDescriptor == 123)
15+
#expect(enriched.deviceOs == "macOS")
16+
#expect(try enriched.coderDesktopVersion.contains(Regex(#"^\d+\.\d+\.\d+$"#)))
17+
let deviceID = enriched.deviceID
18+
#expect(!deviceID.isEmpty)
19+
20+
// check we get the same deviceID from a new enricher
21+
let enricher1 = TelemetryEnricher()
22+
enriched = enricher1.enrich(original)
23+
#expect(enriched.deviceID == deviceID)
24+
}
25+
}

0 commit comments

Comments
 (0)
Please sign in to comment.