From 2becc990c9a10434d77088ff4786b90f1bbf2765 Mon Sep 17 00:00:00 2001 From: WebalexEU <28548335+webalexeu@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:31:40 +0100 Subject: [PATCH] Initial public release --- .gitignore | 6 + LICENCE | 21 + README | 31 + builder/build.ps1 | 28 + builder/files.wxs | 60 ++ builder/main.wxs | 217 +++++ service/WinBGP-Service.ps1 | 949 ++++++++++++++++++++ src/WinBGP-API.ps1 | 571 ++++++++++++ src/WinBGP-Engine.ps1 | 1744 ++++++++++++++++++++++++++++++++++++ src/WinBGP-HealthCheck.ps1 | 522 +++++++++++ src/WinBGP.ps1 | 733 +++++++++++++++ src/winbgp.json.example | 61 ++ 12 files changed, 4943 insertions(+) create mode 100644 .gitignore create mode 100644 LICENCE create mode 100644 README create mode 100644 builder/build.ps1 create mode 100644 builder/files.wxs create mode 100644 builder/main.wxs create mode 100644 service/WinBGP-Service.ps1 create mode 100644 src/WinBGP-API.ps1 create mode 100644 src/WinBGP-Engine.ps1 create mode 100644 src/WinBGP-HealthCheck.ps1 create mode 100644 src/WinBGP.ps1 create mode 100644 src/winbgp.json.example diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..784f04c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Service builder executable outputs +service/*.exe + +# Installer outputs +builder/*.msi +builder/*.wixpdb diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..6b34b1a --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Webalex System + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README b/README new file mode 100644 index 0000000..ae9b2d3 --- /dev/null +++ b/README @@ -0,0 +1,31 @@ +# WinBGP + +WinBGP Service + +## Prerequisites + +Windows Server 2016, 2019, 2022, 2025 + +## Installation + +Standard MSI installation. + + +## Contributing + +Coder or not, you can contribute to the project! We welcome all contributions. + +### For Users + +If you don't code, you still sit on valuable information that can make this project even better. If you experience that the +product does unexpected things, throw errors or is missing functionality, you can help by submitting bugs and feature requests. +Please see the issues tab on this project and submit a new issue that matches your needs. + +### For Developers + +If you do code, we'd love to have your contributions. Please read the [Contribution guidelines](CONTRIBUTING.md) for more information. +You can either help by picking up an existing issue or submit a new one if you have an idea for a new feature or improvement. + +## Links + +- [Semantic Versioning 2.0.0](https://semver.org/) diff --git a/builder/build.ps1 b/builder/build.ps1 new file mode 100644 index 0000000..e196f81 --- /dev/null +++ b/builder/build.ps1 @@ -0,0 +1,28 @@ +[CmdletBinding()] +Param ( + [Parameter(Mandatory = $true)] + [String] $Version, + [Parameter(Mandatory = $false)] + [ValidateSet("amd64", "arm64")] + [String] $Arch = "amd64" +) +$ErrorActionPreference = "Stop" + +# The MSI version is not semver compliant, so just take the numerical parts +$MsiVersion = $Version -replace '^v?([0-9\.]+).*$','$1' + +# Set working dir to this directory, reset previous on exit +Push-Location $PSScriptRoot +Trap { + # Reset working dir on error + Pop-Location +} + + +Write-Verbose "Creating winbgp-${Version}-${Arch}.msi" +$wixArch = @{"amd64" = "x64"; "arm64" = "arm64"}[$Arch] + +Invoke-Expression "wix build -arch $wixArch -o .\WinBGP-$($Version)-$($Arch).msi .\files.wxs .\main.wxs -d ProductName=WinBGP -d Version=$($MsiVersion) -ext WixToolset.Firewall.wixext -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext" + +Write-Verbose "Done!" +Pop-Location diff --git a/builder/files.wxs b/builder/files.wxs new file mode 100644 index 0000000..dc4ee6e --- /dev/null +++ b/builder/files.wxs @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/builder/main.wxs b/builder/main.wxs new file mode 100644 index 0000000..f722fab --- /dev/null +++ b/builder/main.wxs @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/service/WinBGP-Service.ps1 b/service/WinBGP-Service.ps1 new file mode 100644 index 0000000..1078849 --- /dev/null +++ b/service/WinBGP-Service.ps1 @@ -0,0 +1,949 @@ +############################################################################### +# # +# File name PSService.ps1 # +# # +# Description A sample service in a standalone PowerShell script # +# # +# Notes The latest PSService.ps1 version is available in GitHub # +# repository https://github.com/JFLarvoire/SysToolsLib/ , # +# in the PowerShell subdirectory. # +# Please report any problem in the Issues tab in that # +# GitHub repository in # +# https://github.com/JFLarvoire/SysToolsLib/issues # +# If you do submit a pull request, please add a comment at # +# the end of this header with the date, your initials, and # +# a description of the changes. Also update $scriptVersion. # +# # +# The initial version of this script was described in an # +# article published in the May 2016 issue of MSDN Magazine. # +# https://msdn.microsoft.com/en-us/magazine/mt703436.aspx # +# This updated version has one major change: # +# The -Service handler in the end has been rewritten to be # +# event-driven, with a second thread waiting for control # +# messages coming in via a named pipe. # +# This allows fixing a bug of the original version, that # +# did not stop properly, and left a zombie process behind. # +# The drawback is that the new code is significantly longer,# +# due to the added PowerShell thread management routines. # +# On the other hand, these thread management routines are # +# reusable, and will allow building much more powerful # +# services. # +# # +# Dynamically generates a small PSService.exe wrapper # +# application, that in turn invokes this PowerShell script. # +# # +# Some arguments are inspired by Linux' service management # +# arguments: -Start, -Stop, -Restart, -Status # +# Others are more in the Windows' style: -Setup, -Remove # +# # +# The actual start and stop operations are done when # +# running as SYSTEM, under the control of the SCM (Service # +# Control Manager). # +# # +# To create your own service, make a copy of this file and # +# rename it. The file base name becomes the service name. # +# Then implement your own service code in the if ($Service) # +# {block} at the very end of this file. See the TO DO # +# comment there. # +# There are global settings below the script param() block. # +# They can easily be changed, but the defaults should be # +# suitable for most projects. # +# # +# Service installation and usage: See the dynamic help # +# section below, or run: help .\PSService.ps1 -Detailed # +# # +# Debugging: The Log function writes messages into a file # +# called C:\Windows\Logs\PSService.log (or actually # +# ${env:windir}\Logs\$serviceName.log). # +# It is very convenient to monitor what's written into that # +# file with a WIN32 port of the Unix tail program. Usage: # +# tail -f C:\Windows\Logs\PSService.log # +# # +# History # +# 2015-07-10 JFL jf.larvoire@hpe.com created this script. # +# 2015-10-13 JFL Made this script completely generic, and added comments # +# in the header above. # +# 2016-01-02 JFL Moved the Event Log name into new variable $logName. # +# Improved comments. # +# 2016-01-05 JFL Fixed the StartPending state reporting. # +# 2016-03-17 JFL Removed aliases. Added missing explicit argument names. # +# 2016-04-16 JFL Moved the official repository on GitHub. # +# 2016-04-21 JFL Minor bug fix: New-EventLog did not use variable $logName.# +# 2016-05-25 JFL Bug fix: The service task was not properly stopped; Its # +# finally block was not executed, and a zombie task often # +# remained. Fixed by using a named pipe to send messages # +# to the service task. # +# 2016-06-05 JFL Finalized the event-driven service handler. # +# Fixed the default command setting in PowerShell v2. # +# Added a sample -Control option using the new pipe. # +# 2016-06-08 JFL Rewrote the pipe handler using PSThreads instead of Jobs. # +# 2016-06-09 JFL Finalized the PSThread management routines error handling.# +# This finally fixes issue #1. # +# 2016-08-22 JFL Fixed issue #3 creating the log and install directories. # +# Thanks Nischl. # +# 2016-09-06 JFL Fixed issue #4 detecting the System account. Now done in # +# a language-independent way. Thanks A Gonzalez. # +# 2016-09-19 JFL Fixed issue #5 starting services that begin with a number.# +# Added a $ServiceDescription string global setting, and # +# use it for the service registration. # +# Added comments about Windows event logs limitations. # +# 2016-11-17 RBM Fixed issue #6 Mangled hyphen in final Unregister-Event. # +# 2017-05-10 CJG Added execution policy bypass flag. # +# 2017-10-04 RBL rblindberg Updated C# code OnStop() routine fixing # +# orphaned process left after stoping the service. # +# 2017-12-05 NWK omrsafetyo Added ServiceUser and ServicePassword to the # +# script parameters. # +# 2017-12-10 JFL Removed the unreliable service account detection tests, # +# and instead use dedicated -SCMStart and -SCMStop # +# arguments in the PSService.exe helper app. # +# Renamed variable userName as currentUserName. # +# Renamed arguments ServiceUser and ServicePassword to the # +# more standard UserName and Password. # +# Also added the standard argument -Credential. # +# # +############################################################################### +#Requires -version 5.1 + +<# + .SYNOPSIS + A sample Windows service, in a standalone PowerShell script. + + .DESCRIPTION + This script demonstrates how to write a Windows service in pure PowerShell. + It dynamically generates a small PSService.exe wrapper, that in turn + invokes this PowerShell script again for its start and stop events. + + .PARAMETER Start + Start the service. + + .PARAMETER Stop + Stop the service. + + .PARAMETER Restart + Stop then restart the service. + + .PARAMETER Status + Get the current service status: Not installed / Stopped / Running + + .PARAMETER Setup + Install the service. + Optionally use the -Credential or -UserName arguments to specify the user + account for running the service. By default, uses the LocalSystem account. + Known limitation with the old PowerShell v2: It is necessary to use -Credential + or -UserName. For example, use -UserName LocalSystem to emulate the v3+ default. + + .PARAMETER Credential + User and password credential to use for running the service. + For use with the -Setup command. + Generate a PSCredential variable with the Get-Credential command. + + .PARAMETER UserName + User account to use for running the service. + For use with the -Setup command, in the absence of a Credential variable. + The user must have the "Log on as a service" right. To give him that right, + open the Local Security Policy management console, go to the + "\Security Settings\Local Policies\User Rights Assignments" folder, and edit + the "Log on as a service" policy there. + Services should always run using a user account which has the least amount + of privileges necessary to do its job. + Three accounts are special, and do not require a password: + * LocalSystem - The default if no user is specified. Highly privileged. + * LocalService - Very few privileges, lowest security risk. + Apparently not enough privileges for running PowerShell. Do not use. + * NetworkService - Idem, plus network access. Same problems as LocalService. + + .PARAMETER Password + Password for UserName. If not specified, you will be prompted for it. + It is strongly recommended NOT to use that argument, as that password is + visible on the console, and in the task manager list. + Instead, use the -UserName argument alone, and wait for the prompt; + or, even better, use the -Credential argument. + + .PARAMETER Remove + Uninstall the service. + + .PARAMETER Service + Run the service in the background. Used internally by the script. + Do not use, except for test purposes. + + .PARAMETER SCMStart + Process Service Control Manager start requests. Used internally by the script. + Do not use, except for test purposes. + + .PARAMETER SCMResume + Process Service Control Manager resume requests. Used internally by the script. + Do not use, except for test purposes. + + .PARAMETER SCMStop + Process Service Control Manager stop requests. Used internally by the script. + Do not use, except for test purposes. + + .PARAMETER SCMSuspend + Process Service Control Manager suspend requests. Used internally by the script. + Do not use, except for test purposes. + + .PARAMETER Control + Send a control message to the service thread. + + .PARAMETER Version + Display this script version and exit. + + .EXAMPLE + # Setup the service and run it for the first time + C:\PS>.\PSService.ps1 -Status + Not installed + C:\PS>.\PSService.ps1 -Setup + C:\PS># At this stage, a copy of PSService.ps1 is present in the path + C:\PS>PSService -Status + Stopped + C:\PS>PSService -Start + C:\PS>PSService -Status + Running + C:\PS># Load the log file in Notepad.exe for review + C:\PS>notepad ${ENV:windir}\Logs\PSService.log + + .EXAMPLE + # Stop the service and uninstall it. + C:\PS>PSService -Stop + C:\PS>PSService -Status + Stopped + C:\PS>PSService -Remove + C:\PS># At this stage, no copy of PSService.ps1 is present in the path anymore + C:\PS>.\PSService.ps1 -Status + Not installed + + .EXAMPLE + # Configure the service to run as a different user + C:\PS>$cred = Get-Credential -UserName LAB\Assistant + C:\PS>.\PSService -Setup -Credential $cred + + .EXAMPLE + # Send a control message to the service, and verify that it received it. + C:\PS>PSService -Control Hello + C:\PS>Notepad C:\Windows\Logs\PSService.log + # The last lines should contain a trace of the reception of this Hello message +#> + +[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='Setup', Mandatory=$true)] + [Parameter(ParameterSetName='Setup2', Mandatory=$true)] + [Switch]$Setup, # Install the service + + [Parameter(ParameterSetName='Setup', Mandatory=$true)] + [String]$UserName, # Set the service to run as this user + + [Parameter(ParameterSetName='Setup', Mandatory=$false)] + [String]$Password, # Use this password for the user + + [Parameter(ParameterSetName='Setup2', Mandatory=$false)] + [System.Management.Automation.PSCredential]$Credential, # Service account credential + + [Parameter(ParameterSetName='Remove', Mandatory=$true)] + [Switch]$Remove, # Uninstall the service + + [Parameter(ParameterSetName='Build', Mandatory=$true)] + [Switch]$Build, # Run the service (Internal use only) + + [Parameter(ParameterSetName='Version', Mandatory=$true)] + [Switch]$Version # Get this script version +) + +# Don't forget to increment version when updating service +$serviceVersion = '1.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 Engine" +$ServiceDescription = "The BGP swiss army knife of networking on Windows" +$installDir = "$($ENV:ProgramW6432)\WinBGP" # Where to install the service files +$scriptCopy = "$installDir\$scriptName" +$exeName = "$serviceName-Service.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!) +# 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 $serviceVersion +} + +#-----------------------------------------------------------------------------# +# # +# 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 + { + } + } + +#-----------------------------------------------------------------------------# +# # +# Function $source # +# # +# Description C# source of the PSService.exe stub # +# # +# Arguments # +# # +# Notes The lines commented with "SET STATUS" and "EVENT LOG" are # +# optional. (Or blocks between "// SET STATUS [" and # +# "// SET STATUS ]" comments.) # +# SET STATUS lines are useful only for services with a long # +# startup time. # +# EVENT LOG lines are useful for debugging the service. # +# # +# History # +# 2017-10-04 RBL Updated the OnStop() procedure adding the sections # +# try{ # +# }catch{ # +# }finally{ # +# } # +# This resolved the issue where stopping the service would # +# leave the PowerShell process -Service still running. This # +# unclosed process was an orphaned process that would # +# remain until the pid was manually killed or the computer # +# was rebooted # +# # +#-----------------------------------------------------------------------------# + +# Overwrite for builder +$scriptCopy= "$installDir\WinBGP-Engine.ps1" + +$scriptCopyCname = $scriptCopy -replace "\\", "\\" # Double backslashes. (The first \\ is a regexp with \ escaped; The second is a plain string.) +$source = @" + using System; + using System.ServiceProcess; + using System.Diagnostics; + using System.Runtime.InteropServices; // SET STATUS + using System.ComponentModel; // SET STATUS + using System.Reflection; // SET STATUS + + [assembly: AssemblyVersion("$serviceVersion")] // SET VERSION + + public enum ServiceType : int { // SET STATUS [ + SERVICE_WIN32_OWN_PROCESS = 0x00000010, + SERVICE_WIN32_SHARE_PROCESS = 0x00000020, + }; // SET STATUS ] + + public enum ServiceState : int { // SET STATUS [ + SERVICE_STOPPED = 0x00000001, + SERVICE_START_PENDING = 0x00000002, + SERVICE_STOP_PENDING = 0x00000003, + SERVICE_RUNNING = 0x00000004, + SERVICE_CONTINUE_PENDING = 0x00000005, + SERVICE_PAUSE_PENDING = 0x00000006, + SERVICE_PAUSED = 0x00000007, + }; // SET STATUS ] + + [StructLayout(LayoutKind.Sequential)] // SET STATUS [ + public struct ServiceStatus { + public ServiceType dwServiceType; + public ServiceState dwCurrentState; + public int dwControlsAccepted; + public int dwWin32ExitCode; + public int dwServiceSpecificExitCode; + public int dwCheckPoint; + public int dwWaitHint; + }; // SET STATUS ] + + public enum Win32Error : int { // WIN32 errors that we may need to use + NO_ERROR = 0, + ERROR_APP_INIT_FAILURE = 575, + ERROR_FATAL_APP_EXIT = 713, + ERROR_SERVICE_NOT_ACTIVE = 1062, + ERROR_EXCEPTION_IN_SERVICE = 1064, + ERROR_SERVICE_SPECIFIC_ERROR = 1066, + ERROR_PROCESS_ABORTED = 1067, + }; + + public class Service_$serviceName : ServiceBase { // $serviceName may begin with a digit; The class name must begin with a letter + private System.Diagnostics.EventLog eventLog; // EVENT LOG + private ServiceStatus serviceStatus; // SET STATUS + + public const int SERVICE_ACCEPT_PRESHUTDOWN = 0x100; // Preshutdown + public const int SERVICE_CONTROL_PRESHUTDOWN = 0xf; // Preshutdown + + public Service_$serviceName() { + ServiceName = "$serviceName"; + CanStop = true; + CanShutdown = true; + CanPauseAndContinue = true; + AutoLog = true; + + // PreShutdown Section + FieldInfo acceptedCommandsFieldInfo = typeof(ServiceBase).GetField("acceptedCommands", BindingFlags.Instance | BindingFlags.NonPublic); + if (acceptedCommandsFieldInfo == null) + { + throw new ApplicationException("acceptedCommands field not found"); + } + int value = (int)acceptedCommandsFieldInfo.GetValue(this); + acceptedCommandsFieldInfo.SetValue(this, value | SERVICE_ACCEPT_PRESHUTDOWN); + // End PreShutdown Section + + eventLog = new System.Diagnostics.EventLog(); // EVENT LOG [ + if (!System.Diagnostics.EventLog.SourceExists(ServiceName)) { + System.Diagnostics.EventLog.CreateEventSource(ServiceName, "$logName"); + } + eventLog.Source = ServiceName; + eventLog.Log = "$logName"; // EVENT LOG ] + EventLog.WriteEntry(ServiceName, "$exeName $serviceName()"); // EVENT LOG + } + + [DllImport("advapi32.dll", SetLastError=true)] // SET STATUS + private static extern bool SetServiceStatus(IntPtr handle, ref ServiceStatus serviceStatus); + + protected override void OnStart(string [] args) { + EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Entry. Starting script '$scriptCopyCname' -SCMStart"); // EVENT LOG + // Set the service state to Start Pending. // SET STATUS [ + // Only useful if the startup time is long. Not really necessary here for a 2s startup time. + serviceStatus.dwServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS; + serviceStatus.dwCurrentState = ServiceState.SERVICE_START_PENDING; + serviceStatus.dwWin32ExitCode = 0; + serviceStatus.dwWaitHint = 2000; // It takes about 2 seconds to start PowerShell + SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS ] + // Start a child process with another copy of this script + try { + Process p = new Process(); + // Redirect the output stream of the child process. + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.FileName = "PowerShell.exe"; + p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStart"; // Works if path has spaces, but not if it contains ' quotes. + p.Start(); + // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) + string output = p.StandardOutput.ReadToEnd(); + // Wait for the completion of the script startup code, that launches the -Service instance + p.WaitForExit(); + if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); + // Success. Set the service state to Running. // SET STATUS + serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; // SET STATUS + } catch (Exception e) { + EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Failed to start $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG + // Change the service state back to Stopped. // SET STATUS [ + serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED; + Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code + if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is... + w32ex = e.InnerException as Win32Exception; + } + if (w32ex != null) { // Report the actual WIN32 error + serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode; + } else { // Make up a reasonable reason + serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE); + } // SET STATUS ] + } finally { + serviceStatus.dwWaitHint = 0; // SET STATUS + SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS + EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Exit"); // EVENT LOG + } + } + + protected override void OnContinue() { + EventLog.WriteEntry(ServiceName, "$exeName OnContinue() // Entry. Starting script '$scriptCopyCname' -SCMResume"); // EVENT LOG + // Set the service state to Continue Pending. // SET STATUS [ + // Only useful if the startup time is long. Not really necessary here for a 2s startup time. + serviceStatus.dwServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS; + serviceStatus.dwCurrentState = ServiceState.SERVICE_CONTINUE_PENDING; + serviceStatus.dwWin32ExitCode = 0; + serviceStatus.dwWaitHint = 2000; // It takes about 2 seconds to start PowerShell + SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS ] + // Start a child process with another copy of this script + try { + Process p = new Process(); + // Redirect the output stream of the child process. + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.FileName = "PowerShell.exe"; + p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMResume"; // Works if path has spaces, but not if it contains ' quotes. + p.Start(); + // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) + string output = p.StandardOutput.ReadToEnd(); + // Wait for the completion of the script startup code, that launches the -Service instance + p.WaitForExit(); + if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); + // Success. Set the service state to Running. // SET STATUS + serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; // SET STATUS + } catch (Exception e) { + EventLog.WriteEntry(ServiceName, "$exeName OnContinue() // Failed to resume $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG + // Change the service state back to Paused. // SET STATUS [ + serviceStatus.dwCurrentState = ServiceState.SERVICE_PAUSED; + Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code + if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is... + w32ex = e.InnerException as Win32Exception; + } + if (w32ex != null) { // Report the actual WIN32 error + serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode; + } else { // Make up a reasonable reason + serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE); + } // SET STATUS ] + } finally { + serviceStatus.dwWaitHint = 0; // SET STATUS + SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS + EventLog.WriteEntry(ServiceName, "$exeName OnContinue() // Exit"); // EVENT LOG + } + } + + + private void StopSCM() + { + // Start a child process with another copy of ourselves + try { + Process p = new Process(); + // Redirect the output stream of the child process. + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.FileName = "PowerShell.exe"; + p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStop"; // Works if path has spaces, but not if it contains ' quotes. + p.Start(); + // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) + string output = p.StandardOutput.ReadToEnd(); + // Wait for the PowerShell script to be fully stopped. + p.WaitForExit(); + if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); + // Success. Set the service state to Stopped. // SET STATUS + serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED; // SET STATUS + } catch (Exception e) { + EventLog.WriteEntry(ServiceName, "$exeName StopSCM() // Failed to stop $scriptCopyCname.", EventLogEntryType.Error); // EVENT LOG + throw e; // SET STATUS ] + } finally { + serviceStatus.dwWaitHint = 0; // SET STATUS + SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS + } + } + + private void SuspendSCM() + { + // Start a child process with another copy of ourselves + try { + Process p = new Process(); + // Redirect the output stream of the child process. + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.FileName = "PowerShell.exe"; + p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMSuspend"; // Works if path has spaces, but not if it contains ' quotes. + p.Start(); + // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) + string output = p.StandardOutput.ReadToEnd(); + // Wait for the PowerShell script to be fully stopped. + p.WaitForExit(); + if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); + // Success. Set the service state to Suspended. // SET STATUS + serviceStatus.dwCurrentState = ServiceState.SERVICE_PAUSED; // SET STATUS + } catch (Exception e) { + EventLog.WriteEntry(ServiceName, "$exeName SuspendSCM() // Failed to suspend $scriptCopyCname.", EventLogEntryType.Error); // EVENT LOG + throw e; // SET STATUS ] + } finally { + serviceStatus.dwWaitHint = 0; // SET STATUS + SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS + } + } + + protected override void OnStop() { + EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Entry"); // EVENT LOG + try { + this.StopSCM(); + base.OnStop(); + } + catch(Exception e) + { + EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Fail. " + e.Message, EventLogEntryType.Error); // EVENT LOG + throw e; + } + EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Exit"); // EVENT LOG + } + + protected override void OnPause() + { + EventLog.WriteEntry(ServiceName, "$exeName OnPause() // Entry"); // EVENT LOG + try { + this.SuspendSCM(); + base.OnPause(); // This will set the service status to "Paused" + } + catch(Exception e) + { + EventLog.WriteEntry(ServiceName, "$exeName OnPause() // Fail. " + e.Message, EventLogEntryType.Error); // EVENT LOG + throw e; + } + EventLog.WriteEntry(ServiceName, "$exeName OnPause() // Exit"); // EVENT LOG + } + + protected override void OnCustomCommand(int command) + { + if (command == SERVICE_CONTROL_PRESHUTDOWN) + { + EventLog.WriteEntry(ServiceName, "$exeName OnPreshutdown() // Entry"); // EVENT LOG + try{ + this.StopSCM(); + } + catch(Exception e) + { + EventLog.WriteEntry(ServiceName, "$exeName OnPreshutdown() // Fail. " + e.Message, EventLogEntryType.Error); // EVENT LOG + throw e; + } + EventLog.WriteEntry(ServiceName, "$exeName OnPreshutdown() // Exit"); // EVENT LOG + } + base.OnCustomCommand(command); + } + + protected override void OnShutdown() { + // * NOP * + } + + public static void Main() { + System.ServiceProcess.ServiceBase.Run(new Service_$serviceName()); + } + } +"@ + + +#-----------------------------------------------------------------------------# +# # +# 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 ($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 ($Setup) { # Install the service + # Check if it's necessary + try { + $pss = Get-Service $serviceName -ea stop # Will error-out if not installed + # Check if this script is newer than the installed copy. + if ((Get-Item $scriptCopy -ea SilentlyContinue).LastWriteTime -lt (Get-Item $scriptFullName -ea SilentlyContinue).LastWriteTime) { + Write-Verbose "Service $serviceName is already Installed, but requires upgrade" + & $scriptFullName -Remove + throw "continue" + } else { + Write-Verbose "Service $serviceName is already Installed, and up-to-date" + } + exit 0 + } catch { + # This is the normal case here. Do not throw or write any error! + Write-Debug "Installation is necessary" # Also avoids a ScriptAnalyzer warning + # And continue with the installation. + } + if (!(Test-Path $installDir)) { + New-Item -ItemType directory -Path $installDir | Out-Null + } + # Copy the service script into the installation directory +if ($ScriptFullName -ne $scriptCopy) { + Write-Verbose "Installing $scriptCopy" + Copy-Item $ScriptFullName $scriptCopy + } + # Generate the service .EXE from the C# source embedded in this script + try { + Write-Verbose "Compiling $exeFullName" + Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false + } catch { + $msg = $_.Exception.Message + Write-error "Failed to create the $exeFullName service stub. $msg" + exit 1 + } + # Register the service + Write-Verbose "Registering service $serviceName" + if ($UserName -and !$Credential.UserName) { + $emptyPassword = New-Object -Type System.Security.SecureString + switch ($UserName) { + {"LocalService", "NetworkService" -contains $_} { + $Credential = New-Object -Type System.Management.Automation.PSCredential ("NT AUTHORITY\$UserName", $emptyPassword) + } + {"LocalSystem", ".\LocalSystem", "${env:COMPUTERNAME}\LocalSystem", "NT AUTHORITY\LocalService", "NT AUTHORITY\NetworkService" -contains $_} { + $Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $emptyPassword) + } + default { + if (!$Password) { + $Credential = Get-Credential -UserName $UserName -Message "Please enter the password for the service user" + } else { + $securePassword = ConvertTo-SecureString $Password -AsPlainText -Force + $Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $securePassword) + } + } + } + } + #Find if exeFullName contain withespace (Security issue with Unquoted Service Path) # TO IMPROVE - Create PR on GitHub + if ($exeFullName -match "\s") { $exeFullName = "`"$exeFullName`""} + + if ($Credential.UserName) { + Write-Log -Message "Configuring the service to run as $($Credential.UserName)" + # TO IMPROVE - Add variable to manage DependsOn and Create PR on GitHub + $pss = New-Service -Name $serviceName -BinaryPathName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic -Credential $Credential -DependsOn 'RemoteAccess' + } else { + Write-Log -Message "Configuring the service to run by default as LocalSystem" + # TO IMPROVE - Add variable to manage DependsOn and Create PR on GitHub + $pss = New-Service -Name $serviceName -BinaryPathName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic -DependsOn 'RemoteAccess' + } + + return +} + +if ($Build) { # Install the service + # Generate the service .EXE from the C# source embedded in this script + + # Overwrite for builder + $exeFullName=".\$exeName" + + try { + Write-Verbose "Compiling $exeFullName" + Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false + } catch { + $msg = $_.Exception.Message + Write-error "Failed to create the $exeFullName service stub. $msg" + exit 1 + } + + return +} + +if ($Remove) { # Uninstall the service + # Check if it's necessary + try { + $pss = Get-Service $serviceName -ea stop # Will error-out if not installed + } catch { + Write-Verbose "Already uninstalled" + return + } + Stop-Service $serviceName # Make sure it's stopped + # In the absence of a Remove-Service applet, use sc.exe instead. + Write-Verbose "Removing service $serviceName" + $msg = sc.exe delete $serviceName + if ($LastExitCode) { + Write-Error "Failed to remove the service ${serviceName}: $msg" + exit 1 + } else { + Write-Verbose $msg + } + # Remove the installed files + if (Test-Path $installDir) { + foreach ($ext in ("exe", "pdb", "ps1")) { + $file = "$installDir\$serviceName.$ext" + if (Test-Path $file) { + Write-Verbose "Deleting file $file" + Remove-Item $file + } + } + if (!(@(Get-ChildItem $installDir -ea SilentlyContinue)).Count) { + Write-Verbose "Removing directory $installDir" + Remove-Item $installDir + } + } + # Remove file log + # Log "" # Insert one blank line to separate test sessions logs + return +} diff --git a/src/WinBGP-API.ps1 b/src/WinBGP-API.ps1 new file mode 100644 index 0000000..5ae11f5 --- /dev/null +++ b/src/WinBGP-API.ps1 @@ -0,0 +1,571 @@ +############################################################################### +# # +# Name WinBGP-Api # +# # +# Description WinBGP API engine # +# # +# Notes Service is based on stevelee http listener example # +# (https://www.powershellgallery.com/packages/HttpListener) # +# # +# # +# 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 + +# Based on + +Param ( + $Configuration=$false +) + +$scriptVersion = '1.1.1' + +# Create detailled log for WinBGP-API +# New-EventLog –LogName Application –Source 'WinBGP-API' -ErrorAction SilentlyContinue + +# 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 Path + The path to the log file to which you would like to write. By default the function will + create the path and file if it does not exist. + .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)] + [Alias('LogPath')] + [string]$Path, + + [Parameter(Mandatory=$false)] + [ValidateSet("Error","Warning","Information")] + [string]$Level="Information", + + [Parameter(Mandatory=$false)] + [string]$EventLogName='Application', + + [Parameter(Mandatory=$false)] + [string]$EventLogSource='WinBGP', + + [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 + { + #Create Log file only if Path is defined + if ($Path) { + # If the file already exists and NoClobber was specified, do not write to the log. + if ((Test-Path $Path) -AND $NoClobber) { + Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name." + Return + } + + # If attempting to write to a log file in a folder/path that doesn't exist create the file including the path. + elseif (!(Test-Path $Path)) { + Write-Verbose "Creating $Path." + $NewLogFile = New-Item $Path -Force -ItemType File + } + + else { + # Nothing to see here yet. + } + + # Format Date for our Log File + $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + + # Write log entry to $Path + "$FormattedDate $Level $Message" | Out-File -FilePath $Path -Append + } + # 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 + { + } +} + +# Prometheus function +function New-PrometheusMetricDescriptor( + [Parameter(Mandatory = $true)][String] $Name, + [Parameter(Mandatory = $true)][String] $Type, + [Parameter(Mandatory = $true)][String] $Help, + [string[]] $Labels +) { + # Verification + if ($Name -notmatch "^[a-zA-Z_][a-zA-Z0-9_]*$") { + throw "Prometheus Descriptor Name '$($Name)' not valid" + } + foreach ($Label in $Labels) { + if ($Label -notmatch "^[a-zA-Z_][a-zA-Z0-9_]*$") { + throw "Prometheus Descriptor Label Name '$($Label)' not valid" + } + } + + # return object + return [PSCustomObject]@{ + PSTypeName = 'PrometheusMetricDescriptor' + Name = $Name + Help = $Help -replace "[\r\n]+", " " # Strip out new lines + Type = $Type + Labels = $Labels + } +} + +function New-PrometheusMetric ( + [Parameter(Mandatory = $true)][PSTypeName('PrometheusMetricDescriptor')] $PrometheusMetricDescriptor, + [Parameter(Mandatory = $true)][float] $Value, + [string[]] $Labels +) { + # Verification + if (($PrometheusMetricDescriptor.Labels).Count -ne $Labels.Count) { + throw "Metric labels are not matching the labels specified in the PrometheusMetricDescriptor provided" + } + # return object + return [PSCustomObject]@{ + PSTypeName = 'PrometheusMetric' + Name = $PrometheusMetricDescriptor.Name + PrometheusMetricDescriptor = $PrometheusMetricDescriptor + Value = $Value + Labels = $Labels + } +} + +function Export-PrometheusMetrics ( + [PSTypeName('PrometheusMetric')][object[]] $Metrics +) { + $Lines = [System.Collections.Generic.List[String]]::new() + $LastDescriptor = $null + # Parse all metrics + foreach ($metric in $Metrics) { + if ($metric.PrometheusMetricDescriptor -ne $LastDescriptor) { + # Populate last descriptor + $LastDescriptor = $metric.PrometheusMetricDescriptor + $Lines.Add("# HELP $($LastDescriptor.Name) $($LastDescriptor.Help)") + $Lines.Add("# TYPE $($LastDescriptor.Name) $($LastDescriptor.Type)") + } + + $FinalLabels = [System.Collections.Generic.List[String]]::new() + if (($metric.PrometheusMetricDescriptor.Labels).Count -gt 0) { + for ($i = 0; $i -lt ($metric.PrometheusMetricDescriptor.Labels).Count; $i++) { + $label = $metric.PrometheusMetricDescriptor.Labels[$i] + $value = ($metric.Labels[$i]).Replace("\", "\\").Replace("""", "\""").Replace("`n", "\n") + $FinalLabels.Add("$($label)=`"$($value)`"") + } + $StringLabels = $FinalLabels -join "," + $StringLabels = "{$StringLabels}" + } else { + $StringLabels = "" + } + $Lines.Add([String] $metric.Name + $StringLabels + " " + $metric.Value) + } + return $Lines -join "`n" +} + +# Only processing if there is configuration +if ($Configuration) { + # Creating Prefixes variable + $ListenerPrefixes=@() + # Default Authentication Method/Group + $AuthenticationMethod='Anonymous' + $AuthenticationGroup='Administrators' + # Parsing all URIs + foreach ($item in $Configuration) { + [String] $Uri = $item.Uri + [System.Net.AuthenticationSchemes] $AuthMethod = $item.AuthenticationMethod + if ($AuthMethod -eq 'Negotiate') { + [String] $AuthGroup = $item.AuthenticationGroup + } + + # Splitting Uri + [String]$Protocol = $Uri.Split('://')[0] + [String]$IP = $Uri.Split('://')[3] + [String]$Port = $Uri.Split('://')[4] + + # TO IMPROVE + if ($IP -ne '127.0.0.1') { + $AuthenticationMethod=$AuthMethod + $AuthenticationGroup=$AuthGroup + } + + # Manage certificate + $SSLConfigurationError=$false + if ($Protocol -eq 'https') { + # Populate variable + [String] $CertificateThumbprint = $item.CertificateThumbprint + # Managing cert on port + netsh http delete sslcert ipport="$($IP):$($Port)" | Out-Null + netsh http add sslcert ipport="$($IP):$($Port)" certhash="$CertificateThumbprint" appid='{00112233-4455-6677-8899-AABBCCDDEEFF}' | Out-Null + # Parsing netsh output to checck SSL configuration + $netshOutput=netsh http show sslcert ipport="$($IP):$($Port)" | Where-Object {($_.Split("`r`n")) -like '*IP:port*'} + if ($netshOutput -ne " IP:port : $($IP):$($Port)") { + Write-Log -Message "API failed - SSL configuration error" -Level Error + $SSLConfigurationError=$true + } + } + + # If no SSL configuration error, checking + if ($SSLConfigurationError -ne $true) { + # Checking if listening port is available + if (Get-NetTCPConnection -LocalAddress $IP -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) { + Write-Log -Message "Uri failed - Port '$($Port)' on IP '$($IP)' is already in use" -Level Error + } else { + # Adding / on Uri + $ListenerPrefixes+="$($Uri)/" + } + } + } + + # If there is listener to start + if ($ListenerPrefixes) { + $listener = New-Object System.Net.HttpListener + # Adding listener + foreach ($ListenerPrefixe in $ListenerPrefixes) { + $listener.Prefixes.Add($ListenerPrefixe) + } + # Used previously when only one scheme was used + #$listener.AuthenticationSchemes = $AuthMethod + # Dynamic authentication schemes (TO IMPROVE) + $Listener.AuthenticationSchemeSelectorDelegate = { param($request) + # If local means Uri IP is 127.0.0.1 + if ($request.IsLocal) { + # If local, we don't support authentication for now (TO IMPROVE) + return [System.Net.AuthenticationSchemes]::Anonymous + } else { + # TO IMPROVE + switch ( $AuthenticationMethod) { + 'Negotiate' { $AuthenticationSchemes=[System.Net.AuthenticationSchemes]::IntegratedWindowsAuthentication } + 'Anonymous' { $AuthenticationSchemes=[System.Net.AuthenticationSchemes]::Anonymous } + } + # Set default to anonymous + return $AuthenticationSchemes + } + } + # Starting listerner + $listener.Start() + # Output listeners + foreach ($ListenerPrefixe in $ListenerPrefixes) { + [String]$Protocol = $ListenerPrefixe.Split('://')[0] + [String]$IP = $ListenerPrefixe.Split('://')[3] + [String]$Port = $ListenerPrefixe.Split('://')[4] + Write-Log -Message "API started - Listening on '$($IP):$($Port)' (Protocol: $Protocol)" + } + + while ($listener.IsListening) { + # Default return + $statusCode = [System.Net.HttpStatusCode]::OK + $commandOutput = [string]::Empty + $outputHeader = @{} + $context = $listener.GetContext() + $request = $context.Request + [string]$RequestHost=$request.RemoteEndPoint + $RequestHost=($RequestHost).split(':')[0] + # Manage authentication + $Authenticated=$false + if ($AuthenticationMethod -eq 'Negotiate') { + if ($request.IsAuthenticated) { + $RequestUser=$context.User.Identity.Name + if ($($context.User.IsInRole($AuthenticationGroup))) { + $Authenticated=$true + } else { + $statusCode = [System.Net.HttpStatusCode]::Forbidden + } + } else { + $statusCode = [System.Net.HttpStatusCode]::Unauthorized + } + } elseif ($AuthenticationMethod -eq 'Anonymous') { + $Authenticated=$true + $RequestUser='Anonymous' + } + + # If local, we don't support authentication for now (TO IMPROVE) + if ($request.IsLocal -or $Authenticated) { + # Log every api request + # [string]$FullRequest = $request | Format-List * | Out-String + # Write-Log "API request received: $FullRequest" -EventLogSource 'WinBGP-API' + $FullPath=($request.RawUrl).substring(1) + $Path=$FullPath.Split('?')[0] + switch ($request.HttpMethod) { + 'GET' { + if ($FullPath -eq 'api') { + $commandOutput = ConvertTo-Json -InputObject @{'message'='WinBGP API running'} + $statusCode = [System.Net.HttpStatusCode]::OK + } elseif ($FullPath -like 'api/*') { + $Path=$Path.replace('api/','') + $shortPath=$Path.Split('/')[0] + switch ($shortPath) { + 'config' { + if (($Path -eq 'config') -or ($Path -eq 'config/')) { + $commandOutput = WinBGP -Config | ConvertTo-JSON + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } else { + $SubConfig=$Path.Split('/')[1] + $commandOutput = (WinBGP -Config).$SubConfig + if ($commandOutput) { + $commandOutput = $commandOutput | ConvertTo-JSON + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } else { + $statusCode = [System.Net.HttpStatusCode]::NotFound + } + } + } + 'logs' { + $Last = $request.QueryString.Item("Last") + if (!($Last)) { $Last = 10 } + $commandOutput = WinBGP -Logs -Last $Last | Select-Object Index,TimeGenerated,@{Label='EntryType';Expression={($_.EntryType).ToString()}},Message,RouteName | ConvertTo-JSON + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } + 'peers' { + if (($Path -eq 'peers') -or ($Path -eq 'peers/')) { + $commandOutput = ConvertTo-Json -InputObject @(Get-BgpPeer | Select-Object PeerName,LocalIPAddress,LocalASN,PeerIPAddress,PeerASN,@{Label='ConnectivityStatus';Expression={$_.ConnectivityStatus.ToString()}}) #Using @() as inputobject to always return an array + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } else { + $PeerName=$Path.Split('/')[1] + $commandOutput = Get-BgpPeer | Where-Object {$_.PeerName -eq $PeerName} | Select-Object PeerName,LocalIPAddress,PeerIPAddress,PeerASN,@{Label='ConnectivityStatus';Expression={$_.ConnectivityStatus.ToString()}} + if ($commandOutput) { + $commandOutput = $commandOutput | ConvertTo-JSON + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } else { + $statusCode = [System.Net.HttpStatusCode]::NotFound + } + } + } + 'router' { + $commandOutput = Get-BgpRouter | Select-Object BgpIdentifier,LocalASN,PeerName,PolicyName | ConvertTo-JSON + $statusCode = [System.Net.HttpStatusCode]::OK + } + 'routes' { + if (($Path -eq 'routes') -or ($Path -eq 'routes/')) { + $commandOutput = ConvertTo-Json -InputObject @(WinBGP | Select-Object Name,Network,Status,@{Label='MaintenanceTimestamp';Expression={($_.MaintenanceTimestamp).ToString("yyyy-MM-ddTHH:mm:ss.fffK")}}) #Using @() as inputobject to always return an array + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } else { + $RouteName=$Path.Split('/')[1] + $commandOutput = WinBGP | Where-Object {$_.Name -eq $RouteName} | Select-Object Name,Network,Status,@{Label='MaintenanceTimestamp';Expression={($_.MaintenanceTimestamp).ToString("yyyy-MM-ddTHH:mm:ss.fffK")}} + if ($commandOutput) { + $commandOutput = $commandOutput | ConvertTo-JSON + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } else { + $statusCode = [System.Net.HttpStatusCode]::NotFound + } + } + } + 'statistics' { + $commandOutput=(Invoke-CimMethod -ClassName "PS_BgpStatistics" -Namespace 'ROOT\Microsoft\Windows\RemoteAccess' -MethodName Get -OperationTimeoutSec 5).cmdletoutput | Select-Object PeerName,TcpConnectionEstablished,TcpConnectionClosed,@{Label='OpenMessage';Expression={$_.OpenMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='NotificationMessage';Expression={$_.NotificationMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='KeepAliveMessage';Expression={$_.KeepAliveMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='RouteRefreshMessage';Expression={$_.RouteRefreshMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='UpdateMessage';Expression={$_.UpdateMessage.CimInstanceProperties | Select-Object Name,Value}},@{Label='IPv4Route';Expression={$_.IPv4Route.CimInstanceProperties | Select-Object Name,Value}},@{Label='IPv6Route';Expression={$_.IPv6Route.CimInstanceProperties | Select-Object Name,Value}} | ConvertTo-JSON + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } + 'status' { + [string]$status = WinBGP -Status + $commandOutput = ConvertTo-Json -InputObject @{'service'=$status} + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } + 'version' { + $commandOutput = WinBGP -Version | ConvertTo-JSON + $outputHeader.Add('Content-Type', 'application/json') + $statusCode = [System.Net.HttpStatusCode]::OK + } + Default { + $statusCode = [System.Net.HttpStatusCode]::NotImplemented + } + } + } elseif ($FullPath -eq 'metrics') { + # Define WinBGP Prometheus metrics + $WinBGP_metrics=@() + + # WinBGP peer status + $state_peerDescriptor=New-PrometheusMetricDescriptor -Name winbgp_state_peer -Type gauge -Help 'WinBGP Peers status' -Labels local_asn,local_ip,name,peer_asn,peer_ip,state + $peerStatus=@('connected','connecting','stopped') + # Try/catch to detect if BGP is configured properly + $BgpStatus=$null + try { + $peersCurrentStatus=Get-BgpPeer -ErrorAction SilentlyContinue | Select-Object PeerName,LocalIPAddress,LocalASN,PeerIPAddress,PeerASN,@{Label='ConnectivityStatus';Expression={$_.ConnectivityStatus.ToString()}} + } + catch { + #If BGP Router (Local) is not configured, catch it + $BgpStatus=($_).ToString() + } + if ($BgpStatus -eq 'BGP is not configured.') { + $peersCurrentStatus=$null + } + # Parse all peers and generate metric + foreach ($peerCurrentStatus in $peersCurrentStatus) { + foreach ($status in $peerStatus) { + $WinBGP_metrics+=New-PrometheusMetric -PrometheusMetricDescriptor $state_peerDescriptor -Value $(if ($status -eq $peerCurrentStatus.ConnectivityStatus) { 1 } else { 0 }) -Labels $peerCurrentStatus.LocalASN,$peerCurrentStatus.LocalIPAddress,$peerCurrentStatus.PeerName,$peerCurrentStatus.PeerASN,$peerCurrentStatus.PeerIPAddress,$status + } + } + + # WinBGP route status + $state_routeDescriptor=New-PrometheusMetricDescriptor -Name winbgp_state_route -Type gauge -Help 'WinBGP routes status' -Labels family,maintenance_timestamp,name,network,state + $routeStatus=@('down','maintenance','up','warning') + # Silently continue as WinBGP is generating errors when BGP is not configured (TO REVIEW) + $routesCurrentStatus=(WinBGP -ErrorAction SilentlyContinue | Select-Object Name,Network,Status,@{Label='MaintenanceTimestamp';Expression={($_.MaintenanceTimestamp).ToString("yyyy-MM-ddTHH:mm:ss.fffK")}}) + foreach ($routeCurrentStatus in $routesCurrentStatus) { + foreach ($status in $routeStatus) { + $WinBGP_metrics+=New-PrometheusMetric -PrometheusMetricDescriptor $state_routeDescriptor -Value $(if ($status -eq $routeCurrentStatus.Status) { 1 } else { 0 }) -Labels 'ipv4',$routeCurrentStatus.MaintenanceTimestamp,$routeCurrentStatus.Name,$routeCurrentStatus.Network,$status + } + } + + # Return output + $commandOutput = Export-PrometheusMetrics -Metrics $WinBGP_metrics + + # Add header + $outputHeader.Add('Content-Type', 'text/plain; version=0.0.4; charset=utf-8') + $statusCode = [System.Net.HttpStatusCode]::OK + } else { + $statusCode = [System.Net.HttpStatusCode]::NotImplemented + } + } + 'POST' { + if ($FullPath -like 'api/*') { + $RouteName = $request.QueryString.Item("RouteName") + $Path=$Path.replace('api/','') + Write-Log "API received POST request '$Path' from '$RequestUser' - Source IP: '$RequestHost'" -AdditionalFields $RouteName + switch ($Path) { + 'Reload' { + [string]$ActionOutput=WinBGP -Reload + $commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput} + $outputHeader.Add('Content-Type', 'application/json') + } + 'StartMaintenance' { + [string]$ActionOutput=WinBGP -RouteName "$RouteName" -StartMaintenance + $commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput} + $outputHeader.Add('Content-Type', 'application/json') + } + 'StartRoute' { + [string]$ActionOutput=WinBGP -RouteName "$RouteName" -StartRoute + $commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput} + $outputHeader.Add('Content-Type', 'application/json') + } + 'StopMaintenance' { + [string]$ActionOutput=WinBGP -RouteName "$RouteName" -StopMaintenance + $commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput} + $outputHeader.Add('Content-Type', 'application/json') + } + 'StopRoute' { + [string]$ActionOutput=WinBGP -RouteName "$RouteName" -StopRoute + $commandOutput = ConvertTo-Json -InputObject @{'output'=$ActionOutput} + $outputHeader.Add('Content-Type', 'application/json') + } + Default { + $statusCode = [System.Net.HttpStatusCode]::NotImplemented + } + } + switch ($commandOutput.output) { + 'Success' { $statusCode = [System.Net.HttpStatusCode]::OK } + 'WinBGP not ready' { $statusCode = [System.Net.HttpStatusCode]::InternalServerError } + } + } else { + $statusCode = [System.Net.HttpStatusCode]::NotImplemented + } + } + Default { + $statusCode = [System.Net.HttpStatusCode]::NotImplemented + } + } + } + $response = $context.Response + $response.StatusCode = $statusCode + foreach ($header in $outputHeader.Keys) + { + foreach ($headerValue in $outputHeader.$header) + { + $response.Headers.Add($header, $headerValue) + } + } + $buffer = [System.Text.Encoding]::UTF8.GetBytes($commandOutput) + $response.ContentLength64 = $buffer.Length + $output = $response.OutputStream + $output.Write($buffer,0,$buffer.Length) + $output.Close() + } + $listener.Stop() + } else { + Write-Log -Message "API failed - No Uri listener available" -Level Error + } +} else { + $OutputVersion=@{ + 'Version'=$scriptVersion + } + return $OutputVersion +} diff --git a/src/WinBGP-Engine.ps1 b/src/WinBGP-Engine.ps1 new file mode 100644 index 0000000..b46a3e3 --- /dev/null +++ b/src/WinBGP-Engine.ps1 @@ -0,0 +1,1744 @@ +############################################################################### +# # +# 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.0' + + +# 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" +$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\$serviceDisplayName.ps1' -Service*"}).ProcessId + if ($ProcessID) { + # Get API PID + $ApiPID=(Get-WmiObject win32_process -filter "Name='powershell.exe' AND ParentProcessId=$ProcessID").ProcessId + } + 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 - Stopping $($serviceDisplayName) process" -Level Error + # Forcing stop process so service will know that process is not running + Stop-Process -Name $serviceDisplayName -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 Error + } + $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 + } 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 +} diff --git a/src/WinBGP-HealthCheck.ps1 b/src/WinBGP-HealthCheck.ps1 new file mode 100644 index 0000000..4badf31 --- /dev/null +++ b/src/WinBGP-HealthCheck.ps1 @@ -0,0 +1,522 @@ +############################################################################### +# # +# Name WinBGP-HealthCheck # +# # +# Description WinBGP Routes HealthCheck # +# # +# Notes # +# # +# # +# 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 + +Param ( + $Route=$false +) + +$scriptVersion = '1.2.1' + +#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 Path + The path to the log file to which you would like to write. By default the function will + create the path and file if it does not exist. + .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)] + [Alias('LogPath')] + [string]$Path, + + [Parameter(Mandatory=$false)] + [ValidateSet("Error","Warning","Information")] + [string]$Level="Information", + + [Parameter(Mandatory=$false)] + [string]$EventLogName='Application', + + [Parameter(Mandatory=$false)] + [string]$EventLogSource='WinBGP', + + [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 + { + #Create Log file only if Path is defined + if ($Path) { + # If the file already exists and NoClobber was specified, do not write to the log. + if ((Test-Path $Path) -AND $NoClobber) { + Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name." + Return + } + + # If attempting to write to a log file in a folder/path that doesn't exist create the file including the path. + elseif (!(Test-Path $Path)) { + Write-Verbose "Creating $Path." + $NewLogFile = New-Item $Path -Force -ItemType File + } + + else { + # Nothing to see here yet. + } + + # Format Date for our Log File + $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + + # Write log entry to $Path + "$FormattedDate $Level $Message" | Out-File -FilePath $Path -Append + } + # 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 + { + } +} + +#-----------------------------------------------------------------------------# +# # +# 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) + } +} + +# IPC communication with WinBGP-Engine +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 Send-RouteControl { + param ( + [Parameter(Mandatory=$true)] + [String]$RouteName, # Route Name + [Parameter(Mandatory=$true)] + [String]$Control # Control + ) + $PipeStatus=$null + # Performing Action + try { + # Temporary + $pipeName='Service_WinBGP' + $Message="route $($RouteName) $($Control)" + Send-PipeMessage -PipeName $pipeName -Message $Message + } + catch { + $PipeStatus=($_).ToString() + } + if ($PipeStatus -like "*Pipe hasn't been connected yet*") { + return "WinBGP not ready" + } else { + # TO BE IMPROVED to get status + return "Success" + } +} + + +if ($Route) { + # Waiting for Pipe to be started before starting healtcheck (as healtcheck need the pipe to communicate) + # Temporary + $pipeName='Service_WinBGP' + while([System.IO.Directory]::GetFiles("\\.\\pipe\\") -notcontains "\\.\\pipe\\$($pipeName)") { + # Wait 1 seconds before checking again + Start-Sleep -Seconds 1 + } + Write-Log -Message "HealthCheck Process started" -AdditionalFields @($Route.RouteName) + + # Initialize variables + $rise_counter = 0 + $fall_counter = 0 + + # Start a periodic timer + $timer = new-object System.Timers.Timer + $timer.Interval = ($route.Interval * 1000) # Milliseconds + $timer.AutoReset = $true # Make it fire repeatedly + Register-ObjectEvent $timer -EventName Elapsed -SourceIdentifier 'Timer' + $timer.start() # Must be stopped in the finally block + + # Start a watchdog timer + $watchdog = new-object System.Timers.Timer + $watchdog.Interval = (30 * 1000) # Milliseconds + $watchdog.AutoReset = $true # Make it fire repeatedly + Register-ObjectEvent $watchdog -EventName Elapsed -SourceIdentifier 'Watchdog' + $watchdog.start() # Must be stopped in the finally block + + if ($Route.WithdrawOnDown) { + $pos = ($Route.WithdrawOnDownCheck).IndexOf(":") + $check_method = ($Route.WithdrawOnDownCheck).Substring(0, $pos) + $check_name = ($Route.WithdrawOnDownCheck).Substring($pos+2) + switch($check_method) { + 'service' { + # Service check from Json configuration + $check_expression="if ((Get-Service $check_name).Status -eq 'Running') {return `$true} else {return `$false}" + } + 'process' { + # Process check from Json configuration + $check_expression="if ((Get-Process -ProcessName $check_name -ErrorAction SilentlyContinue).count -ge '1') {return `$true} else {return `$false}" + } + 'tcp' { + # TCP port check from Json configuration + $host_to_check=$check_name.split(":")[0] + $port_to_check=$check_name.split(":")[1] + $check_expression="if ((Test-NetConnection $host_to_check -Port $port_to_check).tcptestsucceeded) {return `$true} else {return `$false}" + } + 'cluster' { + # Cluster resource check from Json configuration + $check_expression="if ((Get-ClusterNode -Name `"$env:COMPUTERNAME`" -ErrorAction SilentlyContinue | Get-ClusterResource -Name `"$check_name`" -ErrorAction SilentlyContinue).State -eq 'Online') {return `$true} else {return `$false}" + } + 'custom' { + # Custom check from Json configuration [Return status should be a Boolean (mandatory)] + $check_expression=$check_name + # Rewrite $check_name for logging + $check_name='check' + } + } + $check_method_name=(Get-Culture).textinfo.totitlecase($check_method.tolower()) + $check_log_output="$check_method_name '$check_name'" + } else { + $check_log_output='WithdrawOnDown not enabled' + # Bypass Rise counter + $rise_counter=$Route.Rise + $rise_counter-- + } + + do { + $timer_event = Wait-Event # Wait for the next incoming event + if ($Route.WithdrawOnDown) { + # Default status is false + [bool]$check_status=$false + # Performing check + $check_status=Invoke-Expression -Command $check_expression + } else { + # Check always true as there is no check to perform + [bool]$check_status=$true + } + # Depending on the timer source + switch ($timer_event.SourceIdentifier) { + 'Timer' { + # If check is OK + if ($check_status) { + # Create status log + if ($Route.WithdrawOnDown) { + $check_status_output="$check_log_output UP" + } else { + $check_status_output=$check_log_output + } + # Increment counter + $rise_counter++ + # Waiting for rise threshold + if ($rise_counter -ge $Route.Rise) { + # Reset counter (only when rise has been reached) + $fall_counter=0 + # Only when threshold is reached (Only once) + if ($rise_counter -eq $Route.Rise) { + # If route already announced + if ((Get-BgpCustomRoute).Network -contains "$($Route.Network)") { + Write-Log -Message "$check_status_output - Route already started" -AdditionalFields @($Route.RouteName) + } else { + if ($Route.WithdrawOnDown) { + Write-Log -Message "$check_status_output - Rise threshold reached" -AdditionalFields @($Route.RouteName) + } + Write-Log -Message "$check_status_output - Trigger route start" -AdditionalFields @($Route.RouteName) + # Call function to start BGP route + $output=Send-RouteControl -RouteName $Route.RouteName -Control 'start' + if ($output -ne 'Success') { + $rise_counter-- + Write-Log -Message "Route start error - Trigger retry" -AdditionalFields @($Route.RouteName) -Level Error + } + } + } + } else { + Write-Log -Message "$check_status_output - Rise attempt: $rise_counter (Threshold: $($Route.Rise))" -AdditionalFields @($Route.RouteName) + } + # If check fail + } else { + # Create status log + if ($Route.WithdrawOnDown) { + $check_status_output="$check_log_output DOWN" + } else { + $check_status_output=$check_log_output + } + # Increment counter + $fall_counter++ + # Waiting for fall threshold + if ($fall_counter -ge $Route.Fall) { + # Reset counter (only when fall has been reached) + $rise_counter=0 + # Only when threshold is reached (Only once) + if ($fall_counter -eq $Route.Fall) { + # If route already unannounced + if ((Get-BgpCustomRoute).Network -notcontains "$($Route.Network)") { + Write-Log -Message "$check_status_output - Route already stopped" -AdditionalFields @($Route.RouteName) + } else { + if ($Route.WithdrawOnDown) { + Write-Log -Message "$check_status_output - Fall threshold reached" -AdditionalFields @($Route.RouteName) + } + Write-Log -Message "$check_status_output - Trigger route stop" -AdditionalFields @($Route.RouteName) + # Call function to stop BGP route + $output=Send-RouteControl -RouteName $Route.RouteName -Control 'stop' + if ($output -ne 'Success') { + $fall_counter-- + Write-Log -Message "Route stop error - Trigger retry" -AdditionalFields @($Route.RouteName) -Level Error + } + } + } + } else { + Write-Log -Message "$check_status_output - Fall attempt: $fall_counter (Threshold: $($Route.Fall))" -AdditionalFields @($Route.RouteName) + } + } + } + 'Watchdog' { + # If check is OK + if ($check_status) { + # Waiting for rise threshold + if ($rise_counter -gt $Route.Rise) { + # Announce the route from Json configuration if there is no route + if ((Get-BgpCustomRoute).Network -notcontains "$($Route.Network)") + { + Write-Log -Message "$check_status_output but route not started - Trigger route start (Watchdog)" -AdditionalFields @($Route.RouteName) -Level Warning + # Call function to start BGP route + $output=Send-RouteControl -RouteName $Route.RouteName -Control 'start' + if ($output -ne 'Success') { + Write-Log -Message "Route start error" -AdditionalFields @($Route.RouteName) -Level Error + } + } else { + # Checking IP is mounted properly + if($route.DynamicIpSetup) { + if (!(Get-NetIPAddress -IPAddress "$($route.Network.split('/')[0])")) { + Write-Log "Route announced but IP Address not mounted (Watchdog)" -AdditionalFields @($route.RouteName) -Level Warning + Add-IP $route + } + } + } + } + # If check fail + } else { + # Waiting for fall threshold + if ($fall_counter -gt $Route.Fall) { + # Stop Announce the route from Json configuration + if ((Get-BgpCustomRoute).Network -contains "$($Route.Network)") + { + Write-Log -Message "$check_status_output but route not stopped - Trigger route stop (Watchdog)" -AdditionalFields @($Route.RouteName) + # Call function to remove BGP route + $output=Send-RouteControl -RouteName $Route.RouteName -Control 'stop' + if ($output -ne 'Success') { + Write-Log -Message "Route stop error" -AdditionalFields @($Route.RouteName) -Level Error + } + } else { + # Checking IP is mounted properly + if($route.DynamicIpSetup) { + if (Get-NetIPAddress -IPAddress "$($route.Network.split('/')[0])") { + Write-Log "Route not announced but IP Address still mounted (Watchdog)" -AdditionalFields @($route.RouteName) -Level Warning + Remove-IP $route + } + } + } + } + } + } + } + $timer_event | Remove-Event # Flush the event from the queue + } while ($message -ne "exit") + + # Stopping timers + $timer.stop() + $watchdog.stop() +} else { + $OutputVersion=@{ + 'Version'=$scriptVersion + } + return $OutputVersion +} diff --git a/src/WinBGP.ps1 b/src/WinBGP.ps1 new file mode 100644 index 0000000..c62643c --- /dev/null +++ b/src/WinBGP.ps1 @@ -0,0 +1,733 @@ +############################################################################### +# # +# Name WinBGP-CLI # +# # +# Description WinBGP CLI to manage WinBGP engine # +# # +# Notes Pipe control 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 + +<# + .SYNOPSIS + WinBGP CLI local management. + + .DESCRIPTION + This script manage locally WinBGP. + + .PARAMETER Start + Start the service. + + .PARAMETER Stop + Stop the service. + + .PARAMETER Restart + Stop then restart the service. + + .PARAMETER Status + Get the current service status: Not installed / Stopped / Running + + .PARAMETER Control + Send a control message to the service thread. + + .PARAMETER Version + Display this script version and exit. + + .EXAMPLE + # Setup the service and run it for the first time + C:\PS>.\PSService.ps1 -Status + Not installed + C:\PS>.\PSService.ps1 -Setup + C:\PS># At this stage, a copy of PSService.ps1 is present in the path + C:\PS>PSService -Status + Stopped + C:\PS>PSService -Start + C:\PS>PSService -Status + Running + C:\PS># Load the log file in Notepad.exe for review + C:\PS>notepad ${ENV:windir}\Logs\PSService.log + + .EXAMPLE + # Stop the service and uninstall it. + C:\PS>PSService -Stop + C:\PS>PSService -Status + Stopped + C:\PS>PSService -Remove + C:\PS># At this stage, no copy of PSService.ps1 is present in the path anymore + C:\PS>.\PSService.ps1 -Status + Not installed + + .EXAMPLE + # Configure the service to run as a different user + C:\PS>$cred = Get-Credential -UserName LAB\Assistant + C:\PS>.\PSService -Setup -Credential $cred + + .EXAMPLE + # Send a control message to the service, and verify that it received it. + C:\PS>PSService -Control Hello + C:\PS>Notepad C:\Windows\Logs\PSService.log + # The last lines should contain a trace of the reception of this Hello message +#> +[CmdletBinding(DefaultParameterSetName='BGPStatus')] +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='Control', Mandatory=$true)] + [String]$Control = $null, # Control message to send to the service + + [Parameter(ParameterSetName='Reload', Mandatory=$false)] + [Switch]$Reload = $($PSCmdlet.ParameterSetName -eq 'reload'), # Reload configuration + + [Parameter(ParameterSetName='RouteName', Mandatory=$true)] + [ArgumentCompleter( { + param ( $CommandName, + $ParameterName, + $WordToComplete, + $CommandAst, + $FakeBoundParameters ) + # Dynamically generate routes array + # TO BE IMPROVED - Set to static temporary + $configuration=Get-Content 'C:\Program Files\WinBGP\winbgp.json' | ConvertFrom-Json + [Array] $routes = ($configuration.routes).RouteName + return $routes + })] + [String]$RouteName = $null, # Select route to control + + [Parameter(ParameterSetName='RouteName', Mandatory=$false)] + [Switch]$StartMaintenance, # Control message to send to the service + + [Parameter(ParameterSetName='RouteName', Mandatory=$false)] + [Switch]$StopMaintenance, # Control message to send to the service + + [Parameter(ParameterSetName='RouteName', Mandatory=$false)] + [Switch]$StartRoute, # Control message to send to the service + + [Parameter(ParameterSetName='RouteName', Mandatory=$false)] + [Switch]$StopRoute, # Control message to send to the service + + [Parameter(ParameterSetName='BGPStatus', Mandatory=$false)] + [Switch]$BGPStatus = $($PSCmdlet.ParameterSetName -eq 'BGPStatus'), # Get the current service status + + [Parameter(ParameterSetName='Config', Mandatory=$false)] + [Switch]$Config = $($PSCmdlet.ParameterSetName -eq 'Config'), # Get the current configuration + + [Parameter(ParameterSetName='Logs', Mandatory=$false)] + [Switch]$Logs = $($PSCmdlet.ParameterSetName -eq 'Logs'), # Get the last logs + + [Parameter(ParameterSetName='Logs', Mandatory=$false)] + [Int]$Last = 20, # Define the last logs number + + [Parameter(ParameterSetName='RestartAPI', Mandatory=$false)] + [Switch]$RestartAPI, # RestartAPI + + [Parameter(ParameterSetName='Version', Mandatory=$true)] + [Switch]$Version # Get this script version +) + +# Don't forget to increment version when updating engine +$scriptVersion = '1.0.1' + +# This script name, with various levels of details +# Ex: PSService +$scriptFullName = 'C:\Program Files\WinBGP\WinBGP.ps1' # Ex: C:\Temp\PSService.ps1 + +# Global settings +$serviceName = "WinBGP" # A one-word name used for net start commands +$serviceDisplayName = "WinBGP" +$pipeName = "Service_$serviceName" # Named pipe name. Used for sending messages to the service task +$installDir = "${ENV:ProgramW6432}\$serviceDisplayName" # Where to install the service files +$configfile = "$serviceDisplayName.json" +$configdir = "$installDir\$configfile" +$FunctionCliXml="$installDir\$serviceDisplayName.xml" # Used to stored Maintenance variable +$logName = "Application" # Event Log name (Unrelated to the logFile!) + +# 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 + { + } +} + +#-----------------------------------------------------------------------------# +# # +# 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 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 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" + +# Workaround for PowerShell v2 bug: $PSCmdlet Not yet defined in Param() block +$Status = ($PSCmdlet.ParameterSetName -eq 'Status') + +if ($Start) { # The user tells us to start the service + Write-Verbose "Starting service $serviceName" + Write-Log -Message "Starting service $serviceName" + Start-Service $serviceName # Ask Service Control Manager to start it + return +} + +if ($Stop) { # The user tells us to stop the service + Write-Verbose "Stopping service $serviceName" + Write-Log -Message "Stopping service $serviceName" + Stop-Service $serviceName # Ask Service Control Manager to stop it + return +} + +if ($Restart) { # Restart the service + & $scriptFullName -Stop + & $scriptFullName -Start + 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 ($Control) { # Send a control message to the service + Send-PipeMessage $pipeName $control +} + +#Reload control +if ($Reload) { + $control='reload' + Send-PipeMessage $pipeName $control + + # If Json is valid, reloading + if (Test-ConfigurationFile) { + return 'Success' + } else { + return "Configuration file '$($configdir)' is not valid" + } +} + +# Restart API +if ($RestartAPI) { + $control='restart api' + Send-PipeMessage $pipeName $control + # Output message to be improved + return 'Success' +} + +# Start/stop control or Maintenance control +if ($StartRoute -or $StopRoute -or $StartMaintenance -or $StopMaintenance) { + # Logging + Write-Log "Operation for route '$RouteName' triggered by '$currentUserName'" + # Read configuration + $configuration = Get-Content -Path $configdir | ConvertFrom-Json + $routeCheck=$null + $routeCheck=$configuration.routes | Where-Object {$_.RouteName -eq $RouteName} + # Start/stop control + if ($StartRoute -or $StopRoute) { + # START + if ($StartRoute) { + $control="route $RouteName start" + } + # STOP + if ($StopRoute) { + $control="route $RouteName stop" + } + } + # Maintenance control + if ($StartMaintenance -or $StopMaintenance) { + # START + if ($StartMaintenance) { + $control="maintenance $RouteName start" + } + # STOP + if ($StopMaintenance) { + $control="maintenance $RouteName stop" + } + } + if($routeCheck) { + $PipeStatus=$null + # Performing Action + try { + Send-PipeMessage $pipeName $control + } + catch { + $PipeStatus=($_).ToString() + } + if ($PipeStatus -like "*Pipe hasn't been connected yet*") { + return "WinBGP not ready" + } else { + # TO BE IMPROVED to get status + return "Success" + } + } else { + # Logging + Write-Log "Received control message: $control" + Write-Log "Control return: Route '$RouteName' not found" -Level Warning + return "Route '$RouteName' not found" + } +} + +# Get the current BGP status +if ($BGPStatus) { + # Read configuration + $configuration = Get-Content -Path $configdir | ConvertFrom-Json + # Read maintenance + #If there is a maintenance, import it + if(Test-Path -Path $FunctionCliXml) { + #Import variable + $maintenance=Import-CliXml -Path $FunctionCliXml + } else { + #Otherwise, initialize variable + $maintenance = @{} + } + + # Read BGP routes and policy (To optimize query) + $BGPRoutes=$null + $BGPPolicies=$null + try { + # Use CIM query to improve performance + $BGPRoutes=(Invoke-CimMethod -ClassName "PS_BgpCustomRoute" -Namespace 'ROOT\Microsoft\Windows\RemoteAccess' -MethodName Get).cmdletoutput.Network + $BGPPolicies=(Invoke-CimMethod -ClassName "PS_BgpRoutingPolicy" -Namespace 'ROOT\Microsoft\Windows\RemoteAccess' -MethodName Get).cmdletoutput.PolicyName + } + catch { + } + + # Read IP Addresses (To optimize query) + $IPAddresses=(Get-NetIPAddress -AddressFamily IPv4).IPAddress + + #Parse all routes + $Routes=@() + ForEach ($route in $configuration.routes) { + $RouteStatus=$null + $RouteStatusDetailled=$null + # Check if route is in maintenance mode + if ($maintenance.($route.RouteName)) { + $RouteStatus='maintenance' + # Check if route is up (Only if BGP service is configured) + } else { + if ($BGPRoutes -contains "$($route.Network)") { + # Check route policy + if ($BGPPolicies -contains "$($route.RouteName)") { + # Check IP + if ($route.DynamicIpSetup) { + if ($IPAddresses -contains "$($route.Network.split('/')[0])") { + $RouteStatus='up' + } else { + $RouteStatus='warning' + $RouteStatusDetailled='IP Address not mounted' + } + } else { + $RouteStatus='up' + } + } else { + $RouteStatus='warning' + $RouteStatusDetailled='No routing policy defined' + } + # Route down + } else { + # Check IP + if ($route.DynamicIpSetup) { + if ($IPAddresses -contains "$($route.Network.split('/')[0])") { + $RouteStatus='warning' + $RouteStatusDetailled='IP Address still mounted' + } else { + $RouteStatus='down' + } + } else { + $RouteStatus='down' + } + } + } + $RouteProperties=[PSCustomObject]@{ + Name = $route.RouteName; + Network = $route.Network; + Status = $RouteStatus; + MaintenanceTimestamp = $maintenance.($route.RouteName); + RouteStatusDetailled = $RouteStatusDetailled; + } + # Add route to array + $Routes += $RouteProperties + } + # Select default properties to display + $defaultDisplaySet = 'Name','Network','Status','MaintenanceTimestamp' + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet) + $Routes | Add-Member MemberSet PSStandardMembers $PSStandardMembers + + return $Routes +} + +if ($Config) { + # If Json is valid, reloading + if (Test-ConfigurationFile) { + $configuration = Get-Content -Path $configdir | ConvertFrom-Json + return $configuration + } else { + return "Configuration file '$($configdir)' is not valid" + } +} + +if ($Logs) { + $EventLogs=Get-EventLog -LogName Application -Source WinBGP -Newest $Last | Select-Object Index,TimeGenerated,EntryType,Message,ReplacementStrings + $DisplayLogs=@() + foreach ($log in $EventLogs) { + if($log.ReplacementStrings -gt 1) { + $log | Add-Member -MemberType NoteProperty -Name 'RouteName' -Value $log.ReplacementStrings[1] + } + $log.PsObject.Members.Remove('ReplacementStrings') + $DisplayLogs+=$log + } + + # Select default properties to display + $defaultDisplaySet = 'TimeGenerated','EntryType','Message','RouteName' + $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultDisplaySet) + $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet) + $DisplayLogs | Add-Member MemberSet PSStandardMembers $PSStandardMembers + + return $DisplayLogs +} diff --git a/src/winbgp.json.example b/src/winbgp.json.example new file mode 100644 index 0000000..7388897 --- /dev/null +++ b/src/winbgp.json.example @@ -0,0 +1,61 @@ +{ + "global": { + "Interval": 5, + "Timeout": 1, + "Rise": 3, + "Fall": 2, + "Metric": 100, + "Api": true + }, + "api": [ + { + "Uri": "http://127.0.0.1:8888", + "AuthenticationMethod": "Anonymous" + } + ], + "router": { + "BgpIdentifier": "YOUR_IP", + "LocalASN": "YOUR_ASN" + }, + "peers": [ + { + "PeerName": "Peer1", + "LocalIP": "YOUR_IP", + "PeerIP": "Peer1_IP", + "LocalASN": "YOUR_ASN", + "PeerASN": "Peer1_ASN" + }, + { + "PeerName": "Peer2", + "LocalIP": "Peer2_IP", + "PeerIP": "10.136.21.75", + "LocalASN": "YOUR_ASN", + "PeerASN": "Peer1_ASN" + } + ], + "routes": [ + { + "RouteName": "mywinbgpservice.contoso.com", + "Network": "mywinbgpservice_IP/32", + "Interface": "Ethernet", + "DynamicIpSetup": true, + "WithdrawOnDown": true, + "WithdrawOnDownCheck": "service: W32Time", + "NextHop": "YOUR_IP", + "Community": [ + "BGP_COMMUNITY" + ] + }, + { + "RouteName": "mysecondwinbgpservice.contoso.com", + "Network": "mysecondwinbgpservice_IP/32", + "Interface": "Ethernet", + "DynamicIpSetup": false, + "WithdrawOnDown": false, + "NextHop": "YOUR_IP", + "Community": [ + "BGP_COMMUNITY" + ] + } + ] +}