############################################################################### # # # Name WinBGP-Engine # # # # Description WinBGP Engine (Called by the service) # # # # Notes Service is based on JFLarvoire service example # # (https://github.com/JFLarvoire/SysToolsLib) # # # # # # Copyright (c) 2024 Alexandre JARDON | Webalex System. # # All rights reserved.' # # LicenseUri https://github.com/webalexeu/winbgp/blob/master/LICENSE # # ProjectUri https://github.com/webalexeu/winbgp # # # ############################################################################### #Requires -version 5.1 [CmdletBinding(DefaultParameterSetName='Version')] Param( [Parameter(ParameterSetName='Start', Mandatory=$true)] [Switch]$Start, # Start the service [Parameter(ParameterSetName='Stop', Mandatory=$true)] [Switch]$Stop, # Stop the service [Parameter(ParameterSetName='Restart', Mandatory=$true)] [Switch]$Restart, # Restart the service [Parameter(ParameterSetName='Status', Mandatory=$false)] [Switch]$Status = $($PSCmdlet.ParameterSetName -eq 'Status'), # Get the current service status [Parameter(ParameterSetName='Service', Mandatory=$true)] [Switch]$Service, # Run the service (Internal use only) [Parameter(ParameterSetName='SCMStart', Mandatory=$true)] [Switch]$SCMStart, # Process SCM Start requests (Internal use only) [Parameter(ParameterSetName='SCMResume', Mandatory=$true)] [Switch]$SCMResume, # Process SCM Resume requests (Internal use only) [Parameter(ParameterSetName='SCMStop', Mandatory=$true)] [Switch]$SCMStop, # Process SCM Stop requests (Internal use only) [Parameter(ParameterSetName='SCMSuspend', Mandatory=$true)] [Switch]$SCMSuspend, # Process SCM Suspend requests (Internal use only) [Parameter(ParameterSetName='Control', Mandatory=$true)] [String]$Control = $null, # Control message to send to the service [Parameter(ParameterSetName='Version', Mandatory=$true)] [Switch]$Version # Get this script version ) # Don't forget to increment version when updating engine $scriptVersion = '1.1.1' # This script name, with various levels of details $argv0 = Get-Item $MyInvocation.MyCommand.Definition $script = $argv0.basename # Ex: PSService $scriptName = $argv0.name # Ex: PSService.ps1 $scriptFullName = $argv0.fullname # Ex: C:\Temp\PSService.ps1 # Global settings $serviceName = "WinBGP" # A one-word name used for net start commands $serviceDisplayName = "WinBGP" # To improve (Service name should be rationalized) $serviceInternalName = "$($serviceName)-Service" $engineName = "$($serviceName)-Engine" $ServiceDescription = "The BGP swiss army knife of networking on Windows" $pipeName = "Service_$serviceName" # Named pipe name. Used for sending messages to the service task $installDir = "${ENV:ProgramW6432}\$serviceDisplayName" # Where to install the service files $scriptCopy = "$installDir\$scriptName" $configfile = "$serviceDisplayName.json" $configdir = "$installDir\$configfile" $exeName = "$serviceName.exe" $exeFullName = "$installDir\$exeName" # Remove file log #$logDir = "${ENV:programfiles}\WinBGP\Logs" # Where to log the service messages #$logFile = "$logDir\$serviceName.log" $logName = "Application" # Event Log name (Unrelated to the logFile!) $FunctionCliXml="$installDir\$serviceDisplayName.xml" # Used to stored Maintenance variable # Note: The current implementation only supports "classic" (ie. XP-compatble) event logs. # To support new style (Vista and later) "Applications and Services Logs" folder trees, it would # be necessary to use the new *WinEvent commands instead of the XP-compatible *EventLog commands. # Gotcha: If you change $logName to "NEWLOGNAME", make sure that the registry key below does not exist: # HKLM\System\CurrentControlSet\services\eventlog\Application\NEWLOGNAME # Else, New-EventLog will fail, saying the log NEWLOGNAME is already registered as a source, # even though "Get-WinEvent -ListLog NEWLOGNAME" says this log does not exist! # If the -Version switch is specified, display the script version and exit. if ($Version) { return $scriptVersion } #-----------------------------------------------------------------------------# # # # Function Now # # # # Description Get a string with the current time. # # # # Notes The output string is in the ISO 8601 format, except for # # a space instead of a T between the date and time, to # # improve the readability. # # # # History # # 2015-06-11 JFL Created this routine. # # # #-----------------------------------------------------------------------------# Function Now { Param ( [Switch]$ms, # Append milliseconds [Switch]$ns # Append nanoseconds ) $Date = Get-Date $now = "" $now += "{0:0000}-{1:00}-{2:00} " -f $Date.Year, $Date.Month, $Date.Day $now += "{0:00}:{1:00}:{2:00}" -f $Date.Hour, $Date.Minute, $Date.Second $nsSuffix = "" if ($ns) { if ("$($Date.TimeOfDay)" -match "\.\d\d\d\d\d\d") { $now += $matches[0] $ms = $false } else { $ms = $true $nsSuffix = "000" } } if ($ms) { $now += ".{0:000}$nsSuffix" -f $Date.MilliSecond } return $now } #-----------------------------------------------------------------------------# # # # Function Log # # # # Description Log a string into the PSService.log file # # # # Arguments A string # # # # Notes Prefixes the string with a timestamp and the user name. # # (Except if the string is empty: Then output a blank line.)# # # # History # # 2016-06-05 JFL Also prepend the Process ID. # # 2016-06-08 JFL Allow outputing blank lines. # # # #-----------------------------------------------------------------------------# #Logging function function Write-Log { <# .Synopsis Write-Log writes a message to a specified log file with the current time stamp. .DESCRIPTION The Write-Log function is designed to add logging capability to other scripts. In addition to writing output and/or verbose you can write to a log file for later debugging. .NOTES Created by: Jason Wasser @wasserja Modified: 11/24/2015 09:30:19 AM Changelog: * Code simplification and clarification - thanks to @juneb_get_help * Added documentation. * Renamed LogPath parameter to Path to keep it standard - thanks to @JeffHicks * Revised the Force switch to work as it should - thanks to @JeffHicks To Do: * Add error handling if trying to create a log file in a inaccessible location. * Add ability to write $Message to $Verbose or $Error pipelines to eliminate duplicates. .PARAMETER Message Message is the content that you wish to add to the log file. .PARAMETER Level Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational) .PARAMETER NoClobber Use NoClobber if you do not wish to overwrite an existing file. .EXAMPLE Write-Log -Message 'Log message' Writes the message to c:\Logs\PowerShellLog.log. .EXAMPLE Write-Log -Message 'Restarting Server.' -Path c:\Logs\Scriptoutput.log Writes the content to the specified log file and creates the path and file specified. .EXAMPLE Write-Log -Message 'Folder does not exist.' -Path c:\Logs\Script.log -Level Error Writes the message to the specified log file as an error message, and writes the message to the error pipeline. .LINK https://gallery.technet.microsoft.com/scriptcenter/Write-Log-PowerShell-999c32d0 #> [CmdletBinding()] Param ( [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [Alias("LogContent")] [string]$Message, [Parameter(Mandatory=$false)] [ValidateSet("Error","Warning","Information")] [string]$Level="Information", [Parameter(Mandatory=$false)] [string]$EventLogName=$logName, [Parameter(Mandatory=$false)] [string]$EventLogSource=$serviceName, [Parameter(Mandatory=$false)] [string]$EventLogId=1006, [Parameter(Mandatory=$false)] [string]$EventLogCategory=0, [Parameter(Mandatory=$false)] [Array]$AdditionalFields=$null, [Parameter(Mandatory=$false)] [switch]$NoClobber ) Begin { } Process { # Manage AdditionalFields (Not by default with PowerShell function) if ($AdditionalFields) { $EventInstance = [System.Diagnostics.EventInstance]::new($EventLogId, $EventLogCategory, $Level) $NewEvent = [System.Diagnostics.EventLog]::new() $NewEvent.Log = $EventLogName $NewEvent.Source = $EventLogSource [Array] $JoinedMessage = @( $Message $AdditionalFields | ForEach-Object { $_ } ) $NewEvent.WriteEvent($EventInstance, $JoinedMessage) } else { #Write log to event viewer (Enabled by default) Write-EventLog -LogName $EventLogName -Source $EventLogSource -EventId $EventLogId -EntryType $Level -Category $EventLogCategory -Message "$Message" } } End { } } # Remove file log # Function Log () { # Param( # [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)] # [String]$string # ) # if (!(Test-Path $logDir)) { # New-Item -ItemType directory -Path $logDir | Out-Null # } # if ($String.length) { # # Remove $currentUserName # #$string = "$(Now) $pid $currentUserName $string" # $string = "$(Now) $pid $string" # } # $string | Out-File -Encoding ASCII -Append "$logFile" # } #-----------------------------------------------------------------------------# # # # Function Start-PSThread # # # # Description Start a new PowerShell thread # # # # Arguments See the Param() block # # # # Notes Returns a thread description object. # # The completion can be tested in $_.Handle.IsCompleted # # Alternative: Use a thread completion event. # # # # References # # https://learn-powershell.net/tag/runspace/ # # https://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/ # http://www.codeproject.com/Tips/895840/Multi-Threaded-PowerShell-Cookbook # # # History # # 2016-06-08 JFL Created this function # # # #-----------------------------------------------------------------------------# $PSThreadCount = 0 # Counter of PSThread IDs generated so far $PSThreadList = @{} # Existing PSThreads indexed by Id Function Get-PSThread () { Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)] [int[]]$Id = $PSThreadList.Keys # List of thread IDs ) $Id | ForEach-Object { $PSThreadList.$_ } } Function Start-PSThread () { Param( [Parameter(Mandatory=$true, Position=0)] [ScriptBlock]$ScriptBlock, # The script block to run in a new thread [Parameter(Mandatory=$false)] [String]$Name = "", # Optional thread name. Default: "PSThread$Id" [Parameter(Mandatory=$false)] [String]$Event = "", # Optional thread completion event name. Default: None [Parameter(Mandatory=$false)] [Hashtable]$Variables = @{}, # Optional variables to copy into the script context. [Parameter(Mandatory=$false)] [String[]]$Functions = @(), # Optional functions to copy into the script context. [Parameter(Mandatory=$false)] [Object[]]$Arguments = @() # Optional arguments to pass to the script. ) $Id = $script:PSThreadCount $script:PSThreadCount += 1 if (!$Name.Length) { $Name = "PSThread$Id" } $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() foreach ($VarName in $Variables.Keys) { # Copy the specified variables into the script initial context $value = $Variables.$VarName Write-Debug "Adding variable $VarName=[$($Value.GetType())]$Value" $var = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry($VarName, $value, "") $InitialSessionState.Variables.Add($var) } foreach ($FuncName in $Functions) { # Copy the specified functions into the script initial context $Body = Get-Content function:$FuncName Write-Debug "Adding function $FuncName () {$Body}" $func = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry($FuncName, $Body) $InitialSessionState.Commands.Add($func) } $RunSpace = [RunspaceFactory]::CreateRunspace($InitialSessionState) $RunSpace.Open() $PSPipeline = [powershell]::Create() $PSPipeline.Runspace = $RunSpace $PSPipeline.AddScript($ScriptBlock) | Out-Null $Arguments | ForEach-Object { Write-Debug "Adding argument [$($_.GetType())]'$_'" $PSPipeline.AddArgument($_) | Out-Null } $Handle = $PSPipeline.BeginInvoke() # Start executing the script if ($Event.Length) { # Do this after BeginInvoke(), to avoid getting the start event. Register-ObjectEvent $PSPipeline -EventName InvocationStateChanged -SourceIdentifier $Name -MessageData $Event } $PSThread = New-Object PSObject -Property @{ Id = $Id Name = $Name Event = $Event RunSpace = $RunSpace PSPipeline = $PSPipeline Handle = $Handle } # Return the thread description variables $script:PSThreadList[$Id] = $PSThread $PSThread } #-----------------------------------------------------------------------------# # # # Function Receive-PSThread # # # # Description Get the result of a thread, and optionally clean it up # # # # Arguments See the Param() block # # # # Notes # # # # History # # 2016-06-08 JFL Created this function # # # #-----------------------------------------------------------------------------# Function Receive-PSThread () { [CmdletBinding()] Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)] [PSObject]$PSThread, # Thread descriptor object [Parameter(Mandatory=$false)] [Switch]$AutoRemove # If $True, remove the PSThread object ) Process { if ($PSThread.Event -and $AutoRemove) { Unregister-Event -SourceIdentifier $PSThread.Name Get-Event -SourceIdentifier $PSThread.Name | Remove-Event # Flush remaining events } try { $PSThread.PSPipeline.EndInvoke($PSThread.Handle) # Output the thread pipeline output } catch { $_ # Output the thread pipeline error } if ($AutoRemove) { $PSThread.RunSpace.Close() $PSThread.PSPipeline.Dispose() $PSThreadList.Remove($PSThread.Id) } } } Function Remove-PSThread () { [CmdletBinding()] Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)] [PSObject]$PSThread # Thread descriptor object ) Process { $_ | Receive-PSThread -AutoRemove | Out-Null } } #-----------------------------------------------------------------------------# # # # Function Send-PipeMessage # # # # Description Send a message to a named pipe # # # # Arguments See the Param() block # # # # Notes # # # # History # # 2016-05-25 JFL Created this function # # # #-----------------------------------------------------------------------------# Function Send-PipeMessage () { Param( [Parameter(Mandatory=$true)] [String]$PipeName, # Named pipe name [Parameter(Mandatory=$true)] [String]$Message # Message string ) $PipeDir = [System.IO.Pipes.PipeDirection]::Out $PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous $pipe = $null # Named pipe stream $sw = $null # Stream Writer try { $pipe = new-object System.IO.Pipes.NamedPipeClientStream(".", $PipeName, $PipeDir, $PipeOpt) $sw = new-object System.IO.StreamWriter($pipe) $pipe.Connect(1000) if (!$pipe.IsConnected) { throw "Failed to connect client to pipe $pipeName" } $sw.AutoFlush = $true $sw.WriteLine($Message) } catch { Write-Log "Error sending pipe $pipeName message: $_" -Level Error } finally { if ($sw) { $sw.Dispose() # Release resources $sw = $null # Force the PowerShell garbage collector to delete the .net object } if ($pipe) { $pipe.Dispose() # Release resources $pipe = $null # Force the PowerShell garbage collector to delete the .net object } } } #-----------------------------------------------------------------------------# # # # Function Receive-PipeMessage # # # # Description Wait for a message from a named pipe # # # # Arguments See the Param() block # # # # Notes I tried keeping the pipe open between client connections, # # but for some reason everytime the client closes his end # # of the pipe, this closes the server end as well. # # Any solution on how to fix this would make the code # # more efficient. # # # # History # # 2016-05-25 JFL Created this function # # # #-----------------------------------------------------------------------------# Function Receive-PipeMessage () { Param( [Parameter(Mandatory=$true)] [String]$PipeName # Named pipe name ) $PipeDir = [System.IO.Pipes.PipeDirection]::In $PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous $PipeMode = [System.IO.Pipes.PipeTransmissionMode]::Message try { $pipe = $null # Named pipe stream $pipe = New-Object system.IO.Pipes.NamedPipeServerStream($PipeName, $PipeDir, 1, $PipeMode, $PipeOpt) $sr = $null # Stream Reader $sr = new-object System.IO.StreamReader($pipe) $pipe.WaitForConnection() $Message = $sr.Readline() $Message } catch { Write-Log "Error receiving pipe message: $_" -Level Error } finally { if ($sr) { $sr.Dispose() # Release resources $sr = $null # Force the PowerShell garbage collector to delete the .net object } if ($pipe) { $pipe.Dispose() # Release resources $pipe = $null # Force the PowerShell garbage collector to delete the .net object } } } #-----------------------------------------------------------------------------# # # # Function Start-PipeHandlerThread # # # # Description Start a new thread waiting for control messages on a pipe # # # # Arguments See the Param() block # # # # Notes The pipe handler script uses function Receive-PipeMessage.# # This function must be copied into the thread context. # # # # The other functions and variables copied into that thread # # context are not strictly necessary, but are useful for # # debugging possible issues. # # # # History # # 2016-06-07 JFL Created this function # # # #-----------------------------------------------------------------------------# $pipeThreadName = "Control Pipe Handler" Function Start-PipeHandlerThread () { Param( [Parameter(Mandatory=$true)] [String]$pipeName, # Named pipe name [Parameter(Mandatory=$false)] [String]$Event = "ControlMessage" # Event message ) Start-PSThread -Variables @{ # Copy variables required by function Log() into the thread context # Remove log file #logDir = $logDir #logFile = $logFile currentUserName = $currentUserName } -Functions Now, Write-Log, Receive-PipeMessage -ScriptBlock { Param($pipeName, $pipeThreadName) try { Receive-PipeMessage "$pipeName" # Blocks the thread until the next message is received from the pipe } catch { Write-Log "$pipeThreadName # Error: $_" -Level Error throw $_ # Push the error back to the main thread } } -Name $pipeThreadName -Event $Event -Arguments $pipeName, $pipeThreadName } #-----------------------------------------------------------------------------# # # # Function Receive-PipeHandlerThread # # # # Description Get what the pipe handler thread received # # # # Arguments See the Param() block # # # # Notes # # # # History # # 2016-06-07 JFL Created this function # # # #-----------------------------------------------------------------------------# Function Receive-PipeHandlerThread () { Param( [Parameter(Mandatory=$true)] [PSObject]$pipeThread # Thread descriptor ) Receive-PSThread -PSThread $pipeThread -AutoRemove } #-----------------------------------------------------------------------------# # # # Function Test-ConfigurationFile # # # # Description Test WinBGP configuration file # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# function Test-ConfigurationFile() { Param ( [Parameter(Mandatory=$false)] $Path=$configdir ) # Json validation try { $configuration = Get-Content -Path $Path | ConvertFrom-Json $validJson = $true } catch { $validJson = $false } if ($validJson) { $ValidConfig=$true # Global if ($configuration.global.Interval -isnot [Int32]) {$ValidConfig=$false} if ($configuration.global.Timeout -isnot [Int32]) {$ValidConfig=$false} if ($configuration.global.Rise -isnot [Int32]) {$ValidConfig=$false} if ($configuration.global.Fall -isnot [Int32]) {$ValidConfig=$false} if ($configuration.global.Metric -isnot [Int32]) {$ValidConfig=$false} if ($configuration.global.Api -isnot [Boolean]) {$ValidConfig=$false} # Api (Check only if Api is enabled) if ($configuration.global.Api) { if ($configuration.api -isnot [array]) {$ValidConfig=$false} } # Router if ([string]::IsNullOrEmpty($configuration.router.BgpIdentifier)) {$ValidConfig=$false} if ([string]::IsNullOrEmpty($configuration.router.LocalASN)) {$ValidConfig=$false} # Peers if ($configuration.peers -is [array]) { foreach ($peer in $configuration.peers) { if ([string]::IsNullOrEmpty($peer.PeerName)) {$ValidConfig=$false} if ([string]::IsNullOrEmpty($peer.LocalIP)) {$ValidConfig=$false} if ([string]::IsNullOrEmpty($peer.PeerIP)) {$ValidConfig=$false} if ([string]::IsNullOrEmpty($peer.LocalASN)) {$ValidConfig=$false} if ([string]::IsNullOrEmpty($peer.PeerASN)) {$ValidConfig=$false} } } else { $ValidConfig=$false } # Routes if ($configuration.routes -is [array]) { foreach ($route in $configuration.routes) { if ([string]::IsNullOrEmpty($route.RouteName)) {$ValidConfig=$false} if ([string]::IsNullOrEmpty($route.Network)) {$ValidConfig=$false} if ([string]::IsNullOrEmpty($route.Interface)) {$ValidConfig=$false} if ($route.DynamicIpSetup -isnot [Boolean]) {$ValidConfig=$false} if ($route.WithdrawOnDown -isnot [Boolean]) {$ValidConfig=$false} # Only if WithdrawOnDown is enabled if ($route.WithdrawOnDown) { if ([string]::IsNullOrEmpty($route.WithdrawOnDownCheck)) {$ValidConfig=$false} } if ([string]::IsNullOrEmpty($route.NextHop)) {$ValidConfig=$false} # Community if ($route.Community -is [array]) { # Parsing all Community foreach ($community in $route.Community) { if ([string]::IsNullOrEmpty($community)) {$ValidConfig=$false} } } else { $ValidConfig=$false } } } else { $ValidConfig=$false } } # If Json type and content are valid if (($validJson) -and ($ValidConfig)) { return $true } else { return $false } } #-----------------------------------------------------------------------------# # # # Function Add-BGP # # # # Description Add BGP Route on the network card # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# function Add-Bgp() { Param ( [Parameter(Mandatory=$true)] $Route ) # Manage IP Address $announce_route=Add-IP $route # Add route if ($announce_route) { Write-Log "Announce BGP network '$($route.Network)'" -AdditionalFields @($route.RouteName) if ((Get-BgpCustomRoute).Network -notcontains "$($route.Network)"){Add-BgpCustomRoute -Network "$($route.Network)"} } } #-----------------------------------------------------------------------------# # # # Function remove-BGP # # # # Description remove BGP Route on the network card # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# function remove-Bgp() { Param ( [Parameter(Mandatory=$true)] $Route ) # Remove IP Remove-IP $route # Remove BGP Route if ((Get-BgpCustomRoute).Network -contains "$($route.Network)"){ Write-Log "Unannounce BGP network '$($route.Network)'" -AdditionalFields @($route.RouteName) Remove-BgpCustomRoute -network "$($route.Network)" -Force } } #-----------------------------------------------------------------------------# # # # Function Add-IP # # # # Description Add IP address on the network card # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# function Add-IP() { Param ( [Parameter(Mandatory=$true)] $Route ) if($route.DynamicIpSetup) { #Add IP $IPAddress=$route.Network.split('/')[0] $Netmask=$route.Network.split('/')[1] #Add new IP (SkipAsSource:The addresses are not used for outgoing traffic and are not registered in DNS) if ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)").IPAddress -notcontains "$IPAddress"){ Write-Log "Add IP Address '$($route.Network)' on interface '$($route.Interface)'" -AdditionalFields @($route.RouteName) New-NetIPAddress -InterfaceAlias "$($route.Interface)" -IPAddress $IPAddress -PrefixLength $Netmask -SkipAsSource:$true -PolicyStore ActiveStore # Waiting IP to be mounted while ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)" -IPAddress $IPAddress).AddressState -eq 'Tentative'){} if ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)" -IPAddress $IPAddress).AddressState -eq 'Preferred') { Write-Log "IP Address '$($route.Network)' on interface '$($route.Interface)' successfully added" -AdditionalFields @($route.RouteName) $announce_route=$true } elseif ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)" -IPAddress $IPAddress).AddressState -eq 'Duplicate') { $announce_route=$false Remove-NetIPAddress -IPAddress $IPAddress -Confirm:$false Write-Log "Duplicate IP - Unable to add IP Address '$($route.Network)' on interface '$($route.Interface)'" -Level Error -AdditionalFields @($route.RouteName) Write-Log "Set ArpRetryCount to '0' to avoid this error" -Level Warning } else { $announce_route=$false Write-Log "Unknown error - Unable to add IP Address '$($route.Network)' on interface '$($route.Interface)'" -Level Error -AdditionalFields @($route.RouteName) } } else { # IP already there, announce route $announce_route=$true } } else { # Always announce route $announce_route=$true Write-Log "IP Address '$($route.Network)' not managed by WinBGP Service" -Level Warning -AdditionalFields @($route.RouteName) } # Return status return $announce_route } #-----------------------------------------------------------------------------# # # # Function remove-IP # # # # Description Remove IP address on the network card # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# function Remove-IP() { Param ( [Parameter(Mandatory=$true)] $Route ) if($route.DynamicIpSetup){ #Remove IP $IPAddress=$route.Network.split('/')[0] if ((Get-NetIPAddress -InterfaceAlias "$($route.Interface)").IPAddress -contains "$IPAddress"){ Write-Log "Remove IP Address '$($route.Network)' on interface '$($route.Interface)'" -AdditionalFields @($route.RouteName) Remove-NetIPAddress -IPAddress $IPAddress -Confirm:$false } } else { Write-Log "IP Address '$($route.Network)' not managed by WinBGP Service" -Level Warning -AdditionalFields @($route.RouteName) } } #-----------------------------------------------------------------------------# # # # Function Write-Route # # # # Description Log Route information # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# function Write-Route() { Param ( [Parameter(Mandatory=$true)] $Route ) Write-Log "Route Name : '$($route.Routename)'`nNetwork to Announce : '$($route.Network)'" -AdditionalFields @($Route.RouteName) #Display service to check if WithdrawOnDown is true if ($route.WithdrawOnDown) { if ($route.WithdrawOnDownCheck) { $pos = ($route.WithdrawOnDownCheck).IndexOf(":") $check_method = ($route.WithdrawOnDownCheck).Substring(0, $pos) $check_name = ($route.WithdrawOnDownCheck).Substring($pos+2) # Rewrite $check_name for logging if check is custom if ($check_method -eq 'custom') { $check_name='check' } $Msg="WithdrawOnDownCheck - Method: '$check_method' - Name: '$check_name'" if($route.Interval) { $Msg+=" - Interval: '$($route.Interval)'" } Write-Log $Msg -AdditionalFields @($Route.RouteName) } else { Write-Log "WithdrawOnDownCheck cannot be empty when WithdrawOnDown is set to true" -Level Warning -AdditionalFields @($Route.RouteName) } } } #-----------------------------------------------------------------------------# # # # Function Add-RoutePolicy # # # # Description Add routing policies # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# function Add-RoutePolicy() { Param ( [Parameter(Mandatory=$true)] $Route, [Parameter(Mandatory=$true)] $Peers ) # Generate routing policy parameters $params = @{ Name = $route.RouteName; MatchPrefix = $route.Network; PolicyType = 'ModifyAttribute'; NewMED = $route.Metric; } # Log information Write-Log "BGP Routing Policy - Metric: '$($route.Metric)'" -AdditionalFields @($Route.RouteName) # If Community is specified if ($route.Community) { $params.add('AddCommunity',$route.Community) Write-Log "BGP Routing Policy - Community: '$($Route.Community)'" -AdditionalFields @($Route.RouteName) } # If NextHop is specified if ($route.NextHop) { $params.add('NewNextHop',$route.NextHop) Write-Log "BGP Routing Policy - NextHop: '$($Route.NextHop)'" -AdditionalFields @($Route.RouteName) } # Compare routing policy to avoid deleting each time # Checking if Routing policy already exist # If there is a Routing policy, cleaning it $BGPRoutingPolicy=get-BgpRoutingPolicy -Name $route.RouteName -ErrorAction SilentlyContinue if (($BGPRoutingPolicy.PolicyType -ne 'ModifyAttribute') -or ($BGPRoutingPolicy.MatchPrefix -ne $route.Network) -or ($BGPRoutingPolicy.NewMED -ne $route.Metric) -or ((Compare-Object -DifferenceObject $BGPRoutingPolicy.AddCommunity -ReferenceObject $route.Community).count -ne 0) -or ($BGPRoutingPolicy.NewNextHop -ne $Route.NextHop)) { # If policy exist if ($BGPRoutingPolicy) { # Remove wrongly configured Write-Log "BGP Routing Policy [$($Route.RouteName)] already configured - Cleaning (This situation may occur if the service was not correctly stopped)" -Level Warning -AdditionalFields @($Route.RouteName) Remove-BgpRoutingPolicy -Name $route.RouteName -Force } # Add new routing policy Write-Log "Creating BGP Routing Policy '$($Route.RouteName)'" -AdditionalFields @($Route.RouteName) Add-BgpRoutingPolicy @params -Force } # Declare routing policy on each peer ForEach ($peer in $Peers) { if ((Get-BgpPeer -PeerName $peer.Peername).EgressPolicyList -contains $Route.RouteName) { Write-Log "BGP Routing Policy on Peer $($peer.Peername)" -AdditionalFields @($Route.RouteName) } else { Write-Log "Adding BGP Routing Policy on Peer $($peer.Peername)" -AdditionalFields @($Route.RouteName) Add-BgpRoutingPolicyForPeer -PeerName $peer.Peername -PolicyName $Route.RouteName -Direction 'Egress' -Force } } } #-----------------------------------------------------------------------------# # # # Function Start-API # # # # Description Starting API Engine # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# function Start-API() { Param ( [Parameter(Mandatory=$true)] $ApiConfiguration ) # Start API Write-Log "Starting API engine" # ArgumentList (,$ApiConfiguration) is to handle array as argument Start-Job -Name 'API' -FilePath "$installDir\$serviceDisplayName-API.ps1" -ArgumentList (,$ApiConfiguration) } #-----------------------------------------------------------------------------# # # # Function Stop-API # # # # Description Stopping API Engine # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# function Stop-API() { # Stop API Write-Log "Stopping API engine" ### IMPROVEMENT - To be check if we can kill API properly ### $ProcessID=$null $ApiPID=$null # Get service PID $ProcessID=(Get-CimInstance Win32_Process -Filter "name = 'powershell.exe'" -OperationTimeoutSec 1 | Where-Object {$_.CommandLine -like "*'$installDir\$engineName.ps1' -Service*"}).ProcessId if ($ProcessID) { # Get API PID $ApiPID=(Get-WmiObject win32_process -filter "Name='powershell.exe' AND ParentProcessId=$ProcessID").ProcessId if ($ApiPID) { Stop-Process -Id $ApiPID -Force -ErrorAction SilentlyContinue } } Stop-Job -Name 'API' -ErrorAction SilentlyContinue Remove-Job -Name 'API' -Force -ErrorAction SilentlyContinue } #-----------------------------------------------------------------------------# # # # Function Main # # # # Description Execute the specified actions # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# # Identify the user name. We use that for logging. $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $currentUserName = $identity.Name # Ex: "NT AUTHORITY\SYSTEM" or "Domain\Administrator" # Remove file log # if ($Setup) {Log ""} # Insert one blank line to separate test sessions logs # Log $MyInvocation.Line # The exact command line that was used to start us # The following commands write to the event log, but we need to make sure the PSService source is defined. New-EventLog -LogName $logName -Source $serviceName -ea SilentlyContinue # Workaround for PowerShell v2 bug: $PSCmdlet Not yet defined in Param() block $Status = ($PSCmdlet.ParameterSetName -eq 'Status') if ($SCMStart) { # The SCM tells us to start the service # Do whatever is necessary to start the service script instance Write-Log -Message "SCMStart: Starting script '$scriptFullName' -Service" Start-Process PowerShell.exe -ArgumentList ("-c & '$scriptFullName' -Service") # Waiting for Pipe to be started before confirming service is successfully started while([System.IO.Directory]::GetFiles("\\.\\pipe\\") -notcontains "\\.\\pipe\\$($pipeName)") { # Wait 1 seconds before checking again Start-Sleep -Seconds 1 } return } if ($SCMResume) { # The SCM tells us to resume the service # Do whatever is necessary to resume the service script instance Write-Log -Message "SCMResume: Resuming script '$scriptFullName' -Service" Start-Process PowerShell.exe -ArgumentList ("-c & '$scriptFullName' -Service") # Waiting for Pipe to be started before confirming service is successfully resumed while([System.IO.Directory]::GetFiles("\\.\\pipe\\") -notcontains "\\.\\pipe\\$($pipeName)") { # Wait 1 seconds before checking again Start-Sleep -Seconds 1 } return } if ($SCMStop) { # The SCM tells us to stop the service # Do whatever is necessary to stop the service script instance Write-Log -Message "SCMStop: Stopping script $scriptName -Service" # Send an stop message to the service instance Send-PipeMessage $pipeName 'stop' # Waiting for Pipe to be stopped before confirming service is successfully stopped while([System.IO.Directory]::GetFiles("\\.\\pipe\\") -contains "\\.\\pipe\\$($pipeName)") { # Wait 1 seconds before checking again Start-Sleep -Seconds 1 } return } if ($SCMSuspend) { # The SCM tells us to suspend the service # Do whatever is necessary to stop the service script instance Write-Log -Message "SCMSuspend: Suspending script $scriptName -Service" # Send an suspend message to the service instance Send-PipeMessage $pipeName 'suspend' # Waiting for Pipe to be stopped before confirming service is successfully suspended while([System.IO.Directory]::GetFiles("\\.\\pipe\\") -contains "\\.\\pipe\\$($pipeName)") { # Wait 1 seconds before checking again Start-Sleep -Seconds 1 } return } if ($Status) { # Get the current service status $spid = $null $processes = @(Get-WmiObject Win32_Process -filter "Name = 'powershell.exe'" | Where-Object { $_.CommandLine -match ".*$scriptCopyCname.*-Service" }) foreach ($process in $processes) { # There should be just one, but be prepared for surprises. $spid = $process.ProcessId Write-Verbose "$serviceName Process ID = $spid" } # if (Test-Path "HKLM:\SYSTEM\CurrentControlSet\services\$serviceName") {} try { $pss = Get-Service $serviceName -ea stop # Will error-out if not installed } catch { "Not Installed" return } $pss.Status if (($pss.Status -eq "Running") -and (!$spid)) { # This happened during the debugging phase Write-Error "The Service Control Manager thinks $serviceName is started, but $serviceName.ps1 -Service is not running." exit 1 } return } if ($Service) { # Run the service Write-Log -Message "Beginning background job" # Do the service background job try { ######### TO DO: Implement your own service code here. ########## # Now enter the main service event loop Write-Log -Message "Starting WinBGP Engine" # Checking prerequisites routing status if ((Get-RemoteAccess).RoutingStatus -ne 'Installed') { Write-Log -Message "Routing feature (Remote access) is required to run WinBGP" -Level Error exit 1 } # Read configuration if (Test-ConfigurationFile) { $configuration = Get-Content -Path $configdir | ConvertFrom-Json Write-Log "Loading configuration file '$($configdir)'" } else { Write-Log "Configuration file '$($configdir)' is not valid" -Level Warning Write-Log "Stopping $($serviceInternalName) process" -Level Error # Forcing stop process so service will know that process is not running Stop-Process -Name $serviceInternalName -Force exit 1 } # Log Interval information Write-Log "Global Interval: '$($configuration.global.Interval)' seconds" Write-Log "Global Timeout: '$($configuration.global.Timeout)' seconds" Write-Log "Global Rise: '$($configuration.global.Rise)' checks" Write-Log "Global Fall: '$($configuration.global.Fall)' checks" Write-Log "Global Metric: '$($configuration.global.Metric)'" # Parse all routes foreach ($route in $configuration.routes) { # Log Route information Write-Route $route # Add default interval value if no interval is specified on the route if(!($route.Interval)) { $route | Add-member -MemberType NoteProperty -Name 'Interval' -Value $configuration.global.Interval } # Add default Metric if no metric is specified on the route if(!($route.Metric)) { $route | Add-member -MemberType NoteProperty -Name 'Metric' -Value $configuration.global.Metric } # Add default Rise if no Rise is specified on the route if(!($route.Rise)) { $route | Add-member -MemberType NoteProperty -Name 'Rise' -Value $configuration.global.Rise } # Add default Fall if no Fall is specified on the route if(!($route.Fall)) { $route | Add-member -MemberType NoteProperty -Name 'Fall' -Value $configuration.global.Fall } } #BGP Router (Local) #Getting BGP Router (Local) Status $BgpRouterStatus = $null try { $BgpRouter=Get-BgpRouter -ErrorAction SilentlyContinue } catch { #If BGP Router (Local) is not configured, catch it $BgpRouterStatus=($_).ToString() } #Checking if BGP Router (Local) is correctly configured if (($BgpRouter.BgpIdentifier -eq $configuration.router.BgpIdentifier) -and ($BgpRouter.LocalASN -eq $configuration.router.LocalASN)) { Write-Log "BGP Router (local) [BgpIdentifier: $($configuration.router.BgpIdentifier) - LocalASN: $($configuration.router.LocalASN)]" } else { #BGP Router (Local) not correctly configured, remove it if ($BgpRouterStatus -ne 'BGP is not configured.') { Write-Log "BGP Router (local) not correctly configured - Cleaning (This situation may occur if the service was not correctly stopped)" -Level Warning Remove-BgpRouter -Force } #Create BGP Router Write-Log "Adding BGP Router (local) [BgpIdentifier: $($configuration.router.BgpIdentifier) - LocalASN: $($configuration.router.LocalASN)]" Add-BgpRouter -BgpIdentifier $configuration.router.BgpIdentifier -LocalASN $configuration.router.LocalASN -Force } #Adding Peering ForEach ($peer in $configuration.peers) { #Checking if Peering already exist if (((Get-BgpPeer -Name $peer.Peername -ErrorAction SilentlyContinue).LocalIPAddress -eq $peer.LocalIP) -and ((Get-BgpPeer -Name $peer.Peername -ErrorAction SilentlyContinue).LocalASN -eq $peer.LocalASN) -and ((Get-BgpPeer -Name $peer.Peername -ErrorAction SilentlyContinue).PeerIPAddress -eq $peer.PeerIP) -and ((Get-BgpPeer -Name $peer.Peername -ErrorAction SilentlyContinue).PeerASN -eq $peer.PeerASN)) { Write-Log "BGP Peering '$($peer.Peername)'" } else { #If there is a Peering but not correctly configured, cleaning it if (Get-BgpPeer -Name $peer.Peername -ErrorAction SilentlyContinue) { Write-Log "BGP Peer '$($peer.Peername)' not correctly configured - Cleaning (This situation may occur if the service was not correctly stopped)" -Level Warning Remove-BgpPeer -Name $peer.Peername -Force } Write-Log "Adding BGP Peer '$($peer.Peername)' [IP: $($peer.PeerIP) - LocalASN: $($peer.LocalASN) - PeerASN: $($peer.PeerASN)]" Add-BgpPeer -LocalIPAddress $peer.LocalIP -PeerIPAddress $peer.PeerIP -LocalASN $peer.LocalASN -PeerASN $peer.PeerASN -Name $peer.Peername } } #If there is a maintenance from previous instance (restart of service or reboot), import it if(Test-Path -Path $FunctionCliXml) { #Import variable $maintenance=Import-CliXml -Path $FunctionCliXml } #Otherwise, initialize variable else { $maintenance = @{} } #Parse all routes ForEach ($route in $configuration.routes) { # Routing policies Add-RoutePolicy -Route $route -Peers $configuration.peers #Check if route is in maintenance mode if ($maintenance.($route.RouteName)) { #Maintenance Write-Log "Route '$($route.RouteName)' is in maintenance mode" -AdditionalFields @($route.RouteName) } else { # Starting HealthCheck Job Write-Log "Starting HealthCheck Process" -AdditionalFields @($route.RouteName) Start-Job -Name $route.RouteName -FilePath "$installDir\WinBGP-HealthCheck.ps1" -ArgumentList $route } } # API if ($configuration.global.Api) { # Start API Start-API -ApiConfiguration $configuration.api } # Watchdog timer # Start a periodic timer $timerName = "Sample service timer" $period = 30 # seconds $timer = new-object System.Timers.Timer $timer.Interval = ($period * 1000) # Milliseconds $timer.AutoReset = $true # Make it fire repeatedly Register-ObjectEvent $timer -EventName Elapsed -SourceIdentifier $timerName -MessageData "TimerTick" $timer.start() # Must be stopped in the finally block Write-Log -Message "WinBGP Engine successfully started" # Start the control pipe handler thread $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" ############### do { # Keep running until told to exit by the -Stop handler $event = Wait-Event # Wait for the next incoming event $source = $event.SourceIdentifier $message = $event.MessageData $eventTime = $event.TimeGenerated.TimeofDay Write-Debug "Event at $eventTime from ${source}: $message" $event | Remove-Event # Flush the event from the queue switch ($message) { "ControlMessage" { # Required. Message received by the control pipe thread $state = $event.SourceEventArgs.InvocationStateInfo.state Write-Debug "$script -Service # Thread $source state changed to $state" switch ($state) { "Completed" { $message = Receive-PipeHandlerThread $pipeThread Write-Log "Received control message: $Message" # Reload if ($message -eq "reload") { # Store old configuration $oldConfiguration = $configuration # If Json is valid, reloading if (Test-ConfigurationFile) { Write-Log "Reloading configuration file '$($configdir)'" # Reload configuration file $configuration = Get-Content -Path $configdir | ConvertFrom-Json # Parse all routes foreach ($route in $configuration.routes) { # Add default value interval value if no interval value is specified on the route if(!($route.Interval)) { $route | Add-member -MemberType NoteProperty -Name 'Interval' -Value $configuration.global.Interval } # Add default Metric if no metric is specified on the route if(!($route.Metric)) { $route | Add-member -MemberType NoteProperty -Name 'Metric' -Value $configuration.global.Metric } # Add default Rise if no Rise is specified on the route if(!($route.Rise)) { $route | Add-member -MemberType NoteProperty -Name 'Rise' -Value $configuration.global.Rise } # Add default Fall if no Fall is specified on the route if(!($route.Fall)) { $route | Add-member -MemberType NoteProperty -Name 'Fall' -Value $configuration.global.Fall } } # Config (Global) - Only logging changes if (Compare-Object -ReferenceObject $oldConfiguration.global.PSObject.Properties -DifferenceObject $configuration.global.PSObject.Properties -PassThru) { # Manage global Interval if ($configuration.global.Interval -ne $oldConfiguration.global.Interval) { Write-Log "Global configuration - Old Interval: '$($oldConfiguration.global.Interval)' - New Interval: '$($configuration.global.Interval)'" } # Manage global Rise if ($configuration.global.Rise -ne $oldConfiguration.global.Rise) { Write-Log "Global configuration - Old Rise: '$($oldConfiguration.global.Rise)' - New Rise: '$($configuration.global.Rise)'" } # Manage global Fall if ($configuration.global.Fall -ne $oldConfiguration.global.Fall) { Write-Log "Global configuration - Old Fall: '$($oldConfiguration.global.Fall)' - New Fall: '$($configuration.global.Fall)'" } } # Manage API (Enable/Disable) if ($configuration.global.Api -ne $oldConfiguration.global.Api) { Write-Log "Global configuration - Old API: '$($oldConfiguration.global.Api)' - New API: '$($configuration.global.Api)'" if ($configuration.global.Api) { # Start Api Start-API -ApiConfiguration $configuration.api } else { ### TO BE IMPROVED because killing all healthchecks jobs ### # Stop Api Stop-API } } else { # Manage API config change # Only if API is enabled if ($configuration.global.Api) { if (Compare-Object -ReferenceObject $oldConfiguration.api.PSObject.Properties -DifferenceObject $configuration.api.PSObject.Properties -PassThru) { # Log Write-Log "API configuration change - Restarting API engine" # Stop Api Stop-API # Start Api Start-API -ApiConfiguration $configuration.api } } } # Router (Local) if (Compare-Object -ReferenceObject $oldConfiguration.router.PSObject.Properties -DifferenceObject $configuration.router.PSObject.Properties -PassThru) { # Only log Write-Log "Router configuration change require a service restart" -Level Warning } # Routes $routesReloaded=Compare-Object -ReferenceObject $oldConfiguration.routes -DifferenceObject $configuration.routes -Property 'RouteName' -PassThru -IncludeEqual | Select-Object RouteName,SideIndicator foreach ($routeReloaded in $routesReloaded) { # Old route $oldRoute=$oldConfiguration.routes | Where-Object {$_.RouteName -eq $routeReloaded.RouteName} # New route $route=$configuration.routes | Where-Object {$_.RouteName -eq $routeReloaded.RouteName} if ($routeReloaded.SideIndicator -eq '<=') { Write-Log "Route '$($routeReloaded.RouteName)' removed" -AdditionalFields @($oldRoute.RouteName) # Stopping HealthCheck Job Write-Log "Stopping HealthCheck Process" -AdditionalFields @($oldRoute.RouteName) Stop-Job -Name $oldRoute.RouteName Remove-Job -Name $oldRoute.RouteName -Force # Remove routing policy if (get-BgpRoutingPolicy -Name $oldRoute.RouteName -ErrorAction SilentlyContinue) { Write-Log "Removing BGP Routing Policy [$($oldRoute.RouteName)]" -AdditionalFields @($oldRoute.RouteName) Remove-BgpRoutingPolicy -Name $oldRoute.RouteName -Force } # Stop Announce the route from Json configuration if ((Get-BgpCustomRoute).Network -contains "$($oldRoute.Network)") { Write-Log -Message "Stopping route '$($oldRoute.RouteName)'" -AdditionalFields @($oldRoute.RouteName) # Call function to remove BGP route remove-Bgp -Route $oldRoute } # If route is in maintenance if ($maintenance.($oldRoute.RouteName)) { Write-Log "Stopping maintenance for route '$($oldRoute.RouteName)'" -AdditionalFields @($oldRoute.RouteName) $maintenance.Remove($oldRoute.RouteName) # Export maintenance variable on each change (To be moved to function) $maintenance | Export-CliXml -Path $FunctionCliXml -Force } } elseif ($routeReloaded.SideIndicator -eq '=>') { Write-Log "Route '$($routeReloaded.RouteName)' added" -AdditionalFields @($Route.RouteName) # Log Route information Write-Route $route # Create routing policies Add-RoutePolicy -Route $route -Peers $configuration.peers # Starting HealthCheck Job Write-Log "Starting HealthCheck Process" -AdditionalFields @($route.RouteName) Start-Job -Name $route.RouteName -FilePath "$installDir\WinBGP-HealthCheck.ps1" -ArgumentList $route } elseif ($routeReloaded.SideIndicator -eq '==') { # Comparing old route and new route to check if there are updates to perform if (($route.Network -ne $oldRoute.Network) -or ($route.DynamicIpSetup -ne $oldRoute.DynamicIpSetup) -or ($route.Interface -ne $oldRoute.Interface) -or ($route.Interval -ne $oldRoute.Interval) -or (Compare-Object -ReferenceObject $oldRoute.Community -DifferenceObject $route.Community) -or ($route.Metric -ne $oldRoute.Metric) -or ($route.NextHop -ne $oldRoute.NextHop) -or ($route.WithdrawOnDown -ne $oldRoute.WithdrawOnDown) -or ($route.WithdrawOnDownCheck -ne $oldRoute.WithdrawOnDownCheck)) { # Log changes Write-Log "Route '$($routeReloaded.RouteName)' updated" -AdditionalFields @($Route.RouteName) # Manage WithdrawOnDown change if ($route.WithdrawOnDown -ne $oldRoute.WithdrawOnDown) { Write-Log "WithdrawOnDown change - Old WithdrawOnDown: '$($oldRoute.WithdrawOnDown)' - New WithdrawOnDown: '$($route.WithdrawOnDown)'" -AdditionalFields @($Route.RouteName) # If WithdrawOnDown change, restart healthcheck Write-Log "Restarting HealthCheck Process" -AdditionalFields @($route.RouteName) # Stopping HealthCheck Job Stop-Job -Name $oldRoute.RouteName Remove-Job -Name $oldRoute.RouteName -Force # Starting HealthCheck Job Start-Job -Name $route.RouteName -FilePath "$installDir\WinBGP-HealthCheck.ps1" -ArgumentList $route } # Manage WithdrawOnDownCheck change (Only if WithdrawOnDown was enabled and it still enabled) if ($route.WithdrawOnDown -and $oldRoute.WithdrawOnDown) { if ($route.WithdrawOnDownCheck -ne $oldRoute.WithdrawOnDownCheck) { Write-Log "WithdrawOnDownCheck change - Old Check: '$($oldRoute.WithdrawOnDownCheck)' - New Check: '$($route.WithdrawOnDownCheck)'" -AdditionalFields @($Route.RouteName) Write-Log "Restarting HealthCheck Process" -AdditionalFields @($route.RouteName) # Stopping HealthCheck Job Stop-Job -Name $oldRoute.RouteName Remove-Job -Name $oldRoute.RouteName -Force # Starting HealthCheck Job Start-Job -Name $route.RouteName -FilePath "$installDir\WinBGP-HealthCheck.ps1" -ArgumentList $route } } # Manage interval change if ($route.Interval -ne $oldRoute.Interval) { Write-Log "Interval change - Old Interval: '$oldRouteInterval' - New Interval: '$period'" -AdditionalFields @($Route.RouteName) # Stopping HealthCheck Job Write-Log "Stopping HealthCheck Process" -AdditionalFields @($oldRoute.RouteName) Stop-Job -Name $oldRoute.RouteName Remove-Job -Name $oldRoute.RouteName -Force # Starting HealthCheck Job Write-Log "Starting HealthCheck Process" -AdditionalFields @($route.RouteName) Start-Job -Name $route.RouteName -FilePath "$installDir\WinBGP-HealthCheck.ps1" -ArgumentList $route } # Manage network change if ($route.Network -ne $oldRoute.Network) { Write-Log "Network change - Old Network: '$($oldRoute.Network)' - New Network: '$($Route.Network)'" -AdditionalFields @($Route.RouteName) # Stop Announce the route from Json configuration if ((Get-BgpCustomRoute).Network -contains "$($oldRoute.Network)") { # Removing old network Write-Log -Message "Stopping route '$($oldRoute.RouteName)'" -AdditionalFields @($oldRoute.RouteName) # Call function to remove BGP route remove-Bgp -Route $oldRoute # Adding new network Write-Log -Message "Starting route '$($route.RouteName)'" -AdditionalFields @($Route.RouteName) # Call function to remove BGP route Add-Bgp -Route $Route } } # Manage DynamicIpSetup change if ($route.DynamicIpSetup -ne $oldRoute.DynamicIpSetup) { Write-Log "Old DynamicIpSetup: '$($oldRoute.DynamicIpSetup)' - New DynamicIpSetup: '$($Route.DynamicIpSetup)'" -AdditionalFields @($Route.RouteName) # Only if DynamicIpSetup was enabled and is now disabled if (($oldRoute.DynamicIpSetup) -and (!($route.DynamicIpSetup))) { # Remove IP on interface Remove-IP $oldRoute } else { # Only if DynamicIpSetup is now enabled and was previously disabled # Add IP on interface Add-IP $route } } # Manage Interface change if ($route.Interface -ne $oldRoute.Interface) { Write-Log "Old Interface: '$($oldRoute.Interface)' - New Interface: '$($Route.Interface)'" -AdditionalFields @($Route.RouteName) # Update required only if dynamic setup was enabled Remove-IP $oldRoute # Add IP on new interface Add-IP $route } # Manage policy change if ((Compare-Object -ReferenceObject $oldRoute.Community -DifferenceObject $route.Community) -or ($route.Metric -ne $oldRoute.Metric) -or ($route.NextHop -ne $oldRoute.NextHop)) { # If Metric is specified for the route; Otherwise, use default value if($route.Metric) { $Metric = $route.Metric # seconds } else { $Metric = $configuration.global.Metric } # Generate routing policy parameters $params = @{ Name = $route.RouteName; MatchPrefix = $route.Network; PolicyType = 'ModifyAttribute'; NewMED = $Metric; } # If Metric change if ($route.Metric -ne $oldRoute.Metric) { # If Metric was not defined on old route, it was using default value if($oldRoute.Metric) { $oldRouteMetric=$oldRoute.Metric } else { $oldRouteMetric=$configuration.global.Metric } Write-Log "BGP Routing Policy - Old Metric: '$oldRouteMetric' - New Metric: '$Metric'" -AdditionalFields @($Route.RouteName) } # If Community is specified if ($route.Community) { $params.add('AddCommunity',$route.Community) if (Compare-Object -ReferenceObject $oldRoute.Community -DifferenceObject $route.Community) { Write-Log "BGP Routing Policy - Old Community: '$($oldRoute.Community)' - New Community: '$($Route.Community)'" -AdditionalFields @($Route.RouteName) } } # If NextHop is specified if ($route.NextHop) { $params.add('NewNextHop',$route.NextHop) if ($route.NextHop -ne $oldRoute.NextHop) { Write-Log "BGP Routing Policy - Old NextHop: '$($oldRoute.NextHop)' - New NextHop: '$($Route.NextHop)'" -AdditionalFields @($Route.RouteName) } } # If Routing policy exist, update it if (get-BgpRoutingPolicy -Name $route.RouteName -ErrorAction SilentlyContinue) { Write-Log "Updating BGP Routing Policy '$($Route.RouteName)'" -AdditionalFields @($Route.RouteName) Set-BgpRoutingPolicy @params -Force } else { # Otherwise, create it Write-Log "Creating BGP Routing Policy '$($Route.RouteName)'" -AdditionalFields @($Route.RouteName) Add-BgpRoutingPolicy @params -Force # Declare routing policy on each peer ForEach ($peer in $configuration.peers) { Write-Log "Adding BGP Routing Policy on Peer $($peer.Peername)" -AdditionalFields @($Route.RouteName) Add-BgpRoutingPolicyForPeer -PeerName $peer.Peername -PolicyName $Route.RouteName -Direction 'Egress' -Force } } } } } } # Peers $peersReloaded=Compare-Object -ReferenceObject $oldConfiguration.peers -DifferenceObject $configuration.peers -Property 'PeerName' -PassThru -IncludeEqual | Select-Object PeerName,SideIndicator foreach ($peerReloaded in $peersReloaded) { # Old peer $oldPeer=$oldConfiguration.peers | Where-Object {$_.PeerName -eq $peerReloaded.PeerName} # New peer $peer=$configuration.peers | Where-Object {$_.PeerName -eq $peerReloaded.PeerName} if ($peerReloaded.SideIndicator -eq '<=') { Write-Log "Peer '$($peerReloaded.PeerName)' removed" if (Get-BgpPeer -Name $oldPeer.Peername -ErrorAction SilentlyContinue) { Write-Log "Removing BGP Peer '$($oldPeer.Peername)'" Remove-BgpPeer -Name $oldPeer.PeerName -Force } } elseif ($peerReloaded.SideIndicator -eq '=>') { Write-Log "Peer '$($peerReloaded.PeerName)' added" Write-Log "Adding BGP Peer '$($peer.Peername)'" Add-BgpPeer -LocalIPAddress $peer.LocalIP -PeerIPAddress $peer.PeerIP -LocalASN $peer.LocalASN -PeerASN $peer.PeerASN -Name $peer.Peername Get-BgpRoutingPolicy | Add-BgpRoutingPolicyForPeer -PeerName $peer.Peername -Direction 'Egress' -Force Write-Log "Adding BGP Routing Policy on Peer $($peer.Peername)" } elseif ($peerReloaded.SideIndicator -eq '==') { # Removing unwanted attribute $oldPeer.PSObject.Properties.Remove('SideIndicator') # Comparing if (Compare-Object -ReferenceObject $oldPeer.PSObject.Properties -DifferenceObject $peer.PSObject.Properties -PassThru) { Write-Log "Peer '$($peerReloaded.PeerName)' updated" if (Get-BgpPeer -Name $oldPeer.Peername -ErrorAction SilentlyContinue) { Write-Log "Removing BGP Peer '$($oldPeer.Peername)'" Remove-BgpPeer -Name $oldPeer.PeerName -Force } Write-Log "Adding BGP Peer '$($peer.Peername)'" Add-BgpPeer -LocalIPAddress $peer.LocalIP -PeerIPAddress $peer.PeerIP -LocalASN $peer.LocalASN -PeerASN $peer.PeerASN -Name $peer.Peername Get-BgpRoutingPolicy | Add-BgpRoutingPolicyForPeer -PeerName $peer.Peername -Direction 'Egress' -Force Write-Log "Adding BGP Routing Policy on Peer $($peer.Peername)" } } } } else { Write-Log "Reload aborted - Configuration file '$($configdir)' is not a valid JSON file" -Level Warning } $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" } #Start/Stop mode elseif ($message -like "route*") { $route_to_control=$message.split(' ')[1] $control_action=$message.split(' ')[2] # Grabbing route $route_control=$configuration.routes | Where-Object {$_.RouteName -eq $route_to_control} if ($control_action -eq 'start') { # Control route announcement if maintenance is false if (!$maintenance.($route_control.RouteName)) { # Announce the route if there is no route if ((Get-BgpCustomRoute).Network -notcontains "$($route_control.Network)") { Write-Log -Message "Starting route '$($route_control.RouteName)'" -AdditionalFields @($route_control.RouteName) # Call function to start BGP route Add-BGP -Route $route_control } } else { Write-Log -Message "Route in maintenance - Skipping starting" -AdditionalFields @($route_control.RouteName) } } elseif ($control_action -eq 'stop') { # Control route announcement if maintenance is false if (!$maintenance.($route_control.RouteName)) { # Stop Announce the route if ((Get-BgpCustomRoute).Network -contains "$($route_control.Network)") { Write-Log -Message "Stopping route '$($route_control.RouteName)'" -AdditionalFields @($route_control.RouteName) # Call function to remove BGP route remove-Bgp -Route $route_control } } else { Write-Log -Message "Route in maintenance - Skipping stopping" -AdditionalFields @($route_control.RouteName) } } $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" } #Maintenance mode elseif ($message -like "maintenance*") { $route_in_maintenance=$message.split(' ')[1] $control_action=$message.split(' ')[2] # Grabbing route $route_maintenance=$configuration.routes | Where-Object {$_.RouteName -eq $route_in_maintenance} if ($control_action -eq 'start') { #If route is not in maintenance if (!($maintenance.($route_maintenance.RouteName))) { Write-Log "Starting maintenance for route '$($route_maintenance.RouteName)'" -AdditionalFields @($route_maintenance.RouteName) # Add timestamp for monitoring purpose $MaintenanceTimestamp=Get-Date $maintenance.Add($route_maintenance.RouteName,$MaintenanceTimestamp) # Export maintenance variable on each change (To be moved to function) $maintenance | Export-CliXml -Path $FunctionCliXml -Force # Stopping HealthCheck Job Write-Log "Stopping HealthCheck Process" -AdditionalFields @($route_maintenance.RouteName) Stop-Job -Name $route_maintenance.RouteName Remove-Job -Name $route_maintenance.RouteName -Force # Removing route if ((Get-BgpCustomRoute).Network -contains "$($route_maintenance.Network)") { remove-Bgp -Route $route_maintenance } else { Write-Log "BGP network already unannounced '$($route_maintenance.Network)'" -Level Warning } } else { Write-Log "Route '$($route_maintenance.RouteName)' already in maintenance mode" -Level Warning } } elseif ($control_action -eq 'stop') { #If route is in maintenance if ($maintenance.($route_maintenance.RouteName)) { Write-Log "Stopping maintenance for route '$($route_maintenance.RouteName)'" -AdditionalFields @($route_maintenance.RouteName) $maintenance.Remove($route_maintenance.RouteName) # Export maintenance variable on each change (To be moved to function) $maintenance | Export-CliXml -Path $FunctionCliXml -Force # Starting HealthCheck Job Write-Log "Starting HealthCheck Process" -AdditionalFields @($route_maintenance.RouteName) Start-Job -Name $route_maintenance.RouteName -FilePath "$installDir\WinBGP-HealthCheck.ps1" -ArgumentList $route_maintenance } else { Write-Log "Route '$($route_maintenance.RouteName)' was not in maintenance mode" -Level Warning } } $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" } elseif ($message -eq 'restart api') { # Log Write-Log "Restarting API engine" # Stop Api Stop-API # Start Api Start-API -ApiConfiguration $configuration.api # Start another thread waiting for control messages $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" } elseif (($message -ne "stop") -and ($message -ne "suspend")) { # Start another thread waiting for control messages $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" } } "Failed" { # Getting Errors $err = Receive-PipeHandlerThread $pipeThread Write-Log -Message "$source thread failed: $err" -Level Error Start-Sleep 1 # Avoid getting too many errors $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" # Retry } } } "TimerTick" { # Example. Periodic event generated for this example # Watchdog # Read PowerShell jobs (To optimize query) [Array]$ChildJobs=(Get-Job -ErrorAction SilentlyContinue -State Running).Name # API if ($configuration.global.Api) { if ($ChildJobs -notcontains 'API') { # Start API Write-Log "Restarting API engine (Watchdog)" -Level Warning Remove-Job -Name 'API' -Force -ErrorAction SilentlyContinue # Start API Start-API -ApiConfiguration $configuration.api } } ForEach ($route in $configuration.routes) { #Control route accouncement if maintenance is false if (!$maintenance.($route.RouteName)) { if ($ChildJobs -notcontains "$($route.RouteName)") { # Cleaning unhealthy HealthCheck Write-Log "Restarting HealthCheck Process (Watchdog)" -AdditionalFields @($route.RouteName) -Level Warning Remove-Job -Name $route.RouteName -Force -ErrorAction SilentlyContinue Start-Job -Name $route.RouteName -FilePath "$installDir\WinBGP-HealthCheck.ps1" -ArgumentList $route } } } } default { # Should not happen Write-Log -Message "Unexpected event from ${source}: $Message" -Level Warning } } } while (($message -ne 'stop') -and ($message -ne 'suspend')) # Logging (Set first letter to uppercase and add a 'p' is message is 'stop') Write-Log -Message "$((Get-Culture).TextInfo.ToTitleCase($message))$(if($message -eq 'stop'){'p'})ing WinBGP engine" # Stopping healthchecks Write-Log -Message "Stopping HealthCheck engine" ForEach ($route in $configuration.routes) { # Stopping HealthCheck Job Write-Log "Stopping HealthCheck Process" -AdditionalFields @($route.RouteName) Stop-Job -Name $route.RouteName -ErrorAction SilentlyContinue Remove-Job -Name $route.RouteName -Force -ErrorAction SilentlyContinue } # Stopping API if ($configuration.global.Api) { # Stop API Stop-API } # Stopping the service (not performed when suspending service to keep the BGP engine working) if ($message -eq 'stop') { # Stopping all routes Write-Log -Message "Stopping BGP routes" ForEach ($route in $configuration.routes) { # Unannounce the route from Json configuration if there is no route if ((Get-BgpCustomRoute).Network -contains "$($route.Network)") { Write-Log -Message "Stopping route '$($route.RouteName)'" # Call function to start BGP route Remove-BGP -Route $route } } # Remove BGP Peering Write-Log -Message "Stopping BGP" Get-BgpRoutingPolicy | Remove-BgpRoutingPolicy -Force Get-BgpPeer | Remove-BgpPeer -Force Remove-BgpRouter -Force } #Export the maintenance status (To be kept over a restart or a reboot) if ($maintenance.Count -gt 0) { $maintenance | Export-CliXml -Path $FunctionCliXml -Force } #Otherwise, ensure file doesn't exist else { Remove-Item -Path $FunctionCliXml -Force } } catch { # An exception occurred while runnning the service $msg = $_.Exception.Message $line = $_.InvocationInfo.ScriptLineNumber Write-Log -Message "Error at line ${line}: $msg" -Level Error Write-Log "Stopping $($serviceInternalName) process" -Level Error # Forcing stop process so service will know that process is not running (to avoid having service running without process) Stop-Process -Name $serviceInternalName -Force } finally { # Invoked in all cases: Exception or normally by -Stop # Cleanup the periodic timer used in the above example Unregister-Event -SourceIdentifier $timerName $timer.stop() ############### End of the service code example. ################ # Terminate the control pipe handler thread Get-PSThread | Remove-PSThread # Remove all remaining threads # Flush all leftover events (There may be some that arrived after we exited the while event loop, but before we unregistered the events) $events = Get-Event | Remove-Event # Log a termination event, no matter what the cause is. Write-Log -Message "WinBGP Engine successfully stopped" } return }