Files
netbird/client/ui/scripts/mdm-toggle.ps1
2026-06-16 12:11:47 +02:00

197 lines
7.4 KiB
PowerShell

<#
.SYNOPSIS
Interactive tester for NetBird's MDM policy on Windows.
.DESCRIPTION
Edits HKLM\Software\Policies\NetBird (the same registry key the OS would
populate from Group Policy / Intune ADMX ingestion / a Registry CSP profile,
see client/mdm/policy_windows.go). The MDM ticker re-reads the key within
<=60s; the daemon restarts the engine itself in-process - no service restart
needed for a change to take effect (the [k] kick only forces an immediate
re-read or recovers a wedged daemon).
Value-type mapping (mirrors readRegistryValue):
string -> REG_SZ
integer -> REG_DWORD
bool -> REG_DWORD (1=true, 0=false) GetBool treats !=0 as true
array -> REG_MULTI_SZ
Requires elevation (writing HKLM). Re-launches itself elevated if needed.
Keys: number to edit a field, [k] kick the daemon, [c] clear the whole
policy key, [r] refresh, [q] quit.
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$PolicyPath = 'HKLM:\Software\Policies\NetBird'
# Known keys and their registry types. Order drives the TUI numbering.
$Fields = @(
@{ Key = 'managementURL'; Type = 'string' }
@{ Key = 'preSharedKey'; Type = 'string' }
@{ Key = 'wireguardPort'; Type = 'integer' }
@{ Key = 'splitTunnelMode'; Type = 'string' } # allow / disallow
@{ Key = 'splitTunnelApps'; Type = 'array' }
@{ Key = 'rosenpassEnabled'; Type = 'bool' }
@{ Key = 'rosenpassPermissive'; Type = 'bool' }
@{ Key = 'disableClientRoutes'; Type = 'bool' }
@{ Key = 'disableServerRoutes'; Type = 'bool' }
@{ Key = 'allowServerSSH'; Type = 'bool' }
@{ Key = 'disableAutoConnect'; Type = 'bool' }
@{ Key = 'blockInbound'; Type = 'bool' }
@{ Key = 'disableMetricsCollection'; Type = 'bool' }
@{ Key = 'disableAdvancedView'; Type = 'bool' }
@{ Key = 'disableProfiles'; Type = 'bool' }
@{ Key = 'disableNetworks'; Type = 'bool' }
@{ Key = 'disableUpdateSettings'; Type = 'bool' }
)
# ---- elevation --------------------------------------------------------------
function Test-Admin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
(New-Object Security.Principal.WindowsPrincipal $id).IsInRole(
[Security.Principal.WindowsBuiltinRole]::Administrator)
}
if (-not (Test-Admin)) {
Write-Host 'Elevation required for HKLM writes - relaunching as admin...' -ForegroundColor Yellow
Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"")
exit 0
}
# ---- ANSI helpers (VT; conhost on Win10+ and Windows Terminal support it) ----
$IsTty = -not [Console]::IsOutputRedirected
$e = [char]27
function C($code) { if ($IsTty) { "${e}[${code}m" } else { '' } }
$Reset = C '0'; $Bold = C '1'; $Dim = C '2'
$Green = C '32'; $Red = C '31'; $Yellow = C '33'; $Cyan = C '36'
# ---- service ----------------------------------------------------------------
function Restart-Daemon {
$svc = Get-Service -Name 'Netbird', 'netbird' -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $svc) { Write-Host 'Netbird service not found.' -ForegroundColor Red; return }
Write-Host "restarting service ($($svc.Name))..."
Restart-Service -Name $svc.Name -Force
}
# ---- registry read/write ----------------------------------------------------
function Ensure-PolicyKey {
if (-not (Test-Path $PolicyPath)) { New-Item -Path $PolicyPath -Force | Out-Null }
}
function Read-Value {
param([string]$Key)
if (-not (Test-Path $PolicyPath)) { return $null }
$item = Get-ItemProperty -Path $PolicyPath -Name $Key -ErrorAction SilentlyContinue
if ($null -eq $item) { return $null }
return $item.$Key
}
function Set-Value {
param([string]$Key, [string]$Type, $Value)
Ensure-PolicyKey
switch ($Type) {
'array' { Set-ItemProperty -Path $PolicyPath -Name $Key -Value ([string[]]$Value) -Type MultiString }
'integer' { Set-ItemProperty -Path $PolicyPath -Name $Key -Value ([int]$Value) -Type DWord }
'bool' { Set-ItemProperty -Path $PolicyPath -Name $Key -Value ([int]$Value) -Type DWord }
default { Set-ItemProperty -Path $PolicyPath -Name $Key -Value ([string]$Value) -Type String }
}
}
function Clear-Value {
param([string]$Key)
if (Test-Path $PolicyPath) {
Remove-ItemProperty -Path $PolicyPath -Name $Key -ErrorAction SilentlyContinue
}
}
function Clear-PolicyKey {
if (Test-Path $PolicyPath) {
Remove-Item -Path $PolicyPath -Recurse -Force
Write-Host "removed $PolicyPath"
} else {
Write-Host '(already absent)'
}
}
# ---- TUI --------------------------------------------------------------------
function Render-Value {
param($Raw, [string]$Type)
if ($null -eq $Raw -or ($Raw -is [string] -and $Raw -eq '')) {
return "$Dim-$Reset"
}
switch ($Type) {
'bool' {
if ([int]$Raw -ne 0) { "${Green}[+] true$Reset" } else { "${Red}[-] false$Reset" }
}
'array' { "${Cyan}[$($Raw -join ' ')]$Reset" }
default { "$Yellow$Raw$Reset" }
}
}
function Render-Screen {
Clear-Host
Write-Host "$Bold==== NetBird MDM Tester (Windows) ====$Reset"
Write-Host "${Dim}key:$Reset $PolicyPath`n"
for ($i = 0; $i -lt $Fields.Count; $i++) {
$f = $Fields[$i]
$raw = Read-Value $f.Key
$num = ('[{0,2}]' -f ($i + 1))
$val = Render-Value $raw $f.Type
Write-Host (" $Bold$num$Reset {0,-30} {1} $Dim({2})$Reset" -f $f.Key, $val, $f.Type)
}
Write-Host ''
Write-Host " ${Bold}[k]$Reset kick daemon ${Bold}[c]$Reset clear key ${Bold}[r]$Reset refresh ${Bold}[q]$Reset quit"
Write-Host ''
}
function Edit-Field {
param([int]$Index)
if ($Index -lt 1 -or $Index -gt $Fields.Count) { Write-Host ' (out of range)'; Start-Sleep -Milliseconds 600; return }
$f = $Fields[$Index - 1]
$cur = Read-Value $f.Key
switch ($f.Type) {
'bool' {
# Cycle: unset -> true -> false -> unset
if ($null -eq $cur) { Set-Value $f.Key 'bool' 1 }
elseif ([int]$cur -ne 0) { Set-Value $f.Key 'bool' 0 }
else { Clear-Value $f.Key }
}
'array' {
Write-Host " current: $($cur -join ' ')"
$line = Read-Host ' new array (space-separated, empty to unset)'
if ([string]::IsNullOrWhiteSpace($line)) { Clear-Value $f.Key }
else { Set-Value $f.Key 'array' ($line -split '\s+' | Where-Object { $_ }) }
}
default {
Write-Host " current: $cur"
$line = Read-Host " new $($f.Type) (empty to unset)"
if ([string]::IsNullOrWhiteSpace($line)) { Clear-Value $f.Key }
else { Set-Value $f.Key $f.Type $line }
}
}
}
Write-Host 'starting MDM tester (q to quit)'
while ($true) {
Render-Screen
$choice = Read-Host '>'
switch -Regex ($choice) {
'^(q|quit|exit)$' { Write-Host 'bye'; return }
'^(k|kick)$' { Restart-Daemon; Start-Sleep -Milliseconds 600 }
'^(c|clear)$' { Clear-PolicyKey; Start-Sleep -Milliseconds 600 }
'^(r|)$' { } # redraw
'^\d+$' { Edit-Field ([int]$choice) }
default { Write-Host ' (unknown - number to edit, k/c/r/q)'; Start-Sleep -Milliseconds 600 }
}
}