net-snmp 3.26.3 → 3.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/index.js +118 -1
- package/package.json +1 -1
- package/test/snmpv3-engine-time.test.js +563 -0
package/README.md
CHANGED
|
@@ -351,6 +351,7 @@ Actions
|
|
|
351
351
|
- `11 - EUnexpectedReport`
|
|
352
352
|
- `12 - EResponseNotHandled`
|
|
353
353
|
- `13 - EUnexpectedResponse`
|
|
354
|
+
- `14 - ENotInTimeWindow`
|
|
354
355
|
|
|
355
356
|
## snmp.OidFormat
|
|
356
357
|
- `oid - oid`
|
|
@@ -3755,6 +3756,12 @@ Example programs are included under the module's `example` directory.
|
|
|
3755
3756
|
|
|
3756
3757
|
* Document how MIB scalar and table handlers should interact with `mibRequest.instanceNode.value` for Get vs Set operations
|
|
3757
3758
|
|
|
3759
|
+
# Version 3.27.0 - 11/09/2026
|
|
3760
|
+
|
|
3761
|
+
* Fix SNMPv3 requests being rejected with `usmStatsNotInTimeWindows` once a session has been open for a while - the authoritative engine time is now advanced with a monotonic local clock and resynchronised from every authenticated response, instead of staying frozen at the value learned during discovery
|
|
3762
|
+
|
|
3763
|
+
* Discard SNMPv3 responses that fall outside the USM time window, reported as the new `ResponseInvalidCode.ENotInTimeWindow`
|
|
3764
|
+
|
|
3758
3765
|
# License
|
|
3759
3766
|
|
|
3760
3767
|
Copyright (c) 2020 Mark Abrahams <mark@abrahams.co.nz>
|
package/index.js
CHANGED
|
@@ -9,10 +9,13 @@ const util = require ("util");
|
|
|
9
9
|
const crypto = require ("crypto");
|
|
10
10
|
const mibparser = require ("./lib/mib");
|
|
11
11
|
const Buffer = require('buffer').Buffer;
|
|
12
|
+
const { performance } = require ("perf_hooks");
|
|
12
13
|
|
|
13
14
|
var DEBUG = false;
|
|
14
15
|
var STRICT_INT_RANGE_CHECKS = false;
|
|
15
16
|
|
|
17
|
+
const USM_TIME_WINDOW_SECONDS = 150;
|
|
18
|
+
|
|
16
19
|
const MIN_SIGNED_INT32 = -2147483648;
|
|
17
20
|
const MAX_SIGNED_INT32 = 2147483647;
|
|
18
21
|
const MIN_UNSIGNED_INT32 = 0;
|
|
@@ -280,7 +283,8 @@ var ResponseInvalidCode = {
|
|
|
280
283
|
10: "ECommunityNoMatch",
|
|
281
284
|
11: "EUnexpectedReport",
|
|
282
285
|
12: "EResponseNotHandled",
|
|
283
|
-
13: "EUnexpectedResponse"
|
|
286
|
+
13: "EUnexpectedResponse",
|
|
287
|
+
14: "ENotInTimeWindow"
|
|
284
288
|
};
|
|
285
289
|
|
|
286
290
|
_expandConstantObject (ResponseInvalidCode);
|
|
@@ -2084,6 +2088,18 @@ var Session = function (target, authenticator, options) {
|
|
|
2084
2088
|
this.reqs = {};
|
|
2085
2089
|
this.reqCount = 0;
|
|
2086
2090
|
|
|
2091
|
+
// Local notion of the authoritative (remote) engine's snmpEngineBoots and
|
|
2092
|
+
// snmpEngineTime, per RFC 3414 section 2.3. engineTimeBase is the
|
|
2093
|
+
// snmpEngineTime learned at engineTimeReceivedAt (a monotonic millisecond
|
|
2094
|
+
// reading), and the current notion is derived from the two by
|
|
2095
|
+
// getEngineTime(). latestReceivedEngineTime is the highest snmpEngineTime
|
|
2096
|
+
// ever received from the authoritative engine, and exists solely to stop a
|
|
2097
|
+
// replayed message from holding our notion of time back.
|
|
2098
|
+
this.engineTimeBoots = null;
|
|
2099
|
+
this.engineTimeBase = null;
|
|
2100
|
+
this.engineTimeReceivedAt = null;
|
|
2101
|
+
this.latestReceivedEngineTime = null;
|
|
2102
|
+
|
|
2087
2103
|
const dgramMod = options.dgramModule || dgram;
|
|
2088
2104
|
this.dgram = dgramMod.createSocket (this.transport);
|
|
2089
2105
|
this.dgram.unref();
|
|
@@ -2425,6 +2441,21 @@ Session.prototype.onMsg = function (buffer) {
|
|
|
2425
2441
|
if ( ! message.processIncomingSecurity (this.user, req.responseCb) )
|
|
2426
2442
|
return;
|
|
2427
2443
|
|
|
2444
|
+
// RFC 3414 sections 2.3 and 3.2 step 7b: synchronise our notion of the
|
|
2445
|
+
// authoritative engine's time from this message, then discard the message if
|
|
2446
|
+
// it falls outside the time window. Both are no-ops for SNMPv1 and v2c, and
|
|
2447
|
+
// for v3 messages which were not authenticated.
|
|
2448
|
+
this.setEngineTime (message);
|
|
2449
|
+
if ( ! this.isInTimeWindow (message) ) {
|
|
2450
|
+
req.responseCb (new ResponseInvalidError ("Message with engineBoots '"
|
|
2451
|
+
+ message.msgSecurityParameters.msgAuthoritativeEngineBoots
|
|
2452
|
+
+ "' and engineTime '" + message.msgSecurityParameters.msgAuthoritativeEngineTime
|
|
2453
|
+
+ "' is outside the time window of engineBoots '" + this.engineTimeBoots
|
|
2454
|
+
+ "' and engineTime '" + this.getEngineTime ().engineTime + "'",
|
|
2455
|
+
ResponseInvalidCode.ENotInTimeWindow));
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2428
2459
|
if (message.version != req.message.version) {
|
|
2429
2460
|
req.responseCb (new ResponseInvalidError ("Version in request '"
|
|
2430
2461
|
+ req.message.version + "' does not match version in "
|
|
@@ -2974,7 +3005,93 @@ Session.prototype.walk = function () {
|
|
|
2974
3005
|
return this;
|
|
2975
3006
|
};
|
|
2976
3007
|
|
|
3008
|
+
// RFC 3414 section 2.3: between authentic messages, the non-authoritative
|
|
3009
|
+
// engine's notion of the authoritative engine's snmpEngineTime advances with
|
|
3010
|
+
// the local clock. A monotonic clock source is used, so that a step of the
|
|
3011
|
+
// system clock cannot move our notion of time. Returns null while no notion
|
|
3012
|
+
// has been established.
|
|
3013
|
+
Session.prototype.getEngineTime = function () {
|
|
3014
|
+
if ( this.engineTimeReceivedAt == null )
|
|
3015
|
+
return null;
|
|
3016
|
+
var elapsedSeconds = Math.floor ((performance.now () - this.engineTimeReceivedAt) / 1000);
|
|
3017
|
+
var engineBoots = this.engineTimeBoots;
|
|
3018
|
+
var engineTime = this.engineTimeBase + elapsedSeconds;
|
|
3019
|
+
// RFC 3414 section 2.2.2: when snmpEngineTime reaches its maximum value,
|
|
3020
|
+
// snmpEngineBoots is incremented and snmpEngineTime is reset to zero.
|
|
3021
|
+
// snmpEngineBoots latches at its maximum value and never wraps.
|
|
3022
|
+
if ( engineTime > MAX_SIGNED_INT32 ) {
|
|
3023
|
+
engineBoots = Math.min (engineBoots + Math.floor (engineTime / (MAX_SIGNED_INT32 + 1)),
|
|
3024
|
+
MAX_SIGNED_INT32);
|
|
3025
|
+
engineTime = engineTime % (MAX_SIGNED_INT32 + 1);
|
|
3026
|
+
}
|
|
3027
|
+
return {
|
|
3028
|
+
engineBoots: engineBoots,
|
|
3029
|
+
engineTime: engineTime
|
|
3030
|
+
};
|
|
3031
|
+
};
|
|
3032
|
+
|
|
3033
|
+
// RFC 3414 section 3.2 step 7b(1): update our notion of the authoritative
|
|
3034
|
+
// engine's time from an authentic message, but only when that message advances
|
|
3035
|
+
// it - either snmpEngineBoots has increased, or snmpEngineTime is higher than
|
|
3036
|
+
// any snmpEngineTime yet seen for the current snmpEngineBoots. The latter
|
|
3037
|
+
// comparison is against the highest value received rather than against our
|
|
3038
|
+
// locally advanced notion, so that a fast local clock cannot lock the session
|
|
3039
|
+
// out of ever resynchronising.
|
|
3040
|
+
Session.prototype.setEngineTime = function (message) {
|
|
3041
|
+
var params = message.msgSecurityParameters;
|
|
3042
|
+
if ( ! params || ! message.hasAuthentication () )
|
|
3043
|
+
return;
|
|
3044
|
+
var engineBoots = params.msgAuthoritativeEngineBoots;
|
|
3045
|
+
var engineTime = params.msgAuthoritativeEngineTime;
|
|
3046
|
+
var advances = this.engineTimeBoots == null
|
|
3047
|
+
|| engineBoots > this.engineTimeBoots
|
|
3048
|
+
|| ( engineBoots == this.engineTimeBoots
|
|
3049
|
+
&& engineTime > this.latestReceivedEngineTime );
|
|
3050
|
+
if ( ! advances )
|
|
3051
|
+
return;
|
|
3052
|
+
this.engineTimeBoots = engineBoots;
|
|
3053
|
+
this.engineTimeBase = engineTime;
|
|
3054
|
+
this.latestReceivedEngineTime = engineTime;
|
|
3055
|
+
this.engineTimeReceivedAt = performance.now ();
|
|
3056
|
+
};
|
|
3057
|
+
|
|
3058
|
+
// RFC 3414 section 3.2 step 7b(2): an authentic message from the authoritative
|
|
3059
|
+
// engine is outside the time window - and so must be discarded - if our notion
|
|
3060
|
+
// of its snmpEngineBoots has latched at its maximum value, if the message
|
|
3061
|
+
// disagrees with our notion of snmpEngineBoots, or if it disagrees with our
|
|
3062
|
+
// notion of snmpEngineTime by more than the time window. This must be
|
|
3063
|
+
// evaluated after setEngineTime (), so that a message which legitimately
|
|
3064
|
+
// advances our notion of time is never rejected by it.
|
|
3065
|
+
Session.prototype.isInTimeWindow = function (message) {
|
|
3066
|
+
var params = message.msgSecurityParameters;
|
|
3067
|
+
if ( ! params || ! message.hasAuthentication () )
|
|
3068
|
+
return true;
|
|
3069
|
+
var notion = this.getEngineTime ();
|
|
3070
|
+
if ( ! notion )
|
|
3071
|
+
return true;
|
|
3072
|
+
if ( notion.engineBoots == MAX_SIGNED_INT32 )
|
|
3073
|
+
return false;
|
|
3074
|
+
if ( params.msgAuthoritativeEngineBoots != notion.engineBoots )
|
|
3075
|
+
return false;
|
|
3076
|
+
return Math.abs (params.msgAuthoritativeEngineTime - notion.engineTime)
|
|
3077
|
+
<= USM_TIME_WINDOW_SECONDS;
|
|
3078
|
+
};
|
|
3079
|
+
|
|
3080
|
+
// RFC 3414 section 2.3: an outgoing request carries our current notion of the
|
|
3081
|
+
// authoritative engine's snmpEngineBoots and snmpEngineTime, not the values
|
|
3082
|
+
// learned when the session was first synchronised.
|
|
3083
|
+
Session.prototype.advanceEngineTime = function () {
|
|
3084
|
+
if ( ! this.msgSecurityParameters )
|
|
3085
|
+
return;
|
|
3086
|
+
var notion = this.getEngineTime ();
|
|
3087
|
+
if ( ! notion )
|
|
3088
|
+
return;
|
|
3089
|
+
this.msgSecurityParameters.msgAuthoritativeEngineBoots = notion.engineBoots;
|
|
3090
|
+
this.msgSecurityParameters.msgAuthoritativeEngineTime = notion.engineTime;
|
|
3091
|
+
};
|
|
3092
|
+
|
|
2977
3093
|
Session.prototype.sendV3Req = function (pdu, feedCb, responseCb, options, port, allowReport) {
|
|
3094
|
+
this.advanceEngineTime ();
|
|
2978
3095
|
var message = Message.createRequestV3 (this.user, this.msgSecurityParameters, pdu);
|
|
2979
3096
|
var reqOptions = options || {};
|
|
2980
3097
|
var req = new Req (this, message, feedCb, responseCb, reqOptions);
|
package/package.json
CHANGED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { performance } = require('perf_hooks');
|
|
3
|
+
const snmp = require('../');
|
|
4
|
+
|
|
5
|
+
const MAX_SIGNED_INT32 = 2147483647;
|
|
6
|
+
const USM_TIME_WINDOW_SECONDS = 150;
|
|
7
|
+
|
|
8
|
+
// A stand-in for a parsed Message, carrying only what the engine time methods
|
|
9
|
+
// read: the USM security parameters and whether the message was authenticated.
|
|
10
|
+
// The real Message.prototype.hasAuthentication returns a truthy number rather
|
|
11
|
+
// than a boolean, so this mirrors that. The end-to-end suite at the bottom of
|
|
12
|
+
// this file exercises the same code paths with real parsed messages.
|
|
13
|
+
const fakeMessage = (engineBoots, engineTime, authenticated) => ({
|
|
14
|
+
msgSecurityParameters: {
|
|
15
|
+
msgAuthoritativeEngineID: Buffer.from('8000B98380ABCDEF12345678', 'hex'),
|
|
16
|
+
msgAuthoritativeEngineBoots: engineBoots,
|
|
17
|
+
msgAuthoritativeEngineTime: engineTime
|
|
18
|
+
},
|
|
19
|
+
hasAuthentication: () => authenticated ? 1 : 0
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// An SNMPv1 or v2c message has no USM security parameters at all.
|
|
23
|
+
const fakeCommunityMessage = () => ({
|
|
24
|
+
hasAuthentication: () => undefined
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('SNMPv3 authoritative engine time (RFC 3414)', function () {
|
|
28
|
+
|
|
29
|
+
const user = {
|
|
30
|
+
name: 'betty',
|
|
31
|
+
level: snmp.SecurityLevel.authNoPriv,
|
|
32
|
+
authProtocol: snmp.AuthProtocols.sha,
|
|
33
|
+
authKey: 'illhavesomeauth'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let session;
|
|
37
|
+
|
|
38
|
+
beforeEach(function () {
|
|
39
|
+
session = snmp.createV3Session('127.0.0.1', user, { port: 16210 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(function () {
|
|
43
|
+
if (session) {
|
|
44
|
+
session.close();
|
|
45
|
+
session = null;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Pretend the current anchor was established `seconds` ago, without waiting.
|
|
50
|
+
const rewindAnchor = (seconds) => {
|
|
51
|
+
session.engineTimeReceivedAt -= seconds * 1000;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('setEngineTime - time window updates (section 3.2 step 7b(1))', function () {
|
|
55
|
+
|
|
56
|
+
it('establishes a notion of time from the first authentic message', function () {
|
|
57
|
+
session.setEngineTime(fakeMessage(3, 5000, true));
|
|
58
|
+
|
|
59
|
+
assert.strictEqual(session.engineTimeBoots, 3);
|
|
60
|
+
assert.strictEqual(session.engineTimeBase, 5000);
|
|
61
|
+
assert.strictEqual(session.latestReceivedEngineTime, 5000);
|
|
62
|
+
assert.notStrictEqual(session.engineTimeReceivedAt, null);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('ignores a message that was not authenticated', function () {
|
|
66
|
+
session.setEngineTime(fakeMessage(3, 5000, false));
|
|
67
|
+
|
|
68
|
+
assert.strictEqual(session.engineTimeBoots, null);
|
|
69
|
+
assert.strictEqual(session.engineTimeBase, null);
|
|
70
|
+
assert.strictEqual(session.latestReceivedEngineTime, null);
|
|
71
|
+
assert.strictEqual(session.engineTimeReceivedAt, null);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('ignores a message with no USM security parameters', function () {
|
|
75
|
+
session.setEngineTime(fakeCommunityMessage());
|
|
76
|
+
|
|
77
|
+
assert.strictEqual(session.engineTimeBoots, null);
|
|
78
|
+
assert.strictEqual(session.engineTimeReceivedAt, null);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('updates when engineBoots has increased', function () {
|
|
82
|
+
session.setEngineTime(fakeMessage(3, 5000, true));
|
|
83
|
+
session.setEngineTime(fakeMessage(4, 20, true));
|
|
84
|
+
|
|
85
|
+
assert.strictEqual(session.engineTimeBoots, 4);
|
|
86
|
+
assert.strictEqual(session.engineTimeBase, 20);
|
|
87
|
+
assert.strictEqual(session.latestReceivedEngineTime, 20);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('ignores a message whose engineBoots has decreased', function () {
|
|
91
|
+
session.setEngineTime(fakeMessage(3, 5000, true));
|
|
92
|
+
session.setEngineTime(fakeMessage(2, 900000, true));
|
|
93
|
+
|
|
94
|
+
assert.strictEqual(session.engineTimeBoots, 3);
|
|
95
|
+
assert.strictEqual(session.engineTimeBase, 5000);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('updates when engineBoots is unchanged and engineTime has advanced', function () {
|
|
99
|
+
session.setEngineTime(fakeMessage(3, 5000, true));
|
|
100
|
+
session.setEngineTime(fakeMessage(3, 5060, true));
|
|
101
|
+
|
|
102
|
+
assert.strictEqual(session.engineTimeBoots, 3);
|
|
103
|
+
assert.strictEqual(session.engineTimeBase, 5060);
|
|
104
|
+
assert.strictEqual(session.latestReceivedEngineTime, 5060);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('ignores a replay of the highest engineTime already received', function () {
|
|
108
|
+
session.setEngineTime(fakeMessage(3, 5000, true));
|
|
109
|
+
const anchoredAt = session.engineTimeReceivedAt;
|
|
110
|
+
|
|
111
|
+
session.setEngineTime(fakeMessage(3, 5000, true));
|
|
112
|
+
|
|
113
|
+
assert.strictEqual(session.engineTimeBase, 5000);
|
|
114
|
+
assert.strictEqual(session.engineTimeReceivedAt, anchoredAt);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('ignores a replay of an engineTime below the highest already received', function () {
|
|
118
|
+
session.setEngineTime(fakeMessage(3, 5000, true));
|
|
119
|
+
session.setEngineTime(fakeMessage(3, 4000, true));
|
|
120
|
+
|
|
121
|
+
assert.strictEqual(session.engineTimeBase, 5000);
|
|
122
|
+
assert.strictEqual(session.latestReceivedEngineTime, 5000);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Regression test: the comparison in section 3.2 step 7b(1) is against
|
|
126
|
+
// latestReceivedEngineTime, not against the locally advanced notion of
|
|
127
|
+
// snmpEngineTime. Comparing against the advanced notion would mean that
|
|
128
|
+
// a local clock running ahead of the agent could never resynchronise,
|
|
129
|
+
// leaving the session permanently outside the agent's time window.
|
|
130
|
+
it('resynchronises downwards when the local clock has raced ahead of the agent', function () {
|
|
131
|
+
session.setEngineTime(fakeMessage(1, 1000, true));
|
|
132
|
+
rewindAnchor(2000);
|
|
133
|
+
assert.strictEqual(session.getEngineTime().engineTime, 3000);
|
|
134
|
+
|
|
135
|
+
session.setEngineTime(fakeMessage(1, 1100, true));
|
|
136
|
+
|
|
137
|
+
assert.strictEqual(session.engineTimeBase, 1100);
|
|
138
|
+
assert.strictEqual(session.latestReceivedEngineTime, 1100);
|
|
139
|
+
assert.strictEqual(session.getEngineTime().engineTime, 1100);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('getEngineTime - local advancement (section 2.3)', function () {
|
|
144
|
+
|
|
145
|
+
it('has no notion of time before any authentic message', function () {
|
|
146
|
+
assert.strictEqual(session.getEngineTime(), null);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('advances the notion of engineTime with the local clock', function () {
|
|
150
|
+
session.setEngineTime(fakeMessage(2, 1000, true));
|
|
151
|
+
rewindAnchor(75);
|
|
152
|
+
|
|
153
|
+
const notion = session.getEngineTime();
|
|
154
|
+
assert.strictEqual(notion.engineBoots, 2);
|
|
155
|
+
assert.strictEqual(notion.engineTime, 1075);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('does not advance engineBoots while engineTime is in range', function () {
|
|
159
|
+
session.setEngineTime(fakeMessage(2, 1000, true));
|
|
160
|
+
rewindAnchor(100000);
|
|
161
|
+
|
|
162
|
+
assert.strictEqual(session.getEngineTime().engineBoots, 2);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('uses a monotonic clock, so a system clock step cannot move it', function () {
|
|
166
|
+
session.setEngineTime(fakeMessage(2, 1000, true));
|
|
167
|
+
|
|
168
|
+
// engineTimeReceivedAt is a performance.now() reading, which is
|
|
169
|
+
// unrelated to Date.now() and unaffected by system clock steps.
|
|
170
|
+
assert.ok(Math.abs(session.engineTimeReceivedAt - performance.now()) < 1000);
|
|
171
|
+
assert.ok(Math.abs(session.engineTimeReceivedAt - Date.now()) > 1000000);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// RFC 3414 section 2.2.2: when snmpEngineTime reaches its maximum value,
|
|
175
|
+
// snmpEngineBoots is incremented and snmpEngineTime is reset to zero.
|
|
176
|
+
it('rolls engineTime over and increments engineBoots at the 31-bit limit', function () {
|
|
177
|
+
session.setEngineTime(fakeMessage(7, MAX_SIGNED_INT32 - 10, true));
|
|
178
|
+
rewindAnchor(30);
|
|
179
|
+
|
|
180
|
+
const notion = session.getEngineTime();
|
|
181
|
+
assert.strictEqual(notion.engineBoots, 8);
|
|
182
|
+
assert.strictEqual(notion.engineTime, 19);
|
|
183
|
+
assert.ok(notion.engineTime >= 0 && notion.engineTime <= MAX_SIGNED_INT32);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('emits the maximum engineTime rather than rolling over early', function () {
|
|
187
|
+
session.setEngineTime(fakeMessage(7, MAX_SIGNED_INT32 - 10, true));
|
|
188
|
+
rewindAnchor(10);
|
|
189
|
+
|
|
190
|
+
const notion = session.getEngineTime();
|
|
191
|
+
assert.strictEqual(notion.engineBoots, 7);
|
|
192
|
+
assert.strictEqual(notion.engineTime, MAX_SIGNED_INT32);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('latches engineBoots at its maximum value rather than wrapping', function () {
|
|
196
|
+
session.setEngineTime(fakeMessage(MAX_SIGNED_INT32 - 1, 0, true));
|
|
197
|
+
rewindAnchor((MAX_SIGNED_INT32 + 1) * 5);
|
|
198
|
+
|
|
199
|
+
assert.strictEqual(session.getEngineTime().engineBoots, MAX_SIGNED_INT32);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('isInTimeWindow - timeliness checks (section 3.2 step 7b(2))', function () {
|
|
204
|
+
|
|
205
|
+
it('accepts a message when no notion of time has been established', function () {
|
|
206
|
+
assert.strictEqual(session.isInTimeWindow(fakeMessage(1, 1000, true)), true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('accepts an unauthenticated message without checking the window', function () {
|
|
210
|
+
session.setEngineTime(fakeMessage(1, 1000, true));
|
|
211
|
+
|
|
212
|
+
assert.strictEqual(session.isInTimeWindow(fakeMessage(1, 999999, false)), true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('accepts a message with no USM security parameters', function () {
|
|
216
|
+
assert.strictEqual(session.isInTimeWindow(fakeCommunityMessage()), true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('accepts the message that just established the notion of time', function () {
|
|
220
|
+
const message = fakeMessage(1, 1000, true);
|
|
221
|
+
session.setEngineTime(message);
|
|
222
|
+
|
|
223
|
+
assert.strictEqual(session.isInTimeWindow(message), true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('accepts a message inside the time window', function () {
|
|
227
|
+
session.setEngineTime(fakeMessage(1, 1000, true));
|
|
228
|
+
|
|
229
|
+
assert.strictEqual(session.isInTimeWindow(fakeMessage(1, 900, true)), true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('accepts a message exactly at the edge of the time window', function () {
|
|
233
|
+
session.setEngineTime(fakeMessage(1, 1000, true));
|
|
234
|
+
|
|
235
|
+
assert.strictEqual(
|
|
236
|
+
session.isInTimeWindow(fakeMessage(1, 1000 - USM_TIME_WINDOW_SECONDS, true)), true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('rejects a message more than the time window behind our notion', function () {
|
|
240
|
+
session.setEngineTime(fakeMessage(1, 1000, true));
|
|
241
|
+
|
|
242
|
+
assert.strictEqual(
|
|
243
|
+
session.isInTimeWindow(fakeMessage(1, 1000 - USM_TIME_WINDOW_SECONDS - 1, true)),
|
|
244
|
+
false);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('rejects a message whose engineBoots disagrees with our notion', function () {
|
|
248
|
+
session.setEngineTime(fakeMessage(5, 1000, true));
|
|
249
|
+
|
|
250
|
+
assert.strictEqual(session.isInTimeWindow(fakeMessage(4, 1000, true)), false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('rejects every message once engineBoots has latched at its maximum', function () {
|
|
254
|
+
session.setEngineTime(fakeMessage(MAX_SIGNED_INT32, 1000, true));
|
|
255
|
+
|
|
256
|
+
assert.strictEqual(session.isInTimeWindow(fakeMessage(MAX_SIGNED_INT32, 1000, true)),
|
|
257
|
+
false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('measures the window against the advanced notion, not the anchor', function () {
|
|
261
|
+
session.setEngineTime(fakeMessage(1, 1000, true));
|
|
262
|
+
rewindAnchor(200);
|
|
263
|
+
|
|
264
|
+
// Our notion is now 1200, so the original anchor value of 1000 has
|
|
265
|
+
// fallen outside the window, while 1200 is at its centre.
|
|
266
|
+
assert.strictEqual(session.isInTimeWindow(fakeMessage(1, 1000, true)), false);
|
|
267
|
+
assert.strictEqual(session.isInTimeWindow(fakeMessage(1, 1200, true)), true);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('advanceEngineTime - outgoing requests (section 2.3)', function () {
|
|
272
|
+
|
|
273
|
+
it('does nothing when the session has no cached security parameters', function () {
|
|
274
|
+
session.setEngineTime(fakeMessage(1, 1000, true));
|
|
275
|
+
assert.strictEqual(session.msgSecurityParameters, undefined);
|
|
276
|
+
|
|
277
|
+
session.advanceEngineTime();
|
|
278
|
+
|
|
279
|
+
assert.strictEqual(session.msgSecurityParameters, undefined);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('does nothing when no notion of time has been established', function () {
|
|
283
|
+
session.msgSecurityParameters = {
|
|
284
|
+
msgAuthoritativeEngineBoots: 0,
|
|
285
|
+
msgAuthoritativeEngineTime: 0
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
session.advanceEngineTime();
|
|
289
|
+
|
|
290
|
+
assert.strictEqual(session.msgSecurityParameters.msgAuthoritativeEngineBoots, 0);
|
|
291
|
+
assert.strictEqual(session.msgSecurityParameters.msgAuthoritativeEngineTime, 0);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('writes the advanced notion into the cached security parameters', function () {
|
|
295
|
+
session.msgSecurityParameters = {
|
|
296
|
+
msgAuthoritativeEngineBoots: 1,
|
|
297
|
+
msgAuthoritativeEngineTime: 1000
|
|
298
|
+
};
|
|
299
|
+
session.setEngineTime(fakeMessage(1, 1000, true));
|
|
300
|
+
rewindAnchor(600);
|
|
301
|
+
|
|
302
|
+
session.advanceEngineTime();
|
|
303
|
+
|
|
304
|
+
assert.strictEqual(session.msgSecurityParameters.msgAuthoritativeEngineBoots, 1);
|
|
305
|
+
assert.strictEqual(session.msgSecurityParameters.msgAuthoritativeEngineTime, 1600);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('carries the advanced engineTime on the outgoing request', function () {
|
|
309
|
+
session.msgSecurityParameters = {
|
|
310
|
+
msgAuthoritativeEngineID: Buffer.from('8000B98380ABCDEF12345678', 'hex'),
|
|
311
|
+
msgAuthoritativeEngineBoots: 1,
|
|
312
|
+
msgAuthoritativeEngineTime: 1000
|
|
313
|
+
};
|
|
314
|
+
session.setEngineTime(fakeMessage(1, 1000, true));
|
|
315
|
+
rewindAnchor(420);
|
|
316
|
+
|
|
317
|
+
let sent;
|
|
318
|
+
session.send = (req) => { sent = req; };
|
|
319
|
+
session.get(['1.3.6.1.2.1.1.1.0'], function () {});
|
|
320
|
+
|
|
321
|
+
assert.ok(sent, 'a request should have been handed to send()');
|
|
322
|
+
assert.strictEqual(sent.message.msgSecurityParameters.msgAuthoritativeEngineBoots, 1);
|
|
323
|
+
assert.strictEqual(sent.message.msgSecurityParameters.msgAuthoritativeEngineTime, 1420);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// These tests drive a real agent over the loopback interface, so the engine
|
|
329
|
+
// time methods are fed genuine parsed and authenticated Messages rather than
|
|
330
|
+
// the stand-in above. They confirm the wiring in Session.onMsg as well as the
|
|
331
|
+
// assumption that Message.hasAuthentication distinguishes the authenticated
|
|
332
|
+
// GetResponse from the unauthenticated discovery Report.
|
|
333
|
+
describe('SNMPv3 authoritative engine time over the wire', function () {
|
|
334
|
+
|
|
335
|
+
const agentPort = 16211;
|
|
336
|
+
const engineID = '8000B98380DEADBEEF00000001';
|
|
337
|
+
const sysDescrOid = '1.3.6.1.2.1.1.1.0';
|
|
338
|
+
|
|
339
|
+
const authUser = {
|
|
340
|
+
name: 'betty',
|
|
341
|
+
level: snmp.SecurityLevel.authNoPriv,
|
|
342
|
+
authProtocol: snmp.AuthProtocols.sha,
|
|
343
|
+
authKey: 'illhavesomeauth'
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const authPrivUser = {
|
|
347
|
+
name: 'wilma',
|
|
348
|
+
level: snmp.SecurityLevel.authPriv,
|
|
349
|
+
authProtocol: snmp.AuthProtocols.sha,
|
|
350
|
+
authKey: 'illhavesomeauth',
|
|
351
|
+
privProtocol: snmp.PrivProtocols.aes,
|
|
352
|
+
privKey: 'andsomepriv'
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const noAuthUser = {
|
|
356
|
+
name: 'fred',
|
|
357
|
+
level: snmp.SecurityLevel.noAuthNoPriv
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
let agent;
|
|
361
|
+
let sessions;
|
|
362
|
+
|
|
363
|
+
// The agent echoes the request's engineBoots and engineTime back in its
|
|
364
|
+
// response, so to model an authoritative engine with a clock of its own we
|
|
365
|
+
// overwrite those values on the way out. Clearing the cached buffer makes
|
|
366
|
+
// the message re-serialise and re-authenticate over the new values, so the
|
|
367
|
+
// session receives a properly authenticated message throughout.
|
|
368
|
+
let agentEngineBoots = null;
|
|
369
|
+
let agentEngineTime = null;
|
|
370
|
+
// Values used for the unauthenticated discovery Report only, so that tests
|
|
371
|
+
// can tell which message a session actually synchronised from.
|
|
372
|
+
let agentReportEngineBoots = null;
|
|
373
|
+
let agentReportEngineTime = null;
|
|
374
|
+
|
|
375
|
+
before(function () {
|
|
376
|
+
agent = snmp.createAgent({ port: agentPort, engineID: engineID, disableAuthorization: false },
|
|
377
|
+
function () {});
|
|
378
|
+
const authorizer = agent.getAuthorizer();
|
|
379
|
+
authorizer.addUser(authUser);
|
|
380
|
+
authorizer.addUser(authPrivUser);
|
|
381
|
+
authorizer.addUser(noAuthUser);
|
|
382
|
+
agent.registerProvider({
|
|
383
|
+
name: 'sysDescr',
|
|
384
|
+
type: snmp.MibProviderType.Scalar,
|
|
385
|
+
oid: '1.3.6.1.2.1.1.1',
|
|
386
|
+
scalarType: snmp.ObjectType.OctetString,
|
|
387
|
+
maxAccess: snmp.MaxAccess['read-only']
|
|
388
|
+
});
|
|
389
|
+
agent.getMib().setScalarValue('sysDescr', 'engine time test agent');
|
|
390
|
+
|
|
391
|
+
const listenerSend = agent.listener.send.bind(agent.listener);
|
|
392
|
+
agent.listener.send = function (message, rinfo, socket) {
|
|
393
|
+
const isReport = message.pdu && message.pdu.type === snmp.PduType.Report;
|
|
394
|
+
const boots = isReport && agentReportEngineBoots !== null
|
|
395
|
+
? agentReportEngineBoots
|
|
396
|
+
: agentEngineBoots;
|
|
397
|
+
const time = isReport && agentReportEngineTime !== null
|
|
398
|
+
? agentReportEngineTime
|
|
399
|
+
: agentEngineTime;
|
|
400
|
+
if (time !== null && message.msgSecurityParameters
|
|
401
|
+
&& message.msgSecurityParameters.msgAuthoritativeEngineBoots !== undefined) {
|
|
402
|
+
message.msgSecurityParameters.msgAuthoritativeEngineBoots = boots;
|
|
403
|
+
message.msgSecurityParameters.msgAuthoritativeEngineTime = time;
|
|
404
|
+
message.buffer = null;
|
|
405
|
+
}
|
|
406
|
+
return listenerSend(message, rinfo, socket);
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
after(function (done) {
|
|
411
|
+
agent.close(() => done());
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
beforeEach(function () {
|
|
415
|
+
sessions = [];
|
|
416
|
+
agentEngineBoots = null;
|
|
417
|
+
agentEngineTime = null;
|
|
418
|
+
agentReportEngineBoots = null;
|
|
419
|
+
agentReportEngineTime = null;
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
afterEach(function () {
|
|
423
|
+
sessions.forEach((s) => s.close());
|
|
424
|
+
sessions = [];
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const createSession = (user) => {
|
|
428
|
+
const session = snmp.createV3Session('127.0.0.1', user,
|
|
429
|
+
{ port: agentPort, timeout: 2000, retries: 0 });
|
|
430
|
+
sessions.push(session);
|
|
431
|
+
return session;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const get = (session) => new Promise((resolve, reject) => {
|
|
435
|
+
session.get([sysDescrOid], (error, varbinds) => {
|
|
436
|
+
if (error) {
|
|
437
|
+
reject(error);
|
|
438
|
+
} else if (varbinds[0] instanceof Error) {
|
|
439
|
+
reject(varbinds[0]);
|
|
440
|
+
} else {
|
|
441
|
+
resolve(varbinds);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('synchronises from an authenticated GetResponse, not the discovery Report',
|
|
447
|
+
async function () {
|
|
448
|
+
agentEngineBoots = 12;
|
|
449
|
+
agentEngineTime = 345678;
|
|
450
|
+
// The discovery Report is not authenticated, so these values must be
|
|
451
|
+
// ignored no matter how far ahead of the real engine time they are.
|
|
452
|
+
agentReportEngineBoots = 99;
|
|
453
|
+
agentReportEngineTime = MAX_SIGNED_INT32 - 1;
|
|
454
|
+
|
|
455
|
+
const session = createSession(authUser);
|
|
456
|
+
await get(session);
|
|
457
|
+
|
|
458
|
+
assert.strictEqual(session.engineTimeBoots, 12);
|
|
459
|
+
assert.strictEqual(session.engineTimeBase, 345678);
|
|
460
|
+
assert.strictEqual(session.latestReceivedEngineTime, 345678);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('synchronises over authPriv as well as authNoPriv', async function () {
|
|
464
|
+
agentEngineBoots = 4;
|
|
465
|
+
agentEngineTime = 99000;
|
|
466
|
+
|
|
467
|
+
const session = createSession(authPrivUser);
|
|
468
|
+
await get(session);
|
|
469
|
+
|
|
470
|
+
assert.strictEqual(session.engineTimeBoots, 4);
|
|
471
|
+
assert.strictEqual(session.engineTimeBase, 99000);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('never synchronises for a noAuthNoPriv user, since no message is authentic',
|
|
475
|
+
async function () {
|
|
476
|
+
agentEngineBoots = 4;
|
|
477
|
+
agentEngineTime = 99000;
|
|
478
|
+
|
|
479
|
+
const session = createSession(noAuthUser);
|
|
480
|
+
await get(session);
|
|
481
|
+
|
|
482
|
+
assert.strictEqual(session.engineTimeBoots, null);
|
|
483
|
+
assert.strictEqual(session.engineTimeReceivedAt, null);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('sends an advanced engineTime on a later request', async function () {
|
|
487
|
+
agentEngineBoots = 2;
|
|
488
|
+
agentEngineTime = 1000;
|
|
489
|
+
|
|
490
|
+
const session = createSession(authUser);
|
|
491
|
+
await get(session);
|
|
492
|
+
|
|
493
|
+
// Pretend 500 seconds have passed since the response arrived, and let
|
|
494
|
+
// the agent's clock advance by the same amount.
|
|
495
|
+
session.engineTimeReceivedAt -= 500 * 1000;
|
|
496
|
+
agentEngineTime = 1500;
|
|
497
|
+
|
|
498
|
+
const sent = [];
|
|
499
|
+
const send = session.send.bind(session);
|
|
500
|
+
session.send = function (req, noWait) {
|
|
501
|
+
sent.push(req.message.msgSecurityParameters.msgAuthoritativeEngineTime);
|
|
502
|
+
return send(req, noWait);
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
await get(session);
|
|
506
|
+
|
|
507
|
+
assert.deepStrictEqual(sent, [1500]);
|
|
508
|
+
assert.strictEqual(session.engineTimeBase, 1500);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('tracks the agent across a reboot that resets its engineTime', async function () {
|
|
512
|
+
agentEngineBoots = 2;
|
|
513
|
+
agentEngineTime = 900000;
|
|
514
|
+
|
|
515
|
+
const session = createSession(authUser);
|
|
516
|
+
await get(session);
|
|
517
|
+
assert.strictEqual(session.engineTimeBase, 900000);
|
|
518
|
+
|
|
519
|
+
agentEngineBoots = 3;
|
|
520
|
+
agentEngineTime = 12;
|
|
521
|
+
await get(session);
|
|
522
|
+
|
|
523
|
+
assert.strictEqual(session.engineTimeBoots, 3);
|
|
524
|
+
assert.strictEqual(session.engineTimeBase, 12);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('reports a response that falls outside the time window', async function () {
|
|
528
|
+
agentEngineBoots = 2;
|
|
529
|
+
agentEngineTime = 1000;
|
|
530
|
+
|
|
531
|
+
const session = createSession(authUser);
|
|
532
|
+
await get(session);
|
|
533
|
+
|
|
534
|
+
// A response claiming an engineTime below the highest already received
|
|
535
|
+
// cannot advance our notion of time, and is far enough behind it to be
|
|
536
|
+
// outside the window - RFC 3414 section 3.2 step 7b(2) discards it.
|
|
537
|
+
agentEngineTime = 1000 - 151;
|
|
538
|
+
|
|
539
|
+
await assert.rejects(get(session), (error) => {
|
|
540
|
+
assert.ok(error instanceof snmp.ResponseInvalidError, 'expected ResponseInvalidError');
|
|
541
|
+
assert.strictEqual(error.code, snmp.ResponseInvalidCode.ENotInTimeWindow);
|
|
542
|
+
return true;
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Our notion of the agent's time is left untouched by the discard.
|
|
546
|
+
assert.strictEqual(session.engineTimeBoots, 2);
|
|
547
|
+
assert.strictEqual(session.latestReceivedEngineTime, 1000);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('still accepts a response that is only slightly out of order', async function () {
|
|
551
|
+
agentEngineBoots = 2;
|
|
552
|
+
agentEngineTime = 1000;
|
|
553
|
+
|
|
554
|
+
const session = createSession(authUser);
|
|
555
|
+
await get(session);
|
|
556
|
+
|
|
557
|
+
agentEngineTime = 1000 - 20;
|
|
558
|
+
const varbinds = await get(session);
|
|
559
|
+
|
|
560
|
+
assert.strictEqual(varbinds[0].oid, sysDescrOid);
|
|
561
|
+
assert.strictEqual(session.latestReceivedEngineTime, 1000);
|
|
562
|
+
});
|
|
563
|
+
});
|