Repository navigation
feat: act on SNMPv3 Reports sent without authentication - #48
Merged
Merged
Conversation
Scenarios for the Reports the REPORT resync will act on: an agent that answers stale requests with notInTimeWindow Reports at noAuthNoPriv after an engine time jump or a reboot (authNoPriv, AES and DES), a Report naming another engine, forged engine times, Reports for another or an earlier attempt's msgID, an unknownEngineID Report for another message, an encrypted Report flagged noAuthNoPriv, Reports with two or no varbinds, and an engine ID change at authPriv. Today the client discards every one of them.
The capture's Report (request ID 0, matched by its msgID alone), a forged authNoPriv Report without a user name and engine ID, an unauthenticated Report with a varbind that does not decode or with another security model, an agent silent after an unauthenticated unknownEngineID Report, a hook changing the security model after an unauthenticated notInTimeWindow Report, an authNoPriv Report to a noAuthNoPriv request of a user with keys, and SNMPv3 GetResponse and unknownEngineID Report answers to an SNMPv2c request. The transcript names a plaintext scoped PDU it cannot decode as such instead of calling it encrypted.
The engine accepted any reply with an empty user name and engine ID without its digest, after discovery too: a GetResponse so made was returned as an answer and emptied the client's engine ID, and a forged authNoPriv Report so made counted as authenticated. Once the client knows the agent's engine ID, a reply is now checked with the client's flags and keys without the exception. UnmarshalTrap keeps it.
Some agents (UniFi access points) answer a request whose engine time is stale with a notInTimeWindow Report at noAuthNoPriv. The client checked every reply of an authenticated request against its digest, discarded the Report and timed out with the same stale time on every request. A Report without the authentication its request asked for is now read when its scoped PDU is plaintext, it has the request's security model and exactly one varbind, and it carries the msgID of one of the request's attempts. Its counter's error ends the request, wrongDigest included. A notInTimeWindow or unknownEngineID Report sends the request once more with the Report's boots and time, and the Report's engine ID for unknownEngineID, in the request only: the client keeps engine parameters from authenticated messages only (RFC 3414 section 3.2 step 7) and stores the authenticated answer's. The resynchronization in send also changes: the retransmission returns its own error, a resync Report answering it ends the request with its error instead of a nil one, the engine parameters of the retransmission's answer and of authenticated error Reports are stored, and a Report the client cannot store ends the request with the store error instead of being acted on.
An unauthenticated Report that ends after its security parameters, one flagged privacy without authentication, an authenticated error Report and a retransmission's answer with another security model, and a hook that lowers the client's msgFlags before a forged Report is decoded.
- A reply flagged privacy without authentication is not read as an unauthenticated Report (RFC 3412 section 7.2 discards it). - After discovery the engine checks a reply's digest with its request's msgFlags and user, the ones the acceptance and store rules use, so a hook that changes the client's msgFlags cannot let a forged Report be stored as authenticated. - An error Report keeps its error when its parameters cannot be stored, so errors.Is still classifies it. - The IDs of earlier attempts live in an eight-entry array on the stack: a slice made for Retries+1 entries escaped beyond Retries=3. - Comments say which keys check a reply and that a noAuthNoPriv session stores no Report's parameters; notes on the SNMPv3 answers to an SNMPv2c request and on the discovery exception's stored engine time.
testing.AllocsPerRun counts differently under the race detector; the allocations per request stay checked by the benchmarks.
GetNext and GetBulk after an engine time jump with unauthenticated notInTimeWindow Reports, a Set in the same case and after a reboot (authenticated Report), and a Set of a noAuthNoPriv session answered by a notInTimeWindow Report.
…misuse An authenticated request resent with what an unauthenticated Report names is signed with it. A notInTimeWindow Report naming a future time would get a signed copy an on-path sender replays when the agent's clock gets there, and an unknownEngineID Report naming another agent's engine ID would get the request signed for that agent where it shares the user's credentials. So an authenticated request takes an unauthenticated Report's engine parameters only as a read (Get, GetNext, GetBulk), whose replayed copy is a read, and an engine ID only while the client has none (discovery). Otherwise the request ends with the Report's error: a Set with ErrNotInTimeWindow, any request with ErrUnknownEngineID once the client knows the agent's engine ID, as net-snmp does. A configured engine ID is no longer overridden. Requests at noAuthNoPriv, which carry no signature, resynchronize as before.
After a discovery answer without an engine ID, an unknownEngineID Report from another engine for another message, at noAuthNoPriv and flagged privacy without authentication.
Requiring noAuthNoPriv in withoutAuthentication kept a Report flagged privacy without authentication out of the acceptance path, but also out of the msgID check: while the client has no engine ID such a Report is read with its own flags, and one for another message could name the engine the request is then sent to. The msgID check covers again every Report without the authentication bit; the acceptance path alone requires noAuthNoPriv.
For another message and with the request's msgID: the client takes the other agent's engine and its later requests to its own agent fail.
An empty GetResponse for another message, an SNMPv3 Report with msgID 0 answering an SNMPv2c request, and a hook that sets the client's version to 2c after an unauthenticated notInTimeWindow Report.
The engine judged a reply by its own claims: USM decoding derives keys for the engine ID a reply names, so any agent sharing the user's credentials produced authentic replies; a Report carrying the authentication flag skipped the msgID check; "authenticated" was read from header flags; and the discovery exception applied while the client had no engine ID. With the resync storing more replies, a Report another agent signed for another message moved the client to that agent's engine for good, and a forged empty-identity Report stored its time. Now (RFC 3412 section 7.2, RFC 3414 section 3.2): - a reply to an authenticated request must ask for authentication, name the request's user and engine ID, and carry a digest that checks with the request's key; decode reports that as an explicit verified value, and the engine no longer applies the discovery exception (UnmarshalTrap keeps it); - an SNMPv3 request is answered only by an SNMPv3 reply with the msgID of one of its attempts, and an SNMPv3 reply answers nothing else; - engine parameters are stored from verified replies, an unverified one only in a noAuthNoPriv session and never a Report, so no reply replaces a known engine ID.
After a discovery answer without an engine ID the client has no keys, and a user with a localized key but an empty passphrase loses its key at discovery; in both a forged reply signed with the empty key is returned and its engine time stored.
After a discovery answer without an engine ID no key is derived, and a user with a localized key but an empty passphrase loses it at discovery: the client signed requests and verified replies with the empty key, so anyone could forge a reply. The engine now refuses to encode an authenticated request, or to verify an answer, without a localized key. An authenticated request also never takes an engine ID from an unauthenticated Report, and takes a notInTimeWindow Report's time only when the Report names the request's engine. decode judges a reply by the request's version.
The digest alone does not tie a reply to the request's engine: a reply that names another engine ID but is signed with the key localized to the agent's is discarded, and the client keeps its engine ID.
A notInTimeWindow Report naming another engine ID lends its time only, and after a discovery answer without an engine ID the agent's unknownEngineID Report lends the engine ID and the missing context engine ID.
After a discovery answer without an engine ID, a Logger that sets the client's version to SNMPv2c after the negotiation makes encode skip the key check, so the SNMPv3 request goes out signed with the empty key.
… guards - An authentic Report for another message, and an SNMPv2c message whose body reads as a scoped PDU, answer no SNMPv3 request. - The scenarios after a discovery answer without an engine ID use a configured key, which survives the discovery, so their forged second answers reach the client again; the scenario they duplicated is dropped, and the key-only scenario keeps no unreachable script. - A Logger that changes the client's version after a normal discovery.
…sion The key check ran only when the client's version was SNMPv3, but the packet is signed by its own version, so a Logger that changed the client's version after the negotiation let an authenticated request go out signed with the empty key. The check now follows the packet, as the signature does.
decode gates on the request's version; answers does not compare the user and engine ID of a reply to a request without authentication; mayLend lends a time from a Report naming the request's engine ID; authenticate also receives replies of other versions; errNotAuthentic covers a missing authentication flag.
ilyam8
force-pushed
the
engine-report-resync
branch
from
October 11, 2026 13:54
5461b7c to
6eb526e
Compare
stelfrag
approved these changes
Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports the SNMPv3 REPORT resynchronization the Netdata Agent ships today from a personal fork, so the Agent can move to
this module without losing it. This is the one behavior change of the engine phase.
Some agents (UniFi access points) answer a request whose engine time is stale with a
notInTimeWindowReport sent atnoAuthNoPriv, with the current engine boots and time in its header (RFC 3414 section 3.2 step 7 a asks for
authNoPriv). The client checked every reply of an authenticated request against its digest, discarded the Report, and
timed out on every request with the same stale time.
Acting on such Reports needs every other reply judged against the request it answers, so this PR also checks each
SNMPv3 reply against its request (RFC 3412 section 7.2, RFC 3414 section 3.2).
Behavior
Replies are judged against their request:
a digest that checks with the request's key. Keys are no longer derived for the engine ID a reply names, so another
agent with the same credentials cannot answer for this one, and the engine no longer skips the digest of a reply
with an empty user name and engine ID (the discovery exception).
UnmarshalTrapis unchanged.SNMPv2c requests.
never from a Report it could not check; engine discovery (RFC 3414 section 4) still learns the engine ID, boots and
time from its unauthenticated answer. No reply to an authenticated request replaces the client's engine ID: when the
agent's engine ID changes, such requests now end with
ErrUnknownEngineID, as in net-snmp; an application thatfollows replaced devices clears the client's engine ID, and the context engine ID gosnmp learned with it, on that
error so the next request rediscovers.
signed with an empty key (after a discovery answer without an engine ID, or a configured key that discovery
replaced). Under FIPS 140-only, MD5 and SHA-1 users now get this error instead of the hash's FIPS error, because key
derivation drops that error (a known bug for the bug-fix phase).
Reports without authentication (the port):
request's security model and exactly one varbind, and it carries the msgID of one of the request's attempts. Nothing
unauthenticated is decrypted; a reply flagged privacy without authentication is not read.
ErrUnknownUsername,ErrWrongDigest,ErrDecryption, ...) instead of atimeout.
wrongDigestis not resent.notInTimeWindow: a read request (Get, GetNext, GetBulk, so walks too) is sent once more with the Report's engineboots and time in the request only, when the Report names the request's engine ID; the client keeps engine
parameters from authenticated messages only (RFC 3414 section 3.2 step 7) and stores the authenticated answer's.
Another authenticated request (Set) ends with
ErrNotInTimeWindow: a resend signed with a time an unauthenticatedmessage names could be replayed when the agent's clock gets there.
unknownEngineID: an authenticated request ends withErrUnknownEngineIDand the engine ID is kept; a resendsigned for the engine the Report names could reach another agent with the same credentials. Requests at
noAuthNoPriv, which carry no signature, are sent once more with the Report's engine ID, boots and time.
The resynchronization in
send:returned with a nil error); the engine parameters of the retransmission's answer and of authenticated error Reports
are stored; a resync Report the client cannot store ends the request with the store error (an error Report keeps its
own error).
Commits
test:commits pin each case on the code before the change that flips it: a UniFi model after an engine time jumpor a reboot at authNoPriv, AES and DES; the captured Report with request ID 0; forged times; other and earlier
msgIDs; encrypted, empty, two-varbind, undecodable and privacy-only Reports; another security model; engine-ID
changes; reads and Sets; Reports another agent with the same credentials signed; replies naming another engine ID;
replies signed with the empty key; SNMPv3 replies to SNMPv2c requests.
fix:the discovery exception only while discovering (later removed from the engine by the reply check).feat:the port;fix:review findings;fix:the read-only and no-other-engine rule;fix:the msgID bindingof a privacy-only Report while discovering.
fix:judge an SNMPv3 reply against the request it answers;fix:never sign or check a reply with an empty key;fix:refuse a request without a key whatever the client's version;docs:comment corrections.known bug:note is a behavior fixed here.Validation
walkunchanged;v3-fipschanges three errortexts (above).
(a path an earlier check already closes, identical values, or a digest that would need an HMAC fixed point), one
changes only allocations on a retry.
go test -race, FIPS 140-only,GOARCH=386, the end-to-end suite against snmpd 5.9.3, golangci-lint,cross-builds, govulncheck: pass locally.
review (two state regressions, fixed by the reply check); a design review of the reply check and two reviews of it
(an adversarial attack matrix and a coverage review); their findings are fixed here or listed for the bug-fix phase.
Not tested on a UniFi device: the fake agent models the captured Report.