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"
+ ]
+ }
+ ]
+}