diff --git a/client/ui/scripts/mdm-fields.md b/client/ui/scripts/mdm-fields.md index e166ba778..02a5b51ed 100644 --- a/client/ui/scripts/mdm-fields.md +++ b/client/ui/scripts/mdm-fields.md @@ -1,9 +1,11 @@ -# MDM dev cheatsheet (macOS) +# MDM dev cheatsheet -Edits `/Library/Managed Preferences/io.netbird.client.plist`. Daemon picks up -changes within ≤60s; no restart needed. +Two platform helpers, same field set. The daemon's MDM ticker picks up changes +within ≤60s and restarts the engine in-process — no service restart needed. -Run the interactive TUI: +## macOS — `mdm-toggle.sh` + +Edits `/Library/Managed Preferences/io.netbird.client.plist`. ```bash ./mdm-toggle.sh tui @@ -12,6 +14,25 @@ Run the interactive TUI: Keys: number to edit a field, **k** to kick the daemon, **c** to clear the plist, **l** to view recent MDM log lines, **r** to refresh, **q** to quit. +## Windows — `mdm-toggle.ps1` + +**▶ [Launch the TUI](mdm-tui.bat)** (double-click `mdm-tui.bat` — it self-elevates). + +Edits `HKLM\Software\Policies\NetBird` (the key Group Policy / Intune ADMX / +a Registry CSP profile would populate — see `client/mdm/policy_windows.go`). +Self-elevates (HKLM writes need admin). Or run it directly: + +```powershell +.\mdm-toggle.ps1 +``` + +Same keys as the macOS TUI: number to edit a field, **k** to kick the daemon, +**c** to clear the policy key, **r** to refresh, **q** to quit. Registry type +mapping (mirrors `readRegistryValue`): `string → REG_SZ`, `integer → REG_DWORD`, +`bool → REG_DWORD` (1=true / 0=false), `array → REG_MULTI_SZ`. Value names are +case-insensitive (the loader canonicalises against the known key set); +service: `Netbird`. + ## Known keys - Strings/ints: `managementURL`, `preSharedKey`, `wireguardPort`, `splitTunnelMode` (`allow`/`disallow`) diff --git a/client/ui/scripts/mdm-toggle.ps1 b/client/ui/scripts/mdm-toggle.ps1 new file mode 100644 index 000000000..2d00cdab0 --- /dev/null +++ b/client/ui/scripts/mdm-toggle.ps1 @@ -0,0 +1,196 @@ +<# +.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 } + } +} diff --git a/client/ui/scripts/mdm-tui.bat b/client/ui/scripts/mdm-tui.bat new file mode 100644 index 000000000..d3fc49965 --- /dev/null +++ b/client/ui/scripts/mdm-tui.bat @@ -0,0 +1,4 @@ +@echo off +REM Double-click launcher for the NetBird MDM tester (Windows). +REM The .ps1 self-elevates. +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0mdm-toggle.ps1"