From a9ea97d582ad8146e9188065085d3d522377a1a4 Mon Sep 17 00:00:00 2001 From: ilyam8 Date: Fri, 9 Oct 2026 19:03:22 +0300 Subject: [PATCH 1/7] refactor: move send and sendOneRequest into engine.go The request engine moves out of marshal.go unchanged, so the next commit's decomposition reads as a change within one file. marshal.go keeps the packet types, SafeString and MarshalMsg. No behavior change. --- engine.go | 329 +++++++++++++++++++++++++++++++++++++++++++++++++++++ marshal.go | 320 --------------------------------------------------- 2 files changed, 329 insertions(+), 320 deletions(-) create mode 100644 engine.go diff --git a/engine.go b/engine.go new file mode 100644 index 0000000..a22a24d --- /dev/null +++ b/engine.go @@ -0,0 +1,329 @@ +// Copyright 2012 The GoSNMP Authors. All rights reserved. Use of this +// source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +package gosnmp + +import ( + "context" + "fmt" + "io" + "runtime" + "strings" + "time" +) + +// GoSNMP +// send/receive one snmp request +func (x *GoSNMP) sendOneRequest(packetOut *SnmpPacket) (result *SnmpPacket, err error) { + allReqIDs := make([]uint32, 0, x.Retries+1) + // allMsgIDs := make([]uint32, 0, x.Retries+1) // unused + + timeout := x.Timeout + withContextDeadline := false +sendRetry: + for retries := 0; ; retries++ { + if retries > 0 { + if x.OnRetry != nil { + x.OnRetry(x) + } + + x.Logger.Printf("Retry number %d. Last error was: %v", retries, err) + if withContextDeadline && strings.Contains(err.Error(), "timeout") { + err = context.DeadlineExceeded + break + } + if retries > x.Retries { + if err == nil { + err = fmt.Errorf("max retries (%d) exceeded", x.Retries) + } + if strings.Contains(err.Error(), "timeout") { + err = fmt.Errorf("request timeout (after %d retries)", retries-1) + } + break + } + if x.ExponentialTimeout { + // https://www.webnms.com/snmp/help/snmpapi/snmpv3/v1/timeout.html + timeout *= 2 + } + withContextDeadline = false + } + err = nil + + if x.Context.Err() != nil { + return nil, x.Context.Err() + } + + reqDeadline := time.Now().Add(timeout) + if contextDeadline, ok := x.Context.Deadline(); ok { + if contextDeadline.Before(reqDeadline) { + reqDeadline = contextDeadline + withContextDeadline = true + } + } + + err = x.Conn.SetDeadline(reqDeadline) + if err != nil { + return nil, err + } + + reqID := x.nextRequestID() + allReqIDs = append(allReqIDs, reqID) + + packetOut.RequestID = reqID + + if x.Version == Version3 { + msgID := x.nextMsgID() + + // allMsgIDs = append(allMsgIDs, msgID) // unused + + packetOut.MsgID = msgID + + err = x.initPacket(packetOut) + if err != nil { + break + } + } + if x.Version == Version3 { + packetOut.SecurityParameters.Log() + } + + var outBuf []byte + outBuf, err = packetOut.marshalMsg() + if err != nil { + // Don't retry - not going to get any better! + err = fmt.Errorf("marshal: %w", err) + break + } + + if x.PreSend != nil { + x.PreSend(x) + } + if x.Logger.enabled() { + x.Logger.Printf("SENDING PACKET: %s", packetOut.SafeString()) + } + if err = x.write(outBuf); err != nil { + continue + } + if x.OnSent != nil { + x.OnSent(x) + } + + waitingResponse: + for { + if x.Logger.enabled() { + x.Logger.Print("WAITING RESPONSE...") + } + // Receive response and try receiving again on any decoding error. + // Let the deadline abort us if we don't receive a valid response. + + var resp []byte + resp, err = x.receive() + if err == io.EOF && strings.HasPrefix(x.Transport, tcp) { + x.Logger.Printf("ERROR: EOF. Performing reconnect") + err = x.netConnect() + if err != nil { + return nil, err + } + continue sendRetry + } else if err != nil { + // receive error. retrying won't help. abort + break + } + if x.OnRecv != nil { + x.OnRecv(x) + } + if x.Logger.enabled() { + x.Logger.Printf("GET RESPONSE OK: %+v", resp) + } + result = new(SnmpPacket) + result.Logger = x.Logger + + result.MsgFlags = packetOut.MsgFlags + if packetOut.SecurityParameters != nil { + result.SecurityParameters = packetOut.SecurityParameters.Copy() + } + + var cursor int + cursor, err = x.unmarshalHeader(resp, result) + if err != nil { + x.Logger.Printf("ERROR on unmarshall header: %s", err) + break + } + + if x.Version == Version3 { + usp := usmOf(x.SecurityParameters) + useResponseSecurityParameters := usp != nil && usp.AuthoritativeEngineID == "" + err = x.testAuthentication(resp, result, useResponseSecurityParameters) + if err != nil { + x.Logger.Printf("ERROR on Test Authentication on v3: %s", err) + break + } + resp, cursor, err = unmarshalScopedPDU(resp, cursor, result) + if err != nil { + x.Logger.Printf("ERROR on decryptPacket on v3: %s", err) + break + } + } + + err = unmarshalPayload(resp, cursor, result) + if err != nil { + x.Logger.Printf("ERROR on UnmarshalPayload on v3: %s", err) + break + } + if result.Error == NoError && len(result.Variables) < 1 { + x.Logger.Printf("ERROR on UnmarshalPayload on v3: Empty result") + break + } + + // While Report PDU was defined by RFC 1905 as part of SNMPv2, it was never + // used until SNMPv3. Report PDU's allow a SNMP engine to tell another SNMP + // engine that an error was detected while processing an SNMP message. + // + // The format for a Report PDU is + // ----------------------------------- + // | 0xA8 | reqid | 0 | 0 | varbinds | + // ----------------------------------- + // where: + // - PDU type 0xA8 indicates a Report PDU. + // - reqid is either: + // The request identifier of the message that triggered the report + // or zero if the request identifier cannot be extracted. + // - The variable bindings will contain a single object identifier and its value + // + // usmStatsNotInTimeWindows and usmStatsUnknownEngineIDs are recoverable errors + // and will be retransmitted, for others we return the result with an error. + if result.Version == Version3 && result.PDUType == Report && len(result.Variables) == 1 { + switch result.Variables[0].Name { + case usmStatsUnsupportedSecLevels: + return result, ErrUnknownSecurityLevel + case usmStatsNotInTimeWindows: + break waitingResponse + case usmStatsUnknownUserNames: + return result, ErrUnknownUsername + case usmStatsUnknownEngineIDs: + break waitingResponse + case usmStatsWrongDigests: + return result, ErrWrongDigest + case usmStatsDecryptionErrors: + return result, ErrDecryption + case snmpUnknownSecurityModels: + return result, ErrUnknownSecurityModels + case snmpInvalidMsgs: + return result, ErrInvalidMsgs + case snmpUnknownPDUHandlers: + return result, ErrUnknownPDUHandlers + default: + return result, ErrUnknownReportPDU + } + } + + validID := false + for _, id := range allReqIDs { + if id == result.RequestID { + validID = true + } + } + if result.RequestID == 0 { + validID = true + } + if !validID { + x.Logger.Print("ERROR out of order") + continue + } + + break + } + if err != nil { + continue + } + + if x.OnFinish != nil { + x.OnFinish(x) + } + // Success! + return result, nil + } + + // Return last error + return nil, err +} + +// generic "sender" that negotiate any version of snmp request +func (x *GoSNMP) send(packetOut *SnmpPacket) (result *SnmpPacket, err error) { + defer func() { + if e := recover(); e != nil { + buf := make([]byte, 8192) + runtime.Stack(buf, true) + + err = fmt.Errorf("recover: %v Stack:%v", e, string(buf)) + } + }() + + if x.Conn == nil { + return nil, fmt.Errorf("&GoSNMP.Conn is missing. Provide a connection or use Connect()") + } + + if x.Retries < 0 { + x.Retries = 0 + } + if x.Logger.enabled() { + x.Logger.Print("SEND INIT") + } + if packetOut.Version == Version3 { + if x.Logger.enabled() { + x.Logger.Print("SEND INIT NEGOTIATE SECURITY PARAMS") + } + if err = x.negotiateInitialSecurityParameters(packetOut); err != nil { + return &SnmpPacket{}, err + } + if x.Logger.enabled() { + x.Logger.Print("SEND END NEGOTIATE SECURITY PARAMS") + } + } + + // perform request + result, err = x.sendOneRequest(packetOut) + if err != nil { + x.Logger.Printf("SEND Error on the first Request Error: %s", err) + return result, err + } + + if result.Version == Version3 { + if x.Logger.enabled() { + x.Logger.Printf("SEND STORE SECURITY PARAMS from result: %s", result.SecurityParameters.SafeString()) + } + err = x.storeSecurityParameters(result) + + if result.PDUType == Report && len(result.Variables) == 1 { + switch result.Variables[0].Name { + case usmStatsNotInTimeWindows: + x.Logger.Print("WARNING detected out-of-time-window ERROR") + if err = x.updatePktSecurityParameters(packetOut); err != nil { + x.Logger.Printf("ERROR updatePktSecurityParameters error: %s", err) + return nil, err + } + // retransmit with updated auth engine params + result, err = x.sendOneRequest(packetOut) + if err != nil { + x.Logger.Printf("ERROR out-of-time-window retransmit error: %s", err) + return result, ErrNotInTimeWindow + } + + case usmStatsUnknownEngineIDs: + x.Logger.Print("WARNING detected unknown engine id ERROR") + if err = x.updatePktSecurityParameters(packetOut); err != nil { + x.Logger.Printf("ERROR updatePktSecurityParameters error: %s", err) + return nil, err + } + // retransmit with updated engine id + result, err = x.sendOneRequest(packetOut) + if err != nil { + x.Logger.Printf("ERROR unknown engine id retransmit error: %s", err) + return result, ErrUnknownEngineID + } + } + } + } + return result, err +} diff --git a/marshal.go b/marshal.go index b1feb5e..d5e4337 100644 --- a/marshal.go +++ b/marshal.go @@ -5,14 +5,9 @@ package gosnmp import ( - "context" "encoding/asn1" "errors" "fmt" - "io" - "runtime" - "strings" - "time" ) // @@ -153,321 +148,6 @@ func (packet *SnmpPacket) SafeString() string { ) } -// GoSNMP -// send/receive one snmp request -func (x *GoSNMP) sendOneRequest(packetOut *SnmpPacket) (result *SnmpPacket, err error) { - allReqIDs := make([]uint32, 0, x.Retries+1) - // allMsgIDs := make([]uint32, 0, x.Retries+1) // unused - - timeout := x.Timeout - withContextDeadline := false -sendRetry: - for retries := 0; ; retries++ { - if retries > 0 { - if x.OnRetry != nil { - x.OnRetry(x) - } - - x.Logger.Printf("Retry number %d. Last error was: %v", retries, err) - if withContextDeadline && strings.Contains(err.Error(), "timeout") { - err = context.DeadlineExceeded - break - } - if retries > x.Retries { - if err == nil { - err = fmt.Errorf("max retries (%d) exceeded", x.Retries) - } - if strings.Contains(err.Error(), "timeout") { - err = fmt.Errorf("request timeout (after %d retries)", retries-1) - } - break - } - if x.ExponentialTimeout { - // https://www.webnms.com/snmp/help/snmpapi/snmpv3/v1/timeout.html - timeout *= 2 - } - withContextDeadline = false - } - err = nil - - if x.Context.Err() != nil { - return nil, x.Context.Err() - } - - reqDeadline := time.Now().Add(timeout) - if contextDeadline, ok := x.Context.Deadline(); ok { - if contextDeadline.Before(reqDeadline) { - reqDeadline = contextDeadline - withContextDeadline = true - } - } - - err = x.Conn.SetDeadline(reqDeadline) - if err != nil { - return nil, err - } - - reqID := x.nextRequestID() - allReqIDs = append(allReqIDs, reqID) - - packetOut.RequestID = reqID - - if x.Version == Version3 { - msgID := x.nextMsgID() - - // allMsgIDs = append(allMsgIDs, msgID) // unused - - packetOut.MsgID = msgID - - err = x.initPacket(packetOut) - if err != nil { - break - } - } - if x.Version == Version3 { - packetOut.SecurityParameters.Log() - } - - var outBuf []byte - outBuf, err = packetOut.marshalMsg() - if err != nil { - // Don't retry - not going to get any better! - err = fmt.Errorf("marshal: %w", err) - break - } - - if x.PreSend != nil { - x.PreSend(x) - } - if x.Logger.enabled() { - x.Logger.Printf("SENDING PACKET: %s", packetOut.SafeString()) - } - if err = x.write(outBuf); err != nil { - continue - } - if x.OnSent != nil { - x.OnSent(x) - } - - waitingResponse: - for { - if x.Logger.enabled() { - x.Logger.Print("WAITING RESPONSE...") - } - // Receive response and try receiving again on any decoding error. - // Let the deadline abort us if we don't receive a valid response. - - var resp []byte - resp, err = x.receive() - if err == io.EOF && strings.HasPrefix(x.Transport, tcp) { - x.Logger.Printf("ERROR: EOF. Performing reconnect") - err = x.netConnect() - if err != nil { - return nil, err - } - continue sendRetry - } else if err != nil { - // receive error. retrying won't help. abort - break - } - if x.OnRecv != nil { - x.OnRecv(x) - } - if x.Logger.enabled() { - x.Logger.Printf("GET RESPONSE OK: %+v", resp) - } - result = new(SnmpPacket) - result.Logger = x.Logger - - result.MsgFlags = packetOut.MsgFlags - if packetOut.SecurityParameters != nil { - result.SecurityParameters = packetOut.SecurityParameters.Copy() - } - - var cursor int - cursor, err = x.unmarshalHeader(resp, result) - if err != nil { - x.Logger.Printf("ERROR on unmarshall header: %s", err) - break - } - - if x.Version == Version3 { - usp := usmOf(x.SecurityParameters) - useResponseSecurityParameters := usp != nil && usp.AuthoritativeEngineID == "" - err = x.testAuthentication(resp, result, useResponseSecurityParameters) - if err != nil { - x.Logger.Printf("ERROR on Test Authentication on v3: %s", err) - break - } - resp, cursor, err = unmarshalScopedPDU(resp, cursor, result) - if err != nil { - x.Logger.Printf("ERROR on decryptPacket on v3: %s", err) - break - } - } - - err = unmarshalPayload(resp, cursor, result) - if err != nil { - x.Logger.Printf("ERROR on UnmarshalPayload on v3: %s", err) - break - } - if result.Error == NoError && len(result.Variables) < 1 { - x.Logger.Printf("ERROR on UnmarshalPayload on v3: Empty result") - break - } - - // While Report PDU was defined by RFC 1905 as part of SNMPv2, it was never - // used until SNMPv3. Report PDU's allow a SNMP engine to tell another SNMP - // engine that an error was detected while processing an SNMP message. - // - // The format for a Report PDU is - // ----------------------------------- - // | 0xA8 | reqid | 0 | 0 | varbinds | - // ----------------------------------- - // where: - // - PDU type 0xA8 indicates a Report PDU. - // - reqid is either: - // The request identifier of the message that triggered the report - // or zero if the request identifier cannot be extracted. - // - The variable bindings will contain a single object identifier and its value - // - // usmStatsNotInTimeWindows and usmStatsUnknownEngineIDs are recoverable errors - // and will be retransmitted, for others we return the result with an error. - if result.Version == Version3 && result.PDUType == Report && len(result.Variables) == 1 { - switch result.Variables[0].Name { - case usmStatsUnsupportedSecLevels: - return result, ErrUnknownSecurityLevel - case usmStatsNotInTimeWindows: - break waitingResponse - case usmStatsUnknownUserNames: - return result, ErrUnknownUsername - case usmStatsUnknownEngineIDs: - break waitingResponse - case usmStatsWrongDigests: - return result, ErrWrongDigest - case usmStatsDecryptionErrors: - return result, ErrDecryption - case snmpUnknownSecurityModels: - return result, ErrUnknownSecurityModels - case snmpInvalidMsgs: - return result, ErrInvalidMsgs - case snmpUnknownPDUHandlers: - return result, ErrUnknownPDUHandlers - default: - return result, ErrUnknownReportPDU - } - } - - validID := false - for _, id := range allReqIDs { - if id == result.RequestID { - validID = true - } - } - if result.RequestID == 0 { - validID = true - } - if !validID { - x.Logger.Print("ERROR out of order") - continue - } - - break - } - if err != nil { - continue - } - - if x.OnFinish != nil { - x.OnFinish(x) - } - // Success! - return result, nil - } - - // Return last error - return nil, err -} - -// generic "sender" that negotiate any version of snmp request -func (x *GoSNMP) send(packetOut *SnmpPacket) (result *SnmpPacket, err error) { - defer func() { - if e := recover(); e != nil { - buf := make([]byte, 8192) - runtime.Stack(buf, true) - - err = fmt.Errorf("recover: %v Stack:%v", e, string(buf)) - } - }() - - if x.Conn == nil { - return nil, fmt.Errorf("&GoSNMP.Conn is missing. Provide a connection or use Connect()") - } - - if x.Retries < 0 { - x.Retries = 0 - } - if x.Logger.enabled() { - x.Logger.Print("SEND INIT") - } - if packetOut.Version == Version3 { - if x.Logger.enabled() { - x.Logger.Print("SEND INIT NEGOTIATE SECURITY PARAMS") - } - if err = x.negotiateInitialSecurityParameters(packetOut); err != nil { - return &SnmpPacket{}, err - } - if x.Logger.enabled() { - x.Logger.Print("SEND END NEGOTIATE SECURITY PARAMS") - } - } - - // perform request - result, err = x.sendOneRequest(packetOut) - if err != nil { - x.Logger.Printf("SEND Error on the first Request Error: %s", err) - return result, err - } - - if result.Version == Version3 { - if x.Logger.enabled() { - x.Logger.Printf("SEND STORE SECURITY PARAMS from result: %s", result.SecurityParameters.SafeString()) - } - err = x.storeSecurityParameters(result) - - if result.PDUType == Report && len(result.Variables) == 1 { - switch result.Variables[0].Name { - case usmStatsNotInTimeWindows: - x.Logger.Print("WARNING detected out-of-time-window ERROR") - if err = x.updatePktSecurityParameters(packetOut); err != nil { - x.Logger.Printf("ERROR updatePktSecurityParameters error: %s", err) - return nil, err - } - // retransmit with updated auth engine params - result, err = x.sendOneRequest(packetOut) - if err != nil { - x.Logger.Printf("ERROR out-of-time-window retransmit error: %s", err) - return result, ErrNotInTimeWindow - } - - case usmStatsUnknownEngineIDs: - x.Logger.Print("WARNING detected unknown engine id ERROR") - if err = x.updatePktSecurityParameters(packetOut); err != nil { - x.Logger.Printf("ERROR updatePktSecurityParameters error: %s", err) - return nil, err - } - // retransmit with updated engine id - result, err = x.sendOneRequest(packetOut) - if err != nil { - x.Logger.Printf("ERROR unknown engine id retransmit error: %s", err) - return result, ErrUnknownEngineID - } - } - } - } - return result, err -} - // -- Marshalling Logic -------------------------------------------------------- // MarshalMsg marshalls a snmp packet, ready for sending across the wire From 04fff1558b803de64b1e2a8cda7a315ce30dbffa Mon Sep 17 00:00:00 2001 From: ilyam8 Date: Fri, 9 Oct 2026 19:13:33 +0300 Subject: [PATCH 2/7] test: pin replies of another SNMP version than the client's Mutation probes on the request engine's decomposition found that nothing observed which version picks the decoding of a reply: the client's (today) or the reply's. - v3/answer/other-version/{noauth,md5,sha-aes}: an SNMPv3 client given an SNMPv2c GetResponse with its request ID rejects it at every security level and sends the request again. - requests/answer/other-version-v3: an SNMPv2c client given SNMPv3 replies rejects them; the request fails with the PDU type error. Both pass on the current code. --- engine_requests_test.go | 6 ++ engine_v3_test.go | 14 +++++ testdata/engine/requests.golden | 29 ++++++++++ testdata/engine/v3.golden | 98 +++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+) diff --git a/engine_requests_test.go b/engine_requests_test.go index 7b32479..496b51d 100644 --- a/engine_requests_test.go +++ b/engine_requests_test.go @@ -271,6 +271,12 @@ func engineScenarios() map[string]engineScenario { agent: answerWith(func(p *SnmpPacket) { p.Version = Version1 }, sysDescr), knownBug: "a reply of another SNMP version is accepted, though RFC 3412 hands each message to the model of its own version", }, + "answer/other-version-v3": { + agent: answerWith(func(p *SnmpPacket) { + p.Version, p.MsgFlags, p.SecurityModel = Version3, NoAuthNoPriv, UserSecurityModel + p.SecurityParameters = &UsmSecurityParameters{UserName: "public"} + }, sysDescr), + }, "answer/other-community": {agent: answerWith(func(p *SnmpPacket) { p.Community = "private" }, sysDescr)}, "answer/garbage-then-valid": { agent: func(_ int, req []byte) []agentReply { diff --git a/engine_v3_test.go b/engine_v3_test.go index b9beab7..17b57f0 100644 --- a/engine_v3_test.go +++ b/engine_v3_test.go @@ -103,6 +103,11 @@ func forgeReport(p *SnmpPacket) { usp.AuthoritativeEngineBoots, usp.AuthoritativeEngineTime = 99, 5 } +// asV2c turns an answer into an SNMPv2c message with the same request ID. +func asV2c(p *SnmpPacket) { + p.Version, p.Community = Version2c, "public" +} + // fromOtherEngine makes an answer come from the agent's other engine ID. func fromOtherEngine(p *SnmpPacket) { p.SecurityParameters.usm().AuthoritativeEngineID = agentOtherEngineID @@ -380,6 +385,15 @@ func v3Scenarios() map[string]v3Scenario { }), knownBug: "a reply with an empty msgFlags is accepted and carries the request's flags (RFC 3412 section 6: one octet)", }, + "answer/other-version/noauth": { + user: "codec-noauth", script: onRequest(2, agentAnswer{why: "SNMPv2c GetResponse", edit: asV2c}), + }, + "answer/other-version/md5": { + user: "codec-md5", script: onRequest(2, agentAnswer{level: AuthNoPriv, why: "SNMPv2c GetResponse", edit: asV2c}), + }, + "answer/other-version/sha-aes": { + user: "codec-sha-aes", script: onRequest(2, agentAnswer{level: AuthPriv, why: "SNMPv2c GetResponse", edit: asV2c}), + }, "answer/other-security-model": { user: "codec-noauth", script: onRequest(2, agentAnswer{ why: "GetResponse with security model 2", edit: func(p *SnmpPacket) { p.SecurityModel = 2 }, diff --git a/testdata/engine/requests.golden b/testdata/engine/requests.golden index 354987f..8ff9e39 100644 --- a/testdata/engine/requests.golden +++ b/testdata/engine/requests.golden @@ -274,6 +274,35 @@ 0s client after: retries=2 next request id=1002 0s known bug: a reply of another SNMP version is accepted, though RFC 3412 hands each message to the model of its own version +== requests/answer/other-version-v3 +0s client: 2c timeout=1s retries=2 exponential=false +0s deadline +1s +0s hook PreSend +0s write #1: 2c "public" GetRequest id=1001 [.1.3.6.1.2.1.1.1.0] +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=0 noAuthNoPriv user="public" engine="" boots=0 time=0 context=""/"" GetResponse id=1001 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +0s hook OnRecv +0s hook OnRetry +0s deadline +1s +0s hook PreSend +0s write #2: 2c "public" GetRequest id=1002 [.1.3.6.1.2.1.1.1.0] +0s agent answers #2 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=0 noAuthNoPriv user="public" engine="" boots=0 time=0 context=""/"" GetResponse id=1002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +0s hook OnRecv +0s hook OnRetry +0s deadline +1s +0s hook PreSend +0s write #3: 2c "public" GetRequest id=1003 [.1.3.6.1.2.1.1.1.0] +0s agent answers #3 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=0 noAuthNoPriv user="public" engine="" boots=0 time=0 context=""/"" GetResponse id=1003 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +0s hook OnRecv +0s hook OnRetry +0s result: packet nil, error "unknown PDUType 0x53657175656e6365" +0s client after: retries=2 next request id=1004 + == requests/answer/report 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +1s diff --git a/testdata/engine/v3.golden b/testdata/engine/v3.golden index 8a35718..ea25a2c 100644 --- a/testdata/engine/v3.golden +++ b/testdata/engine/v3.golden @@ -336,6 +336,104 @@ 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: a GetResponse for another user than the request's is accepted (RFC 3412 section 7.2 step 12 b) +== v3/answer/other-version/md5 +0s client: user="codec-md5" authNoPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s deadline +1s +0s hook PreSend +0s write #2: 3 msgID=2002 authNoPriv+reportable user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" GetRequest id=1002 [.1.3.6.1.2.1.1.1.0] +0s agent: answers #2 with GetResponse id=1002 msgID=2002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] at authNoPriv (SNMPv2c GetResponse) +0s agent answers #2 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 2c "public" GetResponse id=1002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +0s hook OnRecv +0s hook OnRetry +0s deadline +1s +0s hook PreSend +0s write #3: 3 msgID=2003 authNoPriv+reportable user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" GetRequest id=1003 [.1.3.6.1.2.1.1.1.0] +0s agent: answers #3 with GetResponse id=1003 msgID=2003 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] at authNoPriv (request accepted) +0s agent answers #3 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2003 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" GetResponse id=1003 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +0s hook OnRecv +0s hook OnFinish +0s result: packet 3 msgID=2003 authNoPriv GetResponse id=1003 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"], no error +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" + +== v3/answer/other-version/noauth +0s client: user="codec-noauth" noAuthNoPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s deadline +1s +0s hook PreSend +0s write #2: 3 msgID=2002 noAuthNoPriv+reportable user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" GetRequest id=1002 [.1.3.6.1.2.1.1.1.0] +0s agent: answers #2 with GetResponse id=1002 msgID=2002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] at noAuthNoPriv (SNMPv2c GetResponse) +0s agent answers #2 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 2c "public" GetResponse id=1002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +0s hook OnRecv +0s hook OnRetry +0s deadline +1s +0s hook PreSend +0s write #3: 3 msgID=2003 noAuthNoPriv+reportable user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" GetRequest id=1003 [.1.3.6.1.2.1.1.1.0] +0s agent: answers #3 with GetResponse id=1003 msgID=2003 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] at noAuthNoPriv (request accepted) +0s agent answers #3 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2003 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" GetResponse id=1003 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +0s hook OnRecv +0s hook OnFinish +0s result: packet 3 msgID=2003 noAuthNoPriv GetResponse id=1003 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"], no error +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" + +== v3/answer/other-version/sha-aes +0s client: user="codec-sha-aes" authPriv+reportable timeout=1s retries=2 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s deadline +1s +0s hook PreSend +0s write #2: 3 msgID=2002 authPriv+reportable user="codec-sha-aes" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 salt=0000000000000102 encrypted +0s agent: #2 decrypts to context="\x80\x00\x1f\x88\x04codec-agent"/"" GetRequest id=1002 [.1.3.6.1.2.1.1.1.0] +0s agent: answers #2 with GetResponse id=1002 msgID=2002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] at authPriv (SNMPv2c GetResponse) +0s agent answers #2 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 2c "public" GetResponse id=1002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +0s hook OnRecv +0s hook OnRetry +0s deadline +1s +0s hook PreSend +0s write #3: 3 msgID=2003 authPriv+reportable user="codec-sha-aes" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 salt=0000000000000103 encrypted +0s agent: #3 decrypts to context="\x80\x00\x1f\x88\x04codec-agent"/"" GetRequest id=1003 [.1.3.6.1.2.1.1.1.0] +0s agent: answers #3 with GetResponse id=1003 msgID=2003 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] at authPriv (request accepted) +0s agent answers #3 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2003 authPriv user="codec-sha-aes" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 salt=0000000000000002 encrypted +0s hook OnRecv +0s hook OnFinish +0s result: packet 3 msgID=2003 authPriv GetResponse id=1003 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-sha-aes"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"], no error +0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" + == v3/answer/report-for-other-message 0s client: user="codec-noauth" noAuthNoPriv+reportable timeout=1s retries=2 0s deadline +1s From c8ad897d8381e272e00f97cc62ecd085db2ea383 Mon Sep 17 00:00:00 2001 From: ilyam8 Date: Fri, 9 Oct 2026 19:21:55 +0300 Subject: [PATCH 3/7] test: pin a reply to the first attempt read in the third A mutation probe on the request engine's decomposition showed that no scenario told apart accepting the request IDs of all earlier attempts from accepting only the previous one's. requests/answer/late-by-two-attempts: the first request's reply arrives during the third attempt and answers the request. Passes on the current code. --- engine_requests_test.go | 6 ++++++ testdata/engine/requests.golden | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/engine_requests_test.go b/engine_requests_test.go index 496b51d..eeb3481 100644 --- a/engine_requests_test.go +++ b/engine_requests_test.go @@ -196,6 +196,12 @@ func engineScenarios() map[string]engineScenario { } return nil }}, + "answer/late-by-two-attempts": {agent: func(n int, req []byte) []agentReply { + if n == 1 { + return []agentReply{{data: replyTo(req, nil, sysDescr), after: 2500 * time.Millisecond}} + } + return nil + }}, "answer/late-reply-read-by-next-request": {run: twoGets, agent: func(n int, req []byte) []agentReply { switch n { case 1: diff --git a/testdata/engine/requests.golden b/testdata/engine/requests.golden index 8ff9e39..ae56b41 100644 --- a/testdata/engine/requests.golden +++ b/testdata/engine/requests.golden @@ -203,6 +203,31 @@ 1.5s result: packet 2c "public" GetResponse id=1001 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"], no error 1.5s client after: retries=2 next request id=1003 +== requests/answer/late-by-two-attempts +0s client: 2c timeout=1s retries=2 exponential=false +0s deadline +1s +0s hook PreSend +0s write #1: 2c "public" GetRequest id=1001 [.1.3.6.1.2.1.1.1.0] +0s agent answers #1 after 2.5s +0s hook OnSent +1s read from 127.0.0.2:161: deadline +1s hook OnRetry +1s deadline +1s +1s hook PreSend +1s write #2: 2c "public" GetRequest id=1002 [.1.3.6.1.2.1.1.1.0] +1s hook OnSent +2s read from 127.0.0.2:161: deadline +2s hook OnRetry +2s deadline +1s +2s hook PreSend +2s write #3: 2c "public" GetRequest id=1003 [.1.3.6.1.2.1.1.1.0] +2s hook OnSent +2.5s read from 127.0.0.2:161: 2c "public" GetResponse id=1001 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] +2.5s hook OnRecv +2.5s hook OnFinish +2.5s result: packet 2c "public" GetResponse id=1001 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"], no error +2.5s client after: retries=2 next request id=1004 + == requests/answer/late-reply-read-by-next-request 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +1s From d475184209bcc4229dfb20a3f7e4b952be0171f2 Mon Sep 17 00:00:00 2001 From: ilyam8 Date: Fri, 9 Oct 2026 19:24:10 +0300 Subject: [PATCH 4/7] refactor: split sendOneRequest into the steps of a request sendOneRequest ran a request in one function: an attempt loop and a read loop, both labeled, steered by one err variable, so which failures retried and which returned depended on the loop a break left and on the value err held at that moment. It now reads as the steps of a request: - sendOneRequest: the attempt loop. It keeps the request IDs of earlier attempts and runs OnFinish when the request succeeds. - beforeRetry: OnRetry, then the request's error when no retry follows (a timeout under the context's deadline, the retries used up), else the next timeout. - attempt: context check, deadline, encode, hooks, write, then await. Its outcome is a value: the request's result, or a retry with its reason. - await: reads until a reply answers the request; a TCP EOF reconnects for the next attempt. - decode: header, then for SNMPv3 authentication and scoped PDU, then PDU. - answers: the empty-reply rule, Reports through a table of counters to errors, the request IDs. No behavior change: every golden is unchanged, and so is the logger output, checked against goldens regenerated with a transcript logger. Each known bug of the function keeps one site with a known-bug comment; the two timeout text matches share isTimeout. The request IDs of earlier attempts stay in sendOneRequest's frame, so the slice stays on the stack as before. --- engine.go | 445 +++++++++++++++++++++++++++++------------------------- 1 file changed, 242 insertions(+), 203 deletions(-) diff --git a/engine.go b/engine.go index a22a24d..c50911a 100644 --- a/engine.go +++ b/engine.go @@ -9,244 +9,283 @@ import ( "fmt" "io" "runtime" + "slices" "strings" "time" ) -// GoSNMP -// send/receive one snmp request -func (x *GoSNMP) sendOneRequest(packetOut *SnmpPacket) (result *SnmpPacket, err error) { - allReqIDs := make([]uint32, 0, x.Retries+1) - // allMsgIDs := make([]uint32, 0, x.Retries+1) // unused +// exchange is one request on its way through the attempts sendOneRequest +// makes. +type exchange struct { + x *GoSNMP + packet *SnmpPacket + + // timeout is the current attempt's timeout; ExponentialTimeout doubles it + // before each retry. + timeout time.Duration + // contextDeadline is set when the context's deadline, not the timeout, + // ends the current attempt. + contextDeadline bool +} - timeout := x.Timeout - withContextDeadline := false -sendRetry: - for retries := 0; ; retries++ { - if retries > 0 { - if x.OnRetry != nil { - x.OnRetry(x) - } +// attemptOutcome is how an attempt ended: with the request's result, or with +// a failure another attempt may cure. +type attemptOutcome struct { + packet *SnmpPacket + err error + // retry asks for another attempt; err is the reason, nil after a TCP + // reconnect. + retry bool +} - x.Logger.Printf("Retry number %d. Last error was: %v", retries, err) - if withContextDeadline && strings.Contains(err.Error(), "timeout") { - err = context.DeadlineExceeded - break - } - if retries > x.Retries { - if err == nil { - err = fmt.Errorf("max retries (%d) exceeded", x.Retries) - } - if strings.Contains(err.Error(), "timeout") { - err = fmt.Errorf("request timeout (after %d retries)", retries-1) - } - break +// sendOneRequest sends packetOut and returns the reply that answers it, +// retrying up to x.Retries times. OnFinish runs when it succeeds. +func (x *GoSNMP) sendOneRequest(packetOut *SnmpPacket) (*SnmpPacket, error) { + e := exchange{x: x, packet: packetOut, timeout: x.Timeout} + // The request IDs of the attempts before the current one. The slice lives + // in this frame, where it stays off the heap. + earlierIDs := make([]uint32, 0, x.Retries+1) + var lastErr error + for n := 0; ; n++ { + if n > 0 { + if err := e.beforeRetry(n, lastErr); err != nil { + return nil, err } - if x.ExponentialTimeout { - // https://www.webnms.com/snmp/help/snmpapi/snmpv3/v1/timeout.html - timeout *= 2 - } - withContextDeadline = false } - err = nil - - if x.Context.Err() != nil { - return nil, x.Context.Err() + out := e.attempt(earlierIDs) + if out.retry { + // An attempt asks for a retry only after it has stamped the + // packet with its request ID. + earlierIDs = append(earlierIDs, packetOut.RequestID) + lastErr = out.err + continue } - - reqDeadline := time.Now().Add(timeout) - if contextDeadline, ok := x.Context.Deadline(); ok { - if contextDeadline.Before(reqDeadline) { - reqDeadline = contextDeadline - withContextDeadline = true - } + if out.err == nil && x.OnFinish != nil { + x.OnFinish(x) } + return out.packet, out.err + } +} - err = x.Conn.SetDeadline(reqDeadline) - if err != nil { - return nil, err +// beforeRetry runs before retry n, after an attempt that failed with lastErr. +// It returns the request's error when no retry follows: the context's +// deadline ended the attempt, or the retries are used up. +func (e *exchange) beforeRetry(n int, lastErr error) error { + x := e.x + // Known bug: OnRetry runs even when no retry follows. + if x.OnRetry != nil { + x.OnRetry(x) + } + x.Logger.Printf("Retry number %d. Last error was: %v", n, lastErr) + + if e.contextDeadline && isTimeout(lastErr) { + return context.DeadlineExceeded + } + if n > x.Retries { + switch { + case lastErr == nil: + return fmt.Errorf("max retries (%d) exceeded", x.Retries) + case isTimeout(lastErr): + return fmt.Errorf("request timeout (after %d retries)", n-1) } + return lastErr + } + if x.ExponentialTimeout { + // https://www.webnms.com/snmp/help/snmpapi/snmpv3/v1/timeout.html + e.timeout *= 2 + } + return nil +} - reqID := x.nextRequestID() - allReqIDs = append(allReqIDs, reqID) +// isTimeout reports whether err ended an attempt by timing out. Known bug: it +// looks for the word in the error's text, so any error mentioning a timeout +// counts, and a nil err panics (the one after a TCP reconnect, under a context +// deadline; send recovers it). +func isTimeout(err error) bool { + return strings.Contains(err.Error(), "timeout") +} - packetOut.RequestID = reqID +// attempt sends the request once and reads until a reply answers it or the +// attempt's deadline passes. A reply to an earlier attempt, whose request ID +// is one of earlierIDs, answers it too. +func (e *exchange) attempt(earlierIDs []uint32) attemptOutcome { + x := e.x + // Known bug: a client given a Conn without Connect has no Context, and this + // panics (send recovers it). + if err := x.Context.Err(); err != nil { + return attemptOutcome{err: err} + } + var deadline time.Time + deadline, e.contextDeadline = x.attemptDeadline(e.timeout) + if err := x.Conn.SetDeadline(deadline); err != nil { + return attemptOutcome{err: err} + } - if x.Version == Version3 { - msgID := x.nextMsgID() + // An encoding failure is not retried: another attempt would fail the same + // way. + outBuf, err := e.encode() + if err != nil { + return attemptOutcome{err: err} + } - // allMsgIDs = append(allMsgIDs, msgID) // unused + if x.PreSend != nil { + x.PreSend(x) + } + if x.Logger.enabled() { + x.Logger.Printf("SENDING PACKET: %s", e.packet.SafeString()) + } + if err := x.write(outBuf); err != nil { + return attemptOutcome{err: err, retry: true} + } + if x.OnSent != nil { + x.OnSent(x) + } + return e.await(earlierIDs) +} - packetOut.MsgID = msgID +// attemptDeadline returns when an attempt with timeout ends: after the +// timeout, or at the context's deadline if that comes first (byContext). +func (x *GoSNMP) attemptDeadline(timeout time.Duration) (deadline time.Time, byContext bool) { + deadline = time.Now().Add(timeout) + if ctxDeadline, ok := x.Context.Deadline(); ok && ctxDeadline.Before(deadline) { + return ctxDeadline, true + } + return deadline, false +} - err = x.initPacket(packetOut) - if err != nil { - break - } - } - if x.Version == Version3 { - packetOut.SecurityParameters.Log() +// encode stamps the request with a new request ID and, for SNMPv3, a new +// message ID and privacy parameters, and marshals it. +func (e *exchange) encode() ([]byte, error) { + x, p := e.x, e.packet + p.RequestID = x.nextRequestID() + if x.Version == Version3 { + p.MsgID = x.nextMsgID() + if err := x.initPacket(p); err != nil { + return nil, err } + p.SecurityParameters.Log() + } + out, err := p.marshalMsg() + if err != nil { + return nil, fmt.Errorf("marshal: %w", err) + } + return out, nil +} - var outBuf []byte - outBuf, err = packetOut.marshalMsg() +// await reads replies until one answers the request. Known bug: it does not +// watch the context, so a cancellation is noticed only when the attempt's +// deadline passes, and in the last attempt it ends as a request timeout. +func (e *exchange) await(earlierIDs []uint32) attemptOutcome { + x := e.x + for { + if x.Logger.enabled() { + x.Logger.Print("WAITING RESPONSE...") + } + resp, err := x.receive() + if err == io.EOF && strings.HasPrefix(x.Transport, tcp) { + // The agent closed the connection: reconnect for the next attempt. + x.Logger.Printf("ERROR: EOF. Performing reconnect") + if err = x.netConnect(); err != nil { + return attemptOutcome{err: err} + } + return attemptOutcome{retry: true} + } if err != nil { - // Don't retry - not going to get any better! - err = fmt.Errorf("marshal: %w", err) - break + return attemptOutcome{err: err, retry: true} } - - if x.PreSend != nil { - x.PreSend(x) + if x.OnRecv != nil { + x.OnRecv(x) } if x.Logger.enabled() { - x.Logger.Printf("SENDING PACKET: %s", packetOut.SafeString()) + x.Logger.Printf("GET RESPONSE OK: %+v", resp) } - if err = x.write(outBuf); err != nil { - continue + + reply, err := e.decode(resp) + if err != nil { + // Known bug: an undecodable reply ends the attempt, and the request + // is sent again at once, instead of reading on. + return attemptOutcome{err: err, retry: true} } - if x.OnSent != nil { - x.OnSent(x) + if answered, err := e.answers(reply, earlierIDs); answered { + return attemptOutcome{packet: reply, err: err} } + x.Logger.Print("ERROR out of order") + } +} - waitingResponse: - for { - if x.Logger.enabled() { - x.Logger.Print("WAITING RESPONSE...") - } - // Receive response and try receiving again on any decoding error. - // Let the deadline abort us if we don't receive a valid response. - - var resp []byte - resp, err = x.receive() - if err == io.EOF && strings.HasPrefix(x.Transport, tcp) { - x.Logger.Printf("ERROR: EOF. Performing reconnect") - err = x.netConnect() - if err != nil { - return nil, err - } - continue sendRetry - } else if err != nil { - // receive error. retrying won't help. abort - break - } - if x.OnRecv != nil { - x.OnRecv(x) - } - if x.Logger.enabled() { - x.Logger.Printf("GET RESPONSE OK: %+v", resp) - } - result = new(SnmpPacket) - result.Logger = x.Logger - - result.MsgFlags = packetOut.MsgFlags - if packetOut.SecurityParameters != nil { - result.SecurityParameters = packetOut.SecurityParameters.Copy() - } - - var cursor int - cursor, err = x.unmarshalHeader(resp, result) - if err != nil { - x.Logger.Printf("ERROR on unmarshall header: %s", err) - break - } - - if x.Version == Version3 { - usp := usmOf(x.SecurityParameters) - useResponseSecurityParameters := usp != nil && usp.AuthoritativeEngineID == "" - err = x.testAuthentication(resp, result, useResponseSecurityParameters) - if err != nil { - x.Logger.Printf("ERROR on Test Authentication on v3: %s", err) - break - } - resp, cursor, err = unmarshalScopedPDU(resp, cursor, result) - if err != nil { - x.Logger.Printf("ERROR on decryptPacket on v3: %s", err) - break - } - } - - err = unmarshalPayload(resp, cursor, result) - if err != nil { - x.Logger.Printf("ERROR on UnmarshalPayload on v3: %s", err) - break - } - if result.Error == NoError && len(result.Variables) < 1 { - x.Logger.Printf("ERROR on UnmarshalPayload on v3: Empty result") - break - } - - // While Report PDU was defined by RFC 1905 as part of SNMPv2, it was never - // used until SNMPv3. Report PDU's allow a SNMP engine to tell another SNMP - // engine that an error was detected while processing an SNMP message. - // - // The format for a Report PDU is - // ----------------------------------- - // | 0xA8 | reqid | 0 | 0 | varbinds | - // ----------------------------------- - // where: - // - PDU type 0xA8 indicates a Report PDU. - // - reqid is either: - // The request identifier of the message that triggered the report - // or zero if the request identifier cannot be extracted. - // - The variable bindings will contain a single object identifier and its value - // - // usmStatsNotInTimeWindows and usmStatsUnknownEngineIDs are recoverable errors - // and will be retransmitted, for others we return the result with an error. - if result.Version == Version3 && result.PDUType == Report && len(result.Variables) == 1 { - switch result.Variables[0].Name { - case usmStatsUnsupportedSecLevels: - return result, ErrUnknownSecurityLevel - case usmStatsNotInTimeWindows: - break waitingResponse - case usmStatsUnknownUserNames: - return result, ErrUnknownUsername - case usmStatsUnknownEngineIDs: - break waitingResponse - case usmStatsWrongDigests: - return result, ErrWrongDigest - case usmStatsDecryptionErrors: - return result, ErrDecryption - case snmpUnknownSecurityModels: - return result, ErrUnknownSecurityModels - case snmpInvalidMsgs: - return result, ErrInvalidMsgs - case snmpUnknownPDUHandlers: - return result, ErrUnknownPDUHandlers - default: - return result, ErrUnknownReportPDU - } - } - - validID := false - for _, id := range allReqIDs { - if id == result.RequestID { - validID = true - } - } - if result.RequestID == 0 { - validID = true - } - if !validID { - x.Logger.Print("ERROR out of order") - continue - } +// decode decodes a reply to the request: the header, for SNMPv3 the +// authentication and the scoped PDU, then the PDU. +func (e *exchange) decode(resp []byte) (*SnmpPacket, error) { + x := e.x + reply := &SnmpPacket{Logger: x.Logger, MsgFlags: e.packet.MsgFlags} + if e.packet.SecurityParameters != nil { + reply.SecurityParameters = e.packet.SecurityParameters.Copy() + } - break + cursor, err := x.unmarshalHeader(resp, reply) + if err != nil { + x.Logger.Printf("ERROR on unmarshall header: %s", err) + return nil, err + } + if x.Version == Version3 { + // Until discovery has set the authoritative engine ID, the reply is + // checked with its own flags and the security parameters it carries. + usp := usmOf(x.SecurityParameters) + fromReply := usp != nil && usp.AuthoritativeEngineID == "" + if err = x.testAuthentication(resp, reply, fromReply); err != nil { + x.Logger.Printf("ERROR on Test Authentication on v3: %s", err) + return nil, err } - if err != nil { - continue + if resp, cursor, err = unmarshalScopedPDU(resp, cursor, reply); err != nil { + x.Logger.Printf("ERROR on decryptPacket on v3: %s", err) + return nil, err } + } + if err = unmarshalPayload(resp, cursor, reply); err != nil { + x.Logger.Printf("ERROR on UnmarshalPayload on v3: %s", err) + return nil, err + } + return reply, nil +} - if x.OnFinish != nil { - x.OnFinish(x) +// answers reports whether reply answers the request, and with which error: a +// reply answers with the current attempt's request ID or one of earlierIDs. +// Known bugs: an empty reply and a Report answer before their request ID is +// checked, request ID 0 answers any request, and the reply's version and PDU +// type are not checked. +func (e *exchange) answers(reply *SnmpPacket, earlierIDs []uint32) (bool, error) { + if reply.Error == NoError && len(reply.Variables) < 1 { + e.x.Logger.Printf("ERROR on UnmarshalPayload on v3: Empty result") + return true, nil + } + if reply.Version == Version3 && reply.PDUType == Report && len(reply.Variables) == 1 { + err, ok := reportErrors[reply.Variables[0].Name] + if !ok { + err = ErrUnknownReportPDU } - // Success! - return result, nil + return true, err } + id := reply.RequestID + return id == e.packet.RequestID || slices.Contains(earlierIDs, id) || id == 0, nil +} - // Return last error - return nil, err +// reportErrors maps the counter an SNMPv3 Report carries (the USM counters of +// RFC 3414, the message processing counters of RFC 3412) to the request's +// error. A Report is a Report PDU with one varbind, the counter of the error +// the agent detected; its request ID is the request's, or 0 when the agent +// could not read it. Reports about the time window and an unknown engine ID +// give no error: the caller takes the engine parameters they carry, and send +// sends the request again. +var reportErrors = map[string]error{ //nolint:gochecknoglobals // a read-only table + usmStatsUnsupportedSecLevels: ErrUnknownSecurityLevel, + usmStatsNotInTimeWindows: nil, + usmStatsUnknownUserNames: ErrUnknownUsername, + usmStatsUnknownEngineIDs: nil, + usmStatsWrongDigests: ErrWrongDigest, + usmStatsDecryptionErrors: ErrDecryption, + snmpUnknownSecurityModels: ErrUnknownSecurityModels, + snmpInvalidMsgs: ErrInvalidMsgs, + snmpUnknownPDUHandlers: ErrUnknownPDUHandlers, } // generic "sender" that negotiate any version of snmp request From 571f14724271a3ea5c6709e8fecde69de428ebbe Mon Sep 17 00:00:00 2001 From: ilyam8 Date: Fri, 9 Oct 2026 20:01:50 +0300 Subject: [PATCH 5/7] refactor: name the remaining known bugs of the request steps Review of the decomposition found pinned known bugs whose sites are in the new functions without a comment: - answers: nothing else of a reply is compared with the request (version, PDU type, msgID, and the security model, level, user, engine ID and context of RFC 3412 section 7.2 step 12 b), and a Report counts only with exactly one varbind, so one with more is returned as a successful reply. The reportErrors doc no longer states the one-varbind condition as what a Report is. - decode: after discovery a reply is checked with the client's flags, so an unauthenticated Report fails the digest check and is discarded; a reply with an empty msgFlags keeps the request's flags. attempt now stamps the request ID before encode, which keeps the per-message work (msg ID, privacy parameters, marshal); the IDs are taken in the same order. The stack note on the earlier request IDs holds for small Retries only, and says so. No behavior change: goldens and logger output unchanged. --- engine.go | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/engine.go b/engine.go index c50911a..36604d9 100644 --- a/engine.go +++ b/engine.go @@ -42,8 +42,9 @@ type attemptOutcome struct { // retrying up to x.Retries times. OnFinish runs when it succeeds. func (x *GoSNMP) sendOneRequest(packetOut *SnmpPacket) (*SnmpPacket, error) { e := exchange{x: x, packet: packetOut, timeout: x.Timeout} - // The request IDs of the attempts before the current one. The slice lives - // in this frame, where it stays off the heap. + // The request IDs of the attempts before the current one. A local, not a + // field of exchange: for small Retries it stays on the stack, where a field + // would always escape to the heap. earlierIDs := make([]uint32, 0, x.Retries+1) var lastErr error for n := 0; ; n++ { @@ -121,6 +122,7 @@ func (e *exchange) attempt(earlierIDs []uint32) attemptOutcome { return attemptOutcome{err: err} } + e.packet.RequestID = x.nextRequestID() // An encoding failure is not retried: another attempt would fail the same // way. outBuf, err := e.encode() @@ -153,11 +155,10 @@ func (x *GoSNMP) attemptDeadline(timeout time.Duration) (deadline time.Time, byC return deadline, false } -// encode stamps the request with a new request ID and, for SNMPv3, a new -// message ID and privacy parameters, and marshals it. +// encode marshals the request, for SNMPv3 with a new message ID and privacy +// parameters. func (e *exchange) encode() ([]byte, error) { x, p := e.x, e.packet - p.RequestID = x.nextRequestID() if x.Version == Version3 { p.MsgID = x.nextMsgID() if err := x.initPacket(p); err != nil { @@ -213,10 +214,11 @@ func (e *exchange) await(earlierIDs []uint32) attemptOutcome { } } -// decode decodes a reply to the request: the header, for SNMPv3 the +// decode decodes a reply to the request: the header, for an SNMPv3 client the // authentication and the scoped PDU, then the PDU. func (e *exchange) decode(resp []byte) (*SnmpPacket, error) { x := e.x + // Known bug: a reply with an empty msgFlags keeps the request's flags. reply := &SnmpPacket{Logger: x.Logger, MsgFlags: e.packet.MsgFlags} if e.packet.SecurityParameters != nil { reply.SecurityParameters = e.packet.SecurityParameters.Copy() @@ -229,7 +231,10 @@ func (e *exchange) decode(resp []byte) (*SnmpPacket, error) { } if x.Version == Version3 { // Until discovery has set the authoritative engine ID, the reply is - // checked with its own flags and the security parameters it carries. + // checked with its own flags and the security parameters it carries, + // afterwards with the client's. Known bug: an unauthenticated Report + // then fails the digest check and is discarded, and the caller never + // sees its error. usp := usmOf(x.SecurityParameters) fromReply := usp != nil && usp.AuthoritativeEngineID == "" if err = x.testAuthentication(resp, reply, fromReply); err != nil { @@ -250,9 +255,12 @@ func (e *exchange) decode(resp []byte) (*SnmpPacket, error) { // answers reports whether reply answers the request, and with which error: a // reply answers with the current attempt's request ID or one of earlierIDs. -// Known bugs: an empty reply and a Report answer before their request ID is -// checked, request ID 0 answers any request, and the reply's version and PDU -// type are not checked. +// Known bugs: nothing else of the reply is compared with the request (its +// version, PDU type and msgID, nor the security model, level, user, engine ID +// and context of RFC 3412 section 7.2 step 12 b); an empty reply and a Report +// answer before their request ID is checked; request ID 0 answers any request; +// a Report counts only with exactly one varbind, so one with more is returned +// as a successful reply. func (e *exchange) answers(reply *SnmpPacket, earlierIDs []uint32) (bool, error) { if reply.Error == NoError && len(reply.Variables) < 1 { e.x.Logger.Printf("ERROR on UnmarshalPayload on v3: Empty result") @@ -271,11 +279,11 @@ func (e *exchange) answers(reply *SnmpPacket, earlierIDs []uint32) (bool, error) // reportErrors maps the counter an SNMPv3 Report carries (the USM counters of // RFC 3414, the message processing counters of RFC 3412) to the request's -// error. A Report is a Report PDU with one varbind, the counter of the error -// the agent detected; its request ID is the request's, or 0 when the agent +// error. The agent puts the counter of the error it detected in the Report's +// varbinds; the Report's request ID is the request's, or 0 when the agent // could not read it. Reports about the time window and an unknown engine ID -// give no error: the caller takes the engine parameters they carry, and send -// sends the request again. +// give no error: the caller takes the engine parameters they carry, and send, +// when it is the caller, sends the request again. var reportErrors = map[string]error{ //nolint:gochecknoglobals // a read-only table usmStatsUnsupportedSecLevels: ErrUnknownSecurityLevel, usmStatsNotInTimeWindows: nil, From 905b2cfe6ac48708a807534533fc2d2ba3f7cd46 Mon Sep 17 00:00:00 2001 From: ilyam8 Date: Fri, 9 Oct 2026 22:28:41 +0300 Subject: [PATCH 6/7] test: pin error identity and the request paths review found unpinned An equivalence review of the request engine's decomposition found behaviors the engine tests did not observe; old and new code agree on each, and each pin passes on both. - describeEngineError adds the error's dynamic type and the sentinels it equals, so a returned error that gets wrapped (still errors.Is) or a context's cause returned instead of its error shows in the goldens. - requests/context/canceled-with-cause-before: a context canceled with a cause gives context.Canceled. - requests/context/deadline-equal-to-timeout: a context deadline equal to the attempt's ends it as a request timeout. - requests/answer/other-version-v3-report: an SNMPv3 Report whose msgData is the bare PDU, sent to an SNMPv2c client, maps to its error. - v3/hooks/privacy-protocol-changed-on-retry: an OnRetry hook that changes the client's privacy protocol makes the next attempt fail in initPacket. - TestEngineTCPReconnect: retries that run out after a timeout and then a reconnect end with "max retries" (the reconnect leaves no error); a reply to the first request read after the reconnect answers it. - TestEngineReplyLogger: a reply carries the client's Logger. --- engine_harness_test.go | 20 +++-- engine_requests_test.go | 151 ++++++++++++++++++++++++++++---- engine_v3_test.go | 13 +++ testdata/engine/requests.golden | 98 +++++++++++++-------- testdata/engine/v3-fips.golden | 16 ++-- testdata/engine/v3.golden | 100 +++++++++++++-------- testdata/engine/walk.golden | 18 ++-- 7 files changed, 304 insertions(+), 112 deletions(-) diff --git a/engine_harness_test.go b/engine_harness_test.go index 17d601d..6d3deb3 100644 --- a/engine_harness_test.go +++ b/engine_harness_test.go @@ -392,9 +392,12 @@ func describeEngineError(err error) string { if stack { text += " Stack: ..." } - var is []string + var equals, is []string for _, s := range engineSentinels { - if errors.Is(err, s.err) { + switch { + case err == s.err: //nolint:errorlint // the identity is what is described + equals = append(equals, s.name) + case errors.Is(err, s.err): is = append(is, s.name) } } @@ -402,10 +405,17 @@ func describeEngineError(err error) string { if errors.As(err, &ne) && ne.Timeout() { is = append(is, "net.Error timeout") } - if len(is) == 0 { - return fmt.Sprintf("error %q", text) + var parts []string + if len(equals) > 0 { + parts = append(parts, "equals "+strings.Join(equals, ", ")) } - return fmt.Sprintf("error %q (is %s)", text, strings.Join(is, ", ")) + if len(is) > 0 { + parts = append(parts, "is "+strings.Join(is, ", ")) + } + if len(parts) == 0 { + return fmt.Sprintf("error %q %T", text, err) + } + return fmt.Sprintf("error %q %T (%s)", text, err, strings.Join(parts, "; ")) } // describeEngineResult describes what a request returned. diff --git a/engine_requests_test.go b/engine_requests_test.go index eeb3481..282c479 100644 --- a/engine_requests_test.go +++ b/engine_requests_test.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "log" "maps" "net" "regexp" @@ -24,6 +25,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/netdata/gosnmp/internal/ber" ) var engineGolden = goldenFiles{dir: "testdata/engine", title: "Engine"} @@ -70,6 +73,43 @@ func answerWith(edit func(*SnmpPacket), vbs ...SnmpPDU) engineAgent { } } +// errEngineCause is the cause a scenario cancels its context with. +var errEngineCause = errors.New("engine test cause") + +// bareV3Report encodes an SNMPv3 noAuthNoPriv Report answering req whose +// msgData is the Report PDU itself, without the scoped PDU around it, so that +// a v1/v2c client reads it as the PDU after the SNMPv3 header. +func bareV3Report(req []byte, counter string) []byte { + in, err := decodeMessage(req) + if err != nil { + panic(fmt.Sprintf("agent cannot decode the request: %v", err)) + } + out := &SnmpPacket{ + Version: Version3, MsgFlags: NoAuthNoPriv, SecurityModel: UserSecurityModel, + SecurityParameters: &UsmSecurityParameters{UserName: "public"}, + PDUType: Report, RequestID: in.RequestID, MsgID: 7, + Variables: []SnmpPDU{{Name: counter, Type: Counter32, Value: uint32(1)}}, + } + b, err := out.marshalMsg() + if err != nil { + panic(fmt.Sprintf("agent cannot encode the reply: %v", err)) + } + m, err := parseV3Message(b) + if err != nil { + panic(fmt.Sprintf("agent cannot read its reply: %v", err)) + } + r := ber.NewReader(m.scoped) + for range 2 { // context engine ID and context name + if _, _, err = r.Next(); err != nil { + panic(err) + } + } + if m.scopedTag, m.scoped, err = r.Next(); err != nil { + panic(err) + } + return m.bytes() +} + // answerFromSecond is an agent that answers the requests from the second on // with vbs. func answerFromSecond(vbs ...SnmpPDU) engineAgent { @@ -283,6 +323,9 @@ func engineScenarios() map[string]engineScenario { p.SecurityParameters = &UsmSecurityParameters{UserName: "public"} }, sysDescr), }, + "answer/other-version-v3-report": {agent: func(_ int, req []byte) []agentReply { + return []agentReply{{data: bareV3Report(req, usmStatsUnknownUserNames)}} + }}, "answer/other-community": {agent: answerWith(func(p *SnmpPacket) { p.Community = "private" }, sysDescr)}, "answer/garbage-then-valid": { agent: func(_ int, req []byte) []agentReply { @@ -353,6 +396,15 @@ func engineScenarios() map[string]engineScenario { cancel() return ctx, cancel }}, + "context/canceled-with-cause-before": {agent: answer(sysDescr), context: func() (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancelCause(context.Background()) + cancel(errEngineCause) + return ctx, func() { cancel(nil) } + }}, + "context/deadline-equal-to-timeout": { + context: withEngineTimeout(time.Second), + setup: func(x *GoSNMP, _ *fakeTransport) { x.Retries = 0 }, + }, "context/deadline-in-first-attempt": {context: withEngineTimeout(700 * time.Millisecond)}, "context/deadline-in-third-attempt": {context: withEngineTimeout(2500 * time.Millisecond)}, "context/deadline-after-all-attempts": {context: withEngineTimeout(time.Hour)}, @@ -477,34 +529,47 @@ func runEngineScenario(t *testing.T, sc engineScenario) string { } // TestEngineTCPReconnect pins, on loopback sockets, what the engine does when -// a TCP agent closes the connection after reading each request: it reconnects +// a TCP agent closes the connection after reading a request: it reconnects // and sends again, and gives up on the reconnect's error or when the retries // run out, known bugs included. func TestEngineTCPReconnect(t *testing.T) { tests := map[string]struct { transport string // "tcp" unless set acceptAgain bool // the agent accepts the reconnection + ignoreFirst bool // the agent reads the first request and keeps the connection open + staleReply bool // the agent answers the request after the reconnect with a reply to the first + timeout time.Duration // the client's Timeout; 10 s unless set contextTimeout time.Duration // a context deadline before the timeout - wantErr string // describeEngineError, the dial address masked + wantErr string // describeEngineError, the dial address masked; "" for a reply wantHooks []string wantRequests int32 // requests the agent read knownBug string }{ "retries run out": { - acceptAgain: true, wantErr: `error "max retries (1) exceeded"`, + acceptAgain: true, wantErr: `error "max retries (1) exceeded" *errors.errorString`, wantHooks: []string{"PreSend", "OnSent", "OnRetry", "PreSend", "OnSent", "OnRetry"}, wantRequests: 2, }, "retries run out, tcp4": { - transport: "tcp4", acceptAgain: true, wantErr: `error "max retries (1) exceeded"`, + transport: "tcp4", acceptAgain: true, wantErr: `error "max retries (1) exceeded" *errors.errorString`, wantHooks: []string{"PreSend", "OnSent", "OnRetry", "PreSend", "OnSent", "OnRetry"}, wantRequests: 2, }, + "retries run out after a timeout": { + acceptAgain: true, ignoreFirst: true, timeout: 200 * time.Millisecond, + wantErr: `error "max retries (1) exceeded" *errors.errorString`, + wantHooks: []string{"PreSend", "OnSent", "OnRetry", "PreSend", "OnSent", "OnRetry"}, wantRequests: 2, + }, + "reply to the first request after the reconnect": { + acceptAgain: true, staleReply: true, timeout: 500 * time.Millisecond, + wantHooks: []string{"PreSend", "OnSent", "OnRetry", "PreSend", "OnSent", "OnRecv", "OnFinish"}, + wantRequests: 2, + }, "reconnect refused": { - wantErr: `error "dial tcp
: connect: connection refused" (is syscall.ECONNREFUSED)`, + wantErr: `error "dial tcp
: connect: connection refused" *net.OpError (is syscall.ECONNREFUSED)`, wantHooks: []string{"PreSend", "OnSent"}, wantRequests: 1, }, "context deadline": { acceptAgain: true, contextTimeout: 5 * time.Second, - wantErr: `error "recover: runtime error: invalid memory address or nil pointer dereference Stack: ..."`, + wantErr: `error "recover: runtime error: invalid memory address or nil pointer dereference Stack: ..." *errors.errorString`, wantHooks: []string{"PreSend", "OnSent", "OnRetry"}, wantRequests: 1, knownBug: "after the reconnect under a context deadline, the engine reads the nil error of the reconnect", }, @@ -516,6 +581,7 @@ func TestEngineTCPReconnect(t *testing.T) { require.NoError(t, err) port := uint16(ln.Addr().(*net.TCPAddr).Port) //nolint:gosec // a TCP port var requests atomic.Int32 + var firstRequest atomic.Pointer[[]byte] done := make(chan struct{}) go func() { defer close(done) @@ -527,12 +593,16 @@ func TestEngineTCPReconnect(t *testing.T) { if first && !tc.acceptAgain { ln.Close() } - go func() { - defer conn.Close() - if n, _ := conn.Read(make([]byte, 2048)); n > 0 { - requests.Add(1) + go serveTCPRequests(conn, func(req []byte) (reply []byte, keepOpen bool) { + switch n := requests.Add(1); { + case n == 1: + firstRequest.Store(&req) + return nil, tc.ignoreFirst + case tc.staleReply: + return replyTo(*firstRequest.Load(), nil, sysDescr), true } - }() + return nil, false + }) } }() defer func() { @@ -550,6 +620,10 @@ func TestEngineTCPReconnect(t *testing.T) { if transport == "" { transport = "tcp" } + timeout := tc.timeout + if timeout == 0 { + timeout = 10 * time.Second + } var hooks []string hook := func(name string) func(*GoSNMP) { return func(*GoSNMP) { hooks = append(hooks, name) } } x := &GoSNMP{ @@ -558,7 +632,7 @@ func TestEngineTCPReconnect(t *testing.T) { Transport: transport, Community: "public", Version: Version2c, - Timeout: 10 * time.Second, + Timeout: timeout, Retries: 1, Context: ctx, PreSend: hook("PreSend"), @@ -570,13 +644,20 @@ func TestEngineTCPReconnect(t *testing.T) { require.NoError(t, x.Connect()) defer x.Close() - _, err = x.Get([]string{engineOID}) - require.Error(t, err) - got := dialAddress.ReplaceAllString(describeEngineError(err), "dial tcp
: ") - if runtime.GOOS == "windows" && name == "reconnect refused" { + res, err := x.Get([]string{engineOID}) + switch { + case tc.wantErr == "": + require.NoError(t, err) + first, decodeErr := decodeMessage(*firstRequest.Load()) + require.NoError(t, decodeErr) + assert.Equal(t, first.RequestID, res.RequestID, "the reply answers the first request") + case runtime.GOOS == "windows" && name == "reconnect refused": // Windows words the refusal differently and reports WSAECONNREFUSED. + got := dialAddress.ReplaceAllString(describeEngineError(err), "dial tcp
: ") assert.True(t, strings.HasPrefix(got, `error "dial tcp
: connectex:`), "got %s", got) - } else { + default: + require.Error(t, err) + got := dialAddress.ReplaceAllString(describeEngineError(err), "dial tcp
: ") assert.Equal(t, tc.wantErr, got, "known bug: %q", tc.knownBug) } assert.Equal(t, tc.wantHooks, hooks, "hooks") @@ -585,6 +666,42 @@ func TestEngineTCPReconnect(t *testing.T) { } } +// serveTCPRequests reads requests from conn, one per read, and hands each to +// handle, which returns the reply to write, if any, and whether to keep the +// connection open; conn closes when it is not kept open or the client closes +// it. +func serveTCPRequests(conn net.Conn, handle func(req []byte) (reply []byte, keepOpen bool)) { + defer conn.Close() + buf := make([]byte, 2048) + for { + n, err := conn.Read(buf) + if n == 0 || err != nil { + return + } + reply, keepOpen := handle(bytes.Clone(buf[:n])) + if reply != nil { + if _, err := conn.Write(reply); err != nil { + return + } + } + if !keepOpen { + return + } + } +} + +// TestEngineReplyLogger pins that a reply carries the client's Logger. +func TestEngineReplyLogger(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + c := newFakeTransport(nil, answer(sysDescr)) + x := newEngineClient(t, nil, Version2c, c) + x.Logger = NewLogger(log.New(io.Discard, "", 0)) + res, err := x.Get([]string{engineOID}) + require.NoError(t, err) + assert.Equal(t, x.Logger, res.Logger) + }) +} + // BenchmarkSendOneRequest measures one SNMPv2c Get through the request engine // on the in-memory transport, without socket system calls; the agent's // decoding of the request and encoding of the reply are included. diff --git a/engine_v3_test.go b/engine_v3_test.go index 17b57f0..4108856 100644 --- a/engine_v3_test.go +++ b/engine_v3_test.go @@ -394,6 +394,19 @@ func v3Scenarios() map[string]v3Scenario { "answer/other-version/sha-aes": { user: "codec-sha-aes", script: onRequest(2, agentAnswer{level: AuthPriv, why: "SNMPv2c GetResponse", edit: asV2c}), }, + "hooks/privacy-protocol-changed-on-retry": { + user: "codec-sha-aes", script: onRequest(2, agentAnswer{drop: true, why: "no answer"}), + setup: func(x *GoSNMP, _ *UsmSecurityParameters) { x.Retries = 1 }, + run: func(x *GoSNMP, a *fakeV3Agent) (*SnmpPacket, error) { + onRetry := x.OnRetry + x.OnRetry = func(x *GoSNMP) { + onRetry(x) + x.SecurityParameters.usm().PrivacyProtocol = DES + a.tr.addf("hook OnRetry sets the client's privacy protocol to DES") + } + return x.Get([]string{engineOID}) + }, + }, "answer/other-security-model": { user: "codec-noauth", script: onRequest(2, agentAnswer{ why: "GetResponse with security model 2", edit: func(p *SnmpPacket) { p.SecurityModel = 2 }, diff --git a/testdata/engine/requests.golden b/testdata/engine/requests.golden index ae56b41..29bc422 100644 --- a/testdata/engine/requests.golden +++ b/testdata/engine/requests.golden @@ -67,7 +67,7 @@ 2s hook OnRecv 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet nil, error "request timeout (after 2 retries)" +3s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 3s client after: retries=2 next request id=1004 == requests/answer/empty-wrong-id @@ -144,7 +144,7 @@ 0s read from 127.0.0.2:161: 4 octets, not decodable (error verifying packet sanity: TLV length exceeds the remaining input) 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "error verifying packet sanity: TLV length exceeds the remaining input" +0s result: packet nil, error "error verifying packet sanity: TLV length exceeds the remaining input" *fmt.wrapError 0s client after: retries=2 next request id=1004 == requests/answer/garbage-then-valid @@ -325,9 +325,21 @@ 0s read from 127.0.0.2:161: 3 msgID=0 noAuthNoPriv user="public" engine="" boots=0 time=0 context=""/"" GetResponse id=1003 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "unknown PDUType 0x53657175656e6365" +0s result: packet nil, error "unknown PDUType 0x53657175656e6365" *errors.errorString 0s client after: retries=2 next request id=1004 +== requests/answer/other-version-v3-report +0s client: 2c timeout=1s retries=2 exponential=false +0s deadline +1s +0s hook PreSend +0s write #1: 2c "public" GetRequest id=1001 [.1.3.6.1.2.1.1.1.0] +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=7 noAuthNoPriv user="public" engine="" boots=0 time=0 encrypted +0s hook OnRecv +0s result: packet 3 msgID=7 noAuthNoPriv Report id=1001 context=""/"" usm="public"/""/0/0 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" *errors.errorString (equals ErrUnknownUsername) +0s client after: retries=2 next request id=1002 + == requests/answer/report 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +1s @@ -372,7 +384,7 @@ 0s read: 10 octets, not decodable (error verifying packet sanity: TLV length exceeds the remaining input) 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "error verifying packet sanity: TLV length exceeds the remaining input" +0s result: packet nil, error "error verifying packet sanity: TLV length exceeds the remaining input" *fmt.wrapError 0s client after: retries=0 next request id=1002 0s known bug: a TCP reply must arrive in one read @@ -400,7 +412,7 @@ 0s hook OnSent 0s read: 65535 octets, not decodable (invalid packet header) 0s hook OnRetry -0s result: packet nil, error "response buffer too small" +0s result: packet nil, error "response buffer too small" *errors.errorString 0s client after: retries=2 next request id=1004 == requests/answer/unknown-pdu-type @@ -429,7 +441,7 @@ 0s read from 127.0.0.2:161: 52 octets, not decodable (unknown PDUType 0x504455547970652831373529) 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "unknown PDUType 0x504455547970652831373529" +0s result: packet nil, error "unknown PDUType 0x504455547970652831373529" *errors.errorString 0s client after: retries=2 next request id=1004 0s known bug: the error text formats the PDU type's String() as hexadecimal @@ -476,7 +488,7 @@ 2s hook OnRecv 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet nil, error "request timeout (after 2 retries)" +3s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 3s client after: retries=2 next request id=1004 == requests/answer/zero-id @@ -495,13 +507,13 @@ == requests/conn-without-connect 0s client: 2c timeout=1s retries=2 exponential=false -0s result: packet nil, error "recover: runtime error: invalid memory address or nil pointer dereference Stack: ..." +0s result: packet nil, error "recover: runtime error: invalid memory address or nil pointer dereference Stack: ..." *errors.errorString 0s client after: retries=2 next request id=1001 0s known bug: a client given a Conn without Connect has no Context and no receive buffer: send recovers the nil dereference into an error == requests/context/canceled-before 0s client: 2c timeout=1s retries=2 exponential=false -0s result: packet nil, error "context canceled" (is context.Canceled) +0s result: packet nil, error "context canceled" *errors.errorString (equals context.Canceled) 0s client after: retries=2 next request id=1001 == requests/context/canceled-in-last-attempt @@ -524,7 +536,7 @@ 2s hook OnSent 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet nil, error "request timeout (after 2 retries)" +3s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 3s client after: retries=2 next request id=1004 3s known bug: a cancellation during the last attempt is reported as a request timeout @@ -536,10 +548,15 @@ 0s hook OnSent 1s read from 127.0.0.2:161: deadline 1s hook OnRetry -1s result: packet nil, error "context canceled" (is context.Canceled) +1s result: packet nil, error "context canceled" *errors.errorString (equals context.Canceled) 1s client after: retries=2 next request id=1002 1s known bug: cancellation is noticed only when the attempt's deadline passes +== requests/context/canceled-with-cause-before +0s client: 2c timeout=1s retries=2 exponential=false +0s result: packet nil, error "context canceled" *errors.errorString (equals context.Canceled) +0s client after: retries=2 next request id=1001 + == requests/context/deadline-after-all-attempts 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +1s @@ -560,7 +577,7 @@ 2s hook OnSent 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet nil, error "request timeout (after 2 retries)" +3s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 3s client after: retries=2 next request id=1004 == requests/context/deadline-during-hook @@ -571,9 +588,20 @@ 0s hook OnSent 1s read from 127.0.0.2:161: deadline 1s hook OnRetry, which takes 1s -2s result: packet nil, error "context deadline exceeded" (is context.DeadlineExceeded, net.Error timeout) +2s result: packet nil, error "context deadline exceeded" context.deadlineExceededError (equals context.DeadlineExceeded; is net.Error timeout) 2s client after: retries=2 next request id=1002 +== requests/context/deadline-equal-to-timeout +0s client: 2c timeout=1s retries=0 exponential=false +0s deadline +1s +0s hook PreSend +0s write #1: 2c "public" GetRequest id=1001 [.1.3.6.1.2.1.1.1.0] +0s hook OnSent +1s read from 127.0.0.2:161: deadline +1s hook OnRetry +1s result: packet nil, error "request timeout (after 0 retries)" *errors.errorString +1s client after: retries=0 next request id=1002 + == requests/context/deadline-in-first-attempt 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +700ms @@ -582,7 +610,7 @@ 0s hook OnSent 700ms read from 127.0.0.2:161: deadline 700ms hook OnRetry -700ms result: packet nil, error "context deadline exceeded" (is context.DeadlineExceeded, net.Error timeout) +700ms result: packet nil, error "context deadline exceeded" context.deadlineExceededError (equals context.DeadlineExceeded; is net.Error timeout) 700ms client after: retries=2 next request id=1002 == requests/context/deadline-in-third-attempt @@ -605,7 +633,7 @@ 2s hook OnSent 2.5s read from 127.0.0.2:161: deadline 2.5s hook OnRetry -2.5s result: packet nil, error "context deadline exceeded" (is context.DeadlineExceeded, net.Error timeout) +2.5s result: packet nil, error "context deadline exceeded" context.deadlineExceededError (equals context.DeadlineExceeded; is net.Error timeout) 2.5s client after: retries=2 next request id=1004 == requests/custom-conn @@ -624,7 +652,7 @@ == requests/deadline-error 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +1s -0s result: packet nil, error "codec deadline failure" +0s result: packet nil, error "codec deadline failure" *errors.errorString 0s client after: retries=2 next request id=1001 == requests/get @@ -643,7 +671,7 @@ == requests/get/invalid-oid 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +1s -0s result: packet nil, error "marshal: unable to marshal varbind list: unable to marshal OID \".1.3.6.1.x\": invalid object identifier" +0s result: packet nil, error "marshal: unable to marshal varbind list: unable to marshal OID \".1.3.6.1.x\": invalid object identifier" *fmt.wrapError 0s client after: retries=2 next request id=1002 == requests/get/max-oids/at-limit @@ -661,12 +689,12 @@ == requests/get/max-oids/over-limit 0s client: 2c timeout=1s retries=2 exponential=false -0s result: packet nil, error "oid count (3) is greater than MaxOids (2)" +0s result: packet nil, error "oid count (3) is greater than MaxOids (2)" *errors.errorString 0s client after: retries=2 next request id=1001 == requests/get/no-conn 0s client: 2c timeout=1s retries=2 exponential=false -0s result: packet nil, error "&GoSNMP.Conn is missing. Provide a connection or use Connect()" +0s result: packet nil, error "&GoSNMP.Conn is missing. Provide a connection or use Connect()" *errors.errorString 0s client after: retries=2 next request id=1001 == requests/get/v1 @@ -710,12 +738,12 @@ == requests/getbulk/max-oids/over-limit 0s client: 2c timeout=1s retries=2 exponential=false -0s result: packet nil, error "oid count (3) is greater than MaxOids (2)" +0s result: packet nil, error "oid count (3) is greater than MaxOids (2)" *errors.errorString 0s client after: retries=2 next request id=1001 == requests/getbulk/v1 0s client: 1 timeout=1s retries=2 exponential=false -0s result: packet nil, error "GETBULK not supported in SNMPv1" +0s result: packet nil, error "GETBULK not supported in SNMPv1" *errors.errorString 0s client after: retries=2 next request id=1001 == requests/getnext @@ -746,13 +774,13 @@ == requests/getnext/max-oids/over-limit 0s client: 2c timeout=1s retries=2 exponential=false -0s result: packet nil, error "oid count (3) is greater than MaxOids (2)" +0s result: packet nil, error "oid count (3) is greater than MaxOids (2)" *errors.errorString 0s client after: retries=2 next request id=1001 == requests/hook-panics 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +1s -0s result: packet nil, error "recover: codec hook failure Stack: ..." +0s result: packet nil, error "recover: codec hook failure Stack: ..." *errors.errorString 0s client after: retries=2 next request id=1002 == requests/mk-snmp-packet @@ -810,7 +838,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: read udp: connection refused 0s hook OnRetry -0s result: packet nil, error "error reading from socket: read udp: connection refused" (is syscall.ECONNREFUSED) +0s result: packet nil, error "error reading from socket: read udp: connection refused" *fmt.wrapError (is syscall.ECONNREFUSED) 0s client after: retries=2 next request id=1004 == requests/read-error/once @@ -1036,7 +1064,7 @@ == requests/set/integer-with-text 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +1s -0s result: packet nil, error "marshal: unable to marshal varbind list: unable to marshal PDU Integer; not int" +0s result: packet nil, error "marshal: unable to marshal varbind list: unable to marshal PDU Integer; not int" *fmt.wrapError 0s client after: retries=2 next request id=1002 == requests/set/max-oids/over-limit @@ -1061,13 +1089,13 @@ == requests/set/second-varbind-unsupported 0s client: 2c timeout=1s retries=2 exponential=false 0s deadline +1s -0s result: packet nil, error "marshal: unable to marshal varbind list: unable to marshal PDU: unknown BER type \"Boolean\"" +0s result: packet nil, error "marshal: unable to marshal varbind list: unable to marshal PDU: unknown BER type \"Boolean\"" *fmt.wrapError 0s client after: retries=2 next request id=1002 0s known bug: Set checks the type of the first varbind only; a later one fails in the encoder, after an attempt has started == requests/set/unsupported-type 0s client: 2c timeout=1s retries=2 exponential=false -0s result: packet nil, error "ERR:gosnmp currently only supports SNMP SETs for Integer, OctetString, Gauge32, IPAddress, ObjectIdentifier, Counter32, Counter64, Null, TimeTicks, Uinteger32, OpaqueFloat, and OpaqueDouble. Not Boolean" +0s result: packet nil, error "ERR:gosnmp currently only supports SNMP SETs for Integer, OctetString, Gauge32, IPAddress, ObjectIdentifier, Counter32, Counter64, Null, TimeTicks, Uinteger32, OpaqueFloat, and OpaqueDouble. Not Boolean" *errors.errorString 0s client after: retries=2 next request id=1001 == requests/timeout @@ -1090,7 +1118,7 @@ 2s hook OnSent 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet nil, error "request timeout (after 2 retries)" +3s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 3s client after: retries=2 next request id=1004 3s known bug: OnRetry runs even when no retry follows @@ -1114,7 +1142,7 @@ 3s hook OnSent 7s read from 127.0.0.2:161: deadline 7s hook OnRetry -7s result: packet nil, error "request timeout (after 2 retries)" +7s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 7s client after: retries=2 next request id=1004 == requests/timeout/negative-retries @@ -1125,7 +1153,7 @@ 0s hook OnSent 1s read from 127.0.0.2:161: deadline 1s hook OnRetry -1s result: packet nil, error "request timeout (after 0 retries)" +1s result: packet nil, error "request timeout (after 0 retries)" *errors.errorString 1s client after: retries=0 next request id=1002 == requests/timeout/no-retries @@ -1136,7 +1164,7 @@ 0s hook OnSent 1s read from 127.0.0.2:161: deadline 1s hook OnRetry -1s result: packet nil, error "request timeout (after 0 retries)" +1s result: packet nil, error "request timeout (after 0 retries)" *errors.errorString 1s client after: retries=0 next request id=1002 == requests/timeout/tcp @@ -1159,7 +1187,7 @@ 2s hook OnSent 3s read: deadline 3s hook OnRetry -3s result: packet nil, error "request timeout (after 2 retries)" +3s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 3s client after: retries=2 next request id=1004 == requests/timeout/zero @@ -1179,7 +1207,7 @@ 0s write #3: 2c "public" GetRequest id=1003 [.1.3.6.1.2.1.1.1.0] 0s write #3: deadline 0s hook OnRetry -0s result: packet nil, error "request timeout (after 2 retries)" +0s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 0s client after: retries=2 next request id=1004 == requests/unconnected @@ -1212,7 +1240,7 @@ 0s write #3: 2c "public" GetRequest id=1003 [.1.3.6.1.2.1.1.1.0] 0s write #3 fails: codec write failure 0s hook OnRetry -0s result: packet nil, error "codec write failure" (is errEngineWrite) +0s result: packet nil, error "codec write failure" *errors.errorString (equals errEngineWrite) 0s client after: retries=2 next request id=1004 == requests/write-error/once @@ -1250,6 +1278,6 @@ 0s write #3: 2c "public" GetRequest id=1003 [.1.3.6.1.2.1.1.1.0] 0s write #3 fails: codec timeout of the write queue 0s hook OnRetry -0s result: packet nil, error "request timeout (after 2 retries)" +0s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 0s client after: retries=2 next request id=1004 0s known bug: a timeout is recognized by the word in the error's text, so another error with that word becomes a request timeout diff --git a/testdata/engine/v3-fips.golden b/testdata/engine/v3-fips.golden index 880dec3..3c458d8 100644 --- a/testdata/engine/v3-fips.golden +++ b/testdata/engine/v3-fips.golden @@ -11,9 +11,9 @@ 0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] 0s hook OnRecv 0s hook OnFinish -0s first Get: packet empty, error "crypto/md5: use of MD5 is not allowed in FIPS 140-only mode" +0s first Get: packet empty, error "crypto/md5: use of MD5 is not allowed in FIPS 140-only mode" *errors.errorString 0s deadline +1s -0s result: packet nil, error "marshal: crypto/md5: use of MD5 is not allowed in FIPS 140-only mode" +0s result: packet nil, error "marshal: crypto/md5: use of MD5 is not allowed in FIPS 140-only mode" *fmt.wrapError 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=0 time=0 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: a failed key derivation leaves the agent's engine ID adopted without keys, boots or time, so the next request skips the discovery @@ -51,9 +51,9 @@ 0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] 0s hook OnRecv 0s hook OnFinish -0s first Get: packet empty, error "crypto/sha1: use of SHA-1 is not allowed in FIPS 140-only mode" +0s first Get: packet empty, error "crypto/sha1: use of SHA-1 is not allowed in FIPS 140-only mode" *errors.errorString 0s deadline +1s -0s result: packet nil, error "marshal: crypto/aes: invalid key size 0" +0s result: packet nil, error "marshal: crypto/aes: invalid key size 0" *fmt.wrapError 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=0 time=0 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: a failed key derivation leaves the agent's engine ID adopted without keys, boots or time, so the next request skips the discovery @@ -69,7 +69,7 @@ 0s hook OnRecv 0s hook OnFinish 0s deadline +1s -0s result: packet nil, error "recover: crypto/cipher: use of CFB is not allowed in FIPS 140-only mode Stack: ..." +0s result: packet nil, error "recover: crypto/cipher: use of CFB is not allowed in FIPS 140-only mode Stack: ..." *errors.errorString 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: AES-CFB panics in FIPS 140-only mode; send recovers it into an error that carries the stack @@ -85,7 +85,7 @@ 0s hook OnRecv 0s hook OnFinish 0s deadline +1s -0s result: packet nil, error "marshal: crypto/des: use of DES is not allowed in FIPS 140-only mode" +0s result: packet nil, error "marshal: crypto/des: use of DES is not allowed in FIPS 140-only mode" *fmt.wrapError 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3-fips/discovery/sha512-aes256c @@ -100,13 +100,13 @@ 0s hook OnRecv 0s hook OnFinish 0s deadline +1s -0s result: packet nil, error "recover: crypto/cipher: use of CFB is not allowed in FIPS 140-only mode Stack: ..." +0s result: packet nil, error "recover: crypto/cipher: use of CFB is not allowed in FIPS 140-only mode Stack: ..." *errors.errorString 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: AES-CFB panics in FIPS 140-only mode; send recovers it into an error that carries the stack == v3-fips/known-engine/md5 0s client: user="codec-md5" authNoPriv+reportable timeout=1s retries=2 0s deadline +1s -0s result: packet nil, error "marshal: crypto/md5: use of MD5 is not allowed in FIPS 140-only mode" +0s result: packet nil, error "marshal: crypto/md5: use of MD5 is not allowed in FIPS 140-only mode" *fmt.wrapError 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=0 time=0 keys=none context engine="" 0s known bug: the key derivation error is ignored when the engine ID is known diff --git a/testdata/engine/v3.golden b/testdata/engine/v3.golden index ea25a2c..1846b12 100644 --- a/testdata/engine/v3.golden +++ b/testdata/engine/v3.golden @@ -41,7 +41,7 @@ 0s read from 127.0.0.2:161: 3 msgID=2004 authPriv user="codec-sha-aes" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 salt=0000000000000003 encrypted 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "error parsing SNMPV3 decrypted scoped PDU: invalid packet length" +0s result: packet nil, error "error parsing SNMPV3 decrypted scoped PDU: invalid packet length" *fmt.wrapError 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/answer/empty-msg-flags @@ -106,7 +106,7 @@ 0s read from 127.0.0.2:161: 3 msgID=2004 authPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 salt=0000000000000001 encrypted 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "error parsing SNMPv3 User Security Model: privacy parameters are not configured to parse incoming encrypted message" +0s result: packet nil, error "error parsing SNMPv3 User Security Model: privacy parameters are not configured to parse incoming encrypted message" *errors.errorString 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/answer/encrypted-with-other-key @@ -276,7 +276,7 @@ 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 model=2 context="\x80\x00\x1f\x88\x04codec-agent"/"" GetResponse id=1002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] 0s hook OnRecv 0s hook OnFinish -0s result: packet 3 msgID=2002 noAuthNoPriv GetResponse id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"], error "connection security model does not match security model extracted from packet" +0s result: packet 3 msgID=2002 noAuthNoPriv GetResponse id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"], error "connection security model does not match security model extracted from packet" *errors.errorString 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: a reply with another security model is accepted, then returned together with the model mismatch error @@ -453,7 +453,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2102 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1102 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2102 noAuthNoPriv Report id=1102 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" (is ErrUnknownUsername) +0s result: packet 3 msgID=2102 noAuthNoPriv Report id=1102 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" *errors.errorString (equals ErrUnknownUsername) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: a Report is mapped without checking its msgID or request ID @@ -790,7 +790,7 @@ 2s hook OnSent 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet empty, error "request timeout (after 2 retries)" +3s result: packet empty, error "request timeout (after 2 retries)" *errors.errorString 3s client after: engine="" boots=0 time=0 keys=none context engine="" == v3/discovery/noauth @@ -826,7 +826,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.1.0=Counter32:1] 0s hook OnRecv -0s result: packet empty, error "unknown security level" (is ErrUnknownSecurityLevel) +0s result: packet empty, error "unknown security level" *errors.errorString (equals ErrUnknownSecurityLevel) 0s client after: engine="" boots=0 time=0 keys=none context engine="" == v3/discovery/other-security-model @@ -840,7 +840,7 @@ 0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 model=2 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] 0s hook OnRecv 0s hook OnFinish -0s result: packet empty, error "connection security model does not match security model extracted from packet" +0s result: packet empty, error "connection security model does not match security model extracted from packet" *errors.errorString 0s client after: engine="" boots=0 time=0 keys=none context engine="" 0s known bug: a discovery Report with another security model fails the request instead of being discarded (RFC 3412 section 7.2 step 4) @@ -899,7 +899,7 @@ 2s hook OnSent 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet nil, error "request timeout (after 2 retries)" +3s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 3s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/discovery/sha-aes @@ -1077,7 +1077,7 @@ 10s read from 127.0.0.2:161: 3 msgID=2005 noAuthNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-other" boots=7 time=1010 context="\x80\x00\x1f\x88\x04codec-other"/"" Report id=1005 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:4] 10s hook OnRecv 10s hook OnRetry -10s result: packet nil, error "incoming packet is not authentic, discarding" +10s result: packet nil, error "incoming packet is not authentic, discarding" *errors.errorString 10s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 10s known bug: the unauthenticated unknownEngineID Report fails the digest check and is discarded, so the client never adopts the engine ID @@ -1221,6 +1221,30 @@ 20s client after: engine="\x80\x00\x1f\x88\x04codec-other" boots=7 time=1020 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 20s known bug: a GetResponse from another engine ID than the request's is accepted and its engine ID adopted (RFC 3412 section 7.2 step 12 b) +== v3/hooks/privacy-protocol-changed-on-retry +0s client: user="codec-sha-aes" authPriv+reportable timeout=1s retries=1 +0s deadline +1s +0s hook PreSend +0s write #1: 3 msgID=2001 noAuthNoPriv+reportable user="" engine="" boots=0 time=0 context=""/"" GetRequest id=1001 [] +0s agent: answers #1 with Report id=1001 msgID=2001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] at noAuthNoPriv (unknown engine ID) +0s agent answers #1 after 0s +0s hook OnSent +0s read from 127.0.0.2:161: 3 msgID=2001 noAuthNoPriv user="" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1001 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:1] +0s hook OnRecv +0s hook OnFinish +0s deadline +1s +0s hook PreSend +0s write #2: 3 msgID=2002 authPriv+reportable user="codec-sha-aes" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 salt=0000000000000102 encrypted +0s agent: #2 decrypts to context="\x80\x00\x1f\x88\x04codec-agent"/"" GetRequest id=1002 [.1.3.6.1.2.1.1.1.0] +0s agent: drops #2 (no answer) +0s hook OnSent +1s read from 127.0.0.2:161: deadline +1s hook OnRetry +1s hook OnRetry sets the client's privacy protocol to DES +1s deadline +1s +1s result: packet nil, error "salt provided to usmSetSalt is not the correct type for the AES privacy protocol" *errors.errorString +1s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=unknown context engine="\x80\x00\x1f\x88\x04codec-agent" + == v3/known-engine/other-engine 0s client: user="codec-md5" authNoPriv+reportable timeout=1s retries=2 0s deadline +1s @@ -1250,7 +1274,7 @@ 0s read from 127.0.0.2:161: 3 msgID=2003 noAuthNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1003 [.1.3.6.1.6.3.15.1.1.4.0=Counter32:3] 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "incoming packet is not authentic, discarding" +0s result: packet nil, error "incoming packet is not authentic, discarding" *errors.errorString 0s client after: engine="\x80\x00\x1f\x88\x04codec-other" boots=0 time=0 keys=none context engine="" 0s known bug: the unauthenticated unknownEngineID Report fails the digest check and is discarded, so the client never adopts the engine ID @@ -1385,7 +1409,7 @@ 12s hook OnSent 13s read from 127.0.0.2:161: deadline 13s hook OnRetry -13s result: packet nil, error "not in time window" (is ErrNotInTimeWindow) +13s result: packet nil, error "not in time window" *errors.errorString (equals ErrNotInTimeWindow) 13s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=8 time=0 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 13s known bug: the retransmission's error is replaced by ErrNotInTimeWindow @@ -1436,7 +1460,7 @@ 10s read from 127.0.0.2:161: 3 msgID=2005 noAuthNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=8 time=0 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1005 [.1.3.6.1.6.3.15.1.1.2.0=Counter32:3] 10s hook OnRecv 10s hook OnRetry -10s result: packet nil, error "incoming packet is not authentic, discarding" +10s result: packet nil, error "incoming packet is not authentic, discarding" *errors.errorString 10s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report-later/md5 @@ -1467,7 +1491,7 @@ 10s hook OnSent 10s read from 127.0.0.2:161: 3 msgID=2003 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1010 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1003 [.1.3.6.1.6.3.12.1.5.0=Counter32:1] 10s hook OnRecv -10s result: packet 3 msgID=2003 authNoPriv Report id=1003 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1010 [.1.3.6.1.6.3.12.1.5.0=Counter32:1], error "unknown report pdu" (is ErrUnknownReportPDU) +10s result: packet 3 msgID=2003 authNoPriv Report id=1003 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1010 [.1.3.6.1.6.3.12.1.5.0=Counter32:1], error "unknown report pdu" *errors.errorString (equals ErrUnknownReportPDU) 10s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 10s known bug: the engine boots and time of an authenticated error Report are not stored (RFC 3414 section 3.2 step 7 b) @@ -1499,7 +1523,7 @@ 10s hook OnSent 10s read from 127.0.0.2:161: 3 msgID=2003 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-other" boots=7 time=1010 context="\x80\x00\x1f\x88\x04codec-other"/"" Report id=1003 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1] 10s hook OnRecv -10s result: packet 3 msgID=2003 noAuthNoPriv Report id=1003 context="\x80\x00\x1f\x88\x04codec-other"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-other"/7/1010 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" (is ErrUnknownUsername) +10s result: packet 3 msgID=2003 noAuthNoPriv Report id=1003 context="\x80\x00\x1f\x88\x04codec-other"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-other"/7/1010 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" *errors.errorString (equals ErrUnknownUsername) 10s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/md5-unauthenticated/decryption-errors @@ -1850,7 +1874,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.6.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.6.0=Counter32:1], error "decryption error" (is ErrDecryption) +0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.6.0=Counter32:1], error "decryption error" *errors.errorString (equals ErrDecryption) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/md5/invalid-msgs @@ -1872,7 +1896,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.11.2.1.2.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.2.0=Counter32:1], error "invalid messages" (is ErrInvalidMsgs) +0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.2.0=Counter32:1], error "invalid messages" *errors.errorString (equals ErrInvalidMsgs) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/md5/not-in-time-windows @@ -1926,7 +1950,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.9.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.9.0=Counter32:1], error "unknown report pdu" (is ErrUnknownReportPDU) +0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.9.0=Counter32:1], error "unknown report pdu" *errors.errorString (equals ErrUnknownReportPDU) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/md5/unknown-engine-ids @@ -1980,7 +2004,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.11.2.1.3.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.3.0=Counter32:1], error "unknown pdu handlers" (is ErrUnknownPDUHandlers) +0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.3.0=Counter32:1], error "unknown pdu handlers" *errors.errorString (equals ErrUnknownPDUHandlers) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/md5/unknown-security-models @@ -2002,7 +2026,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.11.2.1.1.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.1.0=Counter32:1], error "unknown security models" (is ErrUnknownSecurityModels) +0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.1.0=Counter32:1], error "unknown security models" *errors.errorString (equals ErrUnknownSecurityModels) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/md5/unknown-user-names @@ -2024,7 +2048,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" (is ErrUnknownUsername) +0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" *errors.errorString (equals ErrUnknownUsername) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/md5/unsupported-sec-levels @@ -2046,7 +2070,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.1.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.1.0=Counter32:1], error "unknown security level" (is ErrUnknownSecurityLevel) +0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.1.0=Counter32:1], error "unknown security level" *errors.errorString (equals ErrUnknownSecurityLevel) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/md5/wrong-digests @@ -2068,7 +2092,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 authNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.5.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.5.0=Counter32:1], error "wrong digest" (is ErrWrongDigest) +0s result: packet 3 msgID=2002 authNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-md5"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.5.0=Counter32:1], error "wrong digest" *errors.errorString (equals ErrWrongDigest) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/noauth/decryption-errors @@ -2090,7 +2114,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.6.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.6.0=Counter32:1], error "decryption error" (is ErrDecryption) +0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.6.0=Counter32:1], error "decryption error" *errors.errorString (equals ErrDecryption) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/noauth/invalid-msgs @@ -2112,7 +2136,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.11.2.1.2.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.2.0=Counter32:1], error "invalid messages" (is ErrInvalidMsgs) +0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.2.0=Counter32:1], error "invalid messages" *errors.errorString (equals ErrInvalidMsgs) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/noauth/not-in-time-windows @@ -2166,7 +2190,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.9.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.9.0=Counter32:1], error "unknown report pdu" (is ErrUnknownReportPDU) +0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.9.0=Counter32:1], error "unknown report pdu" *errors.errorString (equals ErrUnknownReportPDU) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/noauth/unknown-engine-ids @@ -2220,7 +2244,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.11.2.1.3.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.3.0=Counter32:1], error "unknown pdu handlers" (is ErrUnknownPDUHandlers) +0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.3.0=Counter32:1], error "unknown pdu handlers" *errors.errorString (equals ErrUnknownPDUHandlers) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/noauth/unknown-security-models @@ -2242,7 +2266,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.11.2.1.1.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.1.0=Counter32:1], error "unknown security models" (is ErrUnknownSecurityModels) +0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.11.2.1.1.0=Counter32:1], error "unknown security models" *errors.errorString (equals ErrUnknownSecurityModels) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/noauth/unknown-user-names @@ -2264,7 +2288,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" (is ErrUnknownUsername) +0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" *errors.errorString (equals ErrUnknownUsername) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/noauth/unsupported-sec-levels @@ -2286,7 +2310,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.1.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.1.0=Counter32:1], error "unknown security level" (is ErrUnknownSecurityLevel) +0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.1.0=Counter32:1], error "unknown security level" *errors.errorString (equals ErrUnknownSecurityLevel) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/report/noauth/wrong-digests @@ -2308,7 +2332,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-noauth" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.5.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.5.0=Counter32:1], error "wrong digest" (is ErrWrongDigest) +0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-noauth"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.5.0=Counter32:1], error "wrong digest" *errors.errorString (equals ErrWrongDigest) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/time-window/always-not-in-time-window @@ -2526,7 +2550,7 @@ 2s hook OnSent 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet nil, error "unknown engine id" (is ErrUnknownEngineID) +3s result: packet nil, error "unknown engine id" *errors.errorString (equals ErrUnknownEngineID) 3s client after: engine="\x80\x00\x1f\x88\x04codec-other" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" 3s known bug: the retransmission's error is replaced by ErrUnknownEngineID @@ -2568,7 +2592,7 @@ 0s read from 127.0.0.2:161: 3 msgID=2004 noAuthNoPriv user="codec-nobody" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1004 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:3] 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "incoming packet is not authentic, discarding" +0s result: packet nil, error "incoming packet is not authentic, discarding" *errors.errorString 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: the unauthenticated unknownUserName Report fails the digest check and is discarded: the caller never sees ErrUnknownUsername @@ -2591,7 +2615,7 @@ 0s hook OnSent 0s read from 127.0.0.2:161: 3 msgID=2002 noAuthNoPriv user="codec-nobody" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1002 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1] 0s hook OnRecv -0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-nobody"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" (is ErrUnknownUsername) +0s result: packet 3 msgID=2002 noAuthNoPriv Report id=1002 context="\x80\x00\x1f\x88\x04codec-agent"/"" usm="codec-nobody"/"\x80\x00\x1f\x88\x04codec-agent"/7/1000 [.1.3.6.1.6.3.15.1.1.3.0=Counter32:1], error "unknown username" *errors.errorString (equals ErrUnknownUsername) 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=none context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/unsupported-level @@ -2632,7 +2656,7 @@ 0s read from 127.0.0.2:161: 3 msgID=2004 noAuthNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=0 [.1.3.6.1.6.3.15.1.1.1.0=Counter32:3] 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "incoming packet is not authentic, discarding" +0s result: packet nil, error "incoming packet is not authentic, discarding" *errors.errorString 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: the unauthenticated unsupportedSecLevel Report fails the digest check and is discarded @@ -2674,7 +2698,7 @@ 0s read from 127.0.0.2:161: 3 msgID=2004 noAuthNoPriv user="codec-md5" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=1004 [.1.3.6.1.6.3.15.1.1.5.0=Counter32:3] 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "incoming packet is not authentic, discarding" +0s result: packet nil, error "incoming packet is not authentic, discarding" *errors.errorString 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: the unauthenticated wrongDigest Report fails the digest check and is discarded: the caller never sees ErrWrongDigest @@ -2710,7 +2734,7 @@ 2s hook OnSent 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet nil, error "request timeout (after 2 retries)" +3s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 3s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/wrong-priv-passphrase/des @@ -2745,7 +2769,7 @@ 2s hook OnSent 3s read from 127.0.0.2:161: deadline 3s hook OnRetry -3s result: packet nil, error "request timeout (after 2 retries)" +3s result: packet nil, error "request timeout (after 2 retries)" *errors.errorString 3s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" == v3/wrong-priv-protocol/aes-to-des-user @@ -2786,6 +2810,6 @@ 0s read from 127.0.0.2:161: 3 msgID=2004 noAuthNoPriv user="codec-sha256-des" engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 context="\x80\x00\x1f\x88\x04codec-agent"/"" Report id=0 [.1.3.6.1.6.3.15.1.1.6.0=Counter32:3] 0s hook OnRecv 0s hook OnRetry -0s result: packet nil, error "incoming packet is not authentic, discarding" +0s result: packet nil, error "incoming packet is not authentic, discarding" *errors.errorString 0s client after: engine="\x80\x00\x1f\x88\x04codec-agent" boots=7 time=1000 keys=current context engine="\x80\x00\x1f\x88\x04codec-agent" 0s known bug: the unauthenticated decryptionErrors Report fails the digest check and is discarded: the caller never sees ErrDecryption diff --git a/testdata/engine/walk.golden b/testdata/engine/walk.golden index e69fb69..5635928 100644 --- a/testdata/engine/walk.golden +++ b/testdata/engine/walk.golden @@ -299,7 +299,7 @@ 0s write #2: 2c "public" GetNextRequest id=1002 [.1.3.6.1.2.1.1.1.0] 0s agent answers #2 after 0s 0s read from 127.0.0.2:161: 2c "public" GetResponse id=1002 error=SNMPError(19) index=1 [.1.3.6.1.2.1.1.1.0] -0s result: error "OID not increasing: .1.3.6.1.2.1.1.1.0 >= .1.3.6.1.2.1.1.1.0" +0s result: error "OID not increasing: .1.3.6.1.2.1.1.1.0 >= .1.3.6.1.2.1.1.1.0" *errors.errorString 0s known bug: an error status the walk does not name is ignored and the varbinds walked == walk/error-status/2-NoSuchName @@ -548,7 +548,7 @@ 0s agent answers #2 after 0s 0s read from 127.0.0.2:161: 2c "public" GetResponse id=1002 [.1.3.6.1.2.1.1.5.0=OctetString:"codec-host"] 0s walkFn: [.1.3.6.1.2.1.1.5.0=OctetString:"codec-host"] -0s result: error "codec walkFn stop" +0s result: error "codec walkFn stop" *errors.errorString == walk/max-repetitions/one 0s client: 2c root=".1.3.6.1.2.1.1" bulk=true max-repetitions=1 @@ -681,7 +681,7 @@ 0s write #3: 2c "public" GetNextRequest id=1003 [.1.3.6.1.2.1.2.2.1.2.2] 0s agent answers #3 after 0s 0s read from 127.0.0.2:161: 2c "public" GetResponse id=1003 [.1.3.6.1.2.1.2.2.1.2.1=OctetString:"lo"] -0s result: error "OID not increasing: .1.3.6.1.2.1.2.2.1.2.2 >= .1.3.6.1.2.1.2.2.1.2.1" +0s result: error "OID not increasing: .1.3.6.1.2.1.2.2.1.2.2 >= .1.3.6.1.2.1.2.2.1.2.1" *errors.errorString == walk/not-increasing/equal 0s client: 2c root=".1.3.6.1.2.1.1" bulk=false max-repetitions=0 @@ -694,7 +694,7 @@ 0s write #2: 2c "public" GetNextRequest id=1002 [.1.3.6.1.2.1.1.1.0] 0s agent answers #2 after 0s 0s read from 127.0.0.2:161: 2c "public" GetResponse id=1002 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] -0s result: error "OID not increasing: .1.3.6.1.2.1.1.1.0 >= .1.3.6.1.2.1.1.1.0" +0s result: error "OID not increasing: .1.3.6.1.2.1.1.1.0 >= .1.3.6.1.2.1.1.1.0" *errors.errorString == walk/request-error 0s client: 2c root=".1.3.6.1.2.1.1" bulk=false max-repetitions=0 @@ -722,7 +722,7 @@ 3s read from 127.0.0.2:161: deadline 3s hook OnRetry 3s values: [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent" .1.3.6.1.2.1.1.3.0=TimeTicks:4200] -3s result: error "request timeout (after 2 retries)" +3s result: error "request timeout (after 2 retries)" *errors.errorString == walk/request-error/bulk 0s client: 2c root=".1.3.6.1.2.1.2.2.1.2" bulk=true max-repetitions=1 @@ -750,7 +750,7 @@ 3s read from 127.0.0.2:161: deadline 3s hook OnRetry 3s values: [.1.3.6.1.2.1.2.2.1.2.1=OctetString:"lo" .1.3.6.1.2.1.2.2.1.2.2=OctetString:"eth0"] -3s result: error "request timeout (after 2 retries)" +3s result: error "request timeout (after 2 retries)" *errors.errorString == walk/root-in-bulk-response 0s client: 2c root=".1.3.6.1.2.1.1" bulk=true max-repetitions=0 @@ -914,7 +914,7 @@ == walk/v1-bulk 0s client: 1 root=".1.3.6.1.2.1.1" bulk=true max-repetitions=0 -0s result: error "GETBULK not supported in SNMPv1" +0s result: error "GETBULK not supported in SNMPv1" *errors.errorString == walk/walk-all 0s client: 2c root=".1.3.6.1.2.1.2.2.1.2" bulk=false max-repetitions=0 @@ -958,7 +958,7 @@ 0s agent answers #2 after 0s 0s read from 127.0.0.2:161: 2c "public" GetResponse id=1002 [.1.3.6.1.2.1.1.3.0=TimeTicks:4200] 0s walkFn: [.1.3.6.1.2.1.1.3.0=TimeTicks:4200] -0s result: error "codec walkFn stop" +0s result: error "codec walkFn stop" *errors.errorString == walk/walkfn-error/bulk 0s client: 2c root=".1.3.6.1.2.1.1" bulk=true max-repetitions=0 @@ -968,4 +968,4 @@ 0s read from 127.0.0.2:161: 2c "public" GetResponse id=1001 [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent" .1.3.6.1.2.1.1.3.0=TimeTicks:4200 .1.3.6.1.2.1.1.5.0=OctetString:"codec-host" .1.3.6.1.2.1.2.2.1.2.1=OctetString:"lo" .1.3.6.1.2.1.2.2.1.2.2=OctetString:"eth0" .1.3.6.1.2.1.2.2.1.2.10=OctetString:"eth1" .1.3.6.1.4.1.99.1.0=Integer:7 .1.3.6.1.4.1.99.1.0=EndOfMibView:] 0s walkFn: [.1.3.6.1.2.1.1.1.0=OctetString:"codec agent"] 0s walkFn: [.1.3.6.1.2.1.1.3.0=TimeTicks:4200] -0s result: error "codec walkFn stop" +0s result: error "codec walkFn stop" *errors.errorString From 51f9024257da2b299d3a28f7055bbdb35e2e21a4 Mon Sep 17 00:00:00 2001 From: ilyam8 Date: Fri, 9 Oct 2026 22:28:41 +0300 Subject: [PATCH 7/7] refactor: say which empty reply answers before its ID is checked Only an empty reply without an error status answers early; one with an error status goes through the request ID check. --- engine.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/engine.go b/engine.go index 36604d9..2f960b7 100644 --- a/engine.go +++ b/engine.go @@ -257,10 +257,10 @@ func (e *exchange) decode(resp []byte) (*SnmpPacket, error) { // reply answers with the current attempt's request ID or one of earlierIDs. // Known bugs: nothing else of the reply is compared with the request (its // version, PDU type and msgID, nor the security model, level, user, engine ID -// and context of RFC 3412 section 7.2 step 12 b); an empty reply and a Report -// answer before their request ID is checked; request ID 0 answers any request; -// a Report counts only with exactly one varbind, so one with more is returned -// as a successful reply. +// and context of RFC 3412 section 7.2 step 12 b); an empty reply without an +// error status and a Report answer before their request ID is checked; request +// ID 0 answers any request; a Report counts only with exactly one varbind, so +// one with more is returned as a successful reply. func (e *exchange) answers(reply *SnmpPacket, earlierIDs []uint32) (bool, error) { if reply.Error == NoError && len(reply.Variables) < 1 { e.x.Logger.Printf("ERROR on UnmarshalPayload on v3: Empty result")