-
Notifications
You must be signed in to change notification settings - Fork 6
feat(local-windows-rdp): local Windows RDP using coder desktop #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DevelopmentCats
wants to merge
13
commits into
main
Choose a base branch
from
cat/local-windows-rdp
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
dc0f6d0
feat(local-windows-rdp): add initial RDP module for Coder workspaces …
DevelopmentCats dace493
feat(local-windows-rdp): add comprehensive test suite for RDP module
DevelopmentCats 14ea2b7
feat(local-windows-rdp): add README for Windows RDP Desktop module wi…
DevelopmentCats c0832a1
feat(local-windows-rdp): add RDP setup script resource and output
DevelopmentCats 639b4a2
feat(local-windows-rdp): add PowerShell script for RDP configuration …
DevelopmentCats 3bea8a7
feat(local-windows-rdp): enhance test suite with RDP script validatio…
DevelopmentCats d42de1d
feat(local-windows-rdp): update README to enhance module description,…
DevelopmentCats 30bf0b7
Update registry/coder/modules/local-windows-rdp/README.md
DevelopmentCats 9968c3d
Update registry/coder/modules/local-windows-rdp/main.tf
DevelopmentCats 41dbc18
Update registry/coder/modules/local-windows-rdp/main.tf
DevelopmentCats 1ffa9fa
chore(local-windows-rdp): Removed Stale Requirements, Add required ag…
DevelopmentCats b8f0c91
fix(local-windows-rdp): Update agent_name variable to be required in …
DevelopmentCats 0de978e
Merge branch 'main' into cat/local-windows-rdp
matifali File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
--- | ||
display_name: Windows RDP Desktop | ||
description: Enable RDP on Windows and add a one-click Coder Desktop button for seamless access | ||
icon: ../../../../.icons/desktop.svg | ||
maintainer_github: coder | ||
verified: true | ||
tags: [rdp, windows, desktop, remote] | ||
--- | ||
|
||
# Windows RDP Desktop | ||
|
||
This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds a one-click button to launch RDP sessions directly through Coder Desktop. It provides a complete, standalone solution for RDP access without requiring manual configuration or port forwarding. | ||
|
||
```tf | ||
module "rdp_desktop" { | ||
count = data.coder_workspace.me.start_count | ||
source = "registry.coder.com/coder/local-windows-rdp/coder" | ||
version = "1.0.0" | ||
agent_id = coder_agent.main.id | ||
agent_name = coder_agent.main.name | ||
} | ||
``` | ||
|
||
## Features | ||
|
||
- ✅ **Standalone Solution**: Automatically configures RDP on Windows workspaces | ||
- ✅ **One-click Access**: Launch RDP sessions directly through Coder Desktop | ||
- ✅ **No Port Forwarding**: Uses Coder Desktop URI handling | ||
- ✅ **Auto-configuration**: Sets up Windows firewall, services, and authentication | ||
- ✅ **Secure**: Configurable credentials with sensitive variable handling | ||
- ✅ **Customizable**: Display name, credentials, and UI ordering options | ||
|
||
## What This Module Does | ||
|
||
1. **Enables RDP** on the Windows workspace | ||
2. **Sets the administrator password** for RDP authentication | ||
3. **Configures Windows Firewall** to allow RDP connections | ||
4. **Starts RDP services** automatically | ||
5. **Creates a Coder Desktop button** for one-click access | ||
|
||
## Requirements | ||
|
||
- **Coder Desktop**: Must be installed on the client machine ([Download here](https://coder.com/docs/user-guides/desktop)) | ||
|
||
## Examples | ||
|
||
### Basic Usage | ||
|
||
Uses default credentials (Username: `Administrator`, Password: `coderRDP!`): | ||
|
||
```tf | ||
module "rdp_desktop" { | ||
count = data.coder_workspace.me.start_count | ||
source = "registry.coder.com/coder/local-windows-rdp/coder" | ||
version = "1.0.0" | ||
agent_id = coder_agent.main.id | ||
agent_name = coder_agent.main.name | ||
} | ||
``` | ||
|
||
### Custom display name | ||
|
||
Specify a custom display name for the `coder_app` button: | ||
|
||
```tf | ||
module "rdp_desktop" { | ||
count = data.coder_workspace.me.start_count | ||
source = "registry.coder.com/coder/local-windows-rdp/coder" | ||
version = "1.0.0" | ||
agent_id = coder_agent.windows.id | ||
agent_name = "windows" | ||
display_name = "Windows Desktop" | ||
order = 1 | ||
} | ||
``` |
120 changes: 120 additions & 0 deletions
120
registry/coder/modules/local-windows-rdp/configure-rdp.ps1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# PowerShell script to configure RDP for Coder Desktop access | ||
# This script enables RDP, sets the admin password, and configures necessary settings | ||
|
||
Write-Output "[Coder RDP Setup] Starting RDP configuration..." | ||
|
||
# Function to set the administrator password | ||
function Set-AdminPassword { | ||
param ( | ||
[string]$adminUsername, | ||
[string]$adminPassword | ||
) | ||
|
||
Write-Output "[Coder RDP Setup] Setting password for user: $adminUsername" | ||
|
||
try { | ||
# Convert password to secure string | ||
$securePassword = ConvertTo-SecureString -AsPlainText $adminPassword -Force | ||
|
||
# Set the password for the user | ||
Get-LocalUser -Name $adminUsername | Set-LocalUser -Password $securePassword | ||
|
||
# Enable the user account (in case it's disabled) | ||
Get-LocalUser -Name $adminUsername | Enable-LocalUser | ||
|
||
Write-Output "[Coder RDP Setup] Successfully set password for $adminUsername" | ||
} catch { | ||
Write-Error "[Coder RDP Setup] Failed to set password: $_" | ||
exit 1 | ||
} | ||
} | ||
|
||
# Function to enable and configure RDP | ||
function Enable-RDP { | ||
Write-Output "[Coder RDP Setup] Enabling Remote Desktop..." | ||
|
||
try { | ||
# Enable RDP | ||
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -Force | ||
|
||
# Disable Network Level Authentication (NLA) for easier access | ||
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -Force | ||
|
||
# Set security layer to RDP Security Layer | ||
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -Force | ||
|
||
Write-Output "[Coder RDP Setup] RDP enabled successfully" | ||
} catch { | ||
Write-Error "[Coder RDP Setup] Failed to enable RDP: $_" | ||
exit 1 | ||
} | ||
} | ||
|
||
# Function to configure Windows Firewall for RDP | ||
function Configure-Firewall { | ||
Write-Output "[Coder RDP Setup] Configuring Windows Firewall for RDP..." | ||
|
||
try { | ||
# Enable RDP firewall rules | ||
Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue | ||
|
||
# If the above fails, try alternative method | ||
if ($LASTEXITCODE -ne 0) { | ||
netsh advfirewall firewall set rule group="remote desktop" new enable=Yes | ||
} | ||
|
||
Write-Output "[Coder RDP Setup] Firewall configured successfully" | ||
} catch { | ||
Write-Warning "[Coder RDP Setup] Failed to configure firewall rules: $_" | ||
# Continue anyway as RDP might still work | ||
} | ||
} | ||
|
||
# Function to ensure RDP service is running | ||
function Start-RDPService { | ||
Write-Output "[Coder RDP Setup] Starting Remote Desktop Services..." | ||
|
||
try { | ||
# Start the Terminal Services | ||
Set-Service -Name "TermService" -StartupType Automatic -ErrorAction SilentlyContinue | ||
Start-Service -Name "TermService" -ErrorAction SilentlyContinue | ||
|
||
# Start Remote Desktop Services UserMode Port Redirector | ||
Set-Service -Name "UmRdpService" -StartupType Automatic -ErrorAction SilentlyContinue | ||
Start-Service -Name "UmRdpService" -ErrorAction SilentlyContinue | ||
|
||
Write-Output "[Coder RDP Setup] RDP services started successfully" | ||
} catch { | ||
Write-Warning "[Coder RDP Setup] Some RDP services may not have started: $_" | ||
# Continue anyway | ||
} | ||
} | ||
|
||
# Main execution | ||
try { | ||
# Template variables from Terraform | ||
$username = "${username}" | ||
$password = "${password}" | ||
|
||
# Validate inputs | ||
if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) { | ||
Write-Error "[Coder RDP Setup] Username or password is empty" | ||
exit 1 | ||
} | ||
|
||
# Execute configuration steps | ||
Set-AdminPassword -adminUsername $username -adminPassword $password | ||
Enable-RDP | ||
Configure-Firewall | ||
Start-RDPService | ||
|
||
Write-Output "[Coder RDP Setup] RDP configuration completed successfully!" | ||
Write-Output "[Coder RDP Setup] You can now connect using:" | ||
Write-Output " Username: $username" | ||
Write-Output " Password: [hidden]" | ||
Write-Output " Port: 3389 (default)" | ||
|
||
} catch { | ||
Write-Error "[Coder RDP Setup] An unexpected error occurred: $_" | ||
exit 1 | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import { describe, expect, it } from "bun:test"; | ||
import { | ||
type TerraformState, | ||
runTerraformApply, | ||
runTerraformInit, | ||
testRequiredVariables, | ||
} from "~test"; | ||
|
||
type TestVariables = Readonly<{ | ||
agent_id: string; | ||
agent_name: string; | ||
username?: string; | ||
password?: string; | ||
display_name?: string; | ||
order?: number; | ||
}>; | ||
|
||
function findRdpApp(state: TerraformState) { | ||
for (const resource of state.resources) { | ||
const isRdpAppResource = | ||
resource.type === "coder_app" && resource.name === "rdp_desktop"; | ||
|
||
if (!isRdpAppResource) { | ||
continue; | ||
} | ||
|
||
for (const instance of resource.instances) { | ||
if (instance.attributes.slug === "rdp-desktop") { | ||
return instance.attributes; | ||
} | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
function findRdpScript(state: TerraformState) { | ||
for (const resource of state.resources) { | ||
const isRdpScriptResource = | ||
resource.type === "coder_script" && resource.name === "rdp_setup"; | ||
|
||
if (!isRdpScriptResource) { | ||
continue; | ||
} | ||
|
||
for (const instance of resource.instances) { | ||
if (instance.attributes.display_name === "Configure RDP") { | ||
return instance.attributes; | ||
} | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
describe("local-windows-rdp", async () => { | ||
await runTerraformInit(import.meta.dir); | ||
|
||
testRequiredVariables<TestVariables>(import.meta.dir, { | ||
agent_id: "test-agent-id", | ||
agent_name: "test-agent", | ||
}); | ||
|
||
it("should create RDP app with default values", async () => { | ||
const state = await runTerraformApply<TestVariables>(import.meta.dir, { | ||
agent_id: "test-agent-id", | ||
agent_name: "main", | ||
}); | ||
|
||
const app = findRdpApp(state); | ||
|
||
// Verify the app was created | ||
expect(app).not.toBeNull(); | ||
expect(app?.slug).toBe("rdp-desktop"); | ||
expect(app?.display_name).toBe("RDP Desktop"); | ||
expect(app?.icon).toBe("/icon/desktop.svg"); | ||
expect(app?.external).toBe(true); | ||
|
||
// Verify the URI format | ||
expect(app?.url).toStartWith("coder://"); | ||
expect(app?.url).toContain("/v0/open/ws/"); | ||
expect(app?.url).toContain("/agent/main/rdp"); | ||
expect(app?.url).toContain("username=Administrator"); | ||
expect(app?.url).toContain("password=coderRDP!"); | ||
}); | ||
|
||
it("should create RDP configuration script", async () => { | ||
const state = await runTerraformApply<TestVariables>(import.meta.dir, { | ||
agent_id: "test-agent-id", | ||
agent_name: "main", | ||
}); | ||
|
||
const script = findRdpScript(state); | ||
|
||
// Verify the script was created | ||
expect(script).not.toBeNull(); | ||
expect(script?.display_name).toBe("Configure RDP"); | ||
expect(script?.icon).toBe("/icon/desktop.svg"); | ||
expect(script?.run_on_start).toBe(true); | ||
expect(script?.run_on_stop).toBe(false); | ||
|
||
// Verify the script contains PowerShell configuration | ||
expect(script?.script).toContain("Set-AdminPassword"); | ||
expect(script?.script).toContain("Enable-RDP"); | ||
expect(script?.script).toContain("Configure-Firewall"); | ||
expect(script?.script).toContain("Start-RDPService"); | ||
}); | ||
|
||
it("should create RDP app with custom values", async () => { | ||
const state = await runTerraformApply<TestVariables>(import.meta.dir, { | ||
agent_id: "custom-agent-id", | ||
agent_name: "windows-agent", | ||
username: "CustomUser", | ||
password: "CustomPass123!", | ||
display_name: "Custom RDP", | ||
order: 5, | ||
}); | ||
|
||
const app = findRdpApp(state); | ||
|
||
// Verify custom values | ||
expect(app?.display_name).toBe("Custom RDP"); | ||
expect(app?.order).toBe(5); | ||
|
||
// Verify custom credentials in URI | ||
expect(app?.url).toContain("/agent/windows-agent/rdp"); | ||
expect(app?.url).toContain("username=CustomUser"); | ||
expect(app?.url).toContain("password=CustomPass123!"); | ||
}); | ||
|
||
it("should pass custom credentials to PowerShell script", async () => { | ||
const state = await runTerraformApply<TestVariables>(import.meta.dir, { | ||
agent_id: "test-agent-id", | ||
agent_name: "main", | ||
username: "TestAdmin", | ||
password: "TestPassword123!", | ||
}); | ||
|
||
const script = findRdpScript(state); | ||
|
||
// Verify custom credentials are in the script | ||
expect(script?.script).toContain('$username = "TestAdmin"'); | ||
expect(script?.script).toContain('$password = "TestPassword123!"'); | ||
}); | ||
|
||
it("should handle sensitive password variable", async () => { | ||
const state = await runTerraformApply<TestVariables>(import.meta.dir, { | ||
agent_id: "test-agent-id", | ||
agent_name: "main", | ||
password: "SensitivePass123!", | ||
}); | ||
|
||
const app = findRdpApp(state); | ||
|
||
// Verify password is included in URI even when sensitive | ||
expect(app?.url).toContain("password=SensitivePass123!"); | ||
}); | ||
|
||
it("should use correct default agent name", async () => { | ||
const state = await runTerraformApply<TestVariables>(import.meta.dir, { | ||
agent_id: "test-agent-id", | ||
agent_name: "main", | ||
}); | ||
|
||
const app = findRdpApp(state); | ||
expect(app?.url).toContain("/agent/main/rdp"); | ||
}); | ||
|
||
it("should construct proper Coder URI format", async () => { | ||
const state = await runTerraformApply<TestVariables>(import.meta.dir, { | ||
agent_id: "test-agent-id", | ||
agent_name: "test-agent", | ||
username: "TestUser", | ||
password: "TestPass", | ||
}); | ||
|
||
const app = findRdpApp(state); | ||
|
||
// Verify complete URI structure | ||
expect(app?.url).toMatch( | ||
/^coder:\/\/[^\/]+\/v0\/open\/ws\/[^\/]+\/agent\/test-agent\/rdp\?username=TestUser&password=TestPass$/, | ||
); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See coder/registry-server#204 and coder/registry-server#199. We still need to implement these in the backend.
@Parkreiner, is it fine if we add these extra metadata entries even though they are yet handled in the registry server?