#Requires -RunAsAdministrator param( [Parameter(Mandatory=$true)] [string]$DnsServer, [Parameter(Mandatory=$true)] [string]$Domain ) $ErrorActionPreference = "Stop" # --- Configuration --- $NebulaVersion = "1.10.3" $NebulaUrl = "https://github.com/slackhq/nebula/releases/download/v$NebulaVersion/nebula-windows-amd64.zip" # Verify running as Administrator $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]$identity if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Error "This script must be run as a machine Administrator." exit 1 } $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition $InstallDir = "C:\Program Files\Nebula" $ConfigPath = "$InstallDir\config.yml" $StartupScript = "$InstallDir\set-dns-on-start.ps1" $ExpectedBinPath = "`"$InstallDir\nebula.exe`" -service run -config `"$ConfigPath`"" $TempZip = Join-Path $env:TEMP "nebula-windows-amd64.zip" $TempExtract = Join-Path $env:TEMP "nebula-extract" # Derive the NetBIOS domain name from the FQDN (first label, uppercased) $NetBIOSDomain = ($Domain -split '\.')[0].ToUpper() # --- Validate per-host files before making any changes --- $missing = @() $HostFiles = @("config.yml", "host.crt", "host.key") foreach ($file in $HostFiles) { if (-not (Test-Path (Join-Path $PWD $file))) { $missing += "$file (expected in working directory: $PWD)" } } # ca.crt must be provided alongside the script if (-not (Test-Path (Join-Path $ScriptDir "ca.crt"))) { $missing += "ca.crt (expected in script directory: $ScriptDir)" } if ($missing.Count -gt 0) { Write-Error "Missing required files:`n - $($missing -join "`n - ")" exit 1 } # --- Check if already installed and running correctly --- $existing = Get-Service -Name "nebula" -ErrorAction SilentlyContinue if ($existing) { $svcConfig = sc.exe qc nebula 2>&1 | Out-String $correctBinary = $svcConfig -match [regex]::Escape($ExpectedBinPath) if ($existing.Status -eq "Running" -and $correctBinary) { # Check installed version $installedVersion = & "$InstallDir\nebula.exe" -version 2>&1 | Select-String -Pattern "Version: (.+)" | ForEach-Object { $_.Matches[0].Groups[1].Value } if ($installedVersion -eq $NebulaVersion) { # Compare host files against installed files by hash $allMatch = $true foreach ($file in @("ca.crt")) { $src = (Get-FileHash (Join-Path $ScriptDir $file)).Hash $dst = (Get-FileHash (Join-Path $InstallDir $file) -ErrorAction SilentlyContinue).Hash if ($src -ne $dst) { $allMatch = $false; break } } if ($allMatch) { foreach ($file in $HostFiles) { $src = (Get-FileHash (Join-Path $PWD $file)).Hash $dst = (Get-FileHash (Join-Path $InstallDir $file) -ErrorAction SilentlyContinue).Hash if ($src -ne $dst) { $allMatch = $false; break } } } # Also verify the scheduled task and startup script exist $taskExists = Get-ScheduledTask -TaskName "NebulaDNS" -ErrorAction SilentlyContinue $scriptExists = Test-Path $StartupScript if ($allMatch -and $taskExists -and $scriptExists) { Write-Host "Nebula $NebulaVersion is already installed and running with matching files. No changes needed." exit 0 } } Write-Host "Nebula is running but files or version have changed. Reinstalling..." } Write-Host "Stopping existing Nebula service..." if ($existing.Status -eq "Running") { sc.exe stop nebula | Out-Null $timeout = 10 while ($timeout -gt 0) { $svc = Get-Service -Name "nebula" -ErrorAction SilentlyContinue if ($svc.Status -eq "Stopped") { break } Start-Sleep -Seconds 1 $timeout-- } if ($timeout -eq 0) { Write-Error "Timed out waiting for Nebula service to stop." exit 1 } } sc.exe delete nebula | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "Failed to remove existing Nebula service. A reboot may be required if it is marked for deletion." exit 1 } Start-Sleep -Seconds 1 } # --- Download Nebula --- Write-Host "Downloading Nebula v$NebulaVersion..." try { Invoke-WebRequest -Uri $NebulaUrl -OutFile $TempZip -UseBasicParsing } catch { Write-Error "Failed to download Nebula: $_" exit 1 } # --- Extract and clean up --- if (Test-Path $TempExtract) { Remove-Item $TempExtract -Recurse -Force } try { Expand-Archive -Path $TempZip -DestinationPath $TempExtract -Force } catch { Write-Error "Failed to extract Nebula archive: $_" exit 1 } # Verify nebula.exe was extracted if (-not (Test-Path (Join-Path $TempExtract "nebula.exe"))) { Write-Error "nebula.exe not found in downloaded archive." exit 1 } # Remove nebula-cert.exe — not needed for this module $certExe = Join-Path $TempExtract "nebula-cert.exe" if (Test-Path $certExe) { Remove-Item $certExe -Force } # --- Create install directory --- if (-not (Test-Path $InstallDir)) { try { New-Item -ItemType Directory -Path $InstallDir | Out-Null } catch { Write-Error "Failed to create install directory: $_" exit 1 } } # --- Copy files --- try { Write-Host "Copying Nebula files..." Copy-Item (Join-Path $TempExtract "nebula.exe") $InstallDir -Force Copy-Item (Join-Path $TempExtract "dist") $InstallDir -Recurse -Force Copy-Item (Join-Path $ScriptDir "ca.crt") $InstallDir -Force Write-Host "Copying host files..." foreach ($file in $HostFiles) { Copy-Item (Join-Path $PWD $file) $InstallDir -Force } } catch { Write-Error "Failed to copy files: $_" exit 1 } # --- Clean up temp files --- Remove-Item $TempZip -Force -ErrorAction SilentlyContinue Remove-Item $TempExtract -Recurse -Force -ErrorAction SilentlyContinue # --- Verify files landed in install directory --- foreach ($file in @("nebula.exe", "ca.crt") + $HostFiles) { if (-not (Test-Path (Join-Path $InstallDir $file))) { Write-Error "File copy verification failed: $file not found in $InstallDir" exit 1 } } if (-not (Test-Path (Join-Path $InstallDir "dist"))) { Write-Error "File copy verification failed: dist\ not found in $InstallDir" exit 1 } # --- Install and configure service --- Write-Host "Installing Nebula service..." & "$InstallDir\nebula.exe" -service install -config "$ConfigPath" if ($LASTEXITCODE -ne 0) { Write-Error "Failed to install Nebula service." exit 1 } # Wait for real network connectivity before starting, not just the TCP/IP stack sc.exe config nebula depend= Tcpip/NlaSvc | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "Failed to set service dependencies." exit 1 } # Auto start — the NlaSvc dependency ensures the physical network is up first sc.exe config nebula start= auto | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "Failed to set auto start." exit 1 } # --- Start and verify --- Write-Host "Starting Nebula service..." sc.exe start nebula | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "Failed to start Nebula service. Check Event Viewer (Application log, source 'nebula')." exit 1 } Start-Sleep -Seconds 3 $svc = Get-Service -Name "nebula" if ($svc.Status -eq "Running") { Write-Host "Nebula v$NebulaVersion service is running." } else { Write-Error "Nebula service failed to start. Check Event Viewer (Application log, source 'nebula')." exit 1 } # --- Configure DNS persistence across reboots --- # # Nebula recreates the nebula1 TUN adapter on every start, which wipes any # DNS settings. This scheduled task runs at startup to re-apply DNS, wait # for the domain controller to be reachable, then force Netlogon to # rediscover the DC — ensuring domain authentication works at the login screen. Write-Host "Creating startup DNS script..." $startupScriptContent = @" # Wait up to 120 seconds for the nebula1 adapter to appear `$timeout = 120 while (`$timeout -gt 0) { `$adapter = Get-NetAdapter -Name "nebula1" -ErrorAction SilentlyContinue if (`$adapter -and `$adapter.Status -eq "Up") { break } Start-Sleep -Seconds 2 `$timeout -= 2 } if (`$timeout -le 0) { exit 1 } Set-DnsClientServerAddress -InterfaceAlias "nebula1" -ServerAddresses "$DnsServer" # Wait for DC to become reachable, then force Netlogon to rediscover `$timeout = 180 while (`$timeout -gt 0) { `$result = Resolve-DnsName "$Domain" -Server "$DnsServer" -ErrorAction SilentlyContinue if (`$result) { break } Start-Sleep -Seconds 5 `$timeout -= 5 } if (`$timeout -gt 0) { Clear-DnsClientCache nltest /dsgetdc:$NetBIOSDomain /force 2>&1 | Out-Null } "@ Set-Content -Path $StartupScript -Value $startupScriptContent -Force Write-Host "Registering NebulaDNS scheduled task..." $existingTask = Get-ScheduledTask -TaskName "NebulaDNS" -ErrorAction SilentlyContinue if ($existingTask) { Unregister-ScheduledTask -TaskName "NebulaDNS" -Confirm:$false } $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File `"$StartupScript`"" $trigger = New-ScheduledTaskTrigger -AtStartup $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries Register-ScheduledTask -TaskName "NebulaDNS" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "Sets DNS on the Nebula adapter and forces Netlogon DC rediscovery" | Out-Null Write-Host "NebulaDNS scheduled task registered." # --- Configure Netlogon to wait for slow network links --- # # ExpectedDialupDelay tells Netlogon to keep retrying DC discovery for the # specified number of seconds. This covers the window between Windows boot # and the Nebula tunnel becoming fully operational. $netlogonKey = "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" Set-ItemProperty -Path $netlogonKey -Name "ExpectedDialupDelay" -Value 60 -Type DWord Write-Host "Netlogon ExpectedDialupDelay set to 60 seconds."