net-snmp 3.26.2 → 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.
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: propose-action
3
+ description: Review a GitHub issue or PR by number and propose a concrete action plan — either a reasoned recommendation not to action it, or the code and test changes required to action it. Use when the user asks to "propose an action" for an issue/PR, "look at issue/PR #N", or invokes /propose-action with an issue or PR number. Ends by asking whether to proceed, proceed with modifications, or abandon.
4
+ ---
5
+
6
+ # Propose Action
7
+
8
+ ## When to use
9
+
10
+ - The user asks to review a GitHub issue or PR and recommend what to do about it.
11
+ - The user invokes `/propose-action <number>`.
12
+
13
+ This skill **plans only** for steps 1–5 — it does not edit code, run tests, commit, push, or comment on the issue/PR during the proposal phase. Implementation happens after the user approves the plan in step 5's closing question. Once the user has approved and you have carried out the agreed actions, step 6 produces a ready-to-paste reply to the issue/PR aligned with what was actually done — but never posts it.
14
+
15
+ ## Arguments
16
+
17
+ - `<number>` (required) — the GitHub issue or PR number. Bare integer, or `#N`, or a full GitHub URL are all acceptable forms.
18
+
19
+ If the number is missing, ambiguous, or does not resolve to an issue/PR in the current repository, stop and ask the user to clarify.
20
+
21
+ ## Workflow
22
+
23
+ ### 1. Identify the target
24
+
25
+ - Determine the current repository: `gh repo view --json nameWithOwner -q .nameWithOwner`.
26
+ - Resolve the number to either an issue or a PR. GitHub numbers them in the same namespace, so try PR first, then issue:
27
+ - `gh pr view <number> --json number,title,state,author,body,headRefName,baseRefName,isDraft,mergeable,additions,deletions,changedFiles,labels,comments,reviews`
28
+ - If that fails, `gh issue view <number> --json number,title,state,author,body,labels,comments,assignees`
29
+ - Record whether the target is an **issue** or a **PR** — the two branches below differ.
30
+
31
+ ### 2a. If the target is an issue
32
+
33
+ Gather context in parallel:
34
+ - The issue body and all comments (from the `gh issue view` JSON above).
35
+ - `gh issue view <number> --json linkedPullRequests` — check for linked PRs that may already address it.
36
+ - Identify the files or subsystems the issue points at. Search the repo (`Grep`, `Glob`, `Read`) for the relevant code. Do not guess — read the actual files the issue describes.
37
+ - If the issue references specific OIDs, error messages, function names, or SNMP behaviours, locate them in the source.
38
+
39
+ ### 2b. If the target is a PR
40
+
41
+ Gather context in parallel:
42
+ - PR body, comments, and reviews (from the `gh pr view` JSON above).
43
+ - `gh pr diff <number>` — the full diff.
44
+ - `gh pr view <number> --json files -q '.files[].path'` — changed files.
45
+ - Read the **current** version of each changed file on the base branch so you understand what the PR is replacing, not just what it adds.
46
+ - If the PR closes or references an issue (via `Fixes #N`, `Closes #N`, etc.), also fetch that issue for context.
47
+ - Check CI status if available: `gh pr checks <number>`.
48
+
49
+ ### 3. Form a judgement
50
+
51
+ Decide which of these recommendations fits, and be willing to recommend **against** actioning:
52
+
53
+ - **Do not action** — the issue is invalid, already fixed, out of scope, based on a misunderstanding, a duplicate, or the PR is the wrong approach / would regress behaviour / conflicts with project direction. Explain *why* clearly, citing the code or prior commits that support the conclusion.
54
+ - **Action as-is** — the issue is valid and the fix is clear, or the PR is correct and should be merged as submitted.
55
+ - **Action with modifications** — the issue is valid but the obvious fix is wrong; or the PR has the right idea but needs specific changes before it can be merged.
56
+ - **Needs more information** — the report is plausible but underspecified. List the exact questions to ask the reporter / author before any code change is possible.
57
+
58
+ Base the judgement on what the code actually does today, not on what the issue/PR claims. If the two disagree, surface the disagreement.
59
+
60
+ ### 4. Draft the proposal
61
+
62
+ Structure the written proposal in this order. Keep it concrete — name files, line numbers, function names, and the specific behaviours being changed.
63
+
64
+ 1. **Target** — `Issue #N: <title>` or `PR #N: <title>`, plus one sentence on its state (open/closed/draft/merged, CI status for PRs).
65
+ 2. **Summary of the report** — one short paragraph in your own words describing what the reporter is asking for or proposing. Do not just quote the body.
66
+ 3. **Findings** — what the code currently does, whether the report is accurate, and any related context (linked issues/PRs, prior commits, existing tests covering this area). Cite `file:line` locations.
67
+ 4. **Recommendation** — one of the four options from step 3, stated plainly in a sentence or two.
68
+ 5. **Proposed changes** (omit if the recommendation is *do not action* or *needs more information*):
69
+ - **Code** — for each file to change, list the edit as a bullet: `path/to/file.js:<line-range> — <what changes and why>`. For non-trivial edits, include a short before/after sketch (a few lines, not a full diff).
70
+ - **Tests** — list tests to **add**, **modify**, or **delete**, each with the file and a one-line description of what the test asserts. If no test changes are needed, say so explicitly and justify (e.g. "covered by existing test at `test/x.test.js:42`").
71
+ - **Docs / README** — note any README or inline-doc updates required. Release-note bullets are handled by `/prepare-release`; do not pre-empt them here.
72
+ 6. **Risks and unknowns** — anything that could make the plan wrong: behaviours you could not verify, edge cases the tests would not catch, compatibility concerns, or assumptions about the reporter's environment.
73
+ 7. **Out of scope** — closely-related issues you noticed while investigating but are **not** proposing to fix in this action. Keeps the scope honest.
74
+
75
+ ### 5. Closing question
76
+
77
+ End the response with exactly one question, offering three choices:
78
+
79
+ > Proceed with this plan as-is, proceed with modifications (tell me what to change), or abandon it?
80
+
81
+ Do not start implementing until the user answers. If the user says "proceed", begin implementing the plan in the next turn. If the user asks for modifications, revise the proposal and ask the same question again. If the user says to abandon, stop and skip step 6.
82
+
83
+ ### 6. Ready-to-paste issue/PR reply
84
+
85
+ After the agreed actions have been carried out (code edits, tests, docs, or a reasoned decision not to action), produce a single ready-to-paste reply for the issue/PR. This is the **last** thing you output, rendered in a fenced block or delimited by `---` so the user can copy it cleanly.
86
+
87
+ The reply must be aligned with **what actually happened**, not with the original plan — if the user asked for modifications during implementation, reflect the modified outcome. If a step was skipped or failed, say so honestly; do not describe work that was not done.
88
+
89
+ Tailor the content to the recommendation from step 3:
90
+
91
+ - **Action as-is / with modifications** — acknowledge the report, give the user a short, concrete summary of the fix (with the key code change or snippet inline if it helps them adopt it), point at the files/sections that changed (e.g. "updated in `README.md` under *Scalar providers*"), and note when it will ship (e.g. "will land in the next release" — do not invent a version number unless `package.json` has already been bumped). If there's a workaround the reporter can apply before the release, include it.
92
+ - **Do not action** — explain the reasoning in the reporter's terms, citing the code or prior history that supports the decision. Be respectful: the reporter put effort into the report. Offer an alternative path if one exists (a different API, a related issue, a config change). If the issue should be closed, say so; do not close it yourself.
93
+ - **Needs more information** — list the specific questions you need answered, each as its own bullet. Explain briefly *why* each is needed so the reporter can see what would unblock progress.
94
+
95
+ Style:
96
+
97
+ - Match the tone of prior replies in this repository if they are visible in the issue/PR thread — don't suddenly adopt a different register.
98
+ - Write as the maintainer would: first person, conversational, no corporate voice, no marketing copy, no emoji unless the repo's existing replies use them.
99
+ - Do not include a signature, do not add attribution trailers (no "generated by", no `Co-Authored-By`), do not tag the reporter more than once.
100
+ - Keep it focused — one screen of text is almost always enough. If the reply needs code blocks, prefer short, complete snippets over long diffs.
101
+
102
+ Hand the reply to the user with one sentence above it saying where to paste it (e.g. "Ready-to-paste reply for issue #297:"). Do **not** run `gh issue comment` / `gh pr comment` / `gh issue close` / `gh pr merge` — the user posts it.
103
+
104
+ ## Rules
105
+
106
+ - **Never** edit files, run tests, or modify git state during steps 1–5. Planning only.
107
+ - **Never** post a comment on the issue/PR, add labels, request reviews, merge, or close — not in step 6, not ever within this skill. Those are the user's decision.
108
+ - **Never** fabricate line numbers or function names — if you cite `file:line`, you must have read it.
109
+ - If `gh` is not authenticated or the number does not exist, stop and report the error verbatim.
110
+ - If the issue/PR is in a different repository than the current working directory, stop and confirm with the user before proceeding — the plan would target the wrong codebase.
111
+ - Keep the proposal focused. A plan that touches ten files for a one-line bug is a signal to re-read the issue, not to write a bigger plan.
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: publish-release
3
+ description: Publish the currently-committed release by pushing master to origin, publishing to npm, tagging the commit with the package.json version, and pushing the tag. Use when the user asks to "publish the release", "push and publish", or invokes /publish-release. Stops immediately on any failure.
4
+ ---
5
+
6
+ # Publish Release
7
+
8
+ ## When to use
9
+
10
+ - After `/prepare-release` and the release commit have both been completed — this skill assumes the release commit (version bump + README entry + fix) is already `HEAD` on `master`.
11
+ - User asks to publish / ship / release to npm.
12
+ - User invokes `/publish-release`.
13
+
14
+ Run after `/prepare-release` + `/git-commit`, never in place of them. This skill does not run lint, tests, or modify any files.
15
+
16
+ ## Workflow
17
+
18
+ Run steps in order. **If any step fails, stop immediately, report the failure, and do not proceed.** There is no rollback — each step is externally observable (a push, a publish, a tag on origin), so a mid-flight failure leaves the release in a partial state the user will need to reason about. Be explicit about which steps succeeded and which did not.
19
+
20
+ ### 1. Pre-flight checks (parallel)
21
+
22
+ - `git rev-parse --abbrev-ref HEAD` — confirm the current branch is `master`. If not, stop and ask the user.
23
+ - `git status --short` — confirm the working tree is clean. If not, stop — the release commit must already exist as `HEAD`, with nothing else pending.
24
+ - Read the current version from `package.json` (this becomes the tag name).
25
+ - `git tag --list <version>` — confirm the tag does not already exist locally. If it does, stop.
26
+ - `git ls-remote --tags origin refs/tags/<version>` — confirm the tag does not already exist on the remote. If it does, stop.
27
+ - `git log -1 --format=%s` — capture the subject of the HEAD commit for the summary.
28
+
29
+ ### 2. Push master to origin
30
+
31
+ Run `git push origin master`. **Stop on non-zero exit.** Report the output.
32
+
33
+ ### 3. Publish to npm
34
+
35
+ Run `npm publish`. **Stop on non-zero exit.**
36
+
37
+ Notes:
38
+ - If the registry prompts for a one-time password (2FA OTP), relay the prompt to the user and wait for their input. Do not attempt to bypass 2FA.
39
+ - If the registry rejects with `403` or `You do not have permission`, stop and surface the full error — the user may need to `npm login` or check their credentials.
40
+ - If the registry rejects with `You cannot publish over the previously published versions`, stop — the version was not bumped correctly upstream of this skill.
41
+
42
+ ### 4. Tag the release
43
+
44
+ Tags in this repo are bare version numbers (no `v` prefix) — verified against the existing tag list.
45
+
46
+ Run `git tag <version>` where `<version>` is the exact value read from `package.json` in step 1 (e.g., `3.26.2`, not `v3.26.2`).
47
+
48
+ **Stop on non-zero exit.**
49
+
50
+ ### 5. Push the tag
51
+
52
+ Run `git push origin <version>`. **Stop on non-zero exit.**
53
+
54
+ ### 6. Summary
55
+
56
+ Report to the user, concisely:
57
+ - The version that was published.
58
+ - Confirmation of: master pushed, npm publish succeeded, tag created, tag pushed.
59
+ - The release commit subject (from pre-flight).
60
+ - The npm package URL, if it can be derived from `package.json` name (`https://www.npmjs.com/package/<name>`).
61
+
62
+ ## Things to watch for
63
+
64
+ - If step 2 (push master) succeeds but step 3 (npm publish) fails, the commit is on GitHub but not on npm. Do not tag — the user must either resolve the npm issue and re-run publish, or roll back the push.
65
+ - If step 3 (npm publish) succeeds but step 4 (tag) or step 5 (tag push) fails, the package is on npm but not tagged. Surface this clearly — the user needs to manually create / push the tag, and an un-tagged published release is a fix-it-now situation, not something to leave for later.
66
+ - Never use `--force`, `--no-verify`, or any flag that bypasses checks, even to recover from a mid-flight failure. Always ask the user.
67
+ - Never create or push tags for versions other than the one in `package.json`.
68
+ - Never run this skill on a branch other than `master` without explicit user confirmation.
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`
@@ -1950,7 +1951,12 @@ var myScalarProvider = {
1950
1951
  scalarType: snmp.ObjectType.OctetString,
1951
1952
  maxAccess: snmp.MaxAccess["read-write"],
1952
1953
  handler: function (mibRequest) {
1953
- // e.g. can update the MIB data before responding to the request here
1954
+ // For a dynamically-calculated read-only scalar, publish the current value
1955
+ // by assigning to the instance node before calling done(). For read-write scalars,
1956
+ // the agent has already cast and validated mibRequest.setValue by the
1957
+ // time the handler runs, and will commit it on your behalf — the handler
1958
+ // usually just needs to call done().
1959
+ mibRequest.instanceNode.value = yourFunctionToComputeCurrentValue ();
1954
1960
  mibRequest.done ();
1955
1961
  }
1956
1962
  // Note: handler is optional for scalar providers - if omitted,
@@ -2174,6 +2180,29 @@ objects`, below, for details.
2174
2180
  with the instance OID being operated on, and an `operation` field with the request type from
