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:

  1. DHCP Discovery A PowerShell script to send a DHCP Discover request and...
  2. 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.

2 Responses to this post.

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

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

Respond to this post