nupst/ts/snmp/packet-parser.ts
2025-03-25 09:06:23 +00:00

553 lines
18 KiB
TypeScript

import type { SnmpConfig } from './types.js';
import { SnmpEncoder } from './encoder.js';
/**
* SNMP packet parsing utilities
* Parses SNMP response packets
*/
export class SnmpPacketParser {
/**
* Parse an SNMP response
* @param buffer Response buffer
* @param config SNMP configuration
* @param debug Whether to enable debug output
* @returns Parsed value or null if parsing failed
*/
public static parseSnmpResponse(buffer: Buffer, config: SnmpConfig, debug: boolean = false): any {
// Check if we have a response packet
if (buffer[0] !== 0x30) {
throw new Error('Invalid SNMP response format');
}
// For SNMPv3, we need to handle the message differently
if (config.version === 3) {
return this.parseSnmpV3Response(buffer, debug);
}
if (debug) {
console.log('Parsing SNMPv1/v2 response: ', buffer.toString('hex'));
}
try {
// Enhanced structured parsing approach
// SEQUENCE header
let pos = 0;
if (buffer[pos] !== 0x30) {
throw new Error('Missing SEQUENCE at start of response');
}
// Skip SEQUENCE header - assume length is in single byte for simplicity
// In a more robust implementation, we'd handle multi-byte lengths
pos += 2;
// VERSION
if (buffer[pos] !== 0x02) {
throw new Error('Missing INTEGER for version');
}
const versionLength = buffer[pos + 1];
pos += 2 + versionLength;
// COMMUNITY STRING
if (buffer[pos] !== 0x04) {
throw new Error('Missing OCTET STRING for community');
}
const communityLength = buffer[pos + 1];
pos += 2 + communityLength;
// PDU TYPE - should be RESPONSE (0xA2)
if (buffer[pos] !== 0xA2) {
throw new Error(`Unexpected PDU type: 0x${buffer[pos].toString(16)}, expected 0xA2`);
}
// Skip PDU header
pos += 2;
// REQUEST ID
if (buffer[pos] !== 0x02) {
throw new Error('Missing INTEGER for request ID');
}
const requestIdLength = buffer[pos + 1];
pos += 2 + requestIdLength;
// ERROR STATUS
if (buffer[pos] !== 0x02) {
throw new Error('Missing INTEGER for error status');
}
const errorStatusLength = buffer[pos + 1];
const errorStatus = SnmpEncoder.decodeInteger(buffer, pos + 2, errorStatusLength);
if (errorStatus !== 0) {
throw new Error(`SNMP error status: ${errorStatus}`);
}
pos += 2 + errorStatusLength;
// ERROR INDEX
if (buffer[pos] !== 0x02) {
throw new Error('Missing INTEGER for error index');
}
const errorIndexLength = buffer[pos + 1];
pos += 2 + errorIndexLength;
// VARBIND LIST
if (buffer[pos] !== 0x30) {
throw new Error('Missing SEQUENCE for varbind list');
}
// Skip varbind list header
pos += 2;
// VARBIND
if (buffer[pos] !== 0x30) {
throw new Error('Missing SEQUENCE for varbind');
}
// Skip varbind header
pos += 2;
// OID
if (buffer[pos] !== 0x06) {
throw new Error('Missing OBJECT IDENTIFIER for OID');
}
const oidLength = buffer[pos + 1];
pos += 2 + oidLength;
// VALUE - this is what we want
const valueType = buffer[pos];
const valueLength = buffer[pos + 1];
if (debug) {
console.log(`Found value type: 0x${valueType.toString(16)}, length: ${valueLength}`);
}
return this.parseValueByType(valueType, valueLength, buffer, pos, debug);
} catch (error) {
if (debug) {
console.error('Error in structured parsing:', error);
console.error('Falling back to scan-based parsing method');
}
return this.scanBasedParsing(buffer, debug);
}
}
/**
* Parse value by ASN.1 type
* @param valueType ASN.1 type
* @param valueLength Value length
* @param buffer Buffer containing the value
* @param pos Position of the value in the buffer
* @param debug Whether to enable debug output
* @returns Parsed value
*/
private static parseValueByType(
valueType: number,
valueLength: number,
buffer: Buffer,
pos: number,
debug: boolean
): any {
switch (valueType) {
case 0x02: // INTEGER
{
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
if (debug) {
console.log('Parsed INTEGER value:', value);
}
return value;
}
case 0x04: // OCTET STRING
{
const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString();
if (debug) {
console.log('Parsed OCTET STRING value:', value);
}
return value;
}
case 0x05: // NULL
if (debug) {
console.log('Parsed NULL value');
}
return null;
case 0x06: // OBJECT IDENTIFIER (rare in a value position)
{
// Usually this would be encoded as a string representation
const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString('hex');
if (debug) {
console.log('Parsed OBJECT IDENTIFIER value (hex):', value);
}
return value;
}
case 0x40: // IP ADDRESS
{
if (valueLength !== 4) {
throw new Error(`Invalid IP address length: ${valueLength}, expected 4`);
}
const octets = [];
for (let i = 0; i < 4; i++) {
octets.push(buffer[pos + 2 + i]);
}
const value = octets.join('.');
if (debug) {
console.log('Parsed IP ADDRESS value:', value);
}
return value;
}
case 0x41: // COUNTER
case 0x42: // GAUGE32
case 0x43: // TIMETICKS
case 0x44: // OPAQUE
{
// All these are essentially unsigned 32-bit integers
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
if (debug) {
console.log(`Parsed ${valueType === 0x41 ? 'COUNTER'
: valueType === 0x42 ? 'GAUGE32'
: valueType === 0x43 ? 'TIMETICKS'
: 'OPAQUE'} value:`, value);
}
return value;
}
default:
if (debug) {
console.log(`Unknown value type: 0x${valueType.toString(16)}`);
}
return null;
}
}
/**
* Fallback scan-based parsing method
* @param buffer Buffer containing the SNMP response
* @param debug Whether to enable debug output
* @returns Parsed value or null if parsing failed
*/
private static scanBasedParsing(buffer: Buffer, debug: boolean): any {
// Look for various data types in the response
// The value is near the end of the packet after the OID
// We're looking for one of these:
// 0x02 - Integer - can be at the end of a varbind
// 0x04 - OctetString
// 0x05 - Null
// 0x42 - Gauge32 - special type for unsigned 32-bit integers
// 0x43 - Timeticks - special type for time values
// This algorithm performs a thorough search for data types
// by iterating from the start and watching for varbind structures
// Walk through the buffer looking for varbinds
let i = 0;
// First, find the varbinds section (0x30 sequence)
while (i < buffer.length - 2) {
// Look for a varbinds sequence
if (buffer[i] === 0x30) {
const varbindsLength = buffer[i + 1];
const varbindsEnd = i + 2 + varbindsLength;
// Now search within the varbinds for the value
let j = i + 2;
while (j < varbindsEnd - 2) {
// Look for a varbind (0x30 sequence)
if (buffer[j] === 0x30) {
const varbindLength = buffer[j + 1];
const varbindEnd = j + 2 + varbindLength;
// Skip over the OID and find the value within this varbind
let k = j + 2;
while (k < varbindEnd - 1) {
// First find the OID
if (buffer[k] === 0x06) { // OID
const oidLength = buffer[k + 1];
k += 2 + oidLength; // Skip past the OID
// We should now be at the value
// Check what type it is
if (k < varbindEnd - 1) {
return this.parseValueAtPosition(buffer, k, debug);
}
// If we didn't find a value, move to next byte
k++;
} else {
// Move to next byte
k++;
}
}
// Move to next varbind
j = varbindEnd;
} else {
// Move to next byte
j++;
}
}
// Move to next sequence
i = varbindsEnd;
} else {
// Move to next byte
i++;
}
}
if (debug) {
console.log('No valid value found in SNMP response');
}
return null;
}
/**
* Parse value at a specific position in the buffer
* @param buffer Buffer containing the SNMP response
* @param pos Position of the value in the buffer
* @param debug Whether to enable debug output
* @returns Parsed value or null if parsing failed
*/
private static parseValueAtPosition(buffer: Buffer, pos: number, debug: boolean): any {
if (buffer[pos] === 0x02) { // Integer
const valueLength = buffer[pos + 1];
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
if (debug) {
console.log('Found Integer value:', value);
}
return value;
} else if (buffer[pos] === 0x42) { // Gauge32
const valueLength = buffer[pos + 1];
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
if (debug) {
console.log('Found Gauge32 value:', value);
}
return value;
} else if (buffer[pos] === 0x43) { // TimeTicks
const valueLength = buffer[pos + 1];
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
if (debug) {
console.log('Found Timeticks value:', value);
}
return value;
} else if (buffer[pos] === 0x04) { // OctetString
const valueLength = buffer[pos + 1];
if (debug) {
console.log('Found OctetString value');
}
// Just return the string value as-is
return buffer.slice(pos + 2, pos + 2 + valueLength).toString();
} else if (buffer[pos] === 0x05) { // Null
if (debug) {
console.log('Found Null value');
}
return null;
}
return null;
}
/**
* Parse an SNMPv3 response
* @param buffer Buffer containing the SNMP response
* @param debug Whether to enable debug output
* @returns Parsed value or null if parsing failed
*/
public static parseSnmpV3Response(buffer: Buffer, debug: boolean = false): any {
// SNMPv3 parsing is complex. In a real implementation, we would:
// 1. Parse the header and get the security parameters
// 2. Verify authentication if used
// 3. Decrypt the PDU if privacy was used
// 4. Extract the PDU and parse it
if (debug) {
console.log('Parsing SNMPv3 response: ', buffer.toString('hex'));
}
// Find the scopedPDU - it should be the last OCTET STRING in the message
let scopedPduPos = -1;
for (let i = buffer.length - 50; i >= 0; i--) {
if (buffer[i] === 0x04 && buffer[i + 1] > 10) { // OCTET STRING with reasonable length
scopedPduPos = i;
break;
}
}
if (scopedPduPos === -1) {
if (debug) {
console.log('Could not find scoped PDU in SNMPv3 response');
}
return null;
}
// Skip to the PDU content
let pduContent = buffer.slice(scopedPduPos + 2); // Skip OCTET STRING header
// This improved algorithm performs a more thorough search for varbinds
// in the scoped PDU
// First, look for the response PDU (sequence with tag 0xa2)
let responsePdu = null;
for (let i = 0; i < pduContent.length - 3; i++) {
if (pduContent[i] === 0xa2) {
// Found the response PDU
const pduLength = pduContent[i + 1];
responsePdu = pduContent.slice(i, i + 2 + pduLength);
break;
}
}
if (!responsePdu) {
// Try to find the varbinds directly
for (let i = 0; i < pduContent.length - 3; i++) {
if (pduContent[i] === 0x30) {
const seqLength = pduContent[i + 1];
if (i + 2 + seqLength <= pduContent.length) {
// Check if this sequence might be the varbinds
const possibleVarbinds = pduContent.slice(i, i + 2 + seqLength);
// Look for varbind structure inside
for (let j = 0; j < possibleVarbinds.length - 3; j++) {
if (possibleVarbinds[j] === 0x30) {
// Might be a varbind - look for an OID inside
for (let k = j; k < j + 10 && k < possibleVarbinds.length - 1; k++) {
if (possibleVarbinds[k] === 0x06) {
// Found an OID, so this is likely the varbinds sequence
responsePdu = possibleVarbinds;
break;
}
}
if (responsePdu) break;
}
}
if (responsePdu) break;
}
}
}
}
if (!responsePdu) {
if (debug) {
console.log('Could not find response PDU in SNMPv3 response');
}
return null;
}
// Now that we have the response PDU, search for varbinds
// Skip the first few bytes to get past the header fields
let varbindsPos = -1;
for (let i = 10; i < responsePdu.length - 3; i++) {
if (responsePdu[i] === 0x30) {
// Check if this is the start of the varbinds
// by seeing if it contains a varbind sequence
for (let j = i + 2; j < i + 10 && j < responsePdu.length - 3; j++) {
if (responsePdu[j] === 0x30) {
varbindsPos = i;
break;
}
}
if (varbindsPos !== -1) break;
}
}
if (varbindsPos === -1) {
if (debug) {
console.log('Could not find varbinds in SNMPv3 response');
}
return null;
}
// Get the varbinds
const varbindsLength = responsePdu[varbindsPos + 1];
const varbinds = responsePdu.slice(varbindsPos, varbindsPos + 2 + varbindsLength);
// Now search for values inside the varbinds
for (let i = 2; i < varbinds.length - 3; i++) {
// Look for a varbind sequence
if (varbinds[i] === 0x30) {
const varbindLength = varbinds[i + 1];
const varbind = varbinds.slice(i, i + 2 + varbindLength);
// Inside the varbind, look for the OID and then the value
for (let j = 0; j < varbind.length - 3; j++) {
if (varbind[j] === 0x06) { // OID
const oidLength = varbind[j + 1];
// The value should be right after the OID
const valuePos = j + 2 + oidLength;
if (valuePos < varbind.length - 1) {
// Check what type of value it is
if (varbind[valuePos] === 0x02) { // INTEGER
const valueLength = varbind[valuePos + 1];
const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
if (debug) {
console.log('Found INTEGER value in SNMPv3 response:', value);
}
return value;
} else if (varbind[valuePos] === 0x42) { // Gauge32
const valueLength = varbind[valuePos + 1];
const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
if (debug) {
console.log('Found Gauge32 value in SNMPv3 response:', value);
}
return value;
} else if (varbind[valuePos] === 0x43) { // TimeTicks
const valueLength = varbind[valuePos + 1];
const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
if (debug) {
console.log('Found TimeTicks value in SNMPv3 response:', value);
}
return value;
} else if (varbind[valuePos] === 0x04) { // OctetString
const valueLength = varbind[valuePos + 1];
const value = varbind.slice(valuePos + 2, valuePos + 2 + valueLength).toString();
if (debug) {
console.log('Found OctetString value in SNMPv3 response:', value);
}
return value;
}
}
}
}
}
}
if (debug) {
console.log('No valid value found in SNMPv3 response');
}
return null;
}
/**
* Extract engine ID from SNMPv3 response
* @param buffer Buffer containing the SNMP response
* @param debug Whether to enable debug output
* @returns Extracted engine ID or null if extraction failed
*/
public static extractEngineId(buffer: Buffer, debug: boolean = false): Buffer | null {
try {
// Simple parsing to find the engine ID
// Look for the first octet string with appropriate length
for (let i = 0; i < buffer.length - 10; i++) {
if (buffer[i] === 0x04) { // Octet string
const len = buffer[i + 1];
if (len >= 5 && len <= 32) { // Engine IDs are typically 5-32 bytes
// Verify this looks like an engine ID (usually starts with 0x80)
if (buffer[i + 2] === 0x80) {
if (debug) {
console.log('Found engine ID at position', i);
console.log('Engine ID:', buffer.slice(i + 2, i + 2 + len).toString('hex'));
}
return buffer.slice(i + 2, i + 2 + len);
}
}
}
}
return null;
} catch (error) {
console.error('Error extracting engine ID:', error);
return null;
}
}
}