Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 <mark@abrahams.co.nz>
Expand Down
26 changes: 17 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 () ) {
Expand All @@ -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 {
Expand All @@ -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;
}
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 <mark@abrahams.co.nz>",
"license": "MIT",
Expand Down
153 changes: 153 additions & 0 deletions test/receiver-error-rinfo.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});