diff --git a/.editorconfig b/.editorconfig index d0323ad..cd2fd68 100644 --- a/.editorconfig +++ b/.editorconfig @@ -76,6 +76,10 @@ resharper_web_config_module_not_resolved_highlighting = warning resharper_web_config_type_not_resolved_highlighting = warning resharper_web_config_wrong_module_highlighting = warning -[{*.json,*.jsonc,*.yml,*.yaml,*.proto}] +[{*.json,*.jsonc,*.yml,*.yaml}] indent_style = space indent_size = 2 + +[{*.proto}] +indent_style = tab +indent_size = 1 \ No newline at end of file diff --git a/Tests.Vpn.Service/TelemetryEnricherTest.cs b/Tests.Vpn.Service/TelemetryEnricherTest.cs new file mode 100644 index 0000000..144cd20 --- /dev/null +++ b/Tests.Vpn.Service/TelemetryEnricherTest.cs @@ -0,0 +1,34 @@ +using Coder.Desktop.Vpn.Proto; +using Coder.Desktop.Vpn.Service; + +namespace Coder.Desktop.Tests.Vpn.Service; + +[TestFixture] +public class TelemetryEnricherTest +{ + [Test] + public void EnrichStartRequest() + { + var req = new StartRequest + { + CoderUrl = "https://coder.example.com", + }; + var enricher = new TelemetryEnricher(); + req = enricher.EnrichStartRequest(req); + + // quick sanity check that non-telemetry fields aren't lost or overwritten + Assert.That(req.CoderUrl, Is.EqualTo("https://coder.example.com")); + + Assert.That(req.DeviceOs, Is.EqualTo("Windows")); + // seems that test assemblies always set 1.0.0.0 + Assert.That(req.CoderDesktopVersion, Is.EqualTo("1.0.0.0")); + Assert.That(req.DeviceId, Is.Not.Empty); + var deviceId = req.DeviceId; + + // deviceId is different on different machines, but we can test that + // each instance of the TelemetryEnricher produces the same value. + enricher = new TelemetryEnricher(); + req = enricher.EnrichStartRequest(new StartRequest()); + Assert.That(req.DeviceId, Is.EqualTo(deviceId)); + } +} diff --git a/Vpn.Proto/RpcVersion.cs b/Vpn.Proto/RpcVersion.cs index 33e0c16..641a10d 100644 --- a/Vpn.Proto/RpcVersion.cs +++ b/Vpn.Proto/RpcVersion.cs @@ -5,7 +5,7 @@ namespace Coder.Desktop.Vpn.Proto; /// public class RpcVersion { - public static readonly RpcVersion Current = new(1, 0); + public static readonly RpcVersion Current = new(1, 1); public ulong Major { get; } public ulong Minor { get; } diff --git a/Vpn.Proto/vpn.proto b/Vpn.Proto/vpn.proto index 8a4800d..2561a4b 100644 --- a/Vpn.Proto/vpn.proto +++ b/Vpn.Proto/vpn.proto @@ -17,75 +17,75 @@ package vpn; // msg_id which it sets on the request, the responder sets response_to that msg_id on the response // message message RPC { - uint64 msg_id = 1; - uint64 response_to = 2; + uint64 msg_id = 1; + uint64 response_to = 2; } // ManagerMessage is a message from the manager (to the tunnel). message ManagerMessage { - RPC rpc = 1; - oneof msg { - GetPeerUpdate get_peer_update = 2; - NetworkSettingsResponse network_settings = 3; - StartRequest start = 4; - StopRequest stop = 5; - } + RPC rpc = 1; + oneof msg { + GetPeerUpdate get_peer_update = 2; + NetworkSettingsResponse network_settings = 3; + StartRequest start = 4; + StopRequest stop = 5; + } } // TunnelMessage is a message from the tunnel (to the manager). message TunnelMessage { - RPC rpc = 1; - oneof msg { - Log log = 2; - PeerUpdate peer_update = 3; - NetworkSettingsRequest network_settings = 4; - StartResponse start = 5; - StopResponse stop = 6; - } + RPC rpc = 1; + oneof msg { + Log log = 2; + PeerUpdate peer_update = 3; + NetworkSettingsRequest network_settings = 4; + StartResponse start = 5; + StopResponse stop = 6; + } } // ClientMessage is a message from the client (to the service). Windows only. message ClientMessage { - RPC rpc = 1; - oneof msg { - StartRequest start = 2; - StopRequest stop = 3; - StatusRequest status = 4; + RPC rpc = 1; + oneof msg { + StartRequest start = 2; + StopRequest stop = 3; + StatusRequest status = 4; } } // ServiceMessage is a message from the service (to the client). Windows only. message ServiceMessage { - RPC rpc = 1; - oneof msg { - StartResponse start = 2; - StopResponse stop = 3; - Status status = 4; // either in reply to a StatusRequest or broadcasted - } + RPC rpc = 1; + oneof msg { + StartResponse start = 2; + StopResponse stop = 3; + Status status = 4; // either in reply to a StatusRequest or broadcasted + } } // Log is a log message generated by the tunnel. The manager should log it to the system log. It is // one-way tunnel -> manager with no response. message Log { - enum Level { - // these are designed to match slog levels - DEBUG = 0; - INFO = 1; - WARN = 2; - ERROR = 3; - CRITICAL = 4; - FATAL = 5; - } - Level level = 1; - - string message = 2; - repeated string logger_names = 3; - - message Field { - string name = 1; - string value = 2; - } - repeated Field fields = 4; + enum Level { + // these are designed to match slog levels + DEBUG = 0; + INFO = 1; + WARN = 2; + ERROR = 3; + CRITICAL = 4; + FATAL = 5; + } + Level level = 1; + + string message = 2; + repeated string logger_names = 3; + + message Field { + string name = 1; + string value = 2; + } + repeated Field fields = 4; } // GetPeerUpdate asks for a PeerUpdate with a full set of data. @@ -95,132 +95,138 @@ message GetPeerUpdate {} // response to GetPeerUpdate (which dumps the full set). It is also generated on any changes (not in // response to any request). message PeerUpdate { - repeated Workspace upserted_workspaces = 1; - repeated Agent upserted_agents = 2; - repeated Workspace deleted_workspaces = 3; - repeated Agent deleted_agents = 4; + repeated Workspace upserted_workspaces = 1; + repeated Agent upserted_agents = 2; + repeated Workspace deleted_workspaces = 3; + repeated Agent deleted_agents = 4; } message Workspace { - bytes id = 1; // UUID - string name = 2; - - enum Status { - UNKNOWN = 0; - PENDING = 1; - STARTING = 2; - RUNNING = 3; - STOPPING = 4; - STOPPED = 5; - FAILED = 6; - CANCELING = 7; - CANCELED = 8; - DELETING = 9; - DELETED = 10; - } - Status status = 3; + bytes id = 1; // UUID + string name = 2; + + enum Status { + UNKNOWN = 0; + PENDING = 1; + STARTING = 2; + RUNNING = 3; + STOPPING = 4; + STOPPED = 5; + FAILED = 6; + CANCELING = 7; + CANCELED = 8; + DELETING = 9; + DELETED = 10; + } + Status status = 3; } message Agent { - bytes id = 1; // UUID - string name = 2; - bytes workspace_id = 3; // UUID - repeated string fqdn = 4; - repeated string ip_addrs = 5; - // last_handshake is the primary indicator of whether we are connected to a peer. Zero value or - // anything longer than 5 minutes ago means there is a problem. - google.protobuf.Timestamp last_handshake = 6; + bytes id = 1; // UUID + string name = 2; + bytes workspace_id = 3; // UUID + repeated string fqdn = 4; + repeated string ip_addrs = 5; + // last_handshake is the primary indicator of whether we are connected to a peer. Zero value or + // anything longer than 5 minutes ago means there is a problem. + google.protobuf.Timestamp last_handshake = 6; } // NetworkSettingsRequest is based on // https://developer.apple.com/documentation/networkextension/nepackettunnelnetworksettings for // macOS. It is a request/response message with response NetworkSettingsResponse message NetworkSettingsRequest { - uint32 tunnel_overhead_bytes = 1; - uint32 mtu = 2; - - message DNSSettings { - repeated string servers = 1; - repeated string search_domains = 2; - // domain_name is the primary domain name of the tunnel - string domain_name = 3; - repeated string match_domains = 4; - // match_domains_no_search specifies if the domains in the matchDomains list should not be - // appended to the resolver’s list of search domains. - bool match_domains_no_search = 5; - } - DNSSettings dns_settings = 3; - - string tunnel_remote_address = 4; - - message IPv4Settings { - repeated string addrs = 1; - repeated string subnet_masks = 2; - // router is the next-hop router in dotted-decimal format - string router = 3; - - message IPv4Route { - string destination = 1; - string mask = 2; - // router is the next-hop router in dotted-decimal format - string router = 3; - } - repeated IPv4Route included_routes = 4; - repeated IPv4Route excluded_routes = 5; - } - IPv4Settings ipv4_settings = 5; - - message IPv6Settings { - repeated string addrs = 1; - repeated uint32 prefix_lengths = 2; - - message IPv6Route { - string destination = 1; - uint32 prefix_length = 2; - // router is the address of the next-hop - string router = 3; - } - repeated IPv6Route included_routes = 3; - repeated IPv6Route excluded_routes = 4; - } - IPv6Settings ipv6_settings = 6; + uint32 tunnel_overhead_bytes = 1; + uint32 mtu = 2; + + message DNSSettings { + repeated string servers = 1; + repeated string search_domains = 2; + // domain_name is the primary domain name of the tunnel + string domain_name = 3; + repeated string match_domains = 4; + // match_domains_no_search specifies if the domains in the matchDomains list should not be + // appended to the resolver’s list of search domains. + bool match_domains_no_search = 5; + } + DNSSettings dns_settings = 3; + + string tunnel_remote_address = 4; + + message IPv4Settings { + repeated string addrs = 1; + repeated string subnet_masks = 2; + // router is the next-hop router in dotted-decimal format + string router = 3; + + message IPv4Route { + string destination = 1; + string mask = 2; + // router is the next-hop router in dotted-decimal format + string router = 3; + } + repeated IPv4Route included_routes = 4; + repeated IPv4Route excluded_routes = 5; + } + IPv4Settings ipv4_settings = 5; + + message IPv6Settings { + repeated string addrs = 1; + repeated uint32 prefix_lengths = 2; + + message IPv6Route { + string destination = 1; + uint32 prefix_length = 2; + // router is the address of the next-hop + string router = 3; + } + repeated IPv6Route included_routes = 3; + repeated IPv6Route excluded_routes = 4; + } + IPv6Settings ipv6_settings = 6; } // NetworkSettingsResponse is the response from the manager to the tunnel for a // NetworkSettingsRequest message NetworkSettingsResponse { - bool success = 1; - string error_message = 2; + bool success = 1; + string error_message = 2; } // StartRequest is a request from the manager to start the tunnel. The tunnel replies with a // StartResponse. message StartRequest { - int32 tunnel_file_descriptor = 1; - string coder_url = 2; - string api_token = 3; - // Additional HTTP headers added to all requests - message Header { - string name = 1; - string value = 2; - } - repeated Header headers = 4; + int32 tunnel_file_descriptor = 1; + string coder_url = 2; + string api_token = 3; + // Additional HTTP headers added to all requests + message Header { + string name = 1; + 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 { - bool success = 1; - string error_message = 2; + bool success = 1; + string error_message = 2; } -// StopRequest is a request to stop the tunnel. The tunnel replies with a +// StopRequest is a request from the manager to stop the tunnel. The tunnel replies with a // StopResponse. message StopRequest {} // StopResponse is a response to stopping the tunnel. After sending this response, the tunnel closes // its side of the bidirectional stream for writing. message StopResponse { - bool success = 1; - string error_message = 2; + bool success = 1; + string error_message = 2; } // StatusRequest is a request to get the status of the tunnel. The manager @@ -230,18 +236,18 @@ message StatusRequest {} // Status is sent in response to a StatusRequest or broadcasted to all clients // when the status changes. message Status { - enum Lifecycle { - UNKNOWN = 0; - STARTING = 1; - STARTED = 2; - STOPPING = 3; - STOPPED = 4; - } - Lifecycle lifecycle = 1; - string error_message = 2; - - // This will be a FULL update with all workspaces and agents, so clients - // should replace their current peer state. Only the Upserted fields will - // be populated. - PeerUpdate peer_update = 3; + enum Lifecycle { + UNKNOWN = 0; + STARTING = 1; + STARTED = 2; + STOPPING = 3; + STOPPED = 4; + } + Lifecycle lifecycle = 1; + string error_message = 2; + + // This will be a FULL update with all workspaces and agents, so clients + // should replace their current peer state. Only the Upserted fields will + // be populated. + PeerUpdate peer_update = 3; } diff --git a/Vpn.Service/Manager.cs b/Vpn.Service/Manager.cs index 3345e98..1eca8bf 100644 --- a/Vpn.Service/Manager.cs +++ b/Vpn.Service/Manager.cs @@ -31,6 +31,7 @@ public class Manager : IManager private readonly ILogger _logger; private readonly ITunnelSupervisor _tunnelSupervisor; private readonly IManagerRpc _managerRpc; + private readonly ITelemetryEnricher _telemetryEnricher; private volatile TunnelStatus _status = TunnelStatus.Stopped; @@ -46,7 +47,7 @@ public class Manager : IManager // ReSharper disable once ConvertToPrimaryConstructor public Manager(IOptions config, ILogger logger, IDownloader downloader, - ITunnelSupervisor tunnelSupervisor, IManagerRpc managerRpc) + ITunnelSupervisor tunnelSupervisor, IManagerRpc managerRpc, ITelemetryEnricher telemetryEnricher) { _config = config.Value; _logger = logger; @@ -54,6 +55,7 @@ public Manager(IOptions config, ILogger logger, IDownloa _tunnelSupervisor = tunnelSupervisor; _managerRpc = managerRpc; _managerRpc.OnReceive += HandleClientRpcMessage; + _telemetryEnricher = telemetryEnricher; } public void Dispose() @@ -159,7 +161,7 @@ await _tunnelSupervisor.StartAsync(_config.TunnelBinaryPath, HandleTunnelRpcMess var reply = await _tunnelSupervisor.SendRequestAwaitReply(new ManagerMessage { - Start = message.Start, + Start = _telemetryEnricher.EnrichStartRequest(message.Start), }, ct); if (reply.MsgCase != TunnelMessage.MsgOneofCase.Start) throw new InvalidOperationException("Tunnel did not reply with a Start response"); diff --git a/Vpn.Service/Program.cs b/Vpn.Service/Program.cs index e22daa2..69b6ea8 100644 --- a/Vpn.Service/Program.cs +++ b/Vpn.Service/Program.cs @@ -85,6 +85,7 @@ private static async Task BuildAndRun(string[] args) builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Services if (!Environment.UserInteractive) diff --git a/Vpn.Service/TelemetryEnricher.cs b/Vpn.Service/TelemetryEnricher.cs new file mode 100644 index 0000000..2169334 --- /dev/null +++ b/Vpn.Service/TelemetryEnricher.cs @@ -0,0 +1,51 @@ +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using Coder.Desktop.Vpn.Proto; +using Microsoft.Win32; + +namespace Coder.Desktop.Vpn.Service; + +// +// ITelemetryEnricher contains methods for enriching messages with telemetry +// information +// +public interface ITelemetryEnricher +{ + public StartRequest EnrichStartRequest(StartRequest original); +} + +public class TelemetryEnricher : ITelemetryEnricher +{ + private readonly string? _version; + private readonly string? _deviceID; + + public TelemetryEnricher() + { + var assembly = Assembly.GetExecutingAssembly(); + _version = assembly.GetName().Version?.ToString(); + + using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\SQMClient"); + if (key != null) + { + // this is the "Device ID" shown in settings. I don't think it's personally + // identifiable, but let's hash it just to be sure. + var deviceID = key.GetValue("MachineId") as string; + if (!string.IsNullOrEmpty(deviceID)) + { + var idBytes = Encoding.UTF8.GetBytes(deviceID); + var hash = SHA256.HashData(idBytes); + _deviceID = Convert.ToBase64String(hash); + } + } + } + + public StartRequest EnrichStartRequest(StartRequest original) + { + var req = original.Clone(); + req.DeviceOs = "Windows"; + if (_version != null) req.CoderDesktopVersion = _version; + if (_deviceID != null) req.DeviceId = _deviceID; + return req; + } +} diff --git a/scripts/Publish.ps1 b/scripts/Publish.ps1 index 5f7a25e..dccb39f 100644 --- a/scripts/Publish.ps1 +++ b/scripts/Publish.ps1 @@ -116,14 +116,14 @@ New-Item -ItemType Directory -Path $buildPath -Force & dotnet.exe restore if ($LASTEXITCODE -ne 0) { throw "Failed to dotnet restore" } $servicePublishDir = Join-Path $buildPath "service" -& dotnet.exe publish .\Vpn.Service\Vpn.Service.csproj -c Release -a $arch -o $servicePublishDir +& dotnet.exe publish .\Vpn.Service\Vpn.Service.csproj -c Release -a $arch -o $servicePublishDir /p:Version=$version if ($LASTEXITCODE -ne 0) { throw "Failed to build Vpn.Service" } # App needs to be built with msbuild $appPublishDir = Join-Path $buildPath "app" $msbuildBinary = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe if ($LASTEXITCODE -ne 0) { throw "Failed to find MSBuild" } if (-not (Test-Path $msbuildBinary)) { throw "Failed to find MSBuild at $msbuildBinary" } -& $msbuildBinary .\App\App.csproj /p:Configuration=Release /p:Platform=$arch /p:OutputPath=$appPublishDir +& $msbuildBinary .\App\App.csproj /p:Configuration=Release /p:Platform=$arch /p:OutputPath=$appPublishDir /p:Version=$version if ($LASTEXITCODE -ne 0) { throw "Failed to build App" } # Find any files in the publish directory recursively that match any of our 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