ChangelogHtml(AppCastItem item)
+ {
+ const string htmlTemplate = @"
+
+
+
+
+
+
+
+
+
+
+ {{CONTENT}}
+
+
+
+";
+
+ const string githubMarkdownCssToken = "{{GITHUB_MARKDOWN_CSS}}";
+ const string themeToken = "{{THEME}}";
+ const string contentToken = "{{CONTENT}}";
+
+ // TODO: Avalonia - load and provide GitHub markdown CSS from UI layer.
+ var css = "";
+
+ // We store the changelog in the description field, rather than using
+ // the release notes URL to avoid extra requests.
+ var innerHtml = item.Description;
+ if (string.IsNullOrWhiteSpace(innerHtml))
+ innerHtml = "No release notes available.
";
+
+ // The theme doesn't automatically update.
+ var currentTheme = IsDarkTheme ? "dark" : "light";
+
+ var html = htmlTemplate
+ .Replace(githubMarkdownCssToken, css)
+ .Replace(themeToken, currentTheme)
+ .Replace(contentToken, innerHtml);
+
+ return Task.FromResult(html);
+ }
+
+ public Task Changelog_Loaded(object? sender, EventArgs e)
+ {
+ // TODO: Avalonia - implement WebView/Markdown rendering in UI layer.
+ // WinUI used WebView2 and configured navigation + data folder.
+ _logger.LogDebug("Changelog_Loaded is a no-op in App.Shared");
+ return Task.CompletedTask;
+ }
+
+ private void SendResponse(UpdateAvailableResult result)
+ {
+ Result = result;
+ UserResponded?.Invoke(this, new UpdateResponseEventArgs(result, CurrentItem));
+ }
+
+ public void SkipButton_Click()
+ {
+ if (!SkipButtonVisible || MissingCriticalUpdate)
+ return;
+ SendResponse(UpdateAvailableResult.SkipUpdate);
+ }
+
+ public void RemindMeLaterButton_Click()
+ {
+ if (!RemindMeLaterButtonVisible || MissingCriticalUpdate)
+ return;
+ SendResponse(UpdateAvailableResult.RemindMeLater);
+ }
+
+ public void InstallButton_Click()
+ {
+ SendResponse(UpdateAvailableResult.InstallUpdate);
+ }
+}
diff --git a/App.Windows/App.Windows.csproj b/App.Windows/App.Windows.csproj
new file mode 100644
index 0000000..65a6060
--- /dev/null
+++ b/App.Windows/App.Windows.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Coder.Desktop.App.Windows
+ Coder.Desktop.App
+ net8.0-windows
+ enable
+ enable
+
+
+
diff --git a/App.Windows/Placeholder.cs b/App.Windows/Placeholder.cs
new file mode 100644
index 0000000..4e834a0
--- /dev/null
+++ b/App.Windows/Placeholder.cs
@@ -0,0 +1,3 @@
+// Windows-specific App implementations will be moved here during Phase 2.
+// This project is a placeholder for now.
+namespace Coder.Desktop.App;
diff --git a/App/App.csproj b/App/App.csproj
index 9a91849..b6856c7 100644
--- a/App/App.csproj
+++ b/App/App.csproj
@@ -2,6 +2,7 @@
WinExe
net8.0-windows10.0.19041.0
+ true
10.0.17763.0
Coder.Desktop.App
app.manifest
diff --git a/Coder.Desktop.sln b/Coder.Desktop.sln
index d1f5ac6..11a5526 100644
--- a/Coder.Desktop.sln
+++ b/Coder.Desktop.sln
@@ -29,6 +29,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MutagenSdk", "MutagenSdk\Mu
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.CoderSdk", "Tests.CoderSdk\Tests.CoderSdk.csproj", "{2BDEA023-FE75-476F-81DE-8EF90806C27C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vpn.Windows", "Vpn.Windows\Vpn.Windows.csproj", "{9485496D-1CF9-4B1D-A994-AF8E8617504A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vpn.Linux", "Vpn.Linux\Vpn.Linux.csproj", "{447D594A-14AD-4B63-A188-00AE7B125ECB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App.Windows", "App.Windows\App.Windows.csproj", "{DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App.Linux", "App.Linux\App.Linux.csproj", "{E2605556-CD04-4FD9-9621-C7063F859AED}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App.Shared", "App.Shared\App.Shared.csproj", "{8ECA503C-B827-4665-BE11-9CFF33B75BA0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App.Avalonia", "App.Avalonia\App.Avalonia.csproj", "{AC71930B-17DB-47F1-9880-2FB60C292C15}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -257,6 +269,102 @@ Global
{2BDEA023-FE75-476F-81DE-8EF90806C27C}.Release|x64.Build.0 = Release|Any CPU
{2BDEA023-FE75-476F-81DE-8EF90806C27C}.Release|x86.ActiveCfg = Release|Any CPU
{2BDEA023-FE75-476F-81DE-8EF90806C27C}.Release|x86.Build.0 = Release|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Debug|x64.Build.0 = Debug|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Debug|x86.Build.0 = Debug|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Release|ARM64.Build.0 = Release|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Release|x64.ActiveCfg = Release|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Release|x64.Build.0 = Release|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Release|x86.ActiveCfg = Release|Any CPU
+ {9485496D-1CF9-4B1D-A994-AF8E8617504A}.Release|x86.Build.0 = Release|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Debug|x64.Build.0 = Debug|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Debug|x86.Build.0 = Debug|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Release|ARM64.Build.0 = Release|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Release|x64.ActiveCfg = Release|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Release|x64.Build.0 = Release|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Release|x86.ActiveCfg = Release|Any CPU
+ {447D594A-14AD-4B63-A188-00AE7B125ECB}.Release|x86.Build.0 = Release|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Debug|x64.Build.0 = Debug|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Debug|x86.Build.0 = Debug|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Release|ARM64.Build.0 = Release|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Release|x64.ActiveCfg = Release|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Release|x64.Build.0 = Release|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Release|x86.ActiveCfg = Release|Any CPU
+ {DECAA70B-27E2-4CB0-8C7E-AC271E5446E5}.Release|x86.Build.0 = Release|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Debug|x64.Build.0 = Debug|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Debug|x86.Build.0 = Debug|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Release|ARM64.Build.0 = Release|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Release|x64.ActiveCfg = Release|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Release|x64.Build.0 = Release|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Release|x86.ActiveCfg = Release|Any CPU
+ {E2605556-CD04-4FD9-9621-C7063F859AED}.Release|x86.Build.0 = Release|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Debug|x64.Build.0 = Debug|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Debug|x86.Build.0 = Debug|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Release|ARM64.Build.0 = Release|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Release|x64.ActiveCfg = Release|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Release|x64.Build.0 = Release|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Release|x86.ActiveCfg = Release|Any CPU
+ {8ECA503C-B827-4665-BE11-9CFF33B75BA0}.Release|x86.Build.0 = Release|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Debug|x64.Build.0 = Debug|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Debug|x86.Build.0 = Debug|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Release|ARM64.Build.0 = Release|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Release|x64.ActiveCfg = Release|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Release|x64.Build.0 = Release|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Release|x86.ActiveCfg = Release|Any CPU
+ {AC71930B-17DB-47F1-9880-2FB60C292C15}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MutagenSdk/MutagenClient.cs b/MutagenSdk/MutagenClient.cs
index 89fad29..7a220c6 100644
--- a/MutagenSdk/MutagenClient.cs
+++ b/MutagenSdk/MutagenClient.cs
@@ -1,3 +1,4 @@
+using System.Net.Sockets;
using Coder.Desktop.MutagenSdk.Proto.Service.Daemon;
using Coder.Desktop.MutagenSdk.Proto.Service.Prompting;
using Coder.Desktop.MutagenSdk.Proto.Service.Synchronization;
@@ -16,7 +17,7 @@ public class MutagenClient : IDisposable
public MutagenClient(string dataDir)
{
- // Read the IPC named pipe address from the sock file.
+ // Read the IPC address from the sock file.
var daemonSockFile = Path.Combine(dataDir, "daemon", "daemon.sock");
if (!File.Exists(daemonSockFile))
throw new FileNotFoundException(
@@ -26,36 +27,69 @@ public MutagenClient(string dataDir)
throw new InvalidOperationException(
$"Mutagen daemon socket address from '{daemonSockFile}' is empty, did the mutagen daemon start successfully?");
+ SocketsHttpHandler socketsHttpHandler;
const string namedPipePrefix = @"\\.\pipe\";
- if (!daemonSockAddress.StartsWith(namedPipePrefix) || daemonSockAddress == namedPipePrefix)
- throw new InvalidOperationException(
- $"Mutagen daemon socket address '{daemonSockAddress}' is not a valid named pipe address");
- // Ensure the pipe exists before we try to connect to it. Obviously
- // this is not 100% foolproof, since the pipe could appear/disappear
- // after we check it. This allows us to fail early if the pipe isn't
- // ready yet (and consumers can retry), otherwise the pipe connection
- // may block.
- //
- // Note: we cannot use File.Exists here without breaking the named
- // pipe connection code due to a .NET bug.
- // https://github.com/dotnet/runtime/issues/69604
- var pipeName = daemonSockAddress[namedPipePrefix.Length..];
- var foundPipe = Directory
- .GetFiles(namedPipePrefix, pipeName)
- .FirstOrDefault(p => Path.GetFileName(p) == pipeName);
- if (foundPipe == null)
- throw new FileNotFoundException(
- "Mutagen daemon named pipe not found, did the mutagen daemon start successfully?", daemonSockAddress);
+ if (OperatingSystem.IsWindows() && daemonSockAddress.StartsWith(namedPipePrefix))
+ {
+ // Windows: Named Pipe transport
+ var pipeName = daemonSockAddress[namedPipePrefix.Length..];
+ if (string.IsNullOrEmpty(pipeName))
+ throw new InvalidOperationException(
+ $"Mutagen daemon socket address '{daemonSockAddress}' is not a valid named pipe address");
- var connectionFactory = new NamedPipesConnectionFactory(pipeName);
- var socketsHttpHandler = new SocketsHttpHandler
+ // Ensure the pipe exists before we try to connect to it. Obviously
+ // this is not 100% foolproof, since the pipe could appear/disappear
+ // after we check it. This allows us to fail early if the pipe isn't
+ // ready yet (and consumers can retry), otherwise the pipe connection
+ // may block.
+ //
+ // Note: we cannot use File.Exists here without breaking the named
+ // pipe connection code due to a .NET bug.
+ // https://github.com/dotnet/runtime/issues/69604
+ var foundPipe = Directory
+ .GetFiles(namedPipePrefix, pipeName)
+ .FirstOrDefault(p => Path.GetFileName(p) == pipeName);
+ if (foundPipe == null)
+ throw new FileNotFoundException(
+ "Mutagen daemon named pipe not found, did the mutagen daemon start successfully?",
+ daemonSockAddress);
+
+ var connectionFactory = new NamedPipesConnectionFactory(pipeName);
+ socketsHttpHandler = new SocketsHttpHandler
+ {
+ ConnectCallback = connectionFactory.ConnectAsync,
+ };
+ }
+ else
{
- ConnectCallback = connectionFactory.ConnectAsync,
- };
+ // Linux/macOS: Unix Domain Socket transport
+ if (!File.Exists(daemonSockAddress))
+ throw new FileNotFoundException(
+ "Mutagen daemon Unix socket not found, did the mutagen daemon start successfully?",
+ daemonSockAddress);
+
+ socketsHttpHandler = new SocketsHttpHandler
+ {
+ ConnectCallback = async (_, ct) =>
+ {
+ var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
+ try
+ {
+ await socket.ConnectAsync(new UnixDomainSocketEndPoint(daemonSockAddress), ct);
+ return new NetworkStream(socket, ownsSocket: true);
+ }
+ catch
+ {
+ socket.Dispose();
+ throw;
+ }
+ },
+ };
+ }
- // http://localhost is fake address. The HttpHandler will be used to
- // open a socket to the named pipe.
+ // http://localhost is a fake address. The HttpHandler will be used to
+ // open a socket to the actual endpoint.
_channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
{
Credentials = ChannelCredentials.Insecure,
diff --git a/Packaging.Linux/README.md b/Packaging.Linux/README.md
new file mode 100644
index 0000000..17fb992
--- /dev/null
+++ b/Packaging.Linux/README.md
@@ -0,0 +1,25 @@
+# Linux Packaging
+
+## Building
+
+```bash
+# Build .deb for amd64
+./build-deb.sh amd64
+
+# Build .deb for arm64
+VERSION=1.0.0 ./build-deb.sh arm64
+```
+
+## Package contents
+
+- `/usr/lib/coder-desktop/` — Service binaries
+- `/usr/bin/coder-vpn-service` — Symlink to VPN service
+- `/etc/systemd/system/coder-desktop.service` — Systemd unit
+- `/etc/coder-desktop/config.json` — Default configuration
+- `/usr/share/applications/coder-desktop.desktop` — Desktop entry
+
+## Dependencies
+
+- `libnotify-bin` — Desktop notifications
+- `libsecret-tools` — Credential storage
+- `freerdp2-x11` or `remmina` — RDP client (optional)
diff --git a/Packaging.Linux/build-deb.sh b/Packaging.Linux/build-deb.sh
new file mode 100755
index 0000000..cc5cdf2
--- /dev/null
+++ b/Packaging.Linux/build-deb.sh
@@ -0,0 +1,80 @@
+#!/bin/bash
+set -euo pipefail
+
+# Build a .deb package for Coder Desktop
+# Usage: ./build-deb.sh [amd64|arm64]
+
+ARCH="${1:-amd64}"
+VERSION="${VERSION:-0.1.0}"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+BUILD_DIR="$(mktemp -d)"
+PKG_DIR="$BUILD_DIR/coder-desktop_${VERSION}_${ARCH}"
+
+echo "Building Coder Desktop .deb package v${VERSION} for ${ARCH}..."
+
+# Map architecture names
+case "$ARCH" in
+ amd64) RID="linux-x64" ;;
+ arm64) RID="linux-arm64" ;;
+ *) echo "Unsupported architecture: $ARCH"; exit 1 ;;
+esac
+
+# Build the service
+echo "Publishing Vpn.Service..."
+dotnet publish "$ROOT_DIR/Vpn.Service" -r "$RID" -c Release --self-contained -o "$PKG_DIR/usr/lib/coder-desktop/service"
+
+# Create directory structure
+mkdir -p "$PKG_DIR/usr/bin"
+mkdir -p "$PKG_DIR/usr/share/applications"
+mkdir -p "$PKG_DIR/etc/systemd/system"
+mkdir -p "$PKG_DIR/etc/coder-desktop"
+mkdir -p "$PKG_DIR/DEBIAN"
+
+# Symlinks
+ln -sf "/usr/lib/coder-desktop/service/CoderVpnService" "$PKG_DIR/usr/bin/coder-vpn-service"
+
+# Copy packaging files
+cp "$SCRIPT_DIR/coder-desktop.service" "$PKG_DIR/etc/systemd/system/"
+cp "$SCRIPT_DIR/coder-desktop.desktop" "$PKG_DIR/usr/share/applications/"
+
+# Default config
+cat > "$PKG_DIR/etc/coder-desktop/config.json" <<'EOF'
+{
+ "Manager": {
+ "ServiceRpcSocketPath": "/run/coder-desktop/vpn.sock",
+ "TunnelBinaryPath": "/usr/lib/coder-desktop/coder-vpn",
+ "TunnelBinarySignatureSigner": "",
+ "TunnelBinaryAllowVersionMismatch": false
+ }
+}
+EOF
+
+# Create DEBIAN control file
+cat > "$PKG_DIR/DEBIAN/control" <
+Description: Coder Desktop - Connect to Coder workspaces
+ Provides a VPN tunnel to Coder workspaces with a system
+ service for managing connections.
+Depends: libnotify-bin, libsecret-tools
+Recommends: freerdp2-x11 | remmina
+Section: net
+Priority: optional
+EOF
+
+# Install scripts
+cp "$SCRIPT_DIR/postinst.sh" "$PKG_DIR/DEBIAN/postinst"
+cp "$SCRIPT_DIR/prerm.sh" "$PKG_DIR/DEBIAN/prerm"
+chmod 755 "$PKG_DIR/DEBIAN/postinst" "$PKG_DIR/DEBIAN/prerm"
+
+# Build .deb
+dpkg-deb --build "$PKG_DIR"
+mv "$PKG_DIR.deb" "$ROOT_DIR/coder-desktop_${VERSION}_${ARCH}.deb"
+
+echo "Package built: coder-desktop_${VERSION}_${ARCH}.deb"
+
+# Cleanup
+rm -rf "$BUILD_DIR"
diff --git a/Packaging.Linux/coder-desktop.desktop b/Packaging.Linux/coder-desktop.desktop
new file mode 100644
index 0000000..db8e6c7
--- /dev/null
+++ b/Packaging.Linux/coder-desktop.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Type=Application
+Name=Coder Desktop
+Comment=Connect to Coder workspaces
+Exec=/usr/bin/coder-desktop %u
+Icon=coder-desktop
+Categories=Network;Development;
+MimeType=x-scheme-handler/coder;
+StartupNotify=true
diff --git a/Packaging.Linux/coder-desktop.service b/Packaging.Linux/coder-desktop.service
new file mode 100644
index 0000000..f0c0ca0
--- /dev/null
+++ b/Packaging.Linux/coder-desktop.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=Coder Desktop VPN Service
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=notify
+ExecStart=/usr/lib/coder-desktop/CoderVpnService
+Restart=on-failure
+RestartSec=10
+RuntimeDirectory=coder-desktop
+# Needs root for TUN device creation
+User=root
+
+[Install]
+WantedBy=multi-user.target
diff --git a/Packaging.Linux/postinst.sh b/Packaging.Linux/postinst.sh
new file mode 100755
index 0000000..a4f2c2d
--- /dev/null
+++ b/Packaging.Linux/postinst.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+set -e
+
+# Reload systemd
+systemctl daemon-reload
+
+# Enable and start the service
+systemctl enable coder-desktop.service
+systemctl start coder-desktop.service || true
+
+# Register URI handler
+if command -v xdg-mime &>/dev/null; then
+ xdg-mime default coder-desktop.desktop x-scheme-handler/coder
+fi
+
+# Update desktop database
+if command -v update-desktop-database &>/dev/null; then
+ update-desktop-database /usr/share/applications || true
+fi
diff --git a/Packaging.Linux/prerm.sh b/Packaging.Linux/prerm.sh
new file mode 100755
index 0000000..8bbee2d
--- /dev/null
+++ b/Packaging.Linux/prerm.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -e
+
+# Stop and disable the service
+systemctl stop coder-desktop.service || true
+systemctl disable coder-desktop.service || true
diff --git a/Tests.App/Tests.App.csproj b/Tests.App/Tests.App.csproj
index e20eba1..e9d842e 100644
--- a/Tests.App/Tests.App.csproj
+++ b/Tests.App/Tests.App.csproj
@@ -4,6 +4,7 @@
Coder.Desktop.Tests.App
Coder.Desktop.Tests.App
net8.0-windows10.0.19041.0
+ true
preview
enable
enable
diff --git a/Tests.Vpn.Service/DownloaderTest.cs b/Tests.Vpn.Service/DownloaderTest.cs
index 4d95721..de27806 100644
--- a/Tests.Vpn.Service/DownloaderTest.cs
+++ b/Tests.Vpn.Service/DownloaderTest.cs
@@ -4,6 +4,7 @@
using System.Text;
using Coder.Desktop.Vpn.Service;
using Microsoft.Extensions.Logging.Abstractions;
+using System.Runtime.InteropServices;
namespace Coder.Desktop.Tests.Vpn.Service;
@@ -22,7 +23,9 @@ public Task ValidateAsync(string path, CancellationToken ct = default)
}
}
+#if WINDOWS
[TestFixture]
+[Platform("Win", Reason = "AuthenticodeDownloadValidator requires Windows Authenticode APIs")]
public class AuthenticodeDownloadValidatorTest
{
[Test(Description = "Test an unsigned binary")]
@@ -127,8 +130,10 @@ public void IsEvCert()
}
}
}
+#endif
[TestFixture]
+[Platform("Win", Reason = "AssemblyVersionDownloadValidator tests use Windows PE test binaries")]
public class AssemblyVersionDownloadValidatorTest
{
[Test(Description = "No version on binary")]
diff --git a/Tests.Vpn.Service/TelemetryEnricherTest.cs b/Tests.Vpn.Service/TelemetryEnricherTest.cs
index 144cd20..d5d7169 100644
--- a/Tests.Vpn.Service/TelemetryEnricherTest.cs
+++ b/Tests.Vpn.Service/TelemetryEnricherTest.cs
@@ -19,10 +19,13 @@ public void EnrichStartRequest()
// 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"));
+ var expectedOs = OperatingSystem.IsWindows() ? "Windows" : "Linux";
+ Assert.That(req.DeviceOs, Is.EqualTo(expectedOs));
// 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);
+ // DeviceId may be empty on some Linux CI environments without /etc/machine-id
+ if (OperatingSystem.IsWindows() || File.Exists("/etc/machine-id"))
+ Assert.That(req.DeviceId, Is.Not.Empty);
var deviceId = req.DeviceId;
// deviceId is different on different machines, but we can test that
diff --git a/Tests.Vpn.Service/Tests.Vpn.Service.csproj b/Tests.Vpn.Service/Tests.Vpn.Service.csproj
index c57f85a..668970c 100644
--- a/Tests.Vpn.Service/Tests.Vpn.Service.csproj
+++ b/Tests.Vpn.Service/Tests.Vpn.Service.csproj
@@ -3,15 +3,21 @@
Coder.Desktop.Tests.Vpn.Service
Coder.Desktop.Tests.Vpn.Service
- net8.0-windows
+ net8.0
+ true
enable
+ 12
enable
- true
+ false
false
true
+
+ $(DefineConstants);WINDOWS
+
+
PreserveNewest
diff --git a/Tests.Vpn.Service/packages.lock.json b/Tests.Vpn.Service/packages.lock.json
deleted file mode 100644
index 08a9b56..0000000
--- a/Tests.Vpn.Service/packages.lock.json
+++ /dev/null
@@ -1,525 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net8.0-windows7.0": {
- "coverlet.collector": {
- "type": "Direct",
- "requested": "[6.0.4, )",
- "resolved": "6.0.4",
- "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg=="
- },
- "Microsoft.NET.Test.Sdk": {
- "type": "Direct",
- "requested": "[17.12.0, )",
- "resolved": "17.12.0",
- "contentHash": "kt/PKBZ91rFCWxVIJZSgVLk+YR+4KxTuHf799ho8WNiK5ZQpJNAEZCAWX86vcKrs+DiYjiibpYKdGZP6+/N17w==",
- "dependencies": {
- "Microsoft.CodeCoverage": "17.12.0",
- "Microsoft.TestPlatform.TestHost": "17.12.0"
- }
- },
- "NUnit": {
- "type": "Direct",
- "requested": "[4.3.2, )",
- "resolved": "4.3.2",
- "contentHash": "puVXayXNmEu7MFQSUswGmUjOy3M3baprMbkLl5PAutpeDoGTr+jPv33qAYsqxywi2wJCq8l/O3EhHoLulPE1iQ=="
- },
- "NUnit.Analyzers": {
- "type": "Direct",
- "requested": "[4.6.0, )",
- "resolved": "4.6.0",
- "contentHash": "uK1TEViVBugOO6uDou1amu7CoNhrd2sEUFr/iaEmVfoeY8qq/zzWCCUZi97aCCSZmjnHKCCWKh3RucU27qPlKg=="
- },
- "NUnit3TestAdapter": {
- "type": "Direct",
- "requested": "[4.6.0, )",
- "resolved": "4.6.0",
- "contentHash": "R7e1+a4vuV/YS+ItfL7f//rG+JBvVeVLX4mHzFEZo4W1qEKl8Zz27AqvQSAqo+BtIzUCo4aAJMYa56VXS4hudw=="
- },
- "Google.Protobuf": {
- "type": "Transitive",
- "resolved": "3.29.3",
- "contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
- },
- "Microsoft.CodeCoverage": {
- "type": "Transitive",
- "resolved": "17.12.0",
- "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA=="
- },
- "Microsoft.Extensions.Configuration": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "KIVBrMbItnCJDd1RF4KEaE8jZwDJcDUJW5zXpbwQ05HNYTK1GveHxHK0B3SjgDJuR48GRACXAO+BLhL8h34S7g==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.Binder": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "cdrjcl9RIcwt3ECbnpP0Gt1+pkjdW90mq5yFYy8D9qRj2NqFFcv3yDp141iEamsd9E218sGxK8WHaIOcrqgDJg==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.CommandLine": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "TbM2HElARG7z1gxwakdppmOkm1SykPqDcu3EF97daEwSb/+TXnRrFfJtF+5FWWxcsNhbRrmLfS2WszYcab7u1A==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.EnvironmentVariables": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "2IGiG3FtVnD83IA6HYGuNei8dOw455C09yEhGl8bjcY6aGZgoC6yhYvDnozw8wlTowfoG9bxVrdTsr2ACZOYHg==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.FileExtensions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "UY864WQ3AS2Fkc8fYLombWnjrXwYt+BEHHps0hY4sxlgqaVW06AxbpgRZjfYf8PyRbplJqruzZDB/nSLT+7RLQ==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Physical": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.Json": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "vVXI70CgT/dmXV3MM+n/BR2rLXEoAyoK0hQT+8MrbCMuJBiLRxnTtSrksNiASWCwOtxo/Tyy7CO8AGthbsYxnw==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.FileExtensions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "System.Text.Json": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.UserSecrets": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "zuvyC72gJkJyodyGowCuz3EQ1QvzNXJtKusuRzmjoHr17aeB3X0aSiKFB++HMHnQIWWlPOBf9YHTQfEqzbgl1g==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.Json": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Physical": "9.0.4"
- }
- },
- "Microsoft.Extensions.DependencyInjection": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.DependencyInjection.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg=="
- },
- "Microsoft.Extensions.DependencyModel": {
- "type": "Transitive",
- "resolved": "9.0.0",
- "contentHash": "saxr2XzwgDU77LaQfYFXmddEDRUKHF4DaGMZkNB3qjdVSZlax3//dGJagJkKrGMIPNZs2jVFXITyCCR6UHJNdA==",
- "dependencies": {
- "System.Text.Encodings.Web": "9.0.0",
- "System.Text.Json": "9.0.0"
- }
- },
- "Microsoft.Extensions.Diagnostics": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Diagnostics.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "System.Diagnostics.DiagnosticSource": "9.0.4"
- }
- },
- "Microsoft.Extensions.FileProviders.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "gQN2o/KnBfVk6Bd71E2YsvO5lsqrqHmaepDGk+FB/C4aiQY9B0XKKNKfl5/TqcNOs9OEithm4opiMHAErMFyEw==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.FileProviders.Physical": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "qkQ9V7KFZdTWNThT7ke7E/Jad38s46atSs3QUYZB8f3thBTrcrousdY4Y/tyCtcH5YjsPSiByjuN+L8W/ThMQg==",
- "dependencies": {
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileSystemGlobbing": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.FileSystemGlobbing": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "05Lh2ItSk4mzTdDWATW9nEcSybwprN8Tz42Fs5B+jwdXUpauktdAQUI1Am4sUQi2C63E5hvQp8gXvfwfg9mQGQ=="
- },
- "Microsoft.Extensions.Hosting": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "1rZwLE+tTUIyZRUzmlk/DQj+v+Eqox+rjb+X7Fi+cYTbQfIZPYwpf1pVybsV3oje8+Pe4GaNukpBVUlPYeQdeQ==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.Binder": "9.0.4",
- "Microsoft.Extensions.Configuration.CommandLine": "9.0.4",
- "Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.4",
- "Microsoft.Extensions.Configuration.FileExtensions": "9.0.4",
- "Microsoft.Extensions.Configuration.Json": "9.0.4",
- "Microsoft.Extensions.Configuration.UserSecrets": "9.0.4",
- "Microsoft.Extensions.DependencyInjection": "9.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Diagnostics": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Physical": "9.0.4",
- "Microsoft.Extensions.Hosting.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging.Configuration": "9.0.4",
- "Microsoft.Extensions.Logging.Console": "9.0.4",
- "Microsoft.Extensions.Logging.Debug": "9.0.4",
- "Microsoft.Extensions.Logging.EventLog": "9.0.4",
- "Microsoft.Extensions.Logging.EventSource": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4"
- }
- },
- "Microsoft.Extensions.Hosting.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "bXkwRPMo4x19YKH6/V9XotU7KYQJlihXhcWO1RDclAY3yfY3XNg4QtSEBvng4kK/DnboE0O/nwSl+6Jiv9P+FA==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Hosting.WindowsServices": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "QFeUS0NG4Kwq91Mf1WzVZSbBtw+nKxyOQTi4xTRUEQ2gC7HWiyCUiX0arMJxt9lWwbjXxQY9TQjDptm+ct7BkQ==",
- "dependencies": {
- "Microsoft.Extensions.Hosting": "9.0.4",
- "Microsoft.Extensions.Logging.EventLog": "9.0.4",
- "System.ServiceProcess.ServiceController": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "System.Diagnostics.DiagnosticSource": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.Configuration": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "/kF+rSnoo3/nIwGzWsR4RgBnoTOdZ3lzz2qFRyp/GgaNid4j6hOAQrs/O+QHXhlcAdZxjg37MvtIE+pAvIgi9g==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.Binder": "9.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.Console": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "cI0lQe0js65INCTCtAgnlVJWKgzgoRHVAW1B1zwCbmcliO4IZoTf92f1SYbLeLk7FzMJ/GlCvjLvJegJ6kltmQ==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging.Configuration": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "System.Text.Json": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.Debug": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "D1jy+jy+huUUxnkZ0H480RZK8vqKn8NsQxYpMpPL/ALPPh1WATVLcr/uXI3RUBB45wMW5265O+hk9x3jnnXFuA==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.EventLog": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "bApxdklf7QTsONOLR5ow6SdDFXR5ncHvumSEg2+QnCvxvkzc2z5kNn7yQCyupRLRN4jKbnlTkVX8x9qLlwL6Qg==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "System.Diagnostics.EventLog": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.EventSource": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "R600zTxVJNw2IeAEOvdOJGNA1lHr1m3vo460hSF5G1DjwP0FNpyeH4lpLDMuf34diKwB1LTt5hBw1iF1/iuwsQ==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4",
- "System.Text.Json": "9.0.4"
- }
- },
- "Microsoft.Extensions.Options": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Options.ConfigurationExtensions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "aridVhAT3Ep+vsirR1pzjaOw0Jwiob6dc73VFQn2XmDfBA2X98M8YKO1GarvsXRX7gX1Aj+hj2ijMzrMHDOm0A==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.Binder": "9.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Options.DataAnnotations": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "jJq7xO1PLi//cts59Yp6dKNN07xV0Day/JmVR7aXCdo2rYHAoFlyARyxrfB0CTzsErA+TOhYTz2Ee0poR8SPeQ==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4"
- }
- },
- "Microsoft.Extensions.Primitives": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
- },
- "Microsoft.Security.Extensions": {
- "type": "Transitive",
- "resolved": "1.3.0",
- "contentHash": "xK8WFEo5WMUE8DI8W+GjhRwpVcPrxc4DyTjfxh39+yOyhAtC5TBHDlFEJks5toNZHsUeUuiWELIX25oTWOKPBw=="
- },
- "Microsoft.TestPlatform.ObjectModel": {
- "type": "Transitive",
- "resolved": "17.12.0",
- "contentHash": "TDqkTKLfQuAaPcEb3pDDWnh7b3SyZF+/W9OZvWFp6eJCIiiYFdSB6taE2I6tWrFw5ywhzOb6sreoGJTI6m3rSQ==",
- "dependencies": {
- "System.Reflection.Metadata": "1.6.0"
- }
- },
- "Microsoft.TestPlatform.TestHost": {
- "type": "Transitive",
- "resolved": "17.12.0",
- "contentHash": "MiPEJQNyADfwZ4pJNpQex+t9/jOClBGMiCiVVFuELCMSX2nmNfvUor3uFVxNNCg30uxDP8JDYfPnMXQzsfzYyg==",
- "dependencies": {
- "Microsoft.TestPlatform.ObjectModel": "17.12.0",
- "Newtonsoft.Json": "13.0.1"
- }
- },
- "Newtonsoft.Json": {
- "type": "Transitive",
- "resolved": "13.0.1",
- "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
- },
- "Semver": {
- "type": "Transitive",
- "resolved": "3.0.0",
- "contentHash": "9jZCicsVgTebqkAujRWtC9J1A5EQVlu0TVKHcgoCuv345ve5DYf4D1MjhKEnQjdRZo6x/vdv6QQrYFs7ilGzLA==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "5.0.1"
- }
- },
- "Serilog": {
- "type": "Transitive",
- "resolved": "4.2.0",
- "contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA=="
- },
- "Serilog.Extensions.Hosting": {
- "type": "Transitive",
- "resolved": "9.0.0",
- "contentHash": "u2TRxuxbjvTAldQn7uaAwePkWxTHIqlgjelekBtilAGL5sYyF3+65NWctN4UrwwGLsDC7c3Vz3HnOlu+PcoxXg==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
- "Microsoft.Extensions.Hosting.Abstractions": "9.0.0",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.0",
- "Serilog": "4.2.0",
- "Serilog.Extensions.Logging": "9.0.0"
- }
- },
- "Serilog.Extensions.Logging": {
- "type": "Transitive",
- "resolved": "9.0.0",
- "contentHash": "NwSSYqPJeKNzl5AuXVHpGbr6PkZJFlNa14CdIebVjK3k/76kYj/mz5kiTRNVSsSaxM8kAIa1kpy/qyT9E4npRQ==",
- "dependencies": {
- "Microsoft.Extensions.Logging": "9.0.0",
- "Serilog": "4.2.0"
- }
- },
- "Serilog.Settings.Configuration": {
- "type": "Transitive",
- "resolved": "9.0.0",
- "contentHash": "4/Et4Cqwa+F88l5SeFeNZ4c4Z6dEAIKbu3MaQb2Zz9F/g27T5a3wvfMcmCOaAiACjfUb4A6wrlTVfyYUZk3RRQ==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Binder": "9.0.0",
- "Microsoft.Extensions.DependencyModel": "9.0.0",
- "Serilog": "4.2.0"
- }
- },
- "Serilog.Sinks.Console": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "fQGWqVMClCP2yEyTXPIinSr5c+CBGUvBybPxjAGcf7ctDhadFhrQw03Mv8rJ07/wR5PDfFjewf2LimvXCDzpbA==",
- "dependencies": {
- "Serilog": "4.0.0"
- }
- },
- "Serilog.Sinks.File": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "lxjg89Y8gJMmFxVkbZ+qDgjl+T4yC5F7WSLTvA+5q0R04tfKVLRL/EHpYoJ/MEQd2EeCKDuylBIVnAYMotmh2A==",
- "dependencies": {
- "Serilog": "4.0.0"
- }
- },
- "System.Diagnostics.DiagnosticSource": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "Be0emq8bRmcK4eeJIFUt9+vYPf7kzuQrFs8Ef1CdGvXpq/uSve22PTSkRF09bF/J7wmYJ2DHf2v7GaT3vMXnwQ=="
- },
- "System.Diagnostics.EventLog": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "getRQEXD8idlpb1KW56XuxImMy0FKp2WJPDf3Qr0kI/QKxxJSftqfDFVo0DZ3HCJRLU73qHSruv5q2l5O47jQQ=="
- },
- "System.IO.Pipelines": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "luF2Xba+lTe2GOoNQdZLe8q7K6s7nSpWZl9jIwWNMszN4/Yv0lmxk9HISgMmwdyZ83i3UhAGXaSY9o6IJBUuuA=="
- },
- "System.Reflection.Metadata": {
- "type": "Transitive",
- "resolved": "1.6.0",
- "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ=="
- },
- "System.ServiceProcess.ServiceController": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "j6Z+ED1d/yxe/Cm+UlFf+LNw2HSYBSgtFh71KnEEmUtHIwgoTVQxji5URvXPOAZ7iuKHItjMIzpCLyRZc8OmrQ==",
- "dependencies": {
- "System.Diagnostics.EventLog": "9.0.4"
- }
- },
- "System.Text.Encodings.Web": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "V+5cCPpk1S2ngekUs9nDrQLHGiWFZMg8BthADQr+Fwi59a8DdHFu26S2oi9Bfgv+d67bqmkPqctJXMEXiimXUg=="
- },
- "System.Text.Json": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g==",
- "dependencies": {
- "System.IO.Pipelines": "9.0.4",
- "System.Text.Encodings.Web": "9.0.4"
- }
- },
- "Coder.Desktop.CoderSdk": {
- "type": "Project"
- },
- "Coder.Desktop.Vpn": {
- "type": "Project",
- "dependencies": {
- "Coder.Desktop.Vpn.Proto": "[1.0.0, )",
- "Microsoft.Extensions.Configuration": "[9.0.1, )",
- "Semver": "[3.0.0, )",
- "System.IO.Pipelines": "[9.0.1, )"
- }
- },
- "Coder.Desktop.Vpn.Proto": {
- "type": "Project",
- "dependencies": {
- "Google.Protobuf": "[3.29.3, )"
- }
- },
- "CoderVpnService": {
- "type": "Project",
- "dependencies": {
- "Coder.Desktop.CoderSdk": "[1.0.0, )",
- "Coder.Desktop.Vpn": "[1.0.0, )",
- "Microsoft.Extensions.Hosting": "[9.0.4, )",
- "Microsoft.Extensions.Hosting.WindowsServices": "[9.0.4, )",
- "Microsoft.Extensions.Options.DataAnnotations": "[9.0.4, )",
- "Microsoft.Security.Extensions": "[1.3.0, )",
- "Semver": "[3.0.0, )",
- "Serilog.Extensions.Hosting": "[9.0.0, )",
- "Serilog.Settings.Configuration": "[9.0.0, )",
- "Serilog.Sinks.Console": "[6.0.0, )",
- "Serilog.Sinks.File": "[6.0.0, )"
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Tests.Vpn/Tests.Vpn.csproj b/Tests.Vpn/Tests.Vpn.csproj
index 2b9e30f..bdae868 100644
--- a/Tests.Vpn/Tests.Vpn.csproj
+++ b/Tests.Vpn/Tests.Vpn.csproj
@@ -3,10 +3,10 @@
Coder.Desktop.Tests.Vpn
Coder.Desktop.Tests.Vpn
- net8.0-windows
+ net8.0
enable
enable
- true
+ false
false
true
diff --git a/Tests.Vpn/packages.lock.json b/Tests.Vpn/packages.lock.json
deleted file mode 100644
index 725c743..0000000
--- a/Tests.Vpn/packages.lock.json
+++ /dev/null
@@ -1,128 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net8.0-windows7.0": {
- "coverlet.collector": {
- "type": "Direct",
- "requested": "[6.0.4, )",
- "resolved": "6.0.4",
- "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg=="
- },
- "Microsoft.NET.Test.Sdk": {
- "type": "Direct",
- "requested": "[17.12.0, )",
- "resolved": "17.12.0",
- "contentHash": "kt/PKBZ91rFCWxVIJZSgVLk+YR+4KxTuHf799ho8WNiK5ZQpJNAEZCAWX86vcKrs+DiYjiibpYKdGZP6+/N17w==",
- "dependencies": {
- "Microsoft.CodeCoverage": "17.12.0",
- "Microsoft.TestPlatform.TestHost": "17.12.0"
- }
- },
- "NUnit": {
- "type": "Direct",
- "requested": "[4.3.2, )",
- "resolved": "4.3.2",
- "contentHash": "puVXayXNmEu7MFQSUswGmUjOy3M3baprMbkLl5PAutpeDoGTr+jPv33qAYsqxywi2wJCq8l/O3EhHoLulPE1iQ=="
- },
- "NUnit.Analyzers": {
- "type": "Direct",
- "requested": "[4.6.0, )",
- "resolved": "4.6.0",
- "contentHash": "uK1TEViVBugOO6uDou1amu7CoNhrd2sEUFr/iaEmVfoeY8qq/zzWCCUZi97aCCSZmjnHKCCWKh3RucU27qPlKg=="
- },
- "NUnit3TestAdapter": {
- "type": "Direct",
- "requested": "[4.6.0, )",
- "resolved": "4.6.0",
- "contentHash": "R7e1+a4vuV/YS+ItfL7f//rG+JBvVeVLX4mHzFEZo4W1qEKl8Zz27AqvQSAqo+BtIzUCo4aAJMYa56VXS4hudw=="
- },
- "Google.Protobuf": {
- "type": "Transitive",
- "resolved": "3.29.3",
- "contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
- },
- "Microsoft.CodeCoverage": {
- "type": "Transitive",
- "resolved": "17.12.0",
- "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA=="
- },
- "Microsoft.Extensions.Configuration": {
- "type": "Transitive",
- "resolved": "9.0.1",
- "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
- "Microsoft.Extensions.Primitives": "9.0.1"
- }
- },
- "Microsoft.Extensions.Configuration.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.1",
- "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "9.0.1"
- }
- },
- "Microsoft.Extensions.Primitives": {
- "type": "Transitive",
- "resolved": "9.0.1",
- "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
- },
- "Microsoft.TestPlatform.ObjectModel": {
- "type": "Transitive",
- "resolved": "17.12.0",
- "contentHash": "TDqkTKLfQuAaPcEb3pDDWnh7b3SyZF+/W9OZvWFp6eJCIiiYFdSB6taE2I6tWrFw5ywhzOb6sreoGJTI6m3rSQ==",
- "dependencies": {
- "System.Reflection.Metadata": "1.6.0"
- }
- },
- "Microsoft.TestPlatform.TestHost": {
- "type": "Transitive",
- "resolved": "17.12.0",
- "contentHash": "MiPEJQNyADfwZ4pJNpQex+t9/jOClBGMiCiVVFuELCMSX2nmNfvUor3uFVxNNCg30uxDP8JDYfPnMXQzsfzYyg==",
- "dependencies": {
- "Microsoft.TestPlatform.ObjectModel": "17.12.0",
- "Newtonsoft.Json": "13.0.1"
- }
- },
- "Newtonsoft.Json": {
- "type": "Transitive",
- "resolved": "13.0.1",
- "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
- },
- "Semver": {
- "type": "Transitive",
- "resolved": "3.0.0",
- "contentHash": "9jZCicsVgTebqkAujRWtC9J1A5EQVlu0TVKHcgoCuv345ve5DYf4D1MjhKEnQjdRZo6x/vdv6QQrYFs7ilGzLA==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "5.0.1"
- }
- },
- "System.IO.Pipelines": {
- "type": "Transitive",
- "resolved": "9.0.1",
- "contentHash": "uXf5o8eV/gtzDQY4lGROLFMWQvcViKcF8o4Q6KpIOjloAQXrnscQSu6gTxYJMHuNJnh7szIF9AzkaEq+zDLoEg=="
- },
- "System.Reflection.Metadata": {
- "type": "Transitive",
- "resolved": "1.6.0",
- "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ=="
- },
- "Coder.Desktop.Vpn": {
- "type": "Project",
- "dependencies": {
- "Coder.Desktop.Vpn.Proto": "[1.0.0, )",
- "Microsoft.Extensions.Configuration": "[9.0.1, )",
- "Semver": "[3.0.0, )",
- "System.IO.Pipelines": "[9.0.1, )"
- }
- },
- "Coder.Desktop.Vpn.Proto": {
- "type": "Project",
- "dependencies": {
- "Google.Protobuf": "[3.29.3, )"
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Vpn.DebugClient/Program.cs b/Vpn.DebugClient/Program.cs
index 9facc85..fbeeedc 100644
--- a/Vpn.DebugClient/Program.cs
+++ b/Vpn.DebugClient/Program.cs
@@ -1,4 +1,5 @@
using System.IO.Pipes;
+using System.Net.Sockets;
using Coder.Desktop.Vpn.Proto;
namespace Coder.Desktop.Vpn.DebugClient;
@@ -54,11 +55,23 @@ public static void Main()
private static void Connect()
{
- var client = new NamedPipeClientStream(".", "Coder.Desktop.Vpn", PipeDirection.InOut, PipeOptions.Asynchronous);
- client.Connect();
- Console.WriteLine("Connected to named pipe.");
+ Stream stream;
+ if (OperatingSystem.IsWindows())
+ {
+ var client = new NamedPipeClientStream(".", "Coder.Desktop.Vpn", PipeDirection.InOut, PipeOptions.Asynchronous);
+ client.Connect();
+ stream = client;
+ }
+ else
+ {
+ var socketPath = "/run/coder-desktop/vpn.sock";
+ var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
+ socket.Connect(new UnixDomainSocketEndPoint(socketPath));
+ stream = new NetworkStream(socket, ownsSocket: true);
+ }
+ Console.WriteLine("Connected to RPC server.");
- _speaker = new Speaker(client);
+ _speaker = new Speaker(stream);
_speaker.Receive += message => { Console.WriteLine($"Received({message.Message.MsgCase}: {message.Message}"); };
_speaker.Error += exception =>
{
diff --git a/Vpn.DebugClient/Vpn.DebugClient.csproj b/Vpn.DebugClient/Vpn.DebugClient.csproj
index 0eda43d..bd7bedf 100644
--- a/Vpn.DebugClient/Vpn.DebugClient.csproj
+++ b/Vpn.DebugClient/Vpn.DebugClient.csproj
@@ -1,13 +1,13 @@
-
+
Coder.Desktop.Vpn.DebugClient
Coder.Desktop.Vpn.DebugClient
Exe
- net8.0-windows
+ net8.0
enable
enable
- true
+ false
diff --git a/Vpn.DebugClient/packages.lock.json b/Vpn.DebugClient/packages.lock.json
deleted file mode 100644
index 473422b..0000000
--- a/Vpn.DebugClient/packages.lock.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net8.0-windows7.0": {
- "Google.Protobuf": {
- "type": "Transitive",
- "resolved": "3.29.3",
- "contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
- },
- "Microsoft.Extensions.Configuration": {
- "type": "Transitive",
- "resolved": "9.0.1",
- "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
- "Microsoft.Extensions.Primitives": "9.0.1"
- }
- },
- "Microsoft.Extensions.Configuration.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.1",
- "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "9.0.1"
- }
- },
- "Microsoft.Extensions.Primitives": {
- "type": "Transitive",
- "resolved": "9.0.1",
- "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
- },
- "Semver": {
- "type": "Transitive",
- "resolved": "3.0.0",
- "contentHash": "9jZCicsVgTebqkAujRWtC9J1A5EQVlu0TVKHcgoCuv345ve5DYf4D1MjhKEnQjdRZo6x/vdv6QQrYFs7ilGzLA==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "5.0.1"
- }
- },
- "System.IO.Pipelines": {
- "type": "Transitive",
- "resolved": "9.0.1",
- "contentHash": "uXf5o8eV/gtzDQY4lGROLFMWQvcViKcF8o4Q6KpIOjloAQXrnscQSu6gTxYJMHuNJnh7szIF9AzkaEq+zDLoEg=="
- },
- "Coder.Desktop.Vpn": {
- "type": "Project",
- "dependencies": {
- "Coder.Desktop.Vpn.Proto": "[1.0.0, )",
- "Microsoft.Extensions.Configuration": "[9.0.1, )",
- "Semver": "[3.0.0, )",
- "System.IO.Pipelines": "[9.0.1, )"
- }
- },
- "Coder.Desktop.Vpn.Proto": {
- "type": "Project",
- "dependencies": {
- "Google.Protobuf": "[3.29.3, )"
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Vpn.Linux/UnixSocketClientTransport.cs b/Vpn.Linux/UnixSocketClientTransport.cs
new file mode 100644
index 0000000..491e9b2
--- /dev/null
+++ b/Vpn.Linux/UnixSocketClientTransport.cs
@@ -0,0 +1,30 @@
+using System.Net.Sockets;
+using System.Runtime.Versioning;
+
+namespace Coder.Desktop.Vpn;
+
+[SupportedOSPlatform("linux")]
+public class UnixSocketClientTransport : IRpcClientTransport
+{
+ private readonly string _socketPath;
+
+ public UnixSocketClientTransport(string socketPath = "/run/coder-desktop/vpn.sock")
+ {
+ _socketPath = socketPath;
+ }
+
+ public async Task ConnectAsync(CancellationToken ct)
+ {
+ var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
+ try
+ {
+ await socket.ConnectAsync(new UnixDomainSocketEndPoint(_socketPath), ct);
+ return new NetworkStream(socket, ownsSocket: true);
+ }
+ catch
+ {
+ socket.Dispose();
+ throw;
+ }
+ }
+}
diff --git a/Vpn.Linux/UnixSocketServerTransport.cs b/Vpn.Linux/UnixSocketServerTransport.cs
new file mode 100644
index 0000000..e9fbf83
--- /dev/null
+++ b/Vpn.Linux/UnixSocketServerTransport.cs
@@ -0,0 +1,62 @@
+using System.Net.Sockets;
+using System.Runtime.Versioning;
+
+namespace Coder.Desktop.Vpn;
+
+[SupportedOSPlatform("linux")]
+public class UnixSocketServerTransport : IRpcServerTransport
+{
+ private readonly string _socketPath;
+ private Socket? _listener;
+
+ public UnixSocketServerTransport(string socketPath = "/run/coder-desktop/vpn.sock")
+ {
+ _socketPath = socketPath;
+ }
+
+ public async Task AcceptAsync(CancellationToken ct)
+ {
+ if (_listener == null)
+ {
+ // Clean up stale socket file
+ if (File.Exists(_socketPath))
+ File.Delete(_socketPath);
+
+ // Ensure parent directory exists
+ var dir = Path.GetDirectoryName(_socketPath);
+ if (dir != null && !Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+
+ _listener = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
+ _listener.Bind(new UnixDomainSocketEndPoint(_socketPath));
+
+ // Set permissions so all users can connect (equivalent to WorldSid on Windows)
+ File.SetUnixFileMode(_socketPath,
+ UnixFileMode.UserRead | UnixFileMode.UserWrite |
+ UnixFileMode.GroupRead | UnixFileMode.GroupWrite |
+ UnixFileMode.OtherRead | UnixFileMode.OtherWrite);
+
+ _listener.Listen(5);
+ }
+
+ var client = await _listener.AcceptAsync(ct);
+ return new NetworkStream(client, ownsSocket: true);
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ if (_listener != null)
+ {
+ _listener.Close();
+ _listener.Dispose();
+ _listener = null;
+ }
+
+ if (File.Exists(_socketPath))
+ {
+ try { File.Delete(_socketPath); } catch { /* best effort */ }
+ }
+
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/Vpn.Linux/Vpn.Linux.csproj b/Vpn.Linux/Vpn.Linux.csproj
new file mode 100644
index 0000000..dd18f39
--- /dev/null
+++ b/Vpn.Linux/Vpn.Linux.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Coder.Desktop.Vpn.Linux
+ Coder.Desktop.Vpn
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Vpn.Service/Downloader.cs b/Vpn.Service/Downloader.cs
index c4a916f..5727e71 100644
--- a/Vpn.Service/Downloader.cs
+++ b/Vpn.Service/Downloader.cs
@@ -1,14 +1,20 @@
using System.Collections.Concurrent;
using System.Diagnostics;
+#if WINDOWS
using System.Formats.Asn1;
+#endif
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Security.Cryptography;
+#if WINDOWS
using System.Security.Cryptography.X509Certificates;
+#endif
using Coder.Desktop.Vpn.Utilities;
using Microsoft.Extensions.Logging;
+#if WINDOWS
using Microsoft.Security.Extensions;
+#endif
namespace Coder.Desktop.Vpn.Service;
@@ -38,6 +44,7 @@ public Task ValidateAsync(string path, CancellationToken ct = default)
}
}
+#if WINDOWS
///
/// Ensures the downloaded binary is signed by the expected authenticode organization.
///
@@ -180,6 +187,7 @@ private static void Assert(bool condition, string message)
throw new Exception("Failed certificate parse assertion: " + message);
}
}
+#endif
public class AssemblyVersionDownloadValidator : IDownloadValidator
{
diff --git a/Vpn.Service/Manager.cs b/Vpn.Service/Manager.cs
index 027a882..7a273f3 100644
--- a/Vpn.Service/Manager.cs
+++ b/Vpn.Service/Manager.cs
@@ -403,7 +403,7 @@ private async ValueTask CheckServerVersionAndCredentials(string b
}
///
- /// Fetches the "/bin/coder-windows-{architecture}.exe" binary from the given base URL and writes it to the
+ /// Fetches the "/bin/coder-{os}-{architecture}" binary from the given base URL and writes it to the
/// destination path after validating the signature and checksum.
///
/// Server base URL to download the binary from
@@ -420,7 +420,9 @@ private async Task DownloadTunnelBinaryAsync(string baseUrl, SemVersion expected
url = new Uri(baseUrl, UriKind.Absolute);
if (url.PathAndQuery != "/")
throw new ArgumentException("Base URL must not contain a path", nameof(baseUrl));
- url = new Uri(url, $"/bin/coder-windows-{architecture}.exe");
+ var osName = OperatingSystem.IsWindows() ? "windows" : "linux";
+ var extension = OperatingSystem.IsWindows() ? ".exe" : "";
+ url = new Uri(url, $"/bin/coder-{osName}-{architecture}{extension}");
}
catch (Exception e)
{
@@ -434,9 +436,18 @@ private async Task DownloadTunnelBinaryAsync(string baseUrl, SemVersion expected
var validators = new CombinationDownloadValidator();
if (!string.IsNullOrEmpty(_config.TunnelBinarySignatureSigner))
{
- _logger.LogDebug("Adding Authenticode signature validator for signer '{Signer}'",
- _config.TunnelBinarySignatureSigner);
- validators.Add(new AuthenticodeDownloadValidator(_config.TunnelBinarySignatureSigner));
+ if (OperatingSystem.IsWindows())
+ {
+#if WINDOWS
+ _logger.LogDebug("Adding Authenticode signature validator for signer '{Signer}'",
+ _config.TunnelBinarySignatureSigner);
+ validators.Add(new AuthenticodeDownloadValidator(_config.TunnelBinarySignatureSigner));
+#endif
+ }
+ else
+ {
+ _logger.LogDebug("Authenticode validation is only available on Windows, skipping");
+ }
}
else
{
diff --git a/Vpn.Service/ManagerConfig.cs b/Vpn.Service/ManagerConfig.cs
index c60b1b8..a078f9c 100644
--- a/Vpn.Service/ManagerConfig.cs
+++ b/Vpn.Service/ManagerConfig.cs
@@ -2,21 +2,26 @@
namespace Coder.Desktop.Vpn.Service;
-// These values are the config option names used in the registry. Any option
-// here can be configured with `(Debug)?Manager:OptionName` in the registry.
-//
-// They should not be changed without backwards compatibility considerations.
-// If changed here, they should also be changed in the installer.
public class ManagerConfig
{
[Required]
[RegularExpression(@"^([a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+$")]
public string ServiceRpcPipeName { get; set; } = "Coder.Desktop.Vpn";
- [Required] public string TunnelBinaryPath { get; set; } = @"C:\coder-vpn.exe";
+ ///
+ /// Path to the Unix domain socket for RPC (Linux only).
+ /// If empty, defaults to /run/coder-desktop/vpn.sock.
+ ///
+ public string ServiceRpcSocketPath { get; set; } = "";
+
+ [Required] public string TunnelBinaryPath { get; set; } = OperatingSystem.IsWindows()
+ ? @"C:\coder-vpn.exe"
+ : "/usr/lib/coder-desktop/coder-vpn";
// If empty, signatures will not be verified.
- [Required] public string TunnelBinarySignatureSigner { get; set; } = "Coder Technologies Inc.";
+ [Required] public string TunnelBinarySignatureSigner { get; set; } = OperatingSystem.IsWindows()
+ ? "Coder Technologies Inc."
+ : ""; // No Authenticode on Linux
[Required] public bool TunnelBinaryAllowVersionMismatch { get; set; } = false;
}
diff --git a/Vpn.Service/ManagerRpc.cs b/Vpn.Service/ManagerRpc.cs
index 4920570..36d3fe1 100644
--- a/Vpn.Service/ManagerRpc.cs
+++ b/Vpn.Service/ManagerRpc.cs
@@ -1,10 +1,7 @@
using System.Collections.Concurrent;
-using System.IO.Pipes;
-using System.Security.AccessControl;
-using System.Security.Principal;
+using Coder.Desktop.Vpn;
using Coder.Desktop.Vpn.Proto;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
namespace Coder.Desktop.Vpn.Service;
@@ -22,28 +19,26 @@ delegate Task OnReceiveHandler(ulong clientId, ReplyableRpcMessage
-/// Provides a named pipe server for communication between multiple RpcRole.Client and RpcRole.Manager.
+/// Provides an RPC server for communication between multiple clients and the Manager.
+/// Uses IRpcServerTransport for platform-specific transport (Named Pipes on Windows, Unix Sockets on Linux).
///
public class ManagerRpc : IManagerRpc
{
private readonly ConcurrentDictionary _activeClients = new();
- private readonly ManagerConfig _config;
private readonly CancellationTokenSource _cts = new();
private readonly ILogger _logger;
+ private readonly IRpcServerTransport _transport;
private ulong _lastClientId;
- // ReSharper disable once ConvertToPrimaryConstructor
- public ManagerRpc(IOptions config, ILogger logger)
+ public ManagerRpc(IRpcServerTransport transport, ILogger logger)
{
_logger = logger;
- _config = config.Value;
+ _transport = transport;
}
public event IManagerRpc.OnReceiveHandler? OnReceive;
@@ -60,6 +55,7 @@ public async ValueTask DisposeAsync()
{
}
+ await _transport.DisposeAsync();
_cts.Dispose();
GC.SuppressFinalize(this);
}
@@ -71,87 +67,61 @@ public async Task StopAsync(CancellationToken cancellationToken)
}
///
- /// Starts the named pipe server, listens for incoming connections and starts handling them asynchronously.
+ /// Starts the RPC server, listens for incoming connections and starts handling them asynchronously.
///
public async Task ExecuteAsync(CancellationToken stoppingToken)
{
- _logger.LogInformation(@"Starting continuous named pipe RPC server at \\.\pipe\{PipeName}",
- _config.ServiceRpcPipeName);
-
- // Allow everyone to connect to the named pipe
- var pipeSecurity = new PipeSecurity();
- pipeSecurity.AddAccessRule(new PipeAccessRule(
- new SecurityIdentifier(WellKnownSidType.WorldSid, null),
- PipeAccessRights.FullControl,
- AccessControlType.Allow));
-
- // Starting a named pipe server is not like a TCP server where you can
- // continuously accept new connections. You need to recreate the server
- // after accepting a connection in order to accept new connections.
+ _logger.LogInformation("Starting RPC server");
+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, _cts.Token);
while (!linkedCts.IsCancellationRequested)
{
- var pipeServer = NamedPipeServerStreamAcl.Create(_config.ServiceRpcPipeName, PipeDirection.InOut,
- NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, 0,
- 0, pipeSecurity);
-
+ Stream stream;
try
{
- _logger.LogDebug("Waiting for new named pipe client connection");
- await pipeServer.WaitForConnectionAsync(linkedCts.Token);
-
- var clientId = Interlocked.Add(ref _lastClientId, 1);
- _logger.LogInformation("Handling named pipe client connection for client {ClientId}", clientId);
- var speaker = new Speaker(pipeServer);
- var clientTask = HandleRpcClientAsync(clientId, speaker, linkedCts.Token);
- _activeClients.TryAdd(clientId, new ManagerRpcClient(speaker, clientTask));
- _ = clientTask.ContinueWith(task =>
- {
- if (task.IsFaulted)
- _logger.LogWarning(task.Exception, "Client {ClientId} RPC task faulted", clientId);
- _activeClients.TryRemove(clientId, out _);
- }, CancellationToken.None);
+ _logger.LogDebug("Waiting for new client connection");
+ stream = await _transport.AcceptAsync(linkedCts.Token);
}
catch (OperationCanceledException)
{
- await pipeServer.DisposeAsync();
throw;
}
catch (Exception e)
{
- _logger.LogWarning(e, "Failed to accept named pipe client");
- await pipeServer.DisposeAsync();
+ _logger.LogWarning(e, "Failed to accept client connection");
+ continue;
}
+
+ var clientId = Interlocked.Add(ref _lastClientId, 1);
+ _logger.LogInformation("Handling client connection for client {ClientId}", clientId);
+ var speaker = new Speaker(stream);
+ var clientTask = HandleRpcClientAsync(clientId, speaker, linkedCts.Token);
+ _activeClients.TryAdd(clientId, new ManagerRpcClient(speaker, clientTask));
+ _ = clientTask.ContinueWith(task =>
+ {
+ if (task.IsFaulted)
+ _logger.LogWarning(task.Exception, "Client {ClientId} RPC task faulted", clientId);
+ _activeClients.TryRemove(clientId, out _);
+ }, CancellationToken.None);
}
}
public async Task BroadcastAsync(ServiceMessage message, CancellationToken ct)
{
- // Sends messages to all clients simultaneously and waits for them all
- // to send or fail/timeout.
- //
- // Looping over a ConcurrentDictionary is exception-safe, but any items
- // added or removed during the loop may or may not be included.
await Task.WhenAll(_activeClients.Select(async item =>
{
try
{
- // Enforce upper bound in case a CT with a timeout wasn't
- // supplied.
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(2));
await item.Value.Speaker.SendMessage(message, cts.Token);
}
catch (ObjectDisposedException)
{
- // The speaker was likely closed while we were iterating.
}
catch (Exception e)
{
_logger.LogWarning(e, "Failed to send message to client {ClientId}", item.Key);
- // TODO: this should probably kill the client, but due to the
- // async nature of the client handling, calling Dispose
- // will not remove the client from the active clients list
}
}));
}
diff --git a/Vpn.Service/Program.cs b/Vpn.Service/Program.cs
index 094875d..0e4c724 100644
--- a/Vpn.Service/Program.cs
+++ b/Vpn.Service/Program.cs
@@ -1,7 +1,7 @@
+using Coder.Desktop.Vpn;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using Microsoft.Win32;
using Serilog;
using ILogger = Serilog.ILogger;
@@ -9,18 +9,11 @@ namespace Coder.Desktop.Vpn.Service;
public static class Program
{
- // These values are the service name and the prefix on registry value names.
- // They should not be changed without backwards compatibility
- // considerations. If changed here, they should also be changed in the
- // installer.
#if !DEBUG
private const string ServiceName = "Coder Desktop";
- private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\VpnService";
private const string DefaultLogLevel = "Information";
#else
- // This value matches Create-Service.ps1.
private const string ServiceName = "Coder Desktop (Debug)";
- private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\DebugVpnService";
private const string DefaultLogLevel = "Debug";
#endif
@@ -30,7 +23,6 @@ public static class Program
public static async Task Main(string[] args)
{
- // This logger will only be used until we load our full logging configuration and replace it.
Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console()
.CreateLogger();
MainLogger.Information("Application is starting");
@@ -59,12 +51,11 @@ private static async Task BuildAndRun(string[] args)
// Configuration sources
builder.Configuration.Sources.Clear();
AddDefaultConfig(configBuilder);
- configBuilder.Add(
- new RegistryConfigurationSource(Registry.LocalMachine, ConfigSubKey));
+ AddPlatformConfig(configBuilder);
builder.Configuration.AddEnvironmentVariables("CODER_MANAGER_");
builder.Configuration.AddCommandLine(args);
- // Options types (these get registered as IOptions singletons)
+ // Options types
builder.Services.AddOptions()
.Bind(builder.Configuration.GetSection(ManagerConfigSection))
.ValidateDataAnnotations();
@@ -75,6 +66,9 @@ private static async Task BuildAndRun(string[] args)
loggerConfig.ReadFrom.Configuration(builder.Configuration);
});
+ // Platform-specific services
+ RegisterPlatformServices(builder);
+
// Singletons
builder.Services.AddSingleton();
builder.Services.AddSingleton();
@@ -86,17 +80,6 @@ private static async Task BuildAndRun(string[] args)
builder.Services.AddHostedService();
builder.Services.AddHostedService();
- // Either run as a Windows service or a console application
- if (!Environment.UserInteractive)
- {
- MainLogger.Information("Running as a windows service");
- builder.Services.AddWindowsService(options => { options.ServiceName = ServiceName; });
- }
- else
- {
- MainLogger.Information("Running as a console application");
- }
-
var host = builder.Build();
Log.Logger = (ILogger)host.Services.GetService(typeof(ILogger))!;
MainLogger.Information("Application is starting");
@@ -104,8 +87,76 @@ private static async Task BuildAndRun(string[] args)
await host.RunAsync();
}
+ private static void AddPlatformConfig(IConfigurationBuilder builder)
+ {
+#if WINDOWS
+ if (OperatingSystem.IsWindows())
+ {
+#if !DEBUG
+ const string configSubKey = @"SOFTWARE\Coder Desktop\VpnService";
+#else
+ const string configSubKey = @"SOFTWARE\Coder Desktop\DebugVpnService";
+#endif
+ builder.Add(new RegistryConfigurationSource(
+ Microsoft.Win32.Registry.LocalMachine, configSubKey));
+ }
+#else
+ if (OperatingSystem.IsLinux())
+ {
+ builder.AddJsonFile("/etc/coder-desktop/config.json", optional: true, reloadOnChange: false);
+ }
+#endif
+ }
+
+ private static void RegisterPlatformServices(HostApplicationBuilder builder)
+ {
+#if WINDOWS
+ if (OperatingSystem.IsWindows())
+ {
+ if (!Environment.UserInteractive)
+ {
+ MainLogger.Information("Running as a Windows service");
+ builder.Services.AddWindowsService(options => { options.ServiceName = ServiceName; });
+ }
+ else
+ {
+ MainLogger.Information("Running as a console application");
+ }
+
+ // Register Windows named pipe transport
+ builder.Services.AddSingleton(sp =>
+ {
+ var config = sp.GetRequiredService>().Value;
+ return new NamedPipeServerTransport(config.ServiceRpcPipeName);
+ });
+ }
+#else
+#pragma warning disable CA1416 // Platform compatibility - guarded by OperatingSystem.IsLinux() at runtime
+ if (OperatingSystem.IsLinux())
+ {
+ MainLogger.Information("Running as a systemd service");
+ builder.Services.AddSystemd();
+
+ // Register Unix socket transport
+ builder.Services.AddSingleton(sp =>
+ {
+ var config = sp.GetRequiredService>().Value;
+ var socketPath = string.IsNullOrEmpty(config.ServiceRpcSocketPath)
+ ? "/run/coder-desktop/vpn.sock"
+ : config.ServiceRpcSocketPath;
+ return new UnixSocketServerTransport(socketPath);
+ });
+ }
+#pragma warning restore CA1416
+#endif
+ }
+
private static void AddDefaultConfig(IConfigurationBuilder builder)
{
+ var logPath = OperatingSystem.IsWindows()
+ ? @"C:\coder-desktop-service.log"
+ : "/var/log/coder-desktop-service.log";
+
builder.AddInMemoryCollection(new Dictionary
{
["Serilog:Using:0"] = "Serilog.Sinks.File",
@@ -115,7 +166,7 @@ private static void AddDefaultConfig(IConfigurationBuilder builder)
["Serilog:Enrich:0"] = "FromLogContext",
["Serilog:WriteTo:0:Name"] = "File",
- ["Serilog:WriteTo:0:Args:path"] = @"C:\coder-desktop-service.log",
+ ["Serilog:WriteTo:0:Args:path"] = logPath,
["Serilog:WriteTo:0:Args:outputTemplate"] =
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}",
["Serilog:WriteTo:0:Args:rollingInterval"] = "Day",
diff --git a/Vpn.Service/TelemetryEnricher.cs b/Vpn.Service/TelemetryEnricher.cs
index 2169334..5c1bfe9 100644
--- a/Vpn.Service/TelemetryEnricher.cs
+++ b/Vpn.Service/TelemetryEnricher.cs
@@ -1,15 +1,11 @@
using System.Reflection;
+using System.Runtime.Versioning;
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);
@@ -24,28 +20,53 @@ 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);
- }
- }
+ _deviceID = GetDeviceId();
}
public StartRequest EnrichStartRequest(StartRequest original)
{
var req = original.Clone();
- req.DeviceOs = "Windows";
+ req.DeviceOs = OperatingSystem.IsWindows() ? "Windows" : "Linux";
if (_version != null) req.CoderDesktopVersion = _version;
if (_deviceID != null) req.DeviceId = _deviceID;
return req;
}
+
+ private static string? GetDeviceId()
+ {
+ try
+ {
+ string? rawId = null;
+
+ if (OperatingSystem.IsWindows())
+ {
+ rawId = GetWindowsDeviceId();
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ // /etc/machine-id is standard on systemd systems
+ const string machineIdPath = "/etc/machine-id";
+ if (File.Exists(machineIdPath))
+ rawId = File.ReadAllText(machineIdPath).Trim();
+ }
+
+ if (string.IsNullOrEmpty(rawId))
+ return null;
+
+ var idBytes = Encoding.UTF8.GetBytes(rawId);
+ var hash = SHA256.HashData(idBytes);
+ return Convert.ToBase64String(hash);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ [SupportedOSPlatform("windows")]
+ private static string? GetWindowsDeviceId()
+ {
+ using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\SQMClient");
+ return key?.GetValue("MachineId") as string;
+ }
}
diff --git a/Vpn.Service/Vpn.Service.csproj b/Vpn.Service/Vpn.Service.csproj
index aaed3cc..d70d145 100644
--- a/Vpn.Service/Vpn.Service.csproj
+++ b/Vpn.Service/Vpn.Service.csproj
@@ -1,13 +1,14 @@
-
+
Coder.Desktop.Vpn.Service
Exe
- net8.0-windows
+ net8.0
+ true
enable
enable
- true
- 13
+ false
+ 12
CoderVpnService
coder.ico
@@ -15,20 +16,22 @@
false
-
true
false
+
+
+ $(DefineConstants);WINDOWS
+
+
-
-
@@ -36,6 +39,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Vpn.Service/packages.lock.json b/Vpn.Service/packages.lock.json
deleted file mode 100644
index 09c7b76..0000000
--- a/Vpn.Service/packages.lock.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "version": 1,
- "dependencies": {
- "net8.0-windows7.0": {
- "Microsoft.Extensions.Hosting": {
- "type": "Direct",
- "requested": "[9.0.4, )",
- "resolved": "9.0.4",
- "contentHash": "1rZwLE+tTUIyZRUzmlk/DQj+v+Eqox+rjb+X7Fi+cYTbQfIZPYwpf1pVybsV3oje8+Pe4GaNukpBVUlPYeQdeQ==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.Binder": "9.0.4",
- "Microsoft.Extensions.Configuration.CommandLine": "9.0.4",
- "Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.4",
- "Microsoft.Extensions.Configuration.FileExtensions": "9.0.4",
- "Microsoft.Extensions.Configuration.Json": "9.0.4",
- "Microsoft.Extensions.Configuration.UserSecrets": "9.0.4",
- "Microsoft.Extensions.DependencyInjection": "9.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Diagnostics": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Physical": "9.0.4",
- "Microsoft.Extensions.Hosting.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging.Configuration": "9.0.4",
- "Microsoft.Extensions.Logging.Console": "9.0.4",
- "Microsoft.Extensions.Logging.Debug": "9.0.4",
- "Microsoft.Extensions.Logging.EventLog": "9.0.4",
- "Microsoft.Extensions.Logging.EventSource": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4"
- }
- },
- "Microsoft.Extensions.Hosting.WindowsServices": {
- "type": "Direct",
- "requested": "[9.0.4, )",
- "resolved": "9.0.4",
- "contentHash": "QFeUS0NG4Kwq91Mf1WzVZSbBtw+nKxyOQTi4xTRUEQ2gC7HWiyCUiX0arMJxt9lWwbjXxQY9TQjDptm+ct7BkQ==",
- "dependencies": {
- "Microsoft.Extensions.Hosting": "9.0.4",
- "Microsoft.Extensions.Logging.EventLog": "9.0.4",
- "System.ServiceProcess.ServiceController": "9.0.4"
- }
- },
- "Microsoft.Extensions.Options.DataAnnotations": {
- "type": "Direct",
- "requested": "[9.0.4, )",
- "resolved": "9.0.4",
- "contentHash": "jJq7xO1PLi//cts59Yp6dKNN07xV0Day/JmVR7aXCdo2rYHAoFlyARyxrfB0CTzsErA+TOhYTz2Ee0poR8SPeQ==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4"
- }
- },
- "Microsoft.Security.Extensions": {
- "type": "Direct",
- "requested": "[1.3.0, )",
- "resolved": "1.3.0",
- "contentHash": "xK8WFEo5WMUE8DI8W+GjhRwpVcPrxc4DyTjfxh39+yOyhAtC5TBHDlFEJks5toNZHsUeUuiWELIX25oTWOKPBw=="
- },
- "Semver": {
- "type": "Direct",
- "requested": "[3.0.0, )",
- "resolved": "3.0.0",
- "contentHash": "9jZCicsVgTebqkAujRWtC9J1A5EQVlu0TVKHcgoCuv345ve5DYf4D1MjhKEnQjdRZo6x/vdv6QQrYFs7ilGzLA==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "5.0.1"
- }
- },
- "Serilog.Extensions.Hosting": {
- "type": "Direct",
- "requested": "[9.0.0, )",
- "resolved": "9.0.0",
- "contentHash": "u2TRxuxbjvTAldQn7uaAwePkWxTHIqlgjelekBtilAGL5sYyF3+65NWctN4UrwwGLsDC7c3Vz3HnOlu+PcoxXg==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
- "Microsoft.Extensions.Hosting.Abstractions": "9.0.0",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.0",
- "Serilog": "4.2.0",
- "Serilog.Extensions.Logging": "9.0.0"
- }
- },
- "Serilog.Settings.Configuration": {
- "type": "Direct",
- "requested": "[9.0.0, )",
- "resolved": "9.0.0",
- "contentHash": "4/Et4Cqwa+F88l5SeFeNZ4c4Z6dEAIKbu3MaQb2Zz9F/g27T5a3wvfMcmCOaAiACjfUb4A6wrlTVfyYUZk3RRQ==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Binder": "9.0.0",
- "Microsoft.Extensions.DependencyModel": "9.0.0",
- "Serilog": "4.2.0"
- }
- },
- "Serilog.Sinks.Console": {
- "type": "Direct",
- "requested": "[6.0.0, )",
- "resolved": "6.0.0",
- "contentHash": "fQGWqVMClCP2yEyTXPIinSr5c+CBGUvBybPxjAGcf7ctDhadFhrQw03Mv8rJ07/wR5PDfFjewf2LimvXCDzpbA==",
- "dependencies": {
- "Serilog": "4.0.0"
- }
- },
- "Serilog.Sinks.File": {
- "type": "Direct",
- "requested": "[6.0.0, )",
- "resolved": "6.0.0",
- "contentHash": "lxjg89Y8gJMmFxVkbZ+qDgjl+T4yC5F7WSLTvA+5q0R04tfKVLRL/EHpYoJ/MEQd2EeCKDuylBIVnAYMotmh2A==",
- "dependencies": {
- "Serilog": "4.0.0"
- }
- },
- "Google.Protobuf": {
- "type": "Transitive",
- "resolved": "3.29.3",
- "contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
- },
- "Microsoft.Extensions.Configuration": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "KIVBrMbItnCJDd1RF4KEaE8jZwDJcDUJW5zXpbwQ05HNYTK1GveHxHK0B3SjgDJuR48GRACXAO+BLhL8h34S7g==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.Binder": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "cdrjcl9RIcwt3ECbnpP0Gt1+pkjdW90mq5yFYy8D9qRj2NqFFcv3yDp141iEamsd9E218sGxK8WHaIOcrqgDJg==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.CommandLine": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "TbM2HElARG7z1gxwakdppmOkm1SykPqDcu3EF97daEwSb/+TXnRrFfJtF+5FWWxcsNhbRrmLfS2WszYcab7u1A==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.EnvironmentVariables": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "2IGiG3FtVnD83IA6HYGuNei8dOw455C09yEhGl8bjcY6aGZgoC6yhYvDnozw8wlTowfoG9bxVrdTsr2ACZOYHg==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.FileExtensions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "UY864WQ3AS2Fkc8fYLombWnjrXwYt+BEHHps0hY4sxlgqaVW06AxbpgRZjfYf8PyRbplJqruzZDB/nSLT+7RLQ==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Physical": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.Json": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "vVXI70CgT/dmXV3MM+n/BR2rLXEoAyoK0hQT+8MrbCMuJBiLRxnTtSrksNiASWCwOtxo/Tyy7CO8AGthbsYxnw==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.FileExtensions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "System.Text.Json": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.UserSecrets": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "zuvyC72gJkJyodyGowCuz3EQ1QvzNXJtKusuRzmjoHr17aeB3X0aSiKFB++HMHnQIWWlPOBf9YHTQfEqzbgl1g==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.Json": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Physical": "9.0.4"
- }
- },
- "Microsoft.Extensions.DependencyInjection": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.DependencyInjection.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg=="
- },
- "Microsoft.Extensions.DependencyModel": {
- "type": "Transitive",
- "resolved": "9.0.0",
- "contentHash": "saxr2XzwgDU77LaQfYFXmddEDRUKHF4DaGMZkNB3qjdVSZlax3//dGJagJkKrGMIPNZs2jVFXITyCCR6UHJNdA==",
- "dependencies": {
- "System.Text.Encodings.Web": "9.0.0",
- "System.Text.Json": "9.0.0"
- }
- },
- "Microsoft.Extensions.Diagnostics": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "1bCSQrGv9+bpF5MGKF6THbnRFUZqQDrWPA39NDeVW9djeHBmow8kX4SX6/8KkeKI8gmUDG7jsG/bVuNAcY/ATQ==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Diagnostics.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "IAucBcHYtiCmMyFag+Vrp5m+cjGRlDttJk9Vx7Dqpq+Ama4BzVUOk0JARQakgFFr7ZTBSgLKlHmtY5MiItB7Cg==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "System.Diagnostics.DiagnosticSource": "9.0.4"
- }
- },
- "Microsoft.Extensions.FileProviders.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "gQN2o/KnBfVk6Bd71E2YsvO5lsqrqHmaepDGk+FB/C4aiQY9B0XKKNKfl5/TqcNOs9OEithm4opiMHAErMFyEw==",
- "dependencies": {
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.FileProviders.Physical": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "qkQ9V7KFZdTWNThT7ke7E/Jad38s46atSs3QUYZB8f3thBTrcrousdY4Y/tyCtcH5YjsPSiByjuN+L8W/ThMQg==",
- "dependencies": {
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileSystemGlobbing": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.FileSystemGlobbing": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "05Lh2ItSk4mzTdDWATW9nEcSybwprN8Tz42Fs5B+jwdXUpauktdAQUI1Am4sUQi2C63E5hvQp8gXvfwfg9mQGQ=="
- },
- "Microsoft.Extensions.Hosting.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "bXkwRPMo4x19YKH6/V9XotU7KYQJlihXhcWO1RDclAY3yfY3XNg4QtSEBvng4kK/DnboE0O/nwSl+6Jiv9P+FA==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.4",
- "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.Abstractions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "System.Diagnostics.DiagnosticSource": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.Configuration": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "/kF+rSnoo3/nIwGzWsR4RgBnoTOdZ3lzz2qFRyp/GgaNid4j6hOAQrs/O+QHXhlcAdZxjg37MvtIE+pAvIgi9g==",
- "dependencies": {
- "Microsoft.Extensions.Configuration": "9.0.4",
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.Binder": "9.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.Console": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "cI0lQe0js65INCTCtAgnlVJWKgzgoRHVAW1B1zwCbmcliO4IZoTf92f1SYbLeLk7FzMJ/GlCvjLvJegJ6kltmQ==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging.Configuration": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "System.Text.Json": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.Debug": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "D1jy+jy+huUUxnkZ0H480RZK8vqKn8NsQxYpMpPL/ALPPh1WATVLcr/uXI3RUBB45wMW5265O+hk9x3jnnXFuA==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.EventLog": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "bApxdklf7QTsONOLR5ow6SdDFXR5ncHvumSEg2+QnCvxvkzc2z5kNn7yQCyupRLRN4jKbnlTkVX8x9qLlwL6Qg==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "System.Diagnostics.EventLog": "9.0.4"
- }
- },
- "Microsoft.Extensions.Logging.EventSource": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "R600zTxVJNw2IeAEOvdOJGNA1lHr1m3vo460hSF5G1DjwP0FNpyeH4lpLDMuf34diKwB1LTt5hBw1iF1/iuwsQ==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Logging": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4",
- "System.Text.Json": "9.0.4"
- }
- },
- "Microsoft.Extensions.Options": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==",
- "dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Options.ConfigurationExtensions": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "aridVhAT3Ep+vsirR1pzjaOw0Jwiob6dc73VFQn2XmDfBA2X98M8YKO1GarvsXRX7gX1Aj+hj2ijMzrMHDOm0A==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.Binder": "9.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Primitives": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
- },
- "Serilog": {
- "type": "Transitive",
- "resolved": "4.2.0",
- "contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA=="
- },
- "Serilog.Extensions.Logging": {
- "type": "Transitive",
- "resolved": "9.0.0",
- "contentHash": "NwSSYqPJeKNzl5AuXVHpGbr6PkZJFlNa14CdIebVjK3k/76kYj/mz5kiTRNVSsSaxM8kAIa1kpy/qyT9E4npRQ==",
- "dependencies": {
- "Microsoft.Extensions.Logging": "9.0.0",
- "Serilog": "4.2.0"
- }
- },
- "System.Diagnostics.DiagnosticSource": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "Be0emq8bRmcK4eeJIFUt9+vYPf7kzuQrFs8Ef1CdGvXpq/uSve22PTSkRF09bF/J7wmYJ2DHf2v7GaT3vMXnwQ=="
- },
- "System.Diagnostics.EventLog": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "getRQEXD8idlpb1KW56XuxImMy0FKp2WJPDf3Qr0kI/QKxxJSftqfDFVo0DZ3HCJRLU73qHSruv5q2l5O47jQQ=="
- },
- "System.IO.Pipelines": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "luF2Xba+lTe2GOoNQdZLe8q7K6s7nSpWZl9jIwWNMszN4/Yv0lmxk9HISgMmwdyZ83i3UhAGXaSY9o6IJBUuuA=="
- },
- "System.ServiceProcess.ServiceController": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "j6Z+ED1d/yxe/Cm+UlFf+LNw2HSYBSgtFh71KnEEmUtHIwgoTVQxji5URvXPOAZ7iuKHItjMIzpCLyRZc8OmrQ==",
- "dependencies": {
- "System.Diagnostics.EventLog": "9.0.4"
- }
- },
- "System.Text.Encodings.Web": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "V+5cCPpk1S2ngekUs9nDrQLHGiWFZMg8BthADQr+Fwi59a8DdHFu26S2oi9Bfgv+d67bqmkPqctJXMEXiimXUg=="
- },
- "System.Text.Json": {
- "type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g==",
- "dependencies": {
- "System.IO.Pipelines": "9.0.4",
- "System.Text.Encodings.Web": "9.0.4"
- }
- },
- "Coder.Desktop.CoderSdk": {
- "type": "Project"
- },
- "Coder.Desktop.Vpn": {
- "type": "Project",
- "dependencies": {
- "Coder.Desktop.Vpn.Proto": "[1.0.0, )",
- "Microsoft.Extensions.Configuration": "[9.0.1, )",
- "Semver": "[3.0.0, )",
- "System.IO.Pipelines": "[9.0.1, )"
- }
- },
- "Coder.Desktop.Vpn.Proto": {
- "type": "Project",
- "dependencies": {
- "Google.Protobuf": "[3.29.3, )"
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Vpn.Windows/NamedPipeClientTransport.cs b/Vpn.Windows/NamedPipeClientTransport.cs
new file mode 100644
index 0000000..73a8040
--- /dev/null
+++ b/Vpn.Windows/NamedPipeClientTransport.cs
@@ -0,0 +1,29 @@
+using System.IO.Pipes;
+
+namespace Coder.Desktop.Vpn;
+
+public class NamedPipeClientTransport : IRpcClientTransport
+{
+ private readonly string _pipeName;
+
+ public NamedPipeClientTransport(string pipeName)
+ {
+ _pipeName = pipeName;
+ }
+
+ public async Task ConnectAsync(CancellationToken ct)
+ {
+ var pipe = new NamedPipeClientStream(".", _pipeName,
+ PipeDirection.InOut, PipeOptions.Asynchronous);
+ try
+ {
+ await pipe.ConnectAsync(ct);
+ return pipe;
+ }
+ catch
+ {
+ await pipe.DisposeAsync();
+ throw;
+ }
+ }
+}
diff --git a/Vpn.Windows/NamedPipeServerTransport.cs b/Vpn.Windows/NamedPipeServerTransport.cs
new file mode 100644
index 0000000..d8d3416
--- /dev/null
+++ b/Vpn.Windows/NamedPipeServerTransport.cs
@@ -0,0 +1,42 @@
+using System.IO.Pipes;
+using System.Security.AccessControl;
+using System.Security.Principal;
+
+namespace Coder.Desktop.Vpn;
+
+public class NamedPipeServerTransport : IRpcServerTransport
+{
+ private readonly string _pipeName;
+
+ public NamedPipeServerTransport(string pipeName)
+ {
+ _pipeName = pipeName;
+ }
+
+ public async Task AcceptAsync(CancellationToken ct)
+ {
+ var pipeSecurity = new PipeSecurity();
+ pipeSecurity.AddAccessRule(new PipeAccessRule(
+ new SecurityIdentifier(WellKnownSidType.WorldSid, null),
+ PipeAccessRights.FullControl, AccessControlType.Allow));
+ var pipe = NamedPipeServerStreamAcl.Create(
+ _pipeName, PipeDirection.InOut,
+ NamedPipeServerStream.MaxAllowedServerInstances,
+ PipeTransmissionMode.Byte, PipeOptions.Asynchronous, 0, 0, pipeSecurity);
+ try
+ {
+ await pipe.WaitForConnectionAsync(ct);
+ return pipe;
+ }
+ catch
+ {
+ await pipe.DisposeAsync();
+ throw;
+ }
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/Vpn/RegistryConfigurationSource.cs b/Vpn.Windows/RegistryConfigurationSource.cs
similarity index 100%
rename from Vpn/RegistryConfigurationSource.cs
rename to Vpn.Windows/RegistryConfigurationSource.cs
diff --git a/Vpn.Windows/Vpn.Windows.csproj b/Vpn.Windows/Vpn.Windows.csproj
new file mode 100644
index 0000000..0dc1066
--- /dev/null
+++ b/Vpn.Windows/Vpn.Windows.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Coder.Desktop.Vpn.Windows
+ Coder.Desktop.Vpn
+ net8.0-windows
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Vpn/IRpcTransport.cs b/Vpn/IRpcTransport.cs
new file mode 100644
index 0000000..1b5243d
--- /dev/null
+++ b/Vpn/IRpcTransport.cs
@@ -0,0 +1,20 @@
+namespace Coder.Desktop.Vpn;
+
+///
+/// Server-side transport for accepting RPC connections.
+/// Windows: Named Pipes. Linux: Unix Domain Sockets.
+///
+public interface IRpcServerTransport : IAsyncDisposable
+{
+ /// Accept a single client connection, returning a bidirectional stream.
+ Task AcceptAsync(CancellationToken ct);
+}
+
+///
+/// Client-side transport for connecting to the RPC server.
+///
+public interface IRpcClientTransport
+{
+ /// Connect to the RPC server, returning a bidirectional stream.
+ Task ConnectAsync(CancellationToken ct);
+}
diff --git a/Vpn/Vpn.csproj b/Vpn/Vpn.csproj
index 76a72eb..68755f8 100644
--- a/Vpn/Vpn.csproj
+++ b/Vpn/Vpn.csproj
@@ -1,9 +1,9 @@
-
+
Coder.Desktop.Vpn
Coder.Desktop.Vpn
- net8.0-windows
+ net8.0
enable
enable
true
diff --git a/Vpn/packages.lock.json b/Vpn/packages.lock.json
index 8876fe4..8e56ce8 100644
--- a/Vpn/packages.lock.json
+++ b/Vpn/packages.lock.json
@@ -1,7 +1,7 @@
{
"version": 1,
"dependencies": {
- "net8.0-windows7.0": {
+ "net8.0": {
"Microsoft.Extensions.Configuration": {
"type": "Direct",
"requested": "[9.0.1, )",