diff --git a/README.md b/README.md index d0f521c..919f28d 100644 --- a/README.md +++ b/README.md @@ -1555,7 +1555,11 @@ an object, possibly empty, and can contain the following fields: The `callback` parameter is a callback function of the form `function (error, notification)`. On an error condition, the `notification` -parameter is set to `null`. On successful reception of a notification, the error +parameter is set to `null`. Where the error was produced while processing a +received packet - such as an authorization failure for a community or user +not in the receiver's local authorization lists - the error will include an +`rinfo` attribute containing the sender socket details of the offending packet. +On successful reception of a notification, the error parameter is set to `null`, and the `notification` parameter is set as an object with the notification PDU details in the `pdu` field and the sender socket details in the `rinfo` field. For example: @@ -1723,7 +1727,9 @@ receiver's community authorization list, the receiver will not accept the notifi instead returning a error of class `RequestFailedError` to the supplied callback function. Similarly, if a v3 notification is received with a user whose name is not in the receiver's user authorization list, the receiver will return a -`RequestFailedError`. If the `disableAuthorization` option is supplied for the +`RequestFailedError`. These errors include an `rinfo` attribute containing the +sender socket details of the unauthorized packet, so that its origin can be +identified. If the `disableAuthorization` option is supplied for the receiver on start-up, then these local authorization list checks are disabled for community notifications and noAuthNoPriv user notifications. Note that even with this setting, the user list is *still checked* for authNoPriv and authPriv notifications, @@ -3755,6 +3761,10 @@ Example programs are included under the module's `example` directory. * Document how MIB scalar and table handlers should interact with `mibRequest.instanceNode.value` for Get vs Set operations +# Version 3.26.4 - 09/07/2026 + + * Add rinfo to receiver and agent authorization failure errors + # License Copyright (c) 2020 Mark Abrahams diff --git a/index.js b/index.js index 8cf5fb9..c04bb67 100644 --- a/index.js +++ b/index.js @@ -3141,7 +3141,15 @@ Listener.formatCallbackData = function (pdu, rinfo) { }; }; -Listener.processIncoming = function (buffer, authorizer, callback) { +Listener.processIncoming = function (buffer, authorizer, rinfo, callback) { + // Annotate errors with the packet origin, so that callback consumers can + // identify the source of unauthorized or invalid messages + var callbackWithRinfo = function (error, data) { + if ( error && rinfo && ! error.rinfo ) { + error.rinfo = rinfo; + } + callback (error, data); + }; var message = Message.createFromBuffer (buffer); var community; @@ -3159,7 +3167,7 @@ Listener.processIncoming = function (buffer, authorizer, callback) { errorType: UsmErrorType.UNKNOWN_USER_NAME }; } - callback (new RequestFailedError ("Local user not found for message with user " + + callbackWithRinfo (new RequestFailedError ("Local user not found for message with user " + message.msgSecurityParameters.msgUserName)); return; } else if ( message.hasAuthentication () ) { @@ -3170,7 +3178,7 @@ Listener.processIncoming = function (buffer, authorizer, callback) { errorType: UsmErrorType.UNKNOWN_USER_NAME }; } - callback (new RequestFailedError ("Local user not found and message requires authentication with user " + + callbackWithRinfo (new RequestFailedError ("Local user not found and message requires authentication with user " + message.msgSecurityParameters.msgUserName)); return; } else { @@ -3188,7 +3196,7 @@ Listener.processIncoming = function (buffer, authorizer, callback) { errorType: UsmErrorType.WRONG_DIGESTS }; } - callback (new RequestFailedError ("Local user " + message.msgSecurityParameters.msgUserName + + callbackWithRinfo (new RequestFailedError ("Local user " + message.msgSecurityParameters.msgUserName + " requires authentication but message does not provide it")); return; } @@ -3200,17 +3208,17 @@ Listener.processIncoming = function (buffer, authorizer, callback) { errorType: UsmErrorType.WRONG_DIGESTS }; } - callback (new RequestFailedError ("Local user " + message.msgSecurityParameters.msgUserName + + callbackWithRinfo (new RequestFailedError ("Local user " + message.msgSecurityParameters.msgUserName + " requires privacy but message does not provide it")); return; } - if ( ! message.processIncomingSecurity (message.user, callback) ) { + if ( ! message.processIncomingSecurity (message.user, callbackWithRinfo) ) { return; } } else { community = authorizer.communities.filter( localCommunity => localCommunity == message.community )[0]; if ( ! community && ! authorizer.disableAuthorization ) { - callback (new RequestFailedError ("Local community not found for message with community " + message.community)); + callbackWithRinfo (new RequestFailedError ("Local community not found for message with community " + message.community)); return; } } @@ -3450,7 +3458,7 @@ Receiver.prototype.onMsg = function (socket, buffer, rinfo) { let message; try { - message = Listener.processIncoming (buffer, this.authorizer, this.callback); + message = Listener.processIncoming (buffer, this.authorizer, rinfo, this.callback); } catch (error) { this.callback (new ProcessingError ("Failure to process incoming message", error, rinfo, buffer)); return; @@ -5046,7 +5054,7 @@ Agent.prototype.onMsg = function (socket, buffer, rinfo) { let message; try { - message = Listener.processIncoming (buffer, this.authorizer, this.callback); + message = Listener.processIncoming (buffer, this.authorizer, rinfo, this.callback); } catch (error) { this.callback (new ProcessingError ("Failure to process incoming message", error, rinfo, buffer)); return; diff --git a/package.json b/package.json index ce58313..9cb26ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "net-snmp", - "version": "3.26.3", + "version": "3.26.4", "description": "JavaScript implementation of the Simple Network Management Protocol (SNMP)", "author": "Mark Abrahams ", "license": "MIT", diff --git a/test/receiver-error-rinfo.test.js b/test/receiver-error-rinfo.test.js new file mode 100644 index 0000000..b91d903 --- /dev/null +++ b/test/receiver-error-rinfo.test.js @@ -0,0 +1,153 @@ +const assert = require('assert'); +const snmp = require('../'); + +describe('Receiver error rinfo', function () { + + const testRinfo = { + address: '192.168.1.100', + family: 'IPv4', + port: 50123, + size: 0 + }; + + const authUser = { + name: 'testUser', + level: snmp.SecurityLevel.authNoPriv, + authProtocol: snmp.AuthProtocols.sha, + authKey: 'testAuthPassword' + }; + + function createMockDgram (captured) { + return { + createSocket: function () { + const socket = { + handlers: {}, + on: function (event, handler) { + this.handlers[event] = handler; + }, + bind: function () {}, + close: function () {}, + unref: function () {}, + address: function () { + return { address: '127.0.0.1', family: 'IPv4', port: 1620 }; + }, + send: function (buffer, offset, length, port, address, callback) { + captured.sentBuffers.push (Buffer.from (buffer.slice (offset, offset + length))); + if ( callback ) { + callback (null, length); + } + } + }; + captured.sockets.push (socket); + return socket; + } + }; + } + + // Uses a session with a mock dgram module to capture the exact wire buffer + // for a v2c trap without sending anything over the network + function generateV2TrapBuffer (community) { + const captured = { sockets: [], sentBuffers: [] }; + const session = snmp.createSession ('127.0.0.1', community, { + version: snmp.Version2c, + dgramModule: createMockDgram (captured) + }); + session.trap (snmp.TrapType.LinkDown, function () {}); + session.close (); + assert.strictEqual (captured.sentBuffers.length, 1); + return captured.sentBuffers[0]; + } + + // Same as above for an authenticated v3 trap + function generateV3TrapBuffer (user) { + const captured = { sockets: [], sentBuffers: [] }; + const session = snmp.createV3Session ('127.0.0.1', user, { + dgramModule: createMockDgram (captured) + }); + session.trap (snmp.TrapType.LinkDown, function () {}); + session.close (); + assert.strictEqual (captured.sentBuffers.length, 1); + return captured.sentBuffers[0]; + } + + // Creates a receiver with a mock dgram module and returns a function that + // simulates delivery of a packet to the receiver's listening socket + function createTestReceiver (options, callback) { + const captured = { sockets: [], sentBuffers: [] }; + options.dgramModule = createMockDgram (captured); + options.port = 1620; + const receiver = snmp.createReceiver (options, callback); + const deliverPacket = function (buffer, rinfo) { + captured.sockets[0].handlers.message (buffer, rinfo); + }; + return { receiver, deliverPacket }; + } + + it('includes rinfo on community authorization failure errors', function (done) { + const buffer = generateV2TrapBuffer ('unauthorizedCommunity'); + const { receiver, deliverPacket } = createTestReceiver ({}, function (error, notification) { + assert (error instanceof snmp.RequestFailedError); + assert.match (error.message, /Local community not found for message with community unauthorizedCommunity/); + assert.strictEqual (error.rinfo, testRinfo); + assert.strictEqual (notification, undefined); + receiver.close (); + done (); + }); + deliverPacket (buffer, testRinfo); + }); + + it('includes rinfo on unknown user authorization failure errors', function (done) { + const buffer = generateV3TrapBuffer (authUser); + const { receiver, deliverPacket } = createTestReceiver ({}, function (error) { + assert (error instanceof snmp.RequestFailedError); + assert.match (error.message, /Local user not found for message with user testUser/); + assert.strictEqual (error.rinfo, testRinfo); + receiver.close (); + done (); + }); + deliverPacket (buffer, testRinfo); + }); + + it('includes rinfo on authentication-required failure errors when authorization is disabled', function (done) { + const buffer = generateV3TrapBuffer (authUser); + const { receiver, deliverPacket } = createTestReceiver ({ disableAuthorization: true }, function (error) { + assert (error instanceof snmp.RequestFailedError); + assert.match (error.message, /Local user not found and message requires authentication with user testUser/); + assert.strictEqual (error.rinfo, testRinfo); + receiver.close (); + done (); + }); + deliverPacket (buffer, testRinfo); + }); + + it('includes rinfo on authentication digest failure errors', function (done) { + const buffer = generateV3TrapBuffer (authUser); + const { receiver, deliverPacket } = createTestReceiver ({}, function (error) { + assert (error instanceof snmp.ResponseInvalidError); + assert.strictEqual (error.code, snmp.ResponseInvalidCode.EAuthFailure); + assert.strictEqual (error.rinfo, testRinfo); + receiver.close (); + done (); + }); + receiver.getAuthorizer ().addUser ({ + name: authUser.name, + level: authUser.level, + authProtocol: authUser.authProtocol, + authKey: 'differentAuthPassword' + }); + deliverPacket (buffer, testRinfo); + }); + + it('delivers authorized notifications unchanged', function (done) { + const buffer = generateV2TrapBuffer ('authorizedCommunity'); + const { receiver, deliverPacket } = createTestReceiver ({}, function (error, notification) { + assert.strictEqual (error, null); + assert.strictEqual (notification.rinfo, testRinfo); + assert.strictEqual (notification.pdu.type, snmp.PduType.TrapV2); + receiver.close (); + done (); + }); + receiver.getAuthorizer ().addCommunity ('authorizedCommunity'); + deliverPacket (buffer, testRinfo); + }); +});