Run Devolutions Gateway Locally
Use this skill whenever a coding agent needs to run the Gateway from the repository for local development or testing.
The production configuration template intentionally uses wildcard listener hosts, so do not change
ConfFile::generate_new or installed service configuration to solve a local testing problem.
Instead, use a separate working configuration under the repository's ignored config\ directory
and bind its listeners to 127.0.0.1 by default.
Default launch
Build the Gateway binary:
cargo build -p devolutions-gatewayFrom the repository or a child directory, run this complete preparation block. It creates the ignored
config\directory, generates the required provisioner key pair on first use, rewrites every listener to the selected host, and refuses unapproved remote binds. The first run requires the .NET SDK to build the local PowerShell module.$RepoRoot = (git rev-parse --show-toplevel).Trim() if ($LASTEXITCODE -ne 0) { throw 'Run this command from the Devolutions Gateway repository or one of its child directories.' } $ErrorActionPreference = 'Stop' $ConfigDir = Join-Path $RepoRoot 'config' $ConfigFile = Join-Path $ConfigDir 'gateway.json' $BindHost = '127.0.0.1' $AllowRemoteBinding = $false $LoopbackHosts = @('127.0.0.1', 'localhost', '::1') $BindHost = $BindHost.TrimStart('[').TrimEnd(']') $UrlBindHost = if ($BindHost.Contains(':')) { "[$BindHost]" } else { $BindHost } if ($BindHost -notin $LoopbackHosts -and -not $AllowRemoteBinding) { throw "Refusing non-localhost Gateway binding. Set `$AllowRemoteBinding only for an intentional remote test." } if (-not (Test-Path -LiteralPath $ConfigFile)) { New-Item -ItemType Directory -Force -Path $ConfigDir | Out-Null cargo run -p devolutions-gateway -- --config-path $ConfigDir --config-init-only if ($LASTEXITCODE -ne 0) { throw 'Gateway configuration initialization failed.' } } $Config = Get-Content $ConfigFile -Raw | ConvertFrom-Json if ($null -eq $Config.Listeners -or $Config.Listeners.Count -eq 0) { throw "No Gateway listeners are configured in $ConfigFile." } $HasProvisionerPublicKeyFile = -not [string]::IsNullOrWhiteSpace($Config.ProvisionerPublicKeyFile) $HasProvisionerPublicKeyData = -not [string]::IsNullOrWhiteSpace($Config.ProvisionerPublicKeyData) $ProvisionerPublicKeyPath = $null $NeedsProvisionerKey = -not $HasProvisionerPublicKeyFile -and -not $HasProvisionerPublicKeyData if ($HasProvisionerPublicKeyFile) { $ProvisionerPublicKeyPath = if ([IO.Path]::IsPathRooted($Config.ProvisionerPublicKeyFile)) { $Config.ProvisionerPublicKeyFile } else { Join-Path $ConfigDir $Config.ProvisionerPublicKeyFile } $NeedsProvisionerKey = -not (Test-Path -LiteralPath $ProvisionerPublicKeyPath -PathType Leaf) } if ($NeedsProvisionerKey) { # The PowerShell module honors this variable even when -ConfigPath is supplied. $Env:DGATEWAY_CONFIG_PATH = $ConfigDir & (Join-Path $RepoRoot 'powershell\build.ps1') if ($LASTEXITCODE -ne 0) { throw 'Building the Devolutions Gateway PowerShell module failed.' } $ModuleDir = Join-Path $RepoRoot 'powershell\package\DevolutionsGateway' $ModuleManifest = Join-Path $ModuleDir 'DevolutionsGateway.psd1' $PickyAssembly = Join-Path $ModuleDir 'bin\Devolutions.Picky.dll' if (-not (Test-Path -LiteralPath $ModuleManifest -PathType Leaf) -or -not (Test-Path -LiteralPath $PickyAssembly -PathType Leaf)) { throw 'The Devolutions Gateway PowerShell module build did not produce its required artifacts.' } Import-Module $ModuleManifest New-DGatewayProvisionerKeyPair -ConfigPath $ConfigDir # Preserve all original config fields after the module writes the generated key paths. $Config | Add-Member -NotePropertyName ProvisionerPublicKeyFile -NotePropertyValue 'provisioner.pem' -Force $Config | Add-Member -NotePropertyName ProvisionerPrivateKeyFile -NotePropertyValue 'provisioner.key' -Force } foreach ($Listener in $Config.Listeners) { $Listener.InternalUrl = $Listener.InternalUrl -replace '(?<=://)(\[[^\]]+\]|[^/:]+)', $UrlBindHost $Listener.ExternalUrl = $Listener.ExternalUrl -replace '(?<=://)(\[[^\]]+\]|[^/:]+)', $UrlBindHost } $HttpListener = $Config.Listeners | Where-Object { $_.InternalUrl -match '^https?://' } | Select-Object -First 1 if ($null -eq $HttpListener) { throw 'Gateway configuration has no HTTP listener for the health check.' } $HealthListenerMatch = [regex]::Match( $HttpListener.InternalUrl, '^(?<Scheme>https?)://(?:\[[^\]]+\]|[^/:]+):(?<Port>\d+)/?$' ) if (-not $HealthListenerMatch.Success) { throw "Unable to determine a health-check URL from $($HttpListener.InternalUrl)." } $HealthProbeHost = switch ($BindHost) { '*' { '127.0.0.1' } '0.0.0.0' { '127.0.0.1' } '::' { '[::1]' } default { $UrlBindHost } } $HealthUrl = '{0}://{1}:{2}/jet/health' -f ` $HealthListenerMatch.Groups['Scheme'].Value, ` $HealthProbeHost, ` $HealthListenerMatch.Groups['Port'].Value [IO.File]::WriteAllText( $ConfigFile, ($Config | ConvertTo-Json -Depth 20), $(New-Object System.Text.UTF8Encoding $False) ) Write-Output "Gateway health URL: $HealthUrl"Start the Gateway in a dedicated terminal:
$RepoRoot = (git rev-parse --show-toplevel).Trim() $ConfigDir = Join-Path $RepoRoot 'config' cargo run -p devolutions-gateway -- --config-path $ConfigDirFrom a second terminal, use the
Gateway health URLprinted by the preparation block withInvoke-WebRequest -Uri <health-url>or use the endpoint required by the test. Stop the foreground process withCtrl+Cwhen testing is complete.
Remote-device testing
Binding to a machine address is an explicit exception, not a default. Set $BindHost to the
specific address or hostname that the other device must reach and set $AllowRemoteBinding = $true
in the launch block only for that test. Do not use *, 0.0.0.0, or [::] unless the test
specifically requires all interfaces and the opt-in is documented in the agent's test notes.
Expect Windows Firewall to prompt when a non-loopback listener is started, and restore the
loopback configuration after the remote test.
Never point this workflow at the installed %ProgramData%\Devolutions\Gateway configuration or
modify production listener defaults. Always use --config-path $ConfigDir when running the
Gateway through this workflow.