SysLog in PowerShell
I bumped into a requirement to run a SysLog relay on one of my Windows 2008 R2 systems. After poking around on Google, and after getting a bit bored with the third-party offerings, I threw together a simple server of my own.
There is plenty of room for improvement here, but it works (for me at least) as it stands.
Written for and tested under PowerShell 2.0. The script could probably use some error handling.
# SysLog.ps1
#
# A basic SysLog server. Behaviour should be fairly consistent with
# RFC 3164 (http://www.ietf.org/rfc/rfc3164.txt).
# Network Configuration
$SysLogPort = 514 # Default SysLog Port
$Buffer = New-Object Byte[] 1024 # Maximum SysLog message size
# Server Configuration
$EnableMessageValidation = $True # Enable check of the PRI and Header
$EnableRelay = $True # Enable relay to $RelayTargetIP
$EnableLocalLogging = $True # Enable local logging of received messages
$EnableConsoleLogging = $False # Enable logging to the console
$EnableHostNameLookup = $True # Lookup hostname for connecting IP
$EnableHostNamesOnly = $True # Uses Host Name only instead of FQDNs
$RelayTargetIP = "10.0.0.1" # Must be an IP Address
$LogFolder = "C:\SysLog\LogFiles" # Path must exist
# Global variables used to store day and date-stamp for log roll-over
$Day = (Get-Date).Day
$DateStamp = (Get-Date).ToString("yyyy.MM.dd")
# Relay Initialisation
If ($EnableRelay)
{
$RelayTarget = [Net.IPAddress]::Parse($RelayTargetIP)
$RelayTargetEndPoint = New-Object Net.IPEndPoint($RelayTarget, $SysLogPort)
}
# A launcher for the process
#
# Caller: Manual / Script
Function Start-SysLog
{
$Socket = CreateSocket
StartReceive $Socket
}
# Create and bind to the socket
#
# Caller: Start-SysLog
Function CreateSocket
{
$Socket = New-Object Net.Sockets.Socket(
[Net.Sockets.AddressFamily]::Internetwork,
[Net.Sockets.SocketType]::Dgram,
[Net.Sockets.ProtocolType]::Udp)
$ServerIPEndPoint = New-Object Net.IPEndPoint(
[Net.IPAddress]::Any,
$SysLogPort)
$Socket.Bind($ServerIPEndPoint)
Return $Socket
}
# Recieve a single message
#
# Caller: Start-SysLog
Function StartReceive([Net.Sockets.Socket]$Socket)
{
# Placeholder to store source of incoming packet
$SenderIPEndPoint = New-Object Net.IPEndPoint([Net.IPAddress]::Any, 0)
$SenderEndPoint = [Net.EndPoint]$SenderIPEndPoint
$ServerRunning = $True
While ($ServerRunning -eq $True)
{
$BytesReceived = $Socket.ReceiveFrom($Buffer, [Ref]$SenderEndPoint)
$Message = $Buffer[0..$($BytesReceived - 1)]
$Message = ValidateMessage $Message $SenderEndPoint.Address.IPAddressToString
If ($EnableRelay)
{
RelayMessage $Socket $Message
}
}
}
# Relay the message to an upstream SysLog server. Either basic forwarding,
# or full validation.
#
# Caller: StartReceive
Function RelayMessage([Net.Sockets.Socket]$Socket, [Byte[]]$Message)
{
[Void]$Socket.SendTo($Message, $RelayTargetEndPoint)
}
# Check the validity of the message (if option is enabled). Adjust message
# according to recommendations in RFC 3164.
#
# Caller: StartReceive
Function ValidateMessage([Byte[]]$Message, [String]$HostName)
{
If ($EnableMessageValidation)
{
$MessageString = [Text.Encoding]::ASCII.GetString($Message)
If (IsValidPRI($MessageString))
{
If (!(IsValidDateTime($MessageString)))
{
$PRI = [Int]($MessageString -Replace "<|>.*")
$HostName = GetHostName $HostName
$MessageString = "<$PRI>$(NewDateTimeString) $HostName $MessageString"
$Message = EncodeMessage $MessageString
}
}
Else
{
$HostName = GetHostName $HostName
$MessageString = "<13>$(NewDateTimeString) $HostName $MessageString"
$Message = EncodeMessage $MessageString
}
}
If ($EnableLocalLogging -Or $EnableConsoleLogging)
{
If ($MessageString -eq $Null)
{
$MessageString = [Text.Encoding]::ASCII.GetString($Message)
}
If ($EnableLocalLogging) { WriteToLog $MessageString $HostName }
If ($EnableConsoleLogging) { Write-Host $MessageString }
}
Return $Message
}
# Validate the PRI (Priority Field - Facility and Severity)
# No parsing is performed. No network prioritisation is implemented
#
# Caller: ValidateMessage
Function IsValidPRI([String]$MessageString)
{
If ($MessageString.SubString(0, 1) -ne "<")
{
Return $False
}
If (!$MessageString.SubString(2, 3).Contains(">"))
{
Return $False
}
$PRI = [Int]($MessageString -Replace "<|>.*")
# PRI = (Facility * 8) + Severity. Maximum and minimum values from RFC 3164
If ($PRI -lt 1 -Or $PRI -gt 191)
{
Return $False
}
Return $True
}
# Validate the TimeStamp formatting
#
# Caller: ValidateMessage
Function IsValidDateTime([String]$MessageString)
{
$IsValid = $False
If ($MessageString -Match "(?<=\>)\w{3}\s\s?\d{1,2}\s(\d\d:){2}\d\d(?=\s)")
{
$Date = New-Object DateTime
ForEach ($Format in @("MMM d hh:mm:ss", "MMM dd hh:mm:ss"))
{
$Date = New-Object DateTime
$IsValid = [DateTime]::TryParseExact(
$Matches[0],
$Format,
[Globalization.CultureInfo]::InvariantCulture,
[Globalization.DateTimeStyles]::AssumeUniversal,
[Ref]$Date)
If ($IsValid) { Return $True }
}
}
Return $False
}
# Create a new DateTime String
#
# Caller: ValidateMessage
Function NewDateTimeString
{
$Date = (Get-Date).ToUniversalTime()
If ($Date.Day -lt 10)
{
Return $Date.ToString("MMM d HH:mm:ss")
}
Return $Date.ToString("MMM dd HH:mm:ss")
}
# Attempt to lookup the HostName if an IP value was passed.
# [Net.Dns]::GetHostEntry fails to return if a Forward Lookup record
# does not exist. NsLookup as a simple alternative.
#
# Caller: ValidateMessage
Function GetHostName([String]$HostName)
{
If (!$EnableHostNameLookup) { Return $HostName }
If ([Net.IPAddress]::TryParse($HostName, [Ref]$Null))
{
$Temp = (nslookup -q=ptr $HostName | ?{ $_ -Like "*name = *" })
$Temp = $Temp -Replace ".*name = "
If ($Temp -ne [String]::Empty) { $HostName = $Temp }
}
If ($EnableHostNamesOnly)
{
Return $HostName.Split(".")[0]
}
Return $HostName
}
# Returns a Byte Array representation of the original message.
# If the length is greater than 1024 Bytes the array is truncated
# as stipulated under RFC 3164.
#
# Caller: ValidateMessage
Function EncodeMessage([String]$MessageString)
{
$Message = [Text.Encoding]::ASCII.GetBytes($MessageString)
If ($Message.Length -gt 1024)
{
Return $Message[0..1023]
}
Return $Message
}
# Maintain a per-host log file in the $LogFolder
# Script does not clean up old log files
#
# Caller: ValidateMessage
Function WriteToLog([String]$MessageString, [String]$HostName)
{
# Simple time based roll-over check
If ((Get-Date).Day -ne $Day)
{
$Day = (Get-Date).Day
$DateStamp = (Get-Date).ToString("yyyy.MM.dd")
}
$LogFile = "$LogFolder\$HostName-$DateStamp.log"
$MessageString >> $LogFile
}
# Start the server
Start-SysLog
Related posts:
- DHCP Discovery A PowerShell script to send a DHCP Discover request and...
- IPv4 subnet math with PowerShell Written to complement the VbScript version of this post. This...
Related posts brought to you by Yet Another Related Posts Plugin.
Posted by sawall on 01.12.09 at 4:01 pm
sometimes it works, sometimes it doesn’t. i get this error most of the times i try to run it.
Exception calling “ReceiveFrom” with “2″ argument(s): “You must call the Bind method before performing this operation.”
At C:\Syslog\syslogserver.ps1:78 char:41
+ $BytesReceived = $Socket.ReceiveFrom <<<< ($Buffer, [Ref]$SenderEndPoint)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
i have disabled message validation, that didn't help. my main goal was to not do a DNS lookup on the sending device. but i can't seem to figure out what the above error means or how to fix it. i've done a ton of Google searches.
thanks.
Posted by Chris on 01.12.09 at 4:01 pm
I’ve added in an option to prevent hostname lookup entirely. With this the script will write in the connecting IP address if generating a new Header.
The error message is perhaps most likely caused by the script being run more than once in the same PowerShell session. It needs a Stop-SysLog type command to flush out all the existing network sockets. At the moment the only way to flush those is by closing and re-opening the PS window. I’ll have a look at making that aspect more robust (it’s part of the error handling it needs).
Chris