2175
2181
  `snmp.PduType`. If the `MibRequest` is for a `SetRequest` PDU, then variables `setValue` and
2176
2182
  `setType` contain the value and type received in the `SetRequest` varbind.
2183
+
2184
+ How the handler should interact with the MIB value depends on the request type:
2185
+
2186
+ * For `GetRequest`, `GetNextRequest` and `GetBulkRequest`, the response varbind is taken
2187
+ from `mibRequest.instanceNode.value` at the moment `done()` is called. A handler that
2188
+ serves a dynamically-computed read-only scalar (e.g. `sysUpTime`) should therefore
2189
+ assign the current value to `mibRequest.instanceNode.value` before calling `done()`.
2190
+ Equivalent and validated alternative: call `mib.setScalarValue(providerName, value)`
2191
+ from the handler, which casts and constraint-checks the value before storing it.
2192
+ For table columns the pattern is the same — assign to `mibRequest.instanceNode.value`.
2193
+ * For `SetRequest`, the agent has already cast and validated the incoming value into
2194
+ `mibRequest.setValue` before the handler runs, and will commit it to
2195
+ `mibRequest.instanceNode.value` on your behalf during the commit pass. A handler
2196
+ typically only needs to call `done()` (or `done({errorStatus: ...})` to reject the set).
2197
+ You generally don't need to assign to `instanceNode.value` yourself on the set path.
2198
+
2199
+ If you do need to validate an externally-supplied value inside a handler, three helpers
2200
+ are exposed on the public API: `ObjectTypeUtil.castSetValue(type, value, constraints)`
2201
+ casts and range/constraint-checks a value (throwing on bad input);
2202
+ `MibNode#validateValue(type, value)` returns a boolean against the node's provider
2203
+ constraints; and `MibNode#getConstraintsFromProvider()` returns the constraints object
2204
+ for the current instance node.
2205
+
2177
2206
  * `constraints` *(optional for scalar types)* - an optional object to specify constraints for
2178
2207
  integer-based enumerated types, integer range restrictions and string size restrictions. Note that
2179
2208
  table columns can specify such `constraints` in an identical way, except that these are stored under
@@ -3723,6 +3752,16 @@ Example programs are included under the module's `example` directory.
3723
3752
 
3724
3753
  * Fix TypeError in oidInSubtree when varbind OID is null
3725
3754
 
3755
+ # Version 3.26.3 - 21/04/2026
3756
+
3757
+ * Document how MIB scalar and table handlers should interact with `mibRequest.instanceNode.value` for Get vs Set operations
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
+
3726
3765
  # License
3727
3766
 
3728
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "net-snmp",
3
- "version": "3.26.2",
3
+ "version": "3.27.0",
4
4
  "description": "JavaScript implementation of the Simple Network Management Protocol (SNMP)",
5
5
  "author": "Mark Abrahams <mark@abrahams.co.nz>",
6
6
  "license": "MIT",
@@ -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
+ });