Initial public release

This commit is contained in:
WebalexEU
2024-12-20 16:31:40 +01:00
commit 2becc990c9
12 changed files with 4943 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# Service builder executable outputs
service/*.exe
# Installer outputs
builder/*.msi
builder/*.wixpdb

21
LICENCE Normal file
View File

@@ -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.

31
README Normal file
View File

@@ -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/)

28
builder/build.ps1 Normal file
View File

@@ -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

60
builder/files.wxs Normal file
View File

@@ -0,0 +1,60 @@
<!--
~ Copyright 2024 The Prometheus Authors
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<Wix xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util"
xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<DirectoryRef Id="APPLICATIONFOLDER">
<Component Transitive="yes">
<File Id="winbgp_service" Source="..\service\WinBGP-Service.exe" KeyPath="yes" Vital="yes" Checksum="yes" />
<ServiceInstall
Id="InstallExporterService"
Name="WinBGP"
DisplayName="WinBGP Engine"
Description="The BGP swiss army knife of networking on Windows"
ErrorControl="normal"
Start="auto"
Type="ownProcess"
Vital="yes"
>
<util:ServiceConfig
ResetPeriodInDays="1"
FirstFailureActionType="restart"
SecondFailureActionType="restart"
ThirdFailureActionType="restart"
RestartServiceDelayInSeconds="60"
/>
<ServiceDependency Id="RemoteAccess" />
</ServiceInstall>
<ServiceControl
Id="MyServiceControl"
Name="WinBGP"
Remove="both"
Stop="both"
Wait="yes" />
</Component>
</DirectoryRef>
<ComponentGroup Id="ApplicationFiles">
<ComponentRef Id="winbgp_service" />
<ComponentRef Id="winbgp_engine" />
<ComponentRef Id="winbgp_healthcheck" />
<ComponentRef Id="winbgp_api" />
<ComponentRef Id="winbgp_config" />
<ComponentRef Id="winbgp_config_example" />
<ComponentRef Id="winbgp_cli" />
</ComponentGroup>
</Fragment>
</Wix>

217
builder/main.wxs Normal file
View File

@@ -0,0 +1,217 @@
<?xml version="1.0"?>
<?ifndef Version?>
<?error Version must be defined?>
<?endif?>
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?endif?>
<!--
~ Copyright 2024 The Prometheus Authors
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<Wix xmlns:fw="http://wixtoolset.org/schemas/v4/wxs/firewall"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui"
xmlns="http://wixtoolset.org/schemas/v4/wxs"
>
<Package UpgradeCode="0889d60f-67d5-4bf5-8918-2d5e810f888d" Name="$(var.ProductName)" Version="$(var.Version)"
Manufacturer="Webalex System" Language="1033" Scope="perMachine">
<SummaryInformation Manufacturer="Webalex System" Description="$(var.ProductName) $(var.Version) installer" />
<CustomAction Id="GenerateConfig"
Execute="deferred"
Impersonate="no"
Directory="TARGETDIR"
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command &quot;
Write-Host 'Generating initial configuration file (winbgp.json)'; &#xA;
$config=Get-Content -Path '[APPLICATIONFOLDER]\winbgp.json' | ConvertFrom-Json; &#xA;
$localIP=(Get-WmiObject -Class Win32_NetworkAdapterConfiguration | Where-Object {$_.IPEnabled -eq $true}).IPAddress[0]; &#xA;
$config.router=@([PSCustomObject]@{BgpIdentifier=$localIP;LocalASN='64512'}); &#xA;
$config.peers=@(); &#xA;
$config.routes=@(); &#xA;
$config=$config | ConvertTo-Json; &#xA;
Set-Content -Value $config -Path '[APPLICATIONFOLDER]\winbgp.json' &#xA;
&quot;"
Return="ignore"
/>
<CustomAction Id="EnableWindowsFeature"
Execute="deferred"
Impersonate="no"
Directory="TARGETDIR"
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command &quot;if ((Get-WindowsFeature -Name 'Routing').InstallState -ne 'Installed') { Write-Host 'Adding routing feature (WinBGP prerequisite)'; Add-WindowsFeature -Name 'Routing' -IncludeAllSubFeature }&quot;"
Return="check"
/>
<CustomAction Id="EnableWindowsFeatureRSAT"
Execute="deferred"
Impersonate="no"
Directory="TARGETDIR"
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command &quot;if ((Get-WindowsFeature -Name 'RSAT-RemoteAccess-PowerShell').InstallState -ne 'Installed') { Write-Host 'Adding RSAT routing feature (WinBGP prerequisite)'; Add-WindowsFeature -Name 'RSAT-RemoteAccess-PowerShell' }&quot;"
Return="check"
/>
<!-- ExeCommand="dism.exe /online /enable-feature /featurename:RasRoutingProtocols /featurename:RemoteAccessPowerShell /all" -->
<CustomAction Id="EnableRouting"
Execute="deferred"
Impersonate="no"
Directory="TARGETDIR"
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command &quot;if ((Get-RemoteAccess).RoutingStatus -ne 'Installed') { Write-Host 'Enabling routing (WinBGP prerequisite)'; Install-RemoteAccess -VpnType RoutingOnly }&quot;"
Return="check"
/>
<CustomAction Id="RemoveConfig"
Execute="deferred"
Impersonate="no"
Directory="TARGETDIR"
ExeCommand="powershell.exe -ExecutionPolicy Bypass -Command &quot;
Write-Host 'Removing configuration file (winbgp.json)'; &#xA;
Remove-Item -Path '[APPLICATIONFOLDER]\winbgp.json' &#xA;
&quot;"
Return="ignore"
/>
<InstallExecuteSequence>
<Custom Action="GenerateConfig" After="InstallFiles" Condition="NOT Installed AND NOT WIX_UPGRADE_DETECTED AND NOT WIX_DOWNGRADE_DETECTED" />
<Custom Action="EnableWindowsFeature" After="InstallFiles" Condition="&amp;Features=3 AND NOT Installed" />
<Custom Action="EnableWindowsFeatureRSAT" After="EnableWindowsFeature" Condition="&amp;Features=3 AND NOT Installed" />
<Custom Action="EnableRouting" After="EnableWindowsFeatureRSAT" Condition="&amp;Features=3 AND NOT Installed" /> <!-- TO CHECK to use Condition="MAINTENANCE" for upgrade -->
<ScheduleReboot After="InstallFinalize" Condition="&amp;Features=3 AND NOT Installed" /> <!-- TO CHECK <Condition>REBOOT_REQUIRED</Condition> -->
<Custom Action='RemoveConfig' Before='RemoveFiles' Condition='Installed AND (REMOVE="ALL") AND NOT (WIX_UPGRADE_DETECTED OR UPGRADINGPRODUCTCODE)' />
</InstallExecuteSequence>
<Media Id="1" Cabinet="winbgp.cab" EmbedCab="yes" />
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." AllowSameVersionUpgrades="yes" />
<Property Id="ARPHELPLINK" Value="https://github.com/webalexeu/winbgp/issues" />
<Property Id="ARPSIZE" Value="9000" />
<Property Id="ARPURLINFOABOUT" Value="https://github.com/pwebalexeu/winbgp" />
<!--<Property Id="ARPNOMODIFY" Value="0" />-->
<!--<Property Id="ARPNOREPAIR" Value="1" />-->
<Property Id="START_MENU_FOLDER" Value="0" />
<Property Id="NOSTART" Value="0" />
<Feature
Id="DefaultFeature"
Level="1"
Title="$(var.ProductName) $(var.Version)"
Description="The binary and configuration files for $(var.ProductName)"
Display="expand"
ConfigurableDirectory="APPLICATIONFOLDER"
AllowAdvertise="no"
InstallDefault="local"
AllowAbsent="no"
>
<ComponentGroupRef Id="ApplicationFiles" />
<ComponentGroupRef Id="CG_FirewallException" />
<ComponentGroupRef Id="PATH" />
<!-- Subfeature to install required windows features -->
<Feature
Id="Features"
Level="1"
Title="Routing features"
Description="Enable required windows features for routing capabilities"
Display="expand"
AllowAdvertise="no"
InstallDefault="local"
AllowAbsent="yes">
</Feature>
</Feature>
<UI Id="FeatureTree">
<ui:WixUI Id="WixUI_FeatureTree" />
<UIRef Id="WixUI_ErrorProgressText" />
<!-- skip the license agreement dialog; higher Order takes priority (weird) -->
<Publish
Condition="NOT Installed"
Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="CustomizeDlg"
Order="10"/>
<Publish
Condition="NOT Installed"
Dialog="CustomizeDlg"
Control="Back"
Event="NewDialog"
Value="WelcomeDlg"
Order="10"/>
<!--CustomPropertyDlg-->
</UI>
<!-- InstallLocation key -->
<CustomAction Id="SetInstallLocation" Property="ARPINSTALLLOCATION" Value="[APPLICATIONFOLDER]" />
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="APPLICATIONFOLDER" Name="WinBGP">
<Component Id="winbgp_engine" Guid="a7b5748a-16c9-4594-85ad-de032fb3f39c">
<File Id="winbgp_engine" Source="..\src\WinBGP-Engine.ps1" KeyPath="yes" Vital="yes" Checksum="yes" />
</Component>
<Component Id="winbgp_healthcheck" Guid="8a356315-8307-4508-b19d-00eb05e59428">
<File Id="winbgp_healthcheck" Source="..\src\WinBGP-HealthCheck.ps1" KeyPath="yes" Vital="yes" Checksum="yes" />
</Component>
<Component Id="winbgp_api" Guid="77de0ab1-0e53-4967-9469-142f49b397a4">
<File Id="winbgp_api" Source="..\src\WinBGP-API.ps1" KeyPath="yes" Vital="yes" Checksum="yes" />
</Component>
<Component Id="winbgp_config" Guid="03063c39-1063-4126-b974-5cad888d5804" NeverOverwrite="yes" Permanent="yes" >
<File Id="winbgp_config" Name="winbgp.json" Source="..\src\winbgp.json.example" KeyPath="yes" Checksum="yes" />
</Component>
<Component Id="winbgp_config_example" Guid="b3ee9ef1-0d91-43a4-a5a4-a9074a579fb3" >
<File Id="winbgp_config_example" Source="..\src\winbgp.json.example" KeyPath="yes" Checksum="yes" />
</Component>
<Component Id="winbgp_cli" Guid="b74d4be4-3de4-4006-a271-b56d078d2bcc">
<File Id="winbgp_cli" Source="..\src\WinBGP.ps1" KeyPath="yes" Vital="yes" Checksum="yes" />
</Component>
</Directory>
</StandardDirectory>
<ComponentGroup Id="CG_FirewallException">
<Component Directory="APPLICATIONFOLDER" Id="C_FirewallException" Guid="9f522655-ac0e-42d2-a512-a7b19ebec7f7">
<fw:FirewallException
Id="APIEndpoint"
Name="$(var.ProductName) - API (TCP-In)"
Description="Inbound rule for $(var.ProductName) API HTTP endpoint. [TCP]"
Program="[#winbgp.exe]"
Port="8888"
Protocol="tcp">
</fw:FirewallException>
</Component>
</ComponentGroup>
<ComponentGroup Id="PATH">
<Component Directory="APPLICATIONFOLDER" Id="AddToPath" Guid="bd53afb3-eeb6-4eb8-979e-42a6fee3b84b" KeyPath="yes">
<!-- Modify system PATH variable -->
<Environment Id="AddToSystemPathEnv"
Action="set"
Name="Path"
Value="[APPLICATIONFOLDER]"
Permanent="no"
Part="last"
System="yes"
/>
</Component>
</ComponentGroup>
</Package>
</Wix>

949
service/WinBGP-Service.ps1 Normal file
View File

@@ -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
}

571
src/WinBGP-API.ps1 Normal file
View File

@@ -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
}

1744
src/WinBGP-Engine.ps1 Normal file

File diff suppressed because it is too large Load Diff

522
src/WinBGP-HealthCheck.ps1 Normal file
View File

@@ -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
}

733
src/WinBGP.ps1 Normal file
View File

@@ -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
}

61
src/winbgp.json.example Normal file
View File

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