Private/Get-WinClatSetInstanceScript.ps1
|
Function Get-WinClatSetInstanceScript { <# .SYNOPSIS Returns the self-contained script block that Set-WinClatInstance runs on each confirmed target. .DESCRIPTION The change pass of Set-WinClatInstance (DESIGN.md, section 7.4). The script block takes four positional arguments: the exact InterfaceAlias (empty when selecting by index), the InterfaceIndex (0 when selecting by alias), the requested State ('Enabled' or 'Disabled'), and whether autoenable may be disabled first ([bool], confirmed by the caller). It returns WinClat.SetResult rows: 1. Feature and elevation checks, then the adapter is found and checked again, because the computer may have changed since it was confirmed. 2. -State Enabled is refused, with nothing changed, when permit is disabled in the active store. 3. When autoenable is enabled: if the caller confirmed disabling it, 'set global autoenable=disabled' runs and is verified, and an 'AutoEnable' row is returned. Otherwise nothing is changed, because the confirmation no longer describes the computer. 4. 'set instance interface=<ifIndex> state=<State>' runs, and the instance is read back. The 'State' row is Success when the instance state matches the request. Translation can take several seconds to start after a manual enable (section 10), so translating is not required. netsh informational output is written to the verbose stream. Set-WinClatInstance and the unit tests both use this function, so the tests exercise the script block that is sent remotely. .INPUTS None. This function does not accept pipeline input. .OUTPUTS System.Management.Automation.ScriptBlock. .EXAMPLE & (Get-WinClatSetInstanceScript) 'Internal' 0 'Enabled' $true Disables autoenable if it is enabled, then enables CLAT on the Internal adapter of the local computer. .LINK https://learn.microsoft.com/windows-server/administration/windows-commands/netsh .NOTES Version: 1.0.0 Creation Date: October 28, 2026 Last Updated: October 28, 2026 Author: Richard Hicks Organization: Richard M. Hicks Consulting, Inc. Contact: rich@richardhicks.com Website: https://www.richardhicks.com/ #> [CmdletBinding()] [OutputType([scriptblock])] Param () $HelperName = @( 'ConvertFrom-WinClatNetshOutput' 'ConvertTo-WinClatTimeSpan' 'ConvertTo-WinClatSetResult' 'Get-WinClatNetshFailure' 'Get-WinClatPermitRefusal' 'Get-WinClatPolicyControlled' 'Get-WinClatTargetAdapter' 'Test-WinClatElevation' 'Get-WinClatFeatureFailure' 'Test-WinClatFeature' ) $Body = { Param ($InterfaceAlias, $InterfaceIndex, $State, $DisableAutoEnable) $SelectedAlias = $null $SelectedIndex = $null If ($InterfaceAlias) { $SelectedAlias = $InterfaceAlias } Else { $SelectedIndex = [int]$InterfaceIndex } $StateRow = { Param ($Status, $Message, $Alias, $Index, $Effective, $Changed) $Row = @{ ComputerName = $env:COMPUTERNAME Status = $Status Setting = 'State' InterfaceAlias = $Alias InterfaceIndex = $Index RequestedValue = $State EffectiveValue = $Effective PolicyControlled = $false EffectiveValueChanged = $Changed Message = $Message } ConvertTo-WinClatSetResult @Row } $WriteVerbose = { Param ($Output) ForEach ($Line in $Output) { If ("$Line".Trim()) { Write-Verbose -Message "$($env:COMPUTERNAME): $("$Line".Trim())" } } } # The instance state as 'Enabled' or 'Disabled', or $null when it cannot be read. $ReadState = { Param ($Index) $Output = @(netsh.exe interface clat show instance "interface=$Index") If ($LASTEXITCODE -eq 0) { $Instance = @(ConvertFrom-WinClatNetshOutput -InputObject $Output)[0] If ($Instance -and $Instance.Enabled -is [bool]) { If ($Instance.Enabled) { 'Enabled' } Else { 'Disabled' } } } } $ReadGlobal = { $Output = @(netsh.exe interface clat show global 'store=both') If ($LASTEXITCODE -eq 0) { @(ConvertFrom-WinClatNetshOutput -InputObject $Output) } } If (-not (Test-WinClatFeature -InputObject @(netsh.exe interface '?'))) { $Missing = Get-WinClatFeatureFailure & $StateRow $Missing.Status $Missing.Message $SelectedAlias $SelectedIndex $null $null Return } If (-not (Test-WinClatElevation)) { & $StateRow 'ElevationRequired' 'Changing a WinCLAT instance requires an elevated session. Run PowerShell as administrator, or connect with an account that is a local administrator on the target.' $SelectedAlias $SelectedIndex $null $null Return } $Target = Get-WinClatTargetAdapter -InterfaceAlias $InterfaceAlias -InterfaceIndex ([int]$InterfaceIndex) If ($Target.Problem) { & $StateRow 'Failed' $Target.Problem $Target.InterfaceAlias $Target.InterfaceIndex $null $null Return } $Alias = $Target.InterfaceAlias $Index = $Target.InterfaceIndex $Active = @(@(& $ReadGlobal) | Where-Object -FilterScript { $_.Store -eq 'Active' })[0] If (-not $Active -or $Active.AutoEnable -isnot [bool]) { & $StateRow 'Failed' 'The global configuration could not be read, so nothing was changed.' $Alias $Index $null $null Return } # Checked again, because permit may have been disabled since the read pass; autoenable is not touched. If ($State -eq 'Enabled' -and $Active.Permit -eq $false) { & $StateRow 'Failed' (Get-WinClatPermitRefusal) $Alias $Index $null $null Return } If ($Active.AutoEnable) { If ($State -ne 'Enabled' -or -not $DisableAutoEnable) { & $StateRow 'Failed' 'AutoEnable is enabled on this computer, which no longer matches what was confirmed, so nothing was changed. Run the command again.' $Alias $Index $null $null Return } # Disable autoenable first; netsh refuses 'set instance' in automatic mode (section 10). $Output = @(netsh.exe interface clat set global 'autoenable=disabled') $ExitCode = $LASTEXITCODE & $WriteVerbose $Output $AutoEnableRow = @{ ComputerName = $env:COMPUTERNAME Status = 'Success' Setting = 'AutoEnable' RequestedValue = $false PolicyControlled = $false EffectiveValueChanged = $null Message = $null } If ($ExitCode -ne 0) { $Classified = Get-WinClatNetshFailure -InputObject $Output -ExitCode $ExitCode $AutoEnableRow['Status'] = $Classified.Status $AutoEnableRow['Message'] = $Classified.Message ConvertTo-WinClatSetResult @AutoEnableRow & $StateRow 'Failed' 'The instance was not changed because AutoEnable could not be disabled.' $Alias $Index $null $null Return } $After = @(& $ReadGlobal) $AfterActive = @($After | Where-Object -FilterScript { $_.Store -eq 'Active' })[0] $AfterPersistent = @($After | Where-Object -FilterScript { $_.Store -eq 'Persistent' })[0] If ($AfterActive) { $AutoEnableRow['EffectiveValue'] = $AfterActive.AutoEnable $AutoEnableRow['EffectiveValueChanged'] = $AfterActive.AutoEnable -ne $true } If ($AfterPersistent) { $AutoEnableRow['PersistentValue'] = $AfterPersistent.AutoEnable } If (-not $AfterActive -or $AfterActive.AutoEnable -ne $false) { $AutoEnableRow['Status'] = 'Failed' $AutoEnableRow['Message'] = 'netsh.exe reported success, but AutoEnable is still enabled.' ConvertTo-WinClatSetResult @AutoEnableRow & $StateRow 'Failed' 'The instance was not changed because AutoEnable could not be disabled.' $Alias $Index $null $null Return } $AutoEnableRow['Message'] = 'Disabled so that the instance could be enabled manually. CLAT was stopped on every interface where it was automatically enabled.' ConvertTo-WinClatSetResult @AutoEnableRow } $Before = & $ReadState $Index $Output = @(netsh.exe interface clat set instance "interface=$Index" "state=$($State.ToLowerInvariant())") $ExitCode = $LASTEXITCODE & $WriteVerbose $Output If ($ExitCode -ne 0) { $Classified = Get-WinClatNetshFailure -InputObject $Output -ExitCode $ExitCode & $StateRow $Classified.Status $Classified.Message $Alias $Index $Before $false Return } # set instance prints nothing on success, so read the instance back (section 7.4). $After = & $ReadState $Index $Changed = $null If ($Before -and $After) { $Changed = $After -ne $Before } If ($After -eq $State) { & $StateRow 'Success' $null $Alias $Index $After $Changed } ElseIf ($After) { & $StateRow 'Failed' "netsh.exe reported success, but the instance state is $After." $Alias $Index $After $Changed } Else { & $StateRow 'Failed' 'netsh.exe reported success, but the instance state could not be read back to verify the change.' $Alias $Index $null $null } } Get-WinClatRemoteScript -HelperName $HelperName -ScriptBlock $Body } |