diff --git a/package/AgentWindowsManaged/Actions/AgentActions.cs b/package/AgentWindowsManaged/Actions/AgentActions.cs index 223f1a261..4832e753a 100644 --- a/package/AgentWindowsManaged/Actions/AgentActions.cs +++ b/package/AgentWindowsManaged/Actions/AgentActions.cs @@ -279,6 +279,18 @@ internal static class AgentActions UsesProperties = UseProperties(new[] { AgentProperties.featuresToConfigure }) }; + private static readonly ElevatedManagedAction enrollAgentTunnel = new( + new Id($"CA.{nameof(enrollAgentTunnel)}"), + CustomActions.EnrollAgentTunnel, + Return.check, + When.Before, Step.StartServices, + Features.AGENT_TUNNEL_FEATURE.BeingInstall(), + Sequence.InstallExecuteSequence) + { + Execute = Execute.deferred, + Impersonate = false, + }; + private static readonly ElevatedManagedAction registerExplorerCommand = new( CustomActions.RegisterExplorerCommand ) @@ -352,6 +364,7 @@ private static string UseProperties(IEnumerable properties) setArpInstallLocation, setFeaturesToConfigure, configureFeatures, + enrollAgentTunnel, createProgramDataDirectory, setProgramDataDirectoryPermissions, createProgramDataPedmDirectories, diff --git a/package/AgentWindowsManaged/Actions/CustomActions.cs b/package/AgentWindowsManaged/Actions/CustomActions.cs index 96546c55c..3602c55b9 100644 --- a/package/AgentWindowsManaged/Actions/CustomActions.cs +++ b/package/AgentWindowsManaged/Actions/CustomActions.cs @@ -3,6 +3,7 @@ using Microsoft.Deployment.WindowsInstaller; using Microsoft.Win32; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.ComponentModel; @@ -318,6 +319,124 @@ public static ActionResult SetFeaturesToConfigure(Session session) return ActionResult.Success; } + [CustomAction] + public static ActionResult EnrollAgentTunnel(Session session) + { + string enrollmentString = session.Property(AgentProperties.AgentTunnelEnrollmentString)?.Trim() ?? string.Empty; + string subnetsArg = session.Property(AgentProperties.AgentTunnelAdvertiseSubnets)?.Trim() ?? string.Empty; + string domainsArg = session.Property(AgentProperties.AgentTunnelAdvertiseDomains)?.Trim() ?? string.Empty; + string gatewayUrlArg = session.Property(AgentProperties.AgentTunnelGatewayUrl)?.Trim() ?? string.Empty; + + if (enrollmentString.Length == 0) + { + session.Log("Agent tunnel enrollment string not provided, skipping tunnel setup"); + return ActionResult.Success; + } + + try + { + // The enrollment string is the DVLS-signed JWT verbatim. The agent's + // `up --enrollment-string` parses `jet_gw_url` and `jet_agent_name` from the JWT + // claims itself, so we just hand the JWT through. Advertise domains aren't a CLI + // flag — agent.json carries them — so we patch that after enrollment succeeds. + string installDir = session.Property(AgentProperties.InstallDir); + string exePath = Path.Combine(installDir, Includes.EXECUTABLE_NAME); + + string arguments = $"up --enrollment-string \"{enrollmentString}\""; + if (gatewayUrlArg.Length != 0) arguments += $" --gateway \"{gatewayUrlArg}\""; + if (subnetsArg.Length != 0) arguments += $" --advertise-subnets \"{subnetsArg}\""; + + string Redact(string s) => s.Replace(enrollmentString, "***"); + session.Log($"Running enrollment: {exePath} {Redact(arguments)}"); + + ProcessStartInfo startInfo = new(exePath, arguments) + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = ProgramDataDirectory, + }; + + using Process process = Process.Start(startInfo); + if (!process.WaitForExit(60_000)) + { + try { process.Kill(); } catch { /* already gone */ } + session.Log("Enrollment process timed out after 60 seconds"); + return ActionResult.Failure; + } + string stdout = process.StandardOutput.ReadToEnd(); + string stderr = process.StandardError.ReadToEnd(); + + if (!string.IsNullOrEmpty(stdout)) session.Log($"enrollment stdout: {Redact(stdout)}"); + if (!string.IsNullOrEmpty(stderr)) session.Log($"enrollment stderr: {Redact(stderr)}"); + + if (process.ExitCode != 0) + { + session.Log($"Enrollment failed with exit code {process.ExitCode}"); + return ActionResult.Failure; + } + + if (domainsArg.Length != 0) + { + WriteAdvertiseDomainsToConfig(session, domainsArg); + } + + session.Log("Agent tunnel enrollment completed successfully"); + return ActionResult.Success; + } + catch (Exception e) + { + session.Log($"Agent tunnel enrollment failed: {e}"); + return ActionResult.Failure; + } + } + + private static void WriteAdvertiseDomainsToConfig(Session session, string domainsCsv) + { + string configPath = Path.Combine(ProgramDataDirectory, "agent.json"); + if (!File.Exists(configPath)) + { + session.Log($"agent.json not found at {configPath}; cannot persist advertise_domains"); + return; + } + + try + { + string[] domains = domainsCsv + .Split(',') + .Select(d => d.Trim()) + .Where(d => !string.IsNullOrEmpty(d)) + .ToArray(); + + if (domains.Length == 0) + { + return; + } + + JObject root = JObject.Parse(File.ReadAllText(configPath)); + + // ConfFile uses serde rename_all = "PascalCase", so the tunnel section is keyed + // "Tunnel" and the field is "AdvertiseDomains". + if (root["Tunnel"] is not JObject tunnel) + { + session.Log("agent.json has no Tunnel section after enrollment; skipping advertise_domains write"); + return; + } + + tunnel["AdvertiseDomains"] = new JArray(domains); + + File.WriteAllText(configPath, root.ToString(Formatting.Indented)); + session.Log($"Wrote {domains.Length} advertise_domains entries to agent.json"); + } + catch (Exception e) + { + // Don't fail the install over this — the tunnel works fine without domain + // advertisements (subnets cover IP routing on their own). + session.Log($"Failed to write advertise_domains to agent.json: {e}"); + } + } + [CustomAction] public static ActionResult ConfigureFeatures(Session session) { diff --git a/package/AgentWindowsManaged/Dialogs/AgentTunnelDialog.Designer.cs b/package/AgentWindowsManaged/Dialogs/AgentTunnelDialog.Designer.cs new file mode 100644 index 000000000..2c4f3c0f4 --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/AgentTunnelDialog.Designer.cs @@ -0,0 +1,415 @@ +using WixSharp; +using WixSharp.UI.Forms; + +namespace WixSharpSetup.Dialogs +{ + partial class AgentTunnelDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.middlePanel = new System.Windows.Forms.Panel(); + this.tableLayoutPanel2 = new System.Windows.Forms.TableLayoutPanel(); + this.labelEnrollmentString = new System.Windows.Forms.Label(); + this.enrollmentString = new System.Windows.Forms.TextBox(); + this.labelSubnets = new System.Windows.Forms.Label(); + this.advertiseSubnets = new System.Windows.Forms.TextBox(); + this.labelSubnetsHint = new System.Windows.Forms.Label(); + this.labelDomains = new System.Windows.Forms.Label(); + this.advertiseDomains = new System.Windows.Forms.TextBox(); + this.labelDomainsHint = new System.Windows.Forms.Label(); + this.labelGatewayUrl = new System.Windows.Forms.Label(); + this.gatewayUrl = new System.Windows.Forms.TextBox(); + this.labelGatewayUrlHint = new System.Windows.Forms.Label(); + this.topBorder = new System.Windows.Forms.Panel(); + this.topPanel = new System.Windows.Forms.Panel(); + this.label2 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.banner = new System.Windows.Forms.PictureBox(); + this.bottomPanel = new System.Windows.Forms.Panel(); + this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.back = new System.Windows.Forms.Button(); + this.next = new System.Windows.Forms.Button(); + this.cancel = new System.Windows.Forms.Button(); + this.border1 = new System.Windows.Forms.Panel(); + this.middlePanel.SuspendLayout(); + this.tableLayoutPanel2.SuspendLayout(); + this.topPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).BeginInit(); + this.bottomPanel.SuspendLayout(); + this.tableLayoutPanel1.SuspendLayout(); + this.SuspendLayout(); + // + // middlePanel + // + this.middlePanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.middlePanel.AutoScroll = true; + this.middlePanel.Controls.Add(this.tableLayoutPanel2); + this.middlePanel.Location = new System.Drawing.Point(22, 75); + this.middlePanel.Name = "middlePanel"; + this.middlePanel.Size = new System.Drawing.Size(449, 225); + this.middlePanel.TabIndex = 0; + // + // tableLayoutPanel2 + // + this.tableLayoutPanel2.ColumnCount = 1; + this.tableLayoutPanel2.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel2.Controls.Add(this.labelEnrollmentString, 0, 0); + this.tableLayoutPanel2.Controls.Add(this.enrollmentString, 0, 1); + this.tableLayoutPanel2.Controls.Add(this.labelSubnets, 0, 2); + this.tableLayoutPanel2.Controls.Add(this.advertiseSubnets, 0, 3); + this.tableLayoutPanel2.Controls.Add(this.labelSubnetsHint, 0, 4); + this.tableLayoutPanel2.Controls.Add(this.labelDomains, 0, 5); + this.tableLayoutPanel2.Controls.Add(this.advertiseDomains, 0, 6); + this.tableLayoutPanel2.Controls.Add(this.labelDomainsHint, 0, 7); + this.tableLayoutPanel2.Controls.Add(this.labelGatewayUrl, 0, 8); + this.tableLayoutPanel2.Controls.Add(this.gatewayUrl, 0, 9); + this.tableLayoutPanel2.Controls.Add(this.labelGatewayUrlHint, 0, 10); + this.tableLayoutPanel2.AutoSize = true; + this.tableLayoutPanel2.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.tableLayoutPanel2.Dock = System.Windows.Forms.DockStyle.Top; + this.tableLayoutPanel2.Location = new System.Drawing.Point(0, 0); + this.tableLayoutPanel2.Name = "tableLayoutPanel2"; + this.tableLayoutPanel2.RowCount = 11; + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel2.Size = new System.Drawing.Size(449, 285); + this.tableLayoutPanel2.TabIndex = 0; + // + // labelEnrollmentString + // + this.labelEnrollmentString.AutoSize = true; + this.labelEnrollmentString.BackColor = System.Drawing.Color.Transparent; + this.labelEnrollmentString.Location = new System.Drawing.Point(3, 3); + this.labelEnrollmentString.Margin = new System.Windows.Forms.Padding(3); + this.labelEnrollmentString.Name = "labelEnrollmentString"; + this.labelEnrollmentString.Size = new System.Drawing.Size(200, 13); + this.labelEnrollmentString.TabIndex = 0; + this.labelEnrollmentString.Text = "[AgentTunnelDlgEnrollmentStringLabel]"; + // + // enrollmentString + // + this.enrollmentString.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.enrollmentString.Location = new System.Drawing.Point(3, 22); + this.enrollmentString.Multiline = true; + this.enrollmentString.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.enrollmentString.Name = "enrollmentString"; + this.enrollmentString.Size = new System.Drawing.Size(443, 60); + this.enrollmentString.TabIndex = 1; + // + // labelSubnets + // + this.labelSubnets.AutoSize = true; + this.labelSubnets.BackColor = System.Drawing.Color.Transparent; + this.labelSubnets.Location = new System.Drawing.Point(3, 93); + this.labelSubnets.Margin = new System.Windows.Forms.Padding(3, 8, 3, 3); + this.labelSubnets.Name = "labelSubnets"; + this.labelSubnets.Size = new System.Drawing.Size(200, 13); + this.labelSubnets.TabIndex = 2; + this.labelSubnets.Text = "[AgentTunnelDlgSubnetsLabel]"; + // + // advertiseSubnets + // + this.advertiseSubnets.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.advertiseSubnets.Location = new System.Drawing.Point(3, 112); + this.advertiseSubnets.Name = "advertiseSubnets"; + this.advertiseSubnets.Size = new System.Drawing.Size(443, 20); + this.advertiseSubnets.TabIndex = 3; + // + // labelSubnetsHint + // + this.labelSubnetsHint.AutoSize = true; + this.labelSubnetsHint.BackColor = System.Drawing.Color.Transparent; + this.labelSubnetsHint.ForeColor = System.Drawing.SystemColors.GrayText; + this.labelSubnetsHint.Location = new System.Drawing.Point(3, 138); + this.labelSubnetsHint.Margin = new System.Windows.Forms.Padding(3, 3, 3, 3); + this.labelSubnetsHint.Name = "labelSubnetsHint"; + this.labelSubnetsHint.Size = new System.Drawing.Size(300, 13); + this.labelSubnetsHint.TabIndex = 4; + this.labelSubnetsHint.Text = "[AgentTunnelDlgSubnetsHint]"; + // + // labelDomains + // + this.labelDomains.AutoSize = true; + this.labelDomains.BackColor = System.Drawing.Color.Transparent; + this.labelDomains.Location = new System.Drawing.Point(3, 162); + this.labelDomains.Margin = new System.Windows.Forms.Padding(3, 8, 3, 3); + this.labelDomains.Name = "labelDomains"; + this.labelDomains.Size = new System.Drawing.Size(200, 13); + this.labelDomains.TabIndex = 5; + this.labelDomains.Text = "[AgentTunnelDlgDomainsLabel]"; + // + // advertiseDomains + // + this.advertiseDomains.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.advertiseDomains.Location = new System.Drawing.Point(3, 181); + this.advertiseDomains.Name = "advertiseDomains"; + this.advertiseDomains.Size = new System.Drawing.Size(443, 20); + this.advertiseDomains.TabIndex = 6; + // + // labelDomainsHint + // + this.labelDomainsHint.AutoSize = true; + this.labelDomainsHint.BackColor = System.Drawing.Color.Transparent; + this.labelDomainsHint.ForeColor = System.Drawing.SystemColors.GrayText; + this.labelDomainsHint.Location = new System.Drawing.Point(3, 207); + this.labelDomainsHint.Margin = new System.Windows.Forms.Padding(3, 3, 3, 3); + this.labelDomainsHint.Name = "labelDomainsHint"; + this.labelDomainsHint.Size = new System.Drawing.Size(300, 13); + this.labelDomainsHint.TabIndex = 7; + this.labelDomainsHint.Text = "[AgentTunnelDlgDomainsHint]"; + // + // labelGatewayUrl + // + this.labelGatewayUrl.AutoSize = true; + this.labelGatewayUrl.BackColor = System.Drawing.Color.Transparent; + this.labelGatewayUrl.Location = new System.Drawing.Point(3, 231); + this.labelGatewayUrl.Margin = new System.Windows.Forms.Padding(3, 8, 3, 3); + this.labelGatewayUrl.Name = "labelGatewayUrl"; + this.labelGatewayUrl.Size = new System.Drawing.Size(200, 13); + this.labelGatewayUrl.TabIndex = 8; + this.labelGatewayUrl.Text = "[AgentTunnelDlgGatewayUrlLabel]"; + // + // gatewayUrl + // + this.gatewayUrl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.gatewayUrl.Location = new System.Drawing.Point(3, 250); + this.gatewayUrl.Name = "gatewayUrl"; + this.gatewayUrl.Size = new System.Drawing.Size(443, 20); + this.gatewayUrl.TabIndex = 9; + // + // labelGatewayUrlHint + // + this.labelGatewayUrlHint.AutoSize = true; + this.labelGatewayUrlHint.BackColor = System.Drawing.Color.Transparent; + this.labelGatewayUrlHint.ForeColor = System.Drawing.SystemColors.GrayText; + this.labelGatewayUrlHint.Location = new System.Drawing.Point(3, 276); + this.labelGatewayUrlHint.Margin = new System.Windows.Forms.Padding(3, 3, 3, 3); + this.labelGatewayUrlHint.Name = "labelGatewayUrlHint"; + this.labelGatewayUrlHint.Size = new System.Drawing.Size(300, 13); + this.labelGatewayUrlHint.TabIndex = 10; + this.labelGatewayUrlHint.Text = "[AgentTunnelDlgGatewayUrlHint]"; + // + // topBorder + // + this.topBorder.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topBorder.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.topBorder.Location = new System.Drawing.Point(0, 58); + this.topBorder.Name = "topBorder"; + this.topBorder.Size = new System.Drawing.Size(494, 1); + this.topBorder.TabIndex = 15; + // + // topPanel + // + this.topPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.topPanel.BackColor = System.Drawing.SystemColors.Control; + this.topPanel.Controls.Add(this.label2); + this.topPanel.Controls.Add(this.label1); + this.topPanel.Controls.Add(this.banner); + this.topPanel.Location = new System.Drawing.Point(0, 0); + this.topPanel.Name = "topPanel"; + this.topPanel.Size = new System.Drawing.Size(494, 58); + this.topPanel.TabIndex = 10; + // + // label2 + // + this.label2.AutoEllipsis = true; + this.label2.BackColor = System.Drawing.Color.Transparent; + this.label2.ForeColor = System.Drawing.SystemColors.HighlightText; + this.label2.Location = new System.Drawing.Point(18, 31); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(409, 24); + this.label2.TabIndex = 1; + this.label2.Text = "[AgentTunnelDlgDescription]"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.BackColor = System.Drawing.Color.Transparent; + this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label1.ForeColor = System.Drawing.SystemColors.HighlightText; + this.label1.Location = new System.Drawing.Point(11, 8); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(109, 13); + this.label1.TabIndex = 1; + this.label1.Text = "[AgentTunnelDlgTitle]"; + // + // banner + // + this.banner.BackColor = System.Drawing.Color.White; + this.banner.Location = new System.Drawing.Point(0, 0); + this.banner.Name = "banner"; + this.banner.Size = new System.Drawing.Size(494, 58); + this.banner.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; + this.banner.TabIndex = 0; + this.banner.TabStop = false; + // + // bottomPanel + // + this.bottomPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.bottomPanel.BackColor = System.Drawing.SystemColors.Control; + this.bottomPanel.Controls.Add(this.tableLayoutPanel1); + this.bottomPanel.Controls.Add(this.border1); + this.bottomPanel.Location = new System.Drawing.Point(0, 312); + this.bottomPanel.Name = "bottomPanel"; + this.bottomPanel.Size = new System.Drawing.Size(494, 49); + this.bottomPanel.TabIndex = 9; + // + // tableLayoutPanel1 + // + this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.tableLayoutPanel1.ColumnCount = 5; + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 14F)); + this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanel1.Controls.Add(this.back, 1, 0); + this.tableLayoutPanel1.Controls.Add(this.next, 2, 0); + this.tableLayoutPanel1.Controls.Add(this.cancel, 4, 0); + this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 3); + this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + this.tableLayoutPanel1.RowCount = 1; + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanel1.Size = new System.Drawing.Size(493, 43); + this.tableLayoutPanel1.TabIndex = 8; + // + // back + // + this.back.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.back.AutoSize = true; + this.back.Location = new System.Drawing.Point(224, 10); + this.back.MinimumSize = new System.Drawing.Size(75, 0); + this.back.Name = "back"; + this.back.Size = new System.Drawing.Size(77, 23); + this.back.TabIndex = 1; + this.back.Text = "[WixUIBack]"; + this.back.UseVisualStyleBackColor = true; + this.back.Click += new System.EventHandler(this.Back_Click); + // + // next + // + this.next.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.next.AutoSize = true; + this.next.Location = new System.Drawing.Point(307, 10); + this.next.MinimumSize = new System.Drawing.Size(75, 0); + this.next.Name = "next"; + this.next.Size = new System.Drawing.Size(77, 23); + this.next.TabIndex = 0; + this.next.Text = "[WixUINext]"; + this.next.UseVisualStyleBackColor = true; + this.next.Click += new System.EventHandler(this.Next_Click); + // + // cancel + // + this.cancel.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.cancel.AutoSize = true; + this.cancel.Location = new System.Drawing.Point(404, 10); + this.cancel.MinimumSize = new System.Drawing.Size(75, 0); + this.cancel.Name = "cancel"; + this.cancel.Size = new System.Drawing.Size(86, 23); + this.cancel.TabIndex = 2; + this.cancel.Text = "[WixUICancel]"; + this.cancel.UseVisualStyleBackColor = true; + this.cancel.Click += new System.EventHandler(this.Cancel_Click); + // + // border1 + // + this.border1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.border1.Dock = System.Windows.Forms.DockStyle.Top; + this.border1.Location = new System.Drawing.Point(0, 0); + this.border1.Name = "border1"; + this.border1.Size = new System.Drawing.Size(494, 1); + this.border1.TabIndex = 14; + // + // AgentTunnelDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.ClientSize = new System.Drawing.Size(494, 361); + this.Controls.Add(this.middlePanel); + this.Controls.Add(this.topBorder); + this.Controls.Add(this.topPanel); + this.Controls.Add(this.bottomPanel); + this.Name = "AgentTunnelDialog"; + this.Load += new System.EventHandler(this.OnLoad); + this.middlePanel.ResumeLayout(false); + this.tableLayoutPanel2.ResumeLayout(false); + this.tableLayoutPanel2.PerformLayout(); + this.topPanel.ResumeLayout(false); + this.topPanel.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.banner)).EndInit(); + this.bottomPanel.ResumeLayout(false); + this.tableLayoutPanel1.ResumeLayout(false); + this.tableLayoutPanel1.PerformLayout(); + this.ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.PictureBox banner; + private System.Windows.Forms.Panel topPanel; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Panel bottomPanel; + private System.Windows.Forms.Panel border1; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Button back; + private System.Windows.Forms.Button next; + private System.Windows.Forms.Button cancel; + private System.Windows.Forms.Panel topBorder; + private System.Windows.Forms.Panel middlePanel; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel2; + private System.Windows.Forms.Label labelEnrollmentString; + private System.Windows.Forms.TextBox enrollmentString; + private System.Windows.Forms.Label labelSubnets; + private System.Windows.Forms.TextBox advertiseSubnets; + private System.Windows.Forms.Label labelSubnetsHint; + private System.Windows.Forms.Label labelDomains; + private System.Windows.Forms.TextBox advertiseDomains; + private System.Windows.Forms.Label labelDomainsHint; + private System.Windows.Forms.Label labelGatewayUrl; + private System.Windows.Forms.TextBox gatewayUrl; + private System.Windows.Forms.Label labelGatewayUrlHint; + } +} diff --git a/package/AgentWindowsManaged/Dialogs/AgentTunnelDialog.cs b/package/AgentWindowsManaged/Dialogs/AgentTunnelDialog.cs new file mode 100644 index 000000000..304fe117e --- /dev/null +++ b/package/AgentWindowsManaged/Dialogs/AgentTunnelDialog.cs @@ -0,0 +1,85 @@ +using DevolutionsAgent.Dialogs; +using DevolutionsAgent.Properties; + +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Windows.Forms; + +using WixSharp; + +namespace WixSharpSetup.Dialogs; + +public partial class AgentTunnelDialog : AgentDialog +{ + public AgentTunnelDialog() + { + InitializeComponent(); + label1.MakeTransparentOn(banner); + label2.MakeTransparentOn(banner); + } + + public override bool ToProperties() + { + Runtime.Session[AgentProperties.AgentTunnelEnrollmentString] = enrollmentString.Text.Trim(); + Runtime.Session[AgentProperties.AgentTunnelAdvertiseSubnets] = advertiseSubnets.Text.Trim(); + Runtime.Session[AgentProperties.AgentTunnelAdvertiseDomains] = advertiseDomains.Text.Trim(); + Runtime.Session[AgentProperties.AgentTunnelGatewayUrl] = gatewayUrl.Text.Trim(); + + return true; + } + + public override void OnLoad(object sender, EventArgs e) + { + banner.Image = Runtime.Session.GetResourceBitmap("WixUI_Bmp_Banner"); + + enrollmentString.Text = Runtime.Session.Property(AgentProperties.AgentTunnelEnrollmentString); + advertiseSubnets.Text = Runtime.Session.Property(AgentProperties.AgentTunnelAdvertiseSubnets); + advertiseDomains.Text = Runtime.Session.Property(AgentProperties.AgentTunnelAdvertiseDomains); + gatewayUrl.Text = Runtime.Session.Property(AgentProperties.AgentTunnelGatewayUrl); + + base.OnLoad(sender, e); + } + + public override bool DoValidate() + { + // Tunnel is optional — if enrollment string is empty, skip tunnel setup entirely. + if (string.IsNullOrWhiteSpace(enrollmentString.Text)) + { + return true; + } + + // JWT shape: three base64url segments separated by dots. The agent's `up --enrollment-string` + // parses the JWT claims for jet_gw_url / jet_agent_name, so the dialog only sanity-checks + // shape and base64url decodability here; signature verification happens at the gateway. + string text = Regex.Replace(enrollmentString.Text, @"\s+", ""); + string[] parts = text.Split('.'); + if (parts.Length != 3 || parts.Any(string.IsNullOrEmpty)) + { + ShowValidationErrorString("Enrollment string must be a JWT (three base64url segments separated by dots)."); + return false; + } + foreach (string seg in parts) + { + string b64 = seg.Replace('-', '+').Replace('_', '/'); + b64 = b64.PadRight((b64.Length + 3) & ~3, '='); + try { _ = Convert.FromBase64String(b64); } + catch (FormatException) + { + ShowValidationErrorString("Enrollment string is not valid base64url."); + return false; + } + } + + return true; + } + + // ReSharper disable once RedundantOverriddenMember + protected override void Back_Click(object sender, EventArgs e) => base.Back_Click(sender, e); + + // ReSharper disable once RedundantOverriddenMember + protected override void Next_Click(object sender, EventArgs e) => base.Next_Click(sender, e); + + // ReSharper disable once RedundantOverriddenMember + protected override void Cancel_Click(object sender, EventArgs e) => base.Cancel_Click(sender, e); +} diff --git a/package/AgentWindowsManaged/Dialogs/Wizard.cs b/package/AgentWindowsManaged/Dialogs/Wizard.cs index df1f71f78..bbb34c86f 100644 --- a/package/AgentWindowsManaged/Dialogs/Wizard.cs +++ b/package/AgentWindowsManaged/Dialogs/Wizard.cs @@ -1,5 +1,6 @@ using DevolutionsAgent.Helpers; using DevolutionsAgent.Properties; +using DevolutionsAgent.Resources; using Microsoft.Deployment.WindowsInstaller; using System; using System.Collections.Generic; @@ -21,6 +22,7 @@ static Wizard() { typeof(WelcomeDialog), typeof(FeaturesDialog), + typeof(AgentTunnelDialog), typeof(InstallDirDialog), }; @@ -28,7 +30,7 @@ static Wizard() Sequence = dialogs.ToArray(); } - + internal static IEnumerable Dialogs => Sequence; internal static int Move(IManagedDialog current, bool forward) @@ -36,10 +38,27 @@ internal static int Move(IManagedDialog current, bool forward) Type t = current.GetType(); int index = Dialogs.FindIndex(t); - index = forward ? index + 1 : index - 1; + // Skip dialogs whose preconditions aren't met (e.g. feature unselected). + // Iterating handles both forward and back traversal symmetrically. + while (true) + { + index = forward ? index + 1 : index - 1; + if (index < 0 || index >= Sequence.Length) break; + if (!ShouldSkip(Sequence[index], current)) break; + } return index; } + private static bool ShouldSkip(Type dialogType, IManagedDialog current) + { + if (dialogType == typeof(AgentTunnelDialog)) + { + string addlocal = (current as WixSharp.UI.Forms.ManagedForm)?.MsiRuntime?.Session?["ADDLOCAL"] ?? string.Empty; + return !addlocal.Split(',').Select(s => s.Trim()).Contains(Features.AGENT_TUNNEL_FEATURE.Id); + } + return false; + } + internal static int GetNext(IManagedDialog current) => Move(current, true); internal static int GetPrevious(IManagedDialog current) => Move(current, false); diff --git a/package/AgentWindowsManaged/Program.cs b/package/AgentWindowsManaged/Program.cs index 47338d33a..14b412519 100644 --- a/package/AgentWindowsManaged/Program.cs +++ b/package/AgentWindowsManaged/Program.cs @@ -324,6 +324,17 @@ static void Main() Win64 = project.Platform == Platform.x64, RegistryKeyAction = RegistryKeyAction.create, Feature = Features.AGENT_FEATURE, + }, + // Anchors the AGENT_TUNNEL_FEATURE to a real Component so it shows + // up in the Feature table and the Custom Setup tree. The value + // itself doubles as a diagnostic marker that the feature was + // selected at install time. + new (RegistryHive.LocalMachine, $"Software\\{Includes.VENDOR_NAME}\\{Includes.SHORT_NAME}", "TunnelEnabled", "1") + { + AttributesDefinition = "Type=string", + Win64 = project.Platform == Platform.x64, + RegistryKeyAction = RegistryKeyAction.create, + Feature = Features.AGENT_TUNNEL_FEATURE, } }; diff --git a/package/AgentWindowsManaged/Properties/AgentProperties.cs b/package/AgentWindowsManaged/Properties/AgentProperties.cs index 1010fb969..4aa6365f3 100644 --- a/package/AgentWindowsManaged/Properties/AgentProperties.cs +++ b/package/AgentWindowsManaged/Properties/AgentProperties.cs @@ -16,6 +16,28 @@ internal partial class AgentProperties /// public static string InstallDir = "INSTALLDIR"; + /// + /// Agent tunnel enrollment string (DVLS-signed JWT verbatim) + /// + public static string AgentTunnelEnrollmentString = "AGENT_TUNNEL_ENROLLMENT_STRING"; + + /// + /// Comma-separated subnets to advertise (e.g., "10.10.0.0/24, 192.168.1.0/24") + /// + public static string AgentTunnelAdvertiseSubnets = "AGENT_TUNNEL_ADVERTISE_SUBNETS"; + + /// + /// Comma-separated DNS domains to advertise (e.g., "corp.example.com, lab.example.com") + /// + public static string AgentTunnelAdvertiseDomains = "AGENT_TUNNEL_ADVERTISE_DOMAINS"; + + /// + /// Optional gateway URL override. When set, the agent uses this URL instead of the JWT's + /// jet_gw_url claim. Useful when the JWT was minted with a URL that isn't reachable from + /// the agent's network (e.g. DVLS embedded "localhost" but the agent is remote). + /// + public static string AgentTunnelGatewayUrl = "AGENT_TUNNEL_GATEWAY_URL"; + public AgentProperties(ISession runtimeSession) { this.runtimeSession = runtimeSession; diff --git a/package/AgentWindowsManaged/Resources/DevolutionsAgent_en-us.wxl b/package/AgentWindowsManaged/Resources/DevolutionsAgent_en-us.wxl index ba52af25b..1c4e67813 100644 --- a/package/AgentWindowsManaged/Resources/DevolutionsAgent_en-us.wxl +++ b/package/AgentWindowsManaged/Resources/DevolutionsAgent_en-us.wxl @@ -9,6 +9,8 @@ Devolutions PEDM Installs the RDP Extension RDP Extension + Agent Tunnel + Configure the agent to connect to a Devolutions Gateway via QUIC tunnel. Requires an enrollment string from Devolutions Server, Hub, or Gateway. 1033 System-wide service for extending Devolutions Gateway functionality. Devolutions Inc. @@ -57,4 +59,14 @@ If it appears minimized then active it from the taskbar. Ready to update [ProductName] Welcome to the [ProductName] 20[ProductVersion] Setup Wizard + + Agent Tunnel Configuration + Configure connection to a Devolutions Gateway via QUIC tunnel. + Enrollment String (paste the JWT from Devolutions Server, Hub, or Gateway): + Advertise Subnets: + Comma-separated CIDR notation, e.g. 10.10.0.0/24, 192.168.1.0/24. Leave blank for auto-detection. + Advertise Domains: + Comma-separated DNS suffixes the agent can resolve, e.g. corp.example.com, lab.example.com. Leave blank to skip. + Gateway URL (advanced, optional): + Override the URL embedded in the enrollment JWT. Leave blank to use the JWT's value. diff --git a/package/AgentWindowsManaged/Resources/DevolutionsAgent_fr-fr.wxl b/package/AgentWindowsManaged/Resources/DevolutionsAgent_fr-fr.wxl index eb2213bd0..791721572 100644 --- a/package/AgentWindowsManaged/Resources/DevolutionsAgent_fr-fr.wxl +++ b/package/AgentWindowsManaged/Resources/DevolutionsAgent_fr-fr.wxl @@ -3,6 +3,17 @@ Installe l'extension RDP Extension RDP + Tunnel d'agent + Configurer l'agent pour se connecter à une passerelle Devolutions via un tunnel QUIC. Nécessite une chaîne d'enrôlement depuis Devolutions Server, Hub, ou Gateway. + Configuration du tunnel d'agent + Configurer la connexion à une passerelle Devolutions via un tunnel QUIC. + Chaîne d'enrôlement (collez le JWT depuis Devolutions Server, Hub ou Gateway) : + Sous-réseaux annoncés : + Notation CIDR séparée par des virgules, p. ex. 10.10.0.0/24, 192.168.1.0/24. Laissez vide pour la détection automatique. + Domaines annoncés : + Suffixes DNS séparés par des virgules que l'agent peut résoudre, p. ex. corp.example.com, lab.example.com. Laissez vide pour ignorer. + URL de la passerelle (avancé, facultatif) : + Remplace l'URL incluse dans le JWT d'enrôlement. Laissez vide pour utiliser la valeur du JWT. 1036 Service à l’échelle du système pour étendre les fonctionnalités de Devolutions Gateway. Devolutions Inc. diff --git a/package/AgentWindowsManaged/Resources/Features.cs b/package/AgentWindowsManaged/Resources/Features.cs index f6f1f98ef..4408d44b0 100644 --- a/package/AgentWindowsManaged/Resources/Features.cs +++ b/package/AgentWindowsManaged/Resources/Features.cs @@ -17,11 +17,16 @@ internal static class Features Id = $"{FEATURE_ID_PREFIX}Updater" }; + internal static Feature AGENT_TUNNEL_FEATURE = new("!(loc.FeatureAgentTunnelName)", "!(loc.FeatureAgentTunnelDescription)", isEnabled: false, allowChange: true) + { + Id = $"{FEATURE_ID_PREFIX}Tunnel" + }; + internal static Feature AGENT_FEATURE = new("!(loc.FeatureAgentName)", isEnabled: true, allowChange: false) { - Id = $"{FEATURE_ID_PREFIX}Agent", + Id = $"{FEATURE_ID_PREFIX}Agent", Description = "!(loc.FeatureAgentDescription)", - Children = [ AGENT_UPDATER_FEATURE ] + Children = [ AGENT_UPDATER_FEATURE, AGENT_TUNNEL_FEATURE ] }; internal static Feature PEDM_FEATURE = new("!(loc.FeaturePedmName)", "!(loc.FeaturePedmDescription)", isEnabled: false)