Skip to content

Commit

Permalink
Handle each SNMP OID & more tidying up
Browse files Browse the repository at this point in the history
  • Loading branch information
viral32111 committed Mar 6, 2023
1 parent 60ffa76 commit 3384c15
Showing 1 changed file with 149 additions and 43 deletions.
192 changes: 149 additions & 43 deletions Service/ServerMonitor/Source/Collector/SNMP.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Linq;
using System.Collections.Generic;
Expand Down Expand Up @@ -116,40 +115,39 @@ private AgentInformation FetchAgentInformation( string agentAddress, int agentPo

// Create the PDU containing the OIDs to fetch values for
Pdu pdu = new( PduType.Get );
foreach ( string informationOID in new string[] {
"1.3.6.1.2.1.1.1.0", // SNMPv2-MIB::sysDescr.0 (Description)
"1.3.6.1.2.1.1.2.0", // SNMPv2-MIB::sysObjectID.0 (Object ID)
"1.3.6.1.2.1.1.3.0", // DISMAN-EVENT-MIB::sysUpTimeInstance (Uptime)
"1.3.6.1.2.1.1.4.0", // SNMPv2-MIB::sysContact.0 (Contact)
"1.3.6.1.2.1.1.5.0", // SNMPv2-MIB::sysName.0 (Name)
"1.3.6.1.2.1.1.6.0", // SNMPv2-MIB::sysLocation.0 (Location)
"1.3.6.1.2.1.1.7.0" // SNMPv2-MIB::sysServices.0 (Service Count)
} ) pdu.VbList.Add( informationOID );
pdu.VbList.Add( SNMPOID.SNMPV2_SYSTEM_OBJECT_ID );
pdu.VbList.Add( SNMPOID.SNMPV2_SYSTEM_NAME );
pdu.VbList.Add( SNMPOID.SNMPV2_SYSTEM_DESCRIPTION );
pdu.VbList.Add( SNMPOID.SNMPV2_SYSTEM_CONTACT );
pdu.VbList.Add( SNMPOID.SNMPV2_SYSTEM_LOCATION );
pdu.VbList.Add( SNMPOID.SNMPV2_SYSTEM_SERVICES );
pdu.VbList.Add( SNMPOID.DISMAN_EVENT_SYSTEM_UPTIME );

// Send the request to the agent (timeout after 2 seconds)
logger.LogDebug( "Fetching information about SNMP agent '{0}:{1}'", agentAddress, agentPort );
using ( UdpTarget targetAgent = new( IPAddress.Parse( agentAddress ), agentPort, 2000, 1 ) ) {
SnmpV1Packet agentResponse = ( SnmpV1Packet ) targetAgent.Request( pdu, managerParameters );
if ( agentResponse == null ) throw new Exception( $"No response from SNMP agent '{ agentAddress }:{ agentPort }'" );
if ( agentResponse.Pdu.ErrorStatus != 0 ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned error status '{ agentResponse.Pdu.ErrorStatus }'" );

// Print the response for debugging
//foreach ( Vb varBinding in agentResponse.Pdu.VbList ) logger.LogDebug( "SNMP agent '{0}:{1}' returned '{0}' ({1}) = '{2}'", agentAddress, agentPort, varBinding.Oid.ToString(), SnmpConstants.GetTypeName( varBinding.Value.Type ), varBinding.Value.ToString() );
//foreach ( Vb variableBinding in agentResponse.Pdu.VbList ) logger.LogDebug( "SNMP agent '{0}:{1}' returned '{0}' ({1}) = '{2}'", agentAddress, agentPort, variableBinding.Oid.ToString(), SnmpConstants.GetTypeName( variableBinding.Value.Type ), variableBinding.Value.ToString() );

// Convert the response to a dictionary of OIDs and their values
Dictionary<string, string?> agentInformation = agentResponse.Pdu.VbList.ToDictionary(
vb => vb.Oid.ToString(),
vb => vb.Value.ToString()
Dictionary<string, string?> agentVariables = agentResponse.Pdu.VbList.ToDictionary(
variableBinding => variableBinding.Oid.ToString(),
variableBinding => variableBinding.Value.ToString()
);

// Ensure all of the information returned is valid
if ( agentInformation.TryGetValue( "1.3.6.1.2.1.1.1.0", out string? description ) == false || string.IsNullOrWhiteSpace( description ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned description that is null, empty or whitespace" );
if ( agentInformation.TryGetValue( "1.3.6.1.2.1.1.2.0", out string? objectID ) == false || string.IsNullOrWhiteSpace( objectID ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned object ID that is null, empty or whitespace" );
if ( agentInformation.TryGetValue( "1.3.6.1.2.1.1.3.0", out string? uptimeText ) == false || string.IsNullOrWhiteSpace( uptimeText ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned uptime that is null, empty or whitespace" );
if ( agentInformation.TryGetValue( "1.3.6.1.2.1.1.4.0", out string? contact ) == false || string.IsNullOrWhiteSpace( contact ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned contact that is null, empty or whitespace" );
if ( agentInformation.TryGetValue( "1.3.6.1.2.1.1.5.0", out string? name ) == false || string.IsNullOrWhiteSpace( name ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned name that is null, empty or whitespace" );
if ( agentInformation.TryGetValue( "1.3.6.1.2.1.1.6.0", out string? location ) == false || string.IsNullOrWhiteSpace( location ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned location that is null, empty or whitespace" );
if ( agentInformation.TryGetValue( "1.3.6.1.2.1.1.7.0", out string? serviceCountText ) == false || string.IsNullOrWhiteSpace( serviceCountText ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned service count that is null, empty or whitespace" );

if ( agentVariables.TryGetValue( SNMPOID.SNMPV2_SYSTEM_OBJECT_ID, out string? objectID ) == false || string.IsNullOrWhiteSpace( objectID ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned object ID that is null, empty or whitespace" );
if ( agentVariables.TryGetValue( SNMPOID.SNMPV2_SYSTEM_NAME, out string? name ) == false || string.IsNullOrWhiteSpace( name ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned name that is null, empty or whitespace" );
if ( agentVariables.TryGetValue( SNMPOID.SNMPV2_SYSTEM_DESCRIPTION, out string? description ) == false || string.IsNullOrWhiteSpace( description ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned description that is null, empty or whitespace" );
if ( agentVariables.TryGetValue( SNMPOID.SNMPV2_SYSTEM_CONTACT, out string? contact ) == false || string.IsNullOrWhiteSpace( contact ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned contact that is null, empty or whitespace" );
if ( agentVariables.TryGetValue( SNMPOID.SNMPV2_SYSTEM_LOCATION, out string? location ) == false || string.IsNullOrWhiteSpace( location ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned location that is null, empty or whitespace" );
if ( agentVariables.TryGetValue( SNMPOID.SNMPV2_SYSTEM_SERVICES, out string? services ) == false || string.IsNullOrWhiteSpace( services ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned service count that is null, empty or whitespace" );
if ( agentVariables.TryGetValue( SNMPOID.DISMAN_EVENT_SYSTEM_UPTIME, out string? uptimeText ) == false || string.IsNullOrWhiteSpace( uptimeText ) ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned uptime that is null, empty or whitespace" );
// Parse SNMP uptime string into seconds
Match uptimeMatch = Regex.Match( uptimeText, @"^(\d+)d (\d+)h (\d+)m (\d+)s (\d+)ms$" );
if ( uptimeMatch.Success == false ) throw new Exception( $"SNMP agent '{ agentAddress }:{ agentPort }' returned uptime '{ uptimeText }' in an unexpected format" );
Expand All @@ -161,19 +159,21 @@ private AgentInformation FetchAgentInformation( string agentAddress, int agentPo
TimeSpan uptime = new( uptimeDays, uptimeHours, uptimeMinutes, uptimeSeconds, uptimeMilliseconds );

// Parse service count as integer
if ( int.TryParse( serviceCountText, out int serviceCount ) == false ) throw new Exception( $"Failed to parse service count '{ serviceCountText }' as an integer" );
if ( int.TryParse( services, out int serviceCount ) == false ) throw new Exception( $"Failed to parse service count '{ services }' as an integer" );

// Store all the information in a structure
return new AgentInformation {
Address = agentAddress,
Port = agentPort,

ObjectID = objectID,
Name = name,
Description = description,
Uptime = uptime,
Contact = contact,
Name = name,
Location = location,
ServiceCount = serviceCount,
ObjectID = objectID

Uptime = uptime
};
}

Expand All @@ -185,7 +185,7 @@ private async Task ReceivePackets() {
// Disable timing out when waiting to receive packets
udpSocket.SetSocketOption( SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 0 );

// Receive from any IP address and port
// Receive from any IP address and port, needed so we can get address & port of sender - https://stackoverflow.com/q/5964846
EndPoint remoteEndPoint = new IPEndPoint( IPAddress.Any, 0 );

// Create a buffer to hold the received packet
Expand Down Expand Up @@ -214,56 +214,162 @@ private async Task ReceivePackets() {

// Handles a received trap packet - https://snmpsharpnet.com/index.php/receive-snmp-version-1-and-2c-trap-notifications/
private void ProcessTrapPacket( IPEndPoint remoteEndPoint, byte[] packet, int packetLength ) {

// Get the agent's IP address & port number
string agentAddress = remoteEndPoint.Address.ToString();
int agentPort = remoteEndPoint.Port;

logger.LogDebug( "Received SNMP trap packet of {0} bytes from '{1}:{2}'", packetLength, agentAddress, agentPort );

// We only support SNMP version 1
SnmpVersion snmpVersion = ( SnmpVersion ) SnmpPacket.GetProtocolVersion( packet, packetLength );
if ( snmpVersion != SnmpVersion.Ver1 ) throw new Exception( $"Unsupported SNMP version '{ snmpVersion }' on trap packet" );
logger.LogDebug( "Processing SNMP trap packet (version {0})", ( int ) snmpVersion );

// Parse the trap packet
SnmpV1TrapPacket trapPacket = new();
trapPacket.decode( packet, packetLength );
logger.LogInformation( "Received SNMP trap ({0}, {1}) from agent '{2}:{3}'", trapPacket.Pdu.Generic, trapPacket.Pdu.Specific, agentAddress, agentPort );

// Ensure the IP address in the packet matches the IP address of the sender
if ( trapPacket.Pdu.AgentAddress.ToString() != agentAddress ) throw new Exception( $"SNMP agent address '{ trapPacket.Pdu.AgentAddress }' does not match sender IP address '{ agentAddress }'" );

// Parse the timestamp, which is actually uptime in milliseconds
TimeSpan uptime = new( 0, 0, 0, ( int ) trapPacket.Pdu.TimeStamp );

// Windows event viewer information
int eventId = trapPacket.Pdu.Specific;
int eventSeverity = trapPacket.Pdu.Generic;

// Print information about the trap packet
logger.LogInformation( "Received SNMP trap {0}, {1} from agent '{2}:{3}'", eventId, eventSeverity, agentAddress, agentPort );
foreach ( Vb varBinding in trapPacket.Pdu.VbList ) logger.LogDebug( "\t'{0}' ({1}) = '{2}'", agentAddress, agentPort, varBinding.Oid.ToString(), SnmpConstants.GetTypeName( varBinding.Value.Type ), varBinding.Value.ToString() );
logger.LogDebug( "Agent uptime is {0} day(s), {1} hour(s), {2} minute(s), {3} second(s), {4} millisecond(s)", uptime.Days, uptime.Hours, uptime.Minutes, uptime.Seconds, uptime.Milliseconds );

// Convert the response to a dictionary of OIDs and their values
Dictionary<string, string?> trapVariables = trapPacket.Pdu.VbList.ToDictionary(
variableBinding => variableBinding.Oid.ToString(),
variableBinding => variableBinding.Value.ToString()?.Trim()
);

// Print all the variables in the trap packet
foreach ( KeyValuePair<string, string?> variable in trapVariables ) {
if ( variable.Key.StartsWith( SNMPOID.MICROSOFT_SOFTWARE_EVENTLOG_EVENT_TEXT ) ) {
logger.LogDebug( "Windows event text: '{0}' (event identifier: {1})", variable.Value, trapPacket.Pdu.Specific );
} else if ( variable.Key.StartsWith( SNMPOID.MICROSOFT_SOFTWARE_EVENTLOG_EVENT_USER_ID ) ) {
logger.LogDebug( "Windows event user identifier: '{0}' (event identifier: {1})", variable.Value, trapPacket.Pdu.Specific );
} else if ( variable.Key.StartsWith( SNMPOID.MICROSOFT_SOFTWARE_EVENTLOG_EVENT_SYSTEM ) ) {
logger.LogDebug( "Windows event system name: '{0}' (event identifier: {1})", variable.Value, trapPacket.Pdu.Specific );
} else if ( variable.Key.StartsWith( SNMPOID.MICROSOFT_SOFTWARE_EVENTLOG_EVENT_TYPE ) ) {
logger.LogDebug( "Windows event type: '{0}' (event identifier: {1})", variable.Value, trapPacket.Pdu.Specific );
} else if ( variable.Key.StartsWith( SNMPOID.MICROSOFT_SOFTWARE_EVENTLOG_EVENT_CATEGORY ) ) {
logger.LogDebug( "Windows event category: '{0}' (event identifier: {1})", variable.Value, trapPacket.Pdu.Specific );
} else {
logger.LogWarning( "Unrecognised SNMP OID: '{0}' = '{1}'", variable.Key, variable.Value );
}
}

// Fetch information about this agent
AgentInformation agentInformation = FetchAgentInformation( agentAddress, agentPort );
SNMPAgent snmpAgent = configuration.SNMPAgents.Where( snmpAgent => snmpAgent.Address == agentAddress ).First();
AgentInformation agentInformation = FetchAgentInformation( snmpAgent.Address, snmpAgent.Port );

// Update the exported Prometheus metrics
TrapsReceived.WithLabels( agentAddress, agentPort.ToString(), agentInformation.Name, agentInformation.Description, agentInformation.Contact, agentInformation.Location ).Inc();
logger.LogInformation( "Updated Prometheus metrics for SNMP agent '{0}:{1}'", agentAddress, agentPort );
TrapsReceived.WithLabels( snmpAgent.Address, snmpAgent.Port.ToString(), agentInformation.Name, agentInformation.Description, agentInformation.Contact, agentInformation.Location ).Inc();
logger.LogInformation( "Incremented Prometheus metrics for SNMP agent '{0}:{1}'", snmpAgent.Address, snmpAgent.Port );

}

// Structure to hold information about an SNMP agent
// Structure to hold information fetched from an SNMP agent
struct AgentInformation {
public string Address { get; set; }
public int Port { get; set; }

public string ObjectID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public TimeSpan Uptime { get; set; }
public string Contact { get; set; }
public string Location { get; set; }
public int ServiceCount { get; set; }
public string ObjectID { get; set; }

public TimeSpan Uptime { get; set; }
}

}

// Enumerations for SNMP OIDs
public static class SNMPOID {

// https://snmpsharpnet.com/index.php/snmp-version-1-or-2c-get-request/
public static readonly string SNMPV2_SYSTEM_OBJECT_ID = "1.3.6.1.2.1.1.2.0"; // SNMPv2-MIB::sysObjectID.0 (Object ID)
public static readonly string SNMPV2_SYSTEM_NAME = "1.3.6.1.2.1.1.5.0"; // SNMPv2-MIB::sysName.0 (Name)
public static readonly string SNMPV2_SYSTEM_DESCRIPTION = "1.3.6.1.2.1.1.1.0"; // SNMPv2-MIB::sysDescr.0 (Description)
public static readonly string SNMPV2_SYSTEM_CONTACT = "1.3.6.1.2.1.1.4.0"; // SNMPv2-MIB::sysContact.0 (Contact)
public static readonly string SNMPV2_SYSTEM_LOCATION = "1.3.6.1.2.1.1.6.0"; // SNMPv2-MIB::sysLocation.0 (Location)
public static readonly string SNMPV2_SYSTEM_SERVICES = "1.3.6.1.2.1.1.7.0"; // SNMPv2-MIB::sysServices.0 (Service Count)

// https://bestmonitoringtools.com/mibdb/mibdb_search.php?mib=DISMAN-EVENT-MIB
public static readonly string DISMAN_EVENT_SYSTEM_UPTIME = "1.3.6.1.2.1.1.3.0"; // DISMAN-EVENT-MIB::sysUpTimeInstance (Uptime)

// https://bestmonitoringtools.com/mibdb/mibdb_search.php?mib=EVNTAGENT-MIB
public static readonly string MICROSOFT_SOFTWARE_EVENTLOG_EVENT_TEXT = "1.3.6.1.4.1.311.1.13.1.9999.1";
public static readonly string MICROSOFT_SOFTWARE_EVENTLOG_EVENT_USER_ID = "1.3.6.1.4.1.311.1.13.1.9999.2";
public static readonly string MICROSOFT_SOFTWARE_EVENTLOG_EVENT_SYSTEM = "1.3.6.1.4.1.311.1.13.1.9999.3";
public static readonly string MICROSOFT_SOFTWARE_EVENTLOG_EVENT_TYPE = "1.3.6.1.4.1.311.1.13.1.9999.4";
public static readonly string MICROSOFT_SOFTWARE_EVENTLOG_EVENT_CATEGORY = "1.3.6.1.4.1.311.1.13.1.9999.5";

}

}

/* DHCP Server (ID: 1043, Severity: 6):
'1.3.6.1.4.1.311.1.13.1.9999.1.0' (OctetString) = 'The DHCP/BINL service on the local machine has determined that it is authorized to start. It is servicing clients now.'
'1.3.6.1.4.1.311.1.13.1.9999.2.0' (OctetString) = 'Unknown'
'1.3.6.1.4.1.311.1.13.1.9999.3.0' (OctetString) = 'WINDOWS-SERVER'
'1.3.6.1.4.1.311.1.13.1.9999.4.0' (OctetString) = '4'
'1.3.6.1.4.1.311.1.13.1.9999.5.0' (OctetString) = '0'
'1.3.6.1.4.1.311.1.13.1.9999.6.0' (OctetString) = '?'
'1.3.6.1.4.1.311.1.13.1.9999.7.0' (OctetString) = '?'
'1.3.6.1.4.1.311.1.13.1.9999.8.0' (OctetString) = '0'
*/

/* Docker (ID: 1, Severity: 6):
'1.3.6.1.4.1.311.1.13.1.9999.1.0' (OctetString) = 'Daemon shutdown complete'
'1.3.6.1.4.1.311.1.13.1.9999.2.0' (OctetString) = 'Unknown'
'1.3.6.1.4.1.311.1.13.1.9999.3.0' (OctetString) = 'WINDOWS-SERVER'
'1.3.6.1.4.1.311.1.13.1.9999.4.0' (OctetString) = '4'
'1.3.6.1.4.1.311.1.13.1.9999.5.0' (OctetString) = '0'
'1.3.6.1.4.1.311.1.13.1.9999.6.0' (OctetString) = 'Daemon shutdown complete'
'1.3.6.1.4.1.311.1.13.1.9999.1.0' (OctetString) = 'Starting up'
'1.3.6.1.4.1.311.1.13.1.9999.2.0' (OctetString) = 'Unknown'
'1.3.6.1.4.1.311.1.13.1.9999.3.0' (OctetString) = 'WINDOWS-SERVER'
'1.3.6.1.4.1.311.1.13.1.9999.4.0' (OctetString) = '4'
'1.3.6.1.4.1.311.1.13.1.9999.5.0' (OctetString) = '0'
'1.3.6.1.4.1.311.1.13.1.9999.6.0' (OctetString) = 'Starting up'
'1.3.6.1.4.1.311.1.13.1.9999.1.0' (OctetString) = 'Windows default isolation mode: process'
'1.3.6.1.4.1.311.1.13.1.9999.2.0' (OctetString) = 'Unknown'
'1.3.6.1.4.1.311.1.13.1.9999.3.0' (OctetString) = 'WINDOWS-SERVER'
'1.3.6.1.4.1.311.1.13.1.9999.4.0' (OctetString) = '4'
'1.3.6.1.4.1.311.1.13.1.9999.5.0' (OctetString) = '0'
'1.3.6.1.4.1.311.1.13.1.9999.6.0' (OctetString) = 'Windows default isolation mode: process'
'1.3.6.1.4.1.311.1.13.1.9999.1.0' (OctetString) = 'Loading containers: start.'
'1.3.6.1.4.1.311.1.13.1.9999.2.0' (OctetString) = 'Unknown'
'1.3.6.1.4.1.311.1.13.1.9999.3.0' (OctetString) = 'WINDOWS-SERVER'
'1.3.6.1.4.1.311.1.13.1.9999.4.0' (OctetString) = '4'
'1.3.6.1.4.1.311.1.13.1.9999.5.0' (OctetString) = '0'
'1.3.6.1.4.1.311.1.13.1.9999.6.0' (OctetString) = 'Loading containers: start.'
'1.3.6.1.4.1.311.1.13.1.9999.1.0' (OctetString) = 'Restoring existing overlay networks from HNS into docker'
'1.3.6.1.4.1.311.1.13.1.9999.2.0' (OctetString) = 'Unknown'
'1.3.6.1.4.1.311.1.13.1.9999.3.0' (OctetString) = 'WINDOWS-SERVER'
'1.3.6.1.4.1.311.1.13.1.9999.4.0' (OctetString) = '4'
'1.3.6.1.4.1.311.1.13.1.9999.5.0' (OctetString) = '0'
'1.3.6.1.4.1.311.1.13.1.9999.6.0' (OctetString) = 'Restoring existing overlay networks from HNS into docker'
'1.3.6.1.4.1.311.1.13.1.9999.1.0' (OctetString) = 'Loading containers: done.'
'1.3.6.1.4.1.311.1.13.1.9999.2.0' (OctetString) = 'Unknown'
'1.3.6.1.4.1.311.1.13.1.9999.3.0' (OctetString) = 'WINDOWS-SERVER'
'1.3.6.1.4.1.311.1.13.1.9999.4.0' (OctetString) = '4'
'1.3.6.1.4.1.311.1.13.1.9999.5.0' (OctetString) = '0'
'1.3.6.1.4.1.311.1.13.1.9999.6.0' (OctetString) = 'Loading containers: done.'
'1.3.6.1.4.1.311.1.13.1.9999.1.0' (OctetString) = 'Daemon has completed initialization'
'1.3.6.1.4.1.311.1.13.1.9999.2.0' (OctetString) = 'Unknown'
'1.3.6.1.4.1.311.1.13.1.9999.3.0' (OctetString) = 'WINDOWS-SERVER'
'1.3.6.1.4.1.311.1.13.1.9999.4.0' (OctetString) = '4'
'1.3.6.1.4.1.311.1.13.1.9999.5.0' (OctetString) = '0'
'1.3.6.1.4.1.311.1.13.1.9999.6.0' (OctetString) = 'Daemon has completed initialization'
*/

0 comments on commit 3384c15

Please sign in to comment